Commit b1f7d169de
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
e2e/ci_dedupe_test.go added +81
| @@ -0,0 +1,81 @@ | |||
| 1 | package e2e | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "os" | ||
| 5 | "path/filepath" | ||
| 6 | "strings" | ||
| 7 | "testing" | ||
| 8 | ) | ||
| 9 | |||
| 10 | // A fast-forward lands the exact commit that was already built on its | ||
| 11 | // branch; that commit is not built again. A commit whose build failed is. | ||
| 12 | func TestFastForwardMergeSkipsBuiltCommit(t *testing.T) { | ||
| 13 | inst := startInstance(t) | ||
| 14 | inst.runner = buildRunner(t) | ||
| 15 | aliceKey := inst.newKey(t, "alice") | ||
| 16 | runnerKey := inst.newKey(t, "ci") | ||
| 17 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") | ||
| 18 | inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin") | ||
| 19 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 { | ||
| 20 | t.Fatalf("repo create: %s", errOut) | ||
| 21 | } | ||
| 22 | work := t.TempDir() | ||
| 23 | env := inst.gitEnv(aliceKey) | ||
| 24 | mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w") | ||
| 25 | dir := filepath.Join(work, "w") | ||
| 26 | os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755) | ||
| 27 | os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte("jobs:\n unit:\n steps:\n - test -f f.txt || test ! -f fail.txt\n"), 0o644) | ||
| 28 | mustGit(t, dir, env, "checkout", "-q", "-b", "main") | ||
| 29 | mustGit(t, dir, env, "add", ".") | ||
| 30 | mustGit(t, dir, env, "commit", "-q", "-m", "base") | ||
| 31 | mustGit(t, dir, env, "push", "-q", "origin", "main") | ||
| 32 | inst.runnerOnce(t, runnerKey) // main's own build | ||
| 33 | |||
| 34 | // A branch whose build passes. | ||
| 35 | mustGit(t, dir, env, "checkout", "-q", "-b", "good") | ||
| 36 | os.WriteFile(filepath.Join(dir, "f.txt"), []byte("x\n"), 0o644) | ||
| 37 | mustGit(t, dir, env, "add", ".") | ||
| 38 | mustGit(t, dir, env, "commit", "-q", "-m", "good change") | ||
| 39 | mustGit(t, dir, env, "push", "-q", "origin", "good") | ||
| 40 | inst.runnerOnce(t, runnerKey) | ||
| 41 | list := inst.buildList(t, aliceKey) | ||
| 42 | if !strings.Contains(list, "good\tunit\tsuccess") && !strings.Contains(list, "unit\tsuccess") { | ||
| 43 | t.Fatalf("branch build did not pass:\n%s", list) | ||
| 44 | } | ||
| 45 | if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/app", "--source", "good", "--target", "main", "--title", "good"); code != 0 { | ||
| 46 | t.Fatalf("mr create: %s", errOut) | ||
| 47 | } | ||
| 48 | before := strings.Count(inst.buildList(t, aliceKey), "\n") | ||
| 49 | if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/app", "1", "--strategy", "ff"); code != 0 { | ||
| 50 | t.Fatalf("mr merge: %s", errOut) | ||
| 51 | } | ||
| 52 | if after := strings.Count(inst.buildList(t, aliceKey), "\n"); after != before { | ||
| 53 | t.Fatalf("fast-forward queued a build for an already-built commit:\n%s", inst.buildList(t, aliceKey)) | ||
| 54 | } | ||
| 55 | sha := strings.Fields(strings.TrimSpace(mustGit(t, dir, env, "ls-remote", "origin", "refs/heads/main")))[0] | ||
| 56 | if status, _, _ := inst.ssh(t, aliceKey, "", "status", "list", "alice/app", sha); !strings.Contains(status, "success") || strings.Contains(status, "pending") { | ||
| 57 | t.Fatalf("statuses on the merged commit changed:\n%s", status) | ||
| 58 | } | ||
| 59 | |||
| 60 | // A branch whose build fails is built again when it lands. | ||
| 61 | mustGit(t, dir, env, "checkout", "-q", "-b", "bad") | ||
| 62 | os.WriteFile(filepath.Join(dir, "fail.txt"), []byte("x\n"), 0o644) | ||
| 63 | os.Remove(filepath.Join(dir, "f.txt")) | ||
| 64 | mustGit(t, dir, env, "add", "-A") | ||
| 65 | mustGit(t, dir, env, "commit", "-q", "-m", "bad change") | ||
| 66 | mustGit(t, dir, env, "push", "-q", "origin", "bad") | ||
| 67 | inst.runnerOnce(t, runnerKey) | ||
| 68 | if list := inst.buildList(t, aliceKey); !strings.Contains(list, "unit\tfailure") { | ||
| 69 | t.Fatalf("bad branch build did not fail:\n%s", list) | ||
| 70 | } | ||
| 71 | if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/app", "--source", "bad", "--target", "main", "--title", "bad"); code != 0 { | ||
| 72 | t.Fatalf("mr create: %s", errOut) | ||
| 73 | } | ||
| 74 | 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 { | ||
| 76 | t.Fatalf("mr merge bad: %s", errOut) | ||
| 77 | } | ||
| 78 | if after := strings.Count(inst.buildList(t, aliceKey), "\n"); after != before+1 { | ||
| 79 | t.Fatalf("a failed commit was not rebuilt on landing:\n%s", inst.buildList(t, aliceKey)) | ||
| 80 | } | ||
| 81 | } | ||
internal/control/build.go +11
| @@ -468,12 +468,23 @@ func QueueBranchBuilds( | |||
| 468 | st.SetCommitStatus(repo.ID, sha, "ci/config", "failure", err.Error(), "", userID) | 468 | st.SetCommitStatus(repo.ID, sha, "ci/config", "failure", err.Error(), "", userID) |
| 469 | return | 469 | return |
| 470 | } | 470 | } |
| 471 | // A build is a fact about a commit, not a ref: a job has no branch | ||
| 472 | // filter, so a commit that already passed a job on another branch has | ||
| 473 | // nothing left to prove when a fast-forward lands it here. A failed or | ||
| 474 | // abandoned build does not count; that commit runs again. | ||
| 475 | built, err := st.BuildsForCommit(repo.ID, sha) | ||
| 476 | if err != nil { | ||
| 477 | built = nil | ||
| 478 | } | ||
| 471 | var schedules []store.Schedule | 479 | var schedules []store.Schedule |
| 472 | for _, j := range jobs { | 480 | for _, j := range jobs { |
| 473 | // Tag jobs run on matching tag pushes only. | 481 | // Tag jobs run on matching tag pushes only. |
| 474 | if j.Tags != "" { | 482 | if j.Tags != "" { |
| 475 | continue | 483 | continue |
| 476 | } | 484 | } |
| 485 | if b, ok := built[j.Name]; ok && b.Status == "success" { | ||
| 486 | continue | ||
| 487 | } | ||
| 477 | // Scheduled jobs run on their cron, not on push; a default-branch | 488 | // Scheduled jobs run on their cron, not on push; a default-branch |
| 478 | // push (re)registers them. | 489 | // push (re)registers them. |
| 479 | if j.Schedule != "" { | 490 | if j.Schedule != "" { |