e2e/mrbuilds_test.go
129 lines · 5551 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12// A merge request head is fetched into the target repository, and the
13// target's push jobs run against it there, so a fork's merge request has
14// ci/<job> statuses for require-checks to gate on. Builds used to queue
15// only for branch pushes to the pushed repository, which left a fork's
16// merge request unbuildable and, under require-checks, unmergeable (#98).
17// A head from another repository runs without the target's secrets.
18func TestForkMRHeadIsBuilt(t *testing.T) {
19 inst := startInstance(t)
20 inst.runner = buildRunner(t)
21 aliceKey := inst.newKey(t, "alice")
22 bobKey := inst.newKey(t, "bob")
23 runnerKey := inst.newKey(t, "ci")
24 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub",
25 "--email", "alice@example.test", "--verified")
26 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
27 inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin")
28
29 // alice/app: two push jobs, a secret, require-checks.
30 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
31 t.Fatalf("repo create: %s", errOut)
32 }
33 work := t.TempDir()
34 env := inst.gitEnv(aliceKey)
35 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
36 dir := filepath.Join(work, "w")
37 os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
38 os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte(
39 "jobs:\n one:\n steps:\n - test -z \"$TOKEN\"\n two:\n steps:\n - echo two\n"), 0o644)
40 os.WriteFile(filepath.Join(dir, "f.txt"), []byte("x\n"), 0o644)
41 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
42 mustGit(t, dir, env, "add", ".")
43 mustGit(t, dir, env, "commit", "-q", "-m", "base")
44 mustGit(t, dir, env, "push", "-q", "origin", "main")
45 // The branch push queued two builds for main; clear them so the
46 // queue holds only what the merge request adds.
47 for _, n := range []string{"1", "2"} {
48 if _, _, code := inst.ssh(t, aliceKey, "", "build", "cancel", "alice/app", n); code != 0 {
49 t.Fatalf("build cancel %s failed", n)
50 }
51 }
52 if _, _, code := inst.ssh(t, aliceKey, "s3cret\n", "repo", "secret", "set", "alice/app", "TOKEN"); code != 0 {
53 t.Fatal("secret set failed")
54 }
55 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "settings", "require-checks", "alice/app", "on"); code != 0 {
56 t.Fatal("require-checks failed")
57 }
58
59 // bob forks, pushes a branch to the fork, opens the merge request.
60 if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "fork", "alice/app"); code != 0 {
61 t.Fatalf("fork: %s", errOut)
62 }
63 bwork := t.TempDir()
64 benv := inst.gitEnv(bobKey)
65 mustGit(t, bwork, benv, "clone", inst.sshURL("bob/app"), "w")
66 bdir := filepath.Join(bwork, "w")
67 mustGit(t, bdir, benv, "checkout", "-q", "-b", "feat")
68 os.WriteFile(filepath.Join(bdir, "f.txt"), []byte("y\n"), 0o644)
69 mustGit(t, bdir, benv, "add", ".")
70 mustGit(t, bdir, benv, "commit", "-q", "-m", "change")
71 mustGit(t, bdir, benv, "push", "-q", "origin", "feat")
72 head := strings.TrimSpace(mustGit(t, bdir, benv, "rev-parse", "HEAD"))
73 // The fork carries the same ci.yml, so bob's push queued its own
74 // builds; cancel them so the runner's next claim is the target's.
75 for _, n := range []string{"1", "2"} {
76 if _, _, code := inst.ssh(t, bobKey, "", "build", "cancel", "bob/app", n); code != 0 {
77 t.Fatalf("build cancel bob/app %s failed", n)
78 }
79 }
80 if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "create", "alice/app",
81 "--source", "bob/app:feat", "--target", "main", "--title", "change"); code != 0 {
82 t.Fatalf("mr create: %s", errOut)
83 }
84
85 // Two builds queued in the target, at the merge request ref.
86 out, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/app", "--json")
87 if strings.Count(out, `"status":"pending"`) != 2 || !strings.Contains(out, `"ref":"refs/merge-requests/1/head"`) {
88 t.Fatalf("expected two pending builds at the MR ref:\n%s", out)
89 }
90 if strings.Count(out, head[:10]) < 2 {
91 t.Fatalf("builds are not for the MR head %s:\n%s", head[:10], out)
92 }
93
94 // The claim carries no secrets for a head from another repository.
95 out, errOut, code := inst.ssh(t, runnerKey, "", "runner", "next", "alice/app", "--json")
96 if code != 0 {
97 t.Fatalf("runner next: %s", errOut)
98 }
99 var claim struct {
100 Data struct {
101 ID int64 `json:"id"`
102 Job string `json:"job"`
103 Secrets map[string]string `json:"secrets"`
104 } `json:"data"`
105 }
106 json.Unmarshal([]byte(out), &claim)
107 if claim.Data.Job != "one" || len(claim.Data.Secrets) != 0 {
108 t.Fatalf("fork build claimed with secrets or wrong job:\n%s", out)
109 }
110 if _, _, code := inst.ssh(t, runnerKey, "", "runner", "done", fmt.Sprint(claim.Data.ID), "success"); code != 0 {
111 t.Fatal("runner done failed")
112 }
113
114 // The real runner fetches the merge request ref and runs the second job.
115 log := inst.runnerOnce(t, runnerKey)
116 if !strings.Contains(log, "two") {
117 t.Fatalf("runner did not run the second job:\n%s", log)
118 }
119 out, errOut, _ = inst.ssh(t, aliceKey, "", "status", "list", "alice/app", head, "--json")
120 if strings.Count(out, `"state":"success"`) != 2 {
121 builds, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/app", "--json")
122 t.Fatalf("statuses on the MR head:\n%s%s\nbuilds:\n%s\nrunner log:\n%s", out, errOut, builds, log)
123 }
124
125 // require-checks is satisfied by the builds on the head.
126 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/app", "1"); code != 0 {
127 t.Fatalf("merge under require-checks: %s", errOut)
128 }
129}