vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Phase 4: a rename carries the queue, instead of stranding it
72797dcd4606a81d35fff03124fc1ba627cc1d60 · Keenan Nemetz · 17d ago · parent d798eaac
Phase 4: a rename carries the queue, instead of stranding it Queue rows are keyed by repository name rather than by the repository's stable id, so a rename left every queued job under a name no runner is ever told about — the lease filters by the names an account may run, and the old name no longer exists — and left the ref cursors behind, so the next reconciliation queued the current tip a second time. RenameRepo moves the runs, the cursors and the presence rows in one transaction, and `vole repo rename` calls it. A rename refuses a name that is already taken, so the unique index on (repo, ref, sha, workflow, job) cannot collide.
internal/server/reconcile_test.go +40
| @@ -209,6 +209,46 @@ func TestANewerTipCancelsTheQueuedBuildOfTheOlderOne(t *testing.T) { | |||
| 209 | 209 | } | |
| 210 | 210 | } | |
| 211 | 211 | ||
| 212 | // A rename must carry the queue with it. Rows are keyed by repository name, so a | ||
| 213 | // stranded job is one no runner is ever told about — the lease filters by the names an | ||
| 214 | // account may run, and the old name no longer exists — and a stranded cursor makes the | ||
| 215 | // next reconciliation build the current tip a second time. | ||
| 216 | func TestRenamingARepositoryCarriesItsQueue(t *testing.T) { | ||
| 217 | h := newReconcileHarness(t) | ||
| 218 | h.commit(t, "refs/heads/main", testWorkflow) | ||
| 219 | h.s.reconcileRefs() | ||
| 220 | |||
| 221 | if err := h.s.Repos.Rename("demo", "renamed"); err != nil { | ||
| 222 | t.Fatal(err) | ||
| 223 | } | ||
| 224 | if err := h.runs.RenameRepo("demo", "renamed"); err != nil { | ||
| 225 | t.Fatal(err) | ||
| 226 | } | ||
| 227 | if old, err := h.runs.Recent("demo", 10); err != nil { | ||
| 228 | t.Fatal(err) | ||
| 229 | } else if len(old) != 0 { | ||
| 230 | t.Fatalf("%d run(s) stayed under the old name", len(old)) | ||
| 231 | } | ||
| 232 | kept, err := h.runs.Recent("renamed", 10) | ||
| 233 | if err != nil { | ||
| 234 | t.Fatal(err) | ||
| 235 | } | ||
| 236 | if len(kept) != 1 { | ||
| 237 | t.Fatalf("%d run(s) under the new name, want the queued one to have moved", len(kept)) | ||
| 238 | } | ||
| 239 | |||
| 240 | // The cursor came along, so the sweep has nothing new to queue. | ||
| 241 | h.s.reconcileRefs() | ||
| 242 | after, err := h.runs.Recent("renamed", 10) | ||
| 243 | if err != nil { | ||
| 244 | t.Fatal(err) | ||
| 245 | } | ||
| 246 | if len(after) != len(kept) { | ||
| 247 | t.Fatalf("the sweep queued %d more run(s) after the rename: the cursor did not follow", | ||
| 248 | len(after)-len(kept)) | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 212 | 252 | // A deleted branch drops its cursor, so the branch coming back at a new tip is work | |
| 213 | 253 | // owed rather than work suppressed by a note about a branch that no longer exists. | |
| 214 | 254 | func TestReconcileDropsTheCursorOfADeletedBranch(t *testing.T) { | |