Commit 882f09f5a0
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
cmd/gitbay-runner/main.go +8 −4
| @@ -251,10 +251,14 @@ func (r *runner) run(j job) bool { | |||
| 251 | cloneURL := r.cloneBase + "/" + j.Repo + ".git" | 251 | cloneURL := r.cloneBase + "/" + j.Repo + ".git" |
| 252 | deadline := time.Now().Add(r.timeout) | 252 | deadline := time.Now().Add(r.timeout) |
| 253 | fmt.Fprintf(sink, "$ git clone %s (%.10s)\n", cloneURL, j.SHA) | 253 | fmt.Fprintf(sink, "$ git clone %s (%.10s)\n", cloneURL, j.SHA) |
| 254 | for _, args := range [][]string{ | 254 | // A merge request head lives under refs/merge-requests/, which a |
| 255 | {"clone", "-q", cloneURL, dir}, | 255 | // clone does not fetch; ask for the ref before checking out. |
| 256 | {"-C", dir, "checkout", "-q", j.SHA}, | 256 | steps := [][]string{{"clone", "-q", cloneURL, dir}} |
| 257 | } { | 257 | if strings.HasPrefix(j.Ref, "refs/") { |
| 258 | steps = append(steps, []string{"-C", dir, "fetch", "-q", "origin", j.Ref}) | ||
| 259 | } | ||
| 260 | steps = append(steps, []string{"-C", dir, "checkout", "-q", j.SHA}) | ||
| 261 | for _, args := range steps { | ||
| 258 | cmd := exec.Command("git", args...) | 262 | cmd := exec.Command("git", args...) |
| 259 | cmd.Env = append(os.Environ(), "GIT_SSH_COMMAND="+gitSSH, "GIT_TERMINAL_PROMPT=0") | 263 | cmd.Env = append(os.Environ(), "GIT_SSH_COMMAND="+gitSSH, "GIT_TERMINAL_PROMPT=0") |
| 260 | cmd.Stdout, cmd.Stderr = sink, sink | 264 | cmd.Stdout, cmd.Stderr = sink, sink |
e2e/mrbuilds_test.go added +129
| @@ -0,0 +1,129 @@ | |||
| 1 | package e2e | ||
| 2 | |||
| 3 | import ( | ||
| 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. | ||
| 18 | func 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 | } | ||
internal/ci/sched.go +1 −1
| @@ -101,7 +101,7 @@ func (s *Scheduler) RunDue(now time.Time) { | |||
| 101 | continue | 101 | continue |
| 102 | } | 102 | } |
| 103 | steps, _ := json.Marshal(job.Steps) | 103 | steps, _ := json.Marshal(job.Steps) |
| 104 | n, err := s.St.CreateBuild(repo.ID, job.Name, sha, repo.DefaultBranch, string(steps)) | 104 | n, err := s.St.CreateBuild(repo.ID, job.Name, sha, repo.DefaultBranch, string(steps), true) |
| 105 | if err != nil { | 105 | if err != nil { |
| 106 | slog.Error("scheduler: queueing build", "repo", repo.Path(), "job", job.Name, "err", err) | 106 | slog.Error("scheduler: queueing build", "repo", repo.Path(), "job", job.Name, "err", err) |
| 107 | continue | 107 | continue |
internal/control/build.go +32 −7
| @@ -227,7 +227,7 @@ func runBuildTrigger(c *Ctx, args []string) int { | |||
| 227 | continue | 227 | continue |
| 228 | } | 228 | } |
| 229 | steps, _ := json.Marshal(j.Steps) | 229 | steps, _ := json.Marshal(j.Steps) |
| 230 | n, err := c.Store.CreateBuild(repo.ID, j.Name, sha, repo.DefaultBranch, string(steps)) | 230 | n, err := c.Store.CreateBuild(repo.ID, j.Name, sha, repo.DefaultBranch, string(steps), true) |
| 231 | if err != nil { | 231 | if err != nil { |
| 232 | return c.fail(protocol.ExitFailure, "%v", err) | 232 | return c.fail(protocol.ExitFailure, "%v", err) |
| 233 | } | 233 | } |
| @@ -347,9 +347,12 @@ func runRunnerNext(c *Ctx, args []string) int { | |||
| 347 | json.Unmarshal([]byte(b.Steps), &steps) | 347 | json.Unmarshal([]byte(b.Steps), &steps) |
| 348 | // Secrets ride the claim: this channel is admin-only and the values | 348 | // Secrets ride the claim: this channel is admin-only and the values |
| 349 | // land in the build's environment, nowhere else. | 349 | // land in the build's environment, nowhere else. |
| 350 | secrets, err := c.Store.BuildSecrets(b.RepoID) | 350 | var secrets map[string]string |
| 351 | if err != nil { | 351 | if b.Trusted { |
| 352 | return c.fail(protocol.ExitFailure, "%v", err) | 352 | secrets, err = c.Store.BuildSecrets(b.RepoID) |
| 353 | if err != nil { | ||
| 354 | return c.fail(protocol.ExitFailure, "%v", err) | ||
| 355 | } | ||
| 353 | } | 356 | } |
| 354 | d := struct { | 357 | d := struct { |
| 355 | ID int64 `json:"id"` | 358 | ID int64 `json:"id"` |
| @@ -499,6 +502,28 @@ func runRunnerDone(c *Ctx, args []string) int { | |||
| 499 | func QueueBranchBuilds( | 502 | func QueueBranchBuilds( |
| 500 | st *store.Store, root, siteURL string, | 503 | st *store.Store, root, siteURL string, |
| 501 | repo store.Repo, userID int64, branch, sha string, now time.Time, | 504 | repo store.Repo, userID int64, branch, sha string, now time.Time, |
| 505 | ) { | ||
| 506 | queueJobs(st, root, siteURL, repo, userID, branch, sha, now, true, branch == repo.DefaultBranch) | ||
| 507 | } | ||
| 508 | |||
| 509 | // QueueMRBuilds queues the push jobs for a merge request head fetched | ||
| 510 | // from another repository, which the target holds at | ||
| 511 | // refs/merge-requests/<n>/head, so a fork's merge request has ci/<job> | ||
| 512 | // statuses for require-checks to gate on (#98). The head is untrusted: | ||
| 513 | // its build runs without the target's secrets. A same-repository head is | ||
| 514 | // the branch push's job and is not queued here; a failed one is rebuilt | ||
| 515 | // when it lands, not when it is proposed. | ||
| 516 | func QueueMRBuilds( | ||
| 517 | st *store.Store, root, siteURL string, | ||
| 518 | repo store.Repo, userID, n int64, sha string, | ||
| 519 | ) { | ||
| 520 | queueJobs(st, root, siteURL, repo, userID, mrHeadRef(n), sha, time.Now(), false, false) | ||
| 521 | } | ||
| 522 | |||
| 523 | func queueJobs( | ||
| 524 | st *store.Store, root, siteURL string, | ||
| 525 | repo store.Repo, userID int64, ref, sha string, now time.Time, | ||
| 526 | trusted, syncSchedules bool, | ||
| 502 | ) { | 527 | ) { |
| 503 | dir := RepoDir(root, repo.OwnerName, repo.Name) | 528 | dir := RepoDir(root, repo.OwnerName, repo.Name) |
| 504 | raw, err := gitutil.ReadBlob(dir, sha, ci.ConfigPath, 1<<16) | 529 | raw, err := gitutil.ReadBlob(dir, sha, ci.ConfigPath, 1<<16) |
| @@ -531,7 +556,7 @@ func QueueBranchBuilds( | |||
| 531 | // Scheduled jobs run on their cron, not on push; a default-branch | 556 | // Scheduled jobs run on their cron, not on push; a default-branch |
| 532 | // push (re)registers them. | 557 | // push (re)registers them. |
| 533 | if j.Schedule != "" { | 558 | if j.Schedule != "" { |
| 534 | if branch == repo.DefaultBranch { | 559 | if syncSchedules { |
| 535 | schedules = append(schedules, store.Schedule{ | 560 | schedules = append(schedules, store.Schedule{ |
| 536 | RepoID: repo.ID, Job: j.Name, Cron: j.Schedule, | 561 | RepoID: repo.ID, Job: j.Name, Cron: j.Schedule, |
| 537 | NextRun: ci.NextRun(j.Schedule, now), | 562 | NextRun: ci.NextRun(j.Schedule, now), |
| @@ -540,7 +565,7 @@ func QueueBranchBuilds( | |||
| 540 | continue | 565 | continue |
| 541 | } | 566 | } |
| 542 | steps, _ := json.Marshal(j.Steps) | 567 | steps, _ := json.Marshal(j.Steps) |
| 543 | n, err := st.CreateBuild(repo.ID, j.Name, sha, branch, string(steps)) | 568 | n, err := st.CreateBuild(repo.ID, j.Name, sha, ref, string(steps), trusted) |
| 544 | if err != nil { | 569 | if err != nil { |
| 545 | slog.Error("queueing build", "repo", repo.Path(), "job", j.Name, "err", err) | 570 | slog.Error("queueing build", "repo", repo.Path(), "job", j.Name, "err", err) |
| 546 | continue | 571 | continue |
| @@ -548,7 +573,7 @@ func QueueBranchBuilds( | |||
| 548 | url := fmt.Sprintf("%s/%s/builds/%d", siteURL, repo.Path(), n) | 573 | url := fmt.Sprintf("%s/%s/builds/%d", siteURL, repo.Path(), n) |
| 549 | st.SetCommitStatus(repo.ID, sha, "ci/"+j.Name, "pending", "queued", url, userID) | 574 | st.SetCommitStatus(repo.ID, sha, "ci/"+j.Name, "pending", "queued", url, userID) |
| 550 | } | 575 | } |
| 551 | if branch == repo.DefaultBranch { | 576 | if syncSchedules { |
| 552 | if err := st.SyncSchedules(repo.ID, schedules); err != nil { | 577 | if err := st.SyncSchedules(repo.ID, schedules); err != nil { |
| 553 | slog.Error("syncing schedules", "repo", repo.Path(), "err", err) | 578 | slog.Error("syncing schedules", "repo", repo.Path(), "err", err) |
| 554 | } | 579 | } |
internal/control/mr.go +3
| @@ -314,6 +314,9 @@ func runMRCreate(c *Ctx, args []string) int { | |||
| 314 | if err := gitutil.FetchInto(dstDir, srcDir, headSHA, mrHeadRef(n)); err != nil { | 314 | if err := gitutil.FetchInto(dstDir, srcDir, headSHA, mrHeadRef(n)); err != nil { |
| 315 | return c.fail(protocol.ExitFailure, "recording MR head: %v", err) | 315 | return c.fail(protocol.ExitFailure, "recording MR head: %v", err) |
| 316 | } | 316 | } |
| 317 | if srcRepo.ID != repo.ID { | ||
| 318 | QueueMRBuilds(c.Store, c.Cfg.Server.Root, c.Cfg.Server.SiteURL, repo, c.User.ID, n, headSHA) | ||
| 319 | } | ||
| 317 | c.Store.RecordEvent(repo.ID, c.User.ID, "mr.created", fmt.Sprintf(`{"number":%d}`, n)) | 320 | c.Store.RecordEvent(repo.ID, c.User.ID, "mr.created", fmt.Sprintf(`{"number":%d}`, n)) |
| 318 | if targets, err := c.Store.RepoNotifyTargets(repo); err == nil { | 321 | if targets, err := c.Store.RepoNotifyTargets(repo); err == nil { |
| 319 | notifyUsers(c, targets, mrSubject(repo, n, title), | 322 | notifyUsers(c, targets, mrSubject(repo, n, title), |
internal/hookd/hookd.go +5 −1
| @@ -234,6 +234,10 @@ func (s *Server) postReceive(req Request) { | |||
| 234 | if err := s.st.UpdateMRHead(mr.ID, u.New); err != nil { | 234 | if err := s.st.UpdateMRHead(mr.ID, u.New); err != nil { |
| 235 | slog.Error("post-receive: recording MR head", "mr", mr.Number, "err", err) | 235 | slog.Error("post-receive: recording MR head", "mr", mr.Number, "err", err) |
| 236 | } | 236 | } |
| 237 | if srcRepo.ID != target.ID { | ||
| 238 | control.QueueMRBuilds(s.st, s.cfg.Server.Root, s.cfg.Server.SiteURL, | ||
| 239 | target, req.UserID, mr.Number, u.New) | ||
| 240 | } | ||
| 237 | if mr.State == "source_gone" { | 241 | if mr.State == "source_gone" { |
| 238 | s.st.SetMRState(mr.ID, "open") // branch came back | 242 | s.st.SetMRState(mr.ID, "open") // branch came back |
| 239 | } | 243 | } |
| @@ -274,7 +278,7 @@ func (s *Server) queueTagBuilds(repo store.Repo, userID int64, tag, pushed strin | |||
| 274 | continue | 278 | continue |
| 275 | } | 279 | } |
| 276 | steps, _ := json.Marshal(j.Steps) | 280 | steps, _ := json.Marshal(j.Steps) |
| 277 | n, err := s.st.CreateBuild(repo.ID, j.Name, sha, tag, string(steps)) | 281 | n, err := s.st.CreateBuild(repo.ID, j.Name, sha, tag, string(steps), true) |
| 278 | if err != nil { | 282 | if err != nil { |
| 279 | slog.Error("queueing tag build", "repo", repo.Path(), "job", j.Name, "err", err) | 283 | slog.Error("queueing tag build", "repo", repo.Path(), "job", j.Name, "err", err) |
| 280 | continue | 284 | continue |
internal/store/builds.go +10 −5
| @@ -22,6 +22,9 @@ type Build struct { | |||
| 22 | CreatedAt string | 22 | CreatedAt string |
| 23 | StartedAt string | 23 | StartedAt string |
| 24 | FinishedAt string | 24 | FinishedAt string |
| 25 | // Trusted is false for a merge request head fetched from another | ||
| 26 | // repository: its steps run without the target's secrets. | ||
| 27 | Trusted bool | ||
| 25 | } | 28 | } |
| 26 | 29 | ||
| 27 | // MaxBuildLog caps a build's stored log; appends past it are dropped. | 30 | // MaxBuildLog caps a build's stored log; appends past it are dropped. |
| @@ -35,7 +38,7 @@ var truncNotice = []byte("\n[log truncated: reached the " + | |||
| 35 | 38 | ||
| 36 | // CreateBuild allocates the per-repo build number in the same transaction | 39 | // CreateBuild allocates the per-repo build number in the same transaction |
| 37 | // as the insert, like issue and MR numbers. | 40 | // as the insert, like issue and MR numbers. |
| 38 | func (s *Store) CreateBuild(repoID int64, job, sha, ref, stepsJSON string) (int64, error) { | 41 | func (s *Store) CreateBuild(repoID int64, job, sha, ref, stepsJSON string, trusted bool) (int64, error) { |
| 39 | tx, err := s.DB.Begin() | 42 | tx, err := s.DB.Begin() |
| 40 | if err != nil { | 43 | if err != nil { |
| 41 | return 0, err | 44 | return 0, err |
| @@ -49,21 +52,23 @@ func (s *Store) CreateBuild(repoID int64, job, sha, ref, stepsJSON string) (int6 | |||
| 49 | return 0, err | 52 | return 0, err |
| 50 | } | 53 | } |
| 51 | if _, err := tx.Exec( | 54 | if _, err := tx.Exec( |
| 52 | "INSERT INTO builds (repo_id, number, job, sha, ref, steps) VALUES (?, ?, ?, ?, ?, ?)", | 55 | "INSERT INTO builds (repo_id, number, job, sha, ref, steps, trusted) VALUES (?, ?, ?, ?, ?, ?, ?)", |
| 53 | repoID, n, job, sha, ref, stepsJSON); err != nil { | 56 | repoID, n, job, sha, ref, stepsJSON, trusted); err != nil { |
| 54 | return 0, err | 57 | return 0, err |
| 55 | } | 58 | } |
| 56 | return n, tx.Commit() | 59 | return n, tx.Commit() |
| 57 | } | 60 | } |
| 58 | 61 | ||
| 59 | const buildSelect = ` | 62 | const buildSelect = ` |
| 60 | SELECT id, repo_id, number, job, sha, ref, steps, status, created_at, started_at, finished_at | 63 | SELECT id, repo_id, number, job, sha, ref, steps, status, created_at, started_at, finished_at, trusted |
| 61 | FROM builds` | 64 | FROM builds` |
| 62 | 65 | ||
| 63 | func scanBuild(row interface{ Scan(...any) error }) (Build, error) { | 66 | func scanBuild(row interface{ Scan(...any) error }) (Build, error) { |
| 64 | var b Build | 67 | var b Build |
| 68 | var trusted int | ||
| 65 | err := row.Scan(&b.ID, &b.RepoID, &b.Number, &b.Job, &b.SHA, &b.Ref, &b.Steps, | 69 | err := row.Scan(&b.ID, &b.RepoID, &b.Number, &b.Job, &b.SHA, &b.Ref, &b.Steps, |
| 66 | &b.Status, &b.CreatedAt, &b.StartedAt, &b.FinishedAt) | 70 | &b.Status, &b.CreatedAt, &b.StartedAt, &b.FinishedAt, &trusted) |
| 71 | b.Trusted = trusted != 0 | ||
| 67 | return b, err | 72 | return b, err |
| 68 | } | 73 | } |
| 69 | 74 | ||
internal/store/builds_test.go +7 −7
| @@ -21,11 +21,11 @@ func TestReapStaleBuilds(t *testing.T) { | |||
| 21 | t.Fatal(err) | 21 | t.Fatal(err) |
| 22 | } | 22 | } |
| 23 | 23 | ||
| 24 | stuck, err := s.CreateBuild(1, "test", "abc123", "main", `["true"]`) | 24 | stuck, err := s.CreateBuild(1, "test", "abc123", "main", `["true"]`, true) |
| 25 | if err != nil { | 25 | if err != nil { |
| 26 | t.Fatal(err) | 26 | t.Fatal(err) |
| 27 | } | 27 | } |
| 28 | fresh, err := s.CreateBuild(1, "pages", "abc123", "main", `["true"]`) | 28 | fresh, err := s.CreateBuild(1, "pages", "abc123", "main", `["true"]`, true) |
| 29 | if err != nil { | 29 | if err != nil { |
| 30 | t.Fatal(err) | 30 | t.Fatal(err) |
| 31 | } | 31 | } |
| @@ -87,11 +87,11 @@ func TestBuildsForCommitTiming(t *testing.T) { | |||
| 87 | } | 87 | } |
| 88 | // Two runs of the same job on one commit: the retry is what counts. | 88 | // Two runs of the same job on one commit: the retry is what counts. |
| 89 | for range 2 { | 89 | for range 2 { |
| 90 | if _, err := s.CreateBuild(repoID, "test", "abc123", "main", `["true"]`); err != nil { | 90 | if _, err := s.CreateBuild(repoID, "test", "abc123", "main", `["true"]`, true); err != nil { |
| 91 | t.Fatal(err) | 91 | t.Fatal(err) |
| 92 | } | 92 | } |
| 93 | } | 93 | } |
| 94 | if _, err := s.CreateBuild(repoID, "lint", "def456", "main", `["true"]`); err != nil { | 94 | if _, err := s.CreateBuild(repoID, "lint", "def456", "main", `["true"]`, true); err != nil { |
| 95 | t.Fatal(err) | 95 | t.Fatal(err) |
| 96 | } | 96 | } |
| 97 | if _, err := s.DB.Exec(`UPDATE builds SET started_at = '2026-08-28T04:42:54Z', | 97 | if _, err := s.DB.Exec(`UPDATE builds SET started_at = '2026-08-28T04:42:54Z', |
| @@ -140,10 +140,10 @@ func TestClaimBuildScopedToRepos(t *testing.T) { | |||
| 140 | t.Fatal(err) | 140 | t.Fatal(err) |
| 141 | } | 141 | } |
| 142 | // Queued first, so an unscoped claim would take it. | 142 | // Queued first, so an unscoped claim would take it. |
| 143 | if _, err := s.CreateBuild(theirs, "evil", "abc123", "main", `["true"]`); err != nil { | 143 | if _, err := s.CreateBuild(theirs, "evil", "abc123", "main", `["true"]`, true); err != nil { |
| 144 | t.Fatal(err) | 144 | t.Fatal(err) |
| 145 | } | 145 | } |
| 146 | wanted, err := s.CreateBuild(mine, "deploy", "def456", "main", `["true"]`) | 146 | wanted, err := s.CreateBuild(mine, "deploy", "def456", "main", `["true"]`, true) |
| 147 | if err != nil { | 147 | if err != nil { |
| 148 | t.Fatal(err) | 148 | t.Fatal(err) |
| 149 | } | 149 | } |
| @@ -182,7 +182,7 @@ func TestBuildLogSaysWhenItTruncates(t *testing.T) { | |||
| 182 | if _, err := s.CreateRepo("user", uid, "orgo", "public"); err != nil { | 182 | if _, err := s.CreateRepo("user", uid, "orgo", "public"); err != nil { |
| 183 | t.Fatal(err) | 183 | t.Fatal(err) |
| 184 | } | 184 | } |
| 185 | id, err := s.CreateBuild(1, "test", "abc123", "main", `["true"]`) | 185 | id, err := s.CreateBuild(1, "test", "abc123", "main", `["true"]`, true) |
| 186 | if err != nil { | 186 | if err != nil { |
| 187 | t.Fatal(err) | 187 | t.Fatal(err) |
| 188 | } | 188 | } |
internal/store/migrations/0032_build_trusted.down.sql added +1
| @@ -0,0 +1 @@ | |||
| 1 | ALTER TABLE builds DROP COLUMN trusted; | ||
internal/store/migrations/0032_build_trusted.up.sql added +3
| @@ -0,0 +1,3 @@ | |||
| 1 | -- A build from a merge request head fetched out of another repository | ||
| 2 | -- runs code the target's owners did not write; it gets no secrets. | ||
| 3 | ALTER TABLE builds ADD COLUMN trusted INTEGER NOT NULL DEFAULT 1; | ||
internal/store/statuses_test.go +1 −1
| @@ -17,7 +17,7 @@ func TestChecksForCommit(t *testing.T) { | |||
| 17 | if err != nil { | 17 | if err != nil { |
| 18 | t.Fatal(err) | 18 | t.Fatal(err) |
| 19 | } | 19 | } |
| 20 | if _, err := s.CreateBuild(repoID, "test", "abc123", "main", `["true"]`); err != nil { | 20 | if _, err := s.CreateBuild(repoID, "test", "abc123", "main", `["true"]`, true); err != nil { |
| 21 | t.Fatal(err) | 21 | t.Fatal(err) |
| 22 | } | 22 | } |
| 23 | if _, err := s.DB.Exec(`UPDATE builds SET started_at = '2026-08-28T04:42:54Z', | 23 | if _, err := s.DB.Exec(`UPDATE builds SET started_at = '2026-08-28T04:42:54Z', |