e2e/mrbuilds_test.go

v1.11.0
gitbay/e2e/mrbuilds_test.go history · blame · raw

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}