e2e/mrbuilds_test.go

bd49b87fce895e9f0a7588152548fb6e1821ac7d
gitbay/e2e/mrbuilds_test.go history · blame · raw

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}