Commit 47861ecc58

47861ecc5867852c6491b3b272f9a3a373cd19ae

parent: 17f45745ef

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-02 04:42 UTC

ci: a commit with a build still queued or running is not queued again

The landing-time skip counted only a success build of the commit, so a
merge made before the branch's build had finished queued the same
commit a second time on main. Three of the jobs after !172 were that.

A pending or running build of the commit for the job now counts too:
it will say what the commit does soon enough, and the commit statuses
it writes land on the sha whatever ref it ran for. Failed, abandoned
and cancelled builds still do not count.

Ref #90

Layout: unified · split

e2e/ci_dedupe_test.go +20 −1
@@ -57,6 +57,25 @@ func TestFastForwardMergeSkipsBuiltCommit(t *testing.T) {
5757 t.Fatalf("statuses on the merged commit changed:\n%s", status)
5858 }
5959
60 // A branch merged before its build has run: the queued build will
61 // say, so the merge queues nothing either.
62 mustGit(t, dir, env, "checkout", "-q", "-b", "early")
63 os.WriteFile(filepath.Join(dir, "g.txt"), []byte("y\n"), 0o644)
64 mustGit(t, dir, env, "add", ".")
65 mustGit(t, dir, env, "commit", "-q", "-m", "early change")
66 mustGit(t, dir, env, "push", "-q", "origin", "early")
67 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/app", "--source", "early", "--target", "main", "--title", "early"); code != 0 {
68 t.Fatalf("mr create: %s", errOut)
69 }
70 before = strings.Count(inst.buildList(t, aliceKey), "\n")
71 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/app", "2", "--strategy", "ff"); code != 0 {
72 t.Fatalf("mr merge early: %s", errOut)
73 }
74 if after := strings.Count(inst.buildList(t, aliceKey), "\n"); after != before {
75 t.Fatalf("a merge before the branch build ran queued a second build:\n%s", inst.buildList(t, aliceKey))
76 }
77 inst.runnerOnce(t, runnerKey) // the branch's queued build, now the only one
78
6079 // A branch whose build fails is built again when it lands.
6180 mustGit(t, dir, env, "checkout", "-q", "-b", "bad")
6281 os.WriteFile(filepath.Join(dir, "fail.txt"), []byte("x\n"), 0o644)
@@ -72,7 +91,7 @@ func TestFastForwardMergeSkipsBuiltCommit(t *testing.T) {
7291 t.Fatalf("mr create: %s", errOut)
7392 }
7493 before = strings.Count(inst.buildList(t, aliceKey), "\n")
75 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/app", "2", "--strategy", "ff"); code != 0 {
94 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/app", "3", "--strategy", "ff"); code != 0 {
7695 t.Fatalf("mr merge bad: %s", errOut)
7796 }
7897 if after := strings.Count(inst.buildList(t, aliceKey), "\n"); after != before+1 {
internal/control/build.go +4 −3
@@ -510,8 +510,9 @@ func QueueBranchBuilds(
510510 }
511511 // A build is a fact about a commit, not a ref: a job has no branch
512512 // filter, so a commit that already passed a job on another branch has
513 // nothing left to prove when a fast-forward lands it here. A failed or
514 // abandoned build does not count; that commit runs again.
513 // nothing left to prove when a fast-forward lands it here, and one
514 // still queued or running there will say soon enough. A failed,
515 // abandoned or cancelled build does not count; that commit runs again.
515516 built, err := st.BuildsForCommit(repo.ID, sha)
516517 if err != nil {
517518 built = nil
@@ -522,7 +523,7 @@ func QueueBranchBuilds(
522523 if j.Tags != "" {
523524 continue
524525 }
525 if b, ok := built[j.Name]; ok && b.Status == "success" {
526 if b, ok := built[j.Name]; ok && (b.Status == "success" || b.Status == "pending" || b.Status == "running") {
526527 continue
527528 }
528529 // Scheduled jobs run on their cron, not on push; a default-branch