package e2e import ( "context" "os/exec" "strings" "testing" "github.com/barerepo/server/internal/store" "github.com/barerepo/server/internal/token" ) // Chapter 15A end to end: a repository whose CI is a GitHub workflow and nothing else. func TestWorkflowBuildsWithoutABarerepoConfig(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) tok := in.account("john") ctx := context.Background() work := t.TempDir() run(t, work, "git", "init", "-q", "-b", "master") write(t, work, "config.go", "package main\n") // No [build] command anywhere, which is the whole point: the workflow is the only build. write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n") write(t, work, ".github/workflows/ci.yml", ` name: ci on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: unit run: go test ./... - name: deploy uses: some/deploy-action@v1 `) run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "first") // With no machine attached, the push declines and says where to attach one. out := run(t, work, "git", "push", in.url(tok, "/john/bot"), "master") for _, want := range []string{ "skipped deploy", "some/deploy-action@v1", "wants a ubuntu-latest machine and none is attached", "/john/bot/runners/new", } { if !strings.Contains(out, want) { t.Errorf("the push does not say %q\n%s", want, out) } } if n := queued(t, in, "john/bot"); n != 0 { t.Fatalf("%d jobs were queued with no machine to run them", n) } // A mac cannot take it either, and the job must not be handed to the wrong machine. attach(t, in, "john/bot", "macbook", "darwin", nil) run(t, work, "git", "commit", "-qm", "second", "--allow-empty") out = run(t, work, "git", "push", in.url(tok, "/john/bot"), "master") if !strings.Contains(out, "none is attached") { t.Errorf("a mac was treated as an ubuntu machine\n%s", out) } // Attach linux and the same push queues the job. attach(t, in, "john/bot", "builder", "linux", nil) run(t, work, "git", "commit", "-qm", "third", "--allow-empty") out = run(t, work, "git", "push", in.url(tok, "/john/bot"), "master") if !strings.Contains(out, "queued test from .github/workflows/ci.yml") { t.Errorf("the push did not queue the workflow job\n%s", out) } jobs, err := in.db.QueuedJobs(ctx, "john/bot") if err != nil { t.Fatal(err) } if len(jobs) != 1 { t.Fatalf("got %d queued jobs, want 1", len(jobs)) } j := jobs[0] if !strings.Contains(j.Command, "go test ./...") { t.Errorf("the run step did not reach the job: %q", j.Command) } if len(j.Labels) != 1 || j.Labels[0] != "ubuntu-latest" { t.Errorf("the job did not keep what it asked for: %v", j.Labels) } // The mac must not be able to take it, and the linux machine must. mac := runnerNamed(t, in, "john/bot", "macbook") if got, err := in.db.TakeJob(ctx, "john/bot", mac); err != nil || got != nil { t.Errorf("a mac took an ubuntu job: %+v %v", got, err) } builder := runnerNamed(t, in, "john/bot", "builder") got, err := in.db.TakeJob(ctx, "john/bot", builder) if err != nil || got == nil { t.Fatalf("the linux machine could not take its own job: %+v %v", got, err) } } // A repository with both keeps barerepo's own answer, because [build] command is one command. func TestABarerepoConfigWinsOverAWorkflow(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) tok := in.account("john") work := t.TempDir() run(t, work, "git", "init", "-q", "-b", "master") write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n[build]\ncommand = \"make ci\"\n") write(t, work, ".github/workflows/ci.yml", "jobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: go test ./...\n") run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "first") out := run(t, work, "git", "push", in.url(tok, "/john/bot"), "master") if !strings.Contains(out, "queued a build for") { t.Errorf("the repository's own build did not run\n%s", out) } if strings.Contains(out, "workflows/ci.yml") { t.Errorf("the workflow ran as well, so the build happened twice\n%s", out) } jobs, err := in.db.QueuedJobs(context.Background(), "john/bot") if err != nil { t.Fatal(err) } if len(jobs) != 1 || jobs[0].Command != "make ci" { t.Errorf("queued %d jobs, first %q", len(jobs), jobs[0].Command) } } func queued(t *testing.T, in *instance, repo string) int { t.Helper() jobs, err := in.db.QueuedJobs(context.Background(), repo) if err != nil { t.Fatal(err) } return len(jobs) } // attach registers a machine the way a real runner does, so the matching is the real matching. func attach(t *testing.T, in *instance, repo, host, os string, labels []string) { t.Helper() ctx := context.Background() owner, _, _ := strings.Cut(repo, "/") tok, _, err := in.db.CreateToken(ctx, token.Runner, owner, repo, "runner for "+host) if err != nil { t.Fatal(err) } rec, err := in.db.AccountForToken(ctx, token.Runner, tok) if err != nil { t.Fatal(err) } if _, err := in.db.AttachRunner(ctx, rec.ID, repo, host, os, "amd64", labels); err != nil { t.Fatal(err) } } func runnerNamed(t *testing.T, in *instance, repo, host string) store.Runner { t.Helper() runners, err := in.db.RunnersOf(context.Background(), repo) if err != nil { t.Fatal(err) } for _, r := range runners { if r.Hostname == host { return r } } t.Fatalf("no runner named %s", host) return store.Runner{} } // A matrix builds one commit several times, and each build has to be told apart. Chapter 15A. func TestMatrixQueuesOneNamedJobPerCombination(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) tok := in.account("john") ctx := context.Background() work := t.TempDir() run(t, work, "git", "init", "-q", "-b", "master") write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n") write(t, work, ".github/workflows/ci.yml", ` name: ci jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] steps: - run: go test ./... `) run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "first") // Only linux is attached, so the mac half is declined and the linux half is queued. attach(t, in, "john/bot", "builder", "linux", nil) out := run(t, work, "git", "push", in.url(tok, "/john/bot"), "master") if !strings.Contains(out, "wants a macos-latest machine and none is attached") { t.Errorf("the mac combination was not declined\n%s", out) } if !strings.Contains(out, "queued test (os ubuntu-latest)") { t.Errorf("the linux combination was not queued\n%s", out) } jobs, err := in.db.QueuedJobs(ctx, "john/bot") if err != nil { t.Fatal(err) } if len(jobs) != 1 { t.Fatalf("got %d queued jobs, want 1: %+v", len(jobs), jobs) } // The name has to survive to the job, or two builds of one commit are indistinguishable. if jobs[0].Name != "test (os ubuntu-latest)" { t.Errorf("the job is named %q", jobs[0].Name) } // Attach a mac and the other half queues on the next push. attach(t, in, "john/bot", "macbook", "darwin", nil) run(t, work, "git", "commit", "-qm", "second", "--allow-empty") out = run(t, work, "git", "push", in.url(tok, "/john/bot"), "master") if !strings.Contains(out, "queued test (os macos-latest)") { t.Errorf("the mac combination did not queue once a mac was attached\n%s", out) } // Each machine takes only its own combination. mac := runnerNamed(t, in, "john/bot", "macbook") got, err := in.db.TakeJob(ctx, "john/bot", mac) if err != nil || got == nil { t.Fatalf("the mac could not take its own combination: %+v %v", got, err) } if !strings.Contains(got.Name, "macos-latest") { t.Errorf("the mac took %q", got.Name) } }