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