Commit b1f4bf1aaf
Verified · cmc
Layout: unified · split
internal/control/build.go +7 −2
| @@ -857,10 +857,15 @@ func queueJobs( | ||
| 857 | 857 | if j.Tags != "" { |
| 858 | 858 | continue |
| 859 | 859 | } |
| 860 | if b, ok := built[j.Name]; ok && (b.Status == "success" || b.Status == "pending" || b.Status == "running") { | |
| 860 | // A build of this commit that passed, or is queued or running, | |
| 861 | // stands for it — unless this queue is trusted and that build was | |
| 862 | // not: a fork's head that lands on a branch is built again as the | |
| 863 | // repository's own (#258). | |
| 864 | if b, ok := built[j.Name]; ok && (b.Trusted || !trusted) && | |
| 865 | (b.Status == "success" || b.Status == "pending" || b.Status == "running") { | |
| 861 | 866 | continue |
| 862 | 867 | } |
| 863 | if prev, ok, _ := st.SuccessBuildForTree(repo.ID, tree, j.Name); ok && prev.SHA != sha { | |
| 868 | if prev, ok, _ := st.SuccessBuildForTree(repo.ID, tree, j.Name, j.Image); ok && prev.SHA != sha { | |
| 864 | 869 | url := fmt.Sprintf("%s/%s/builds/%d", siteURL, repo.Path(), prev.Number) |
| 865 | 870 | st.SetCommitStatus(repo.ID, sha, "ci/"+j.Name, "success", |
| 866 | 871 | fmt.Sprintf("passed in build %d as %.10s, same tree", prev.Number, prev.SHA), url, userID) |
internal/control/build_test.go +47
| @@ -669,3 +669,50 @@ func TestBuildListFlagsFilter(t *testing.T) { | ||
| 669 | 669 | t.Fatalf("bad --status error does not name the valid states: %s", errOut.String()) |
| 670 | 670 | } |
| 671 | 671 | } |
| 672 | ||
| 673 | // A fork's green build of a commit does not stand for the repository's | |
| 674 | // own: the same commit landing on a branch, or a commit with the same | |
| 675 | // tree, is built again as trusted (#258). | |
| 676 | func TestQueueBranchBuildsRebuildsWhatOnlyAForkBuilt(t *testing.T) { | |
| 677 | st, repo, uid := newQueueTestRepo(t) | |
| 678 | git := gitRunner(t) | |
| 679 | root := t.TempDir() | |
| 680 | ||
| 681 | src := filepath.Join(root, "src") | |
| 682 | os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755) | |
| 683 | os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte( | |
| 684 | "jobs:\n unit:\n steps:\n - echo hi\n"), 0o644) | |
| 685 | git(root, "init", "-q", "-b", "main", "src") | |
| 686 | git(src, "add", ".") | |
| 687 | git(src, "commit", "-q", "-m", "base") | |
| 688 | first := strings.TrimSpace(git(src, "rev-parse", "HEAD")) | |
| 689 | git(src, "commit", "-q", "--allow-empty", "-m", "same tree") | |
| 690 | second := strings.TrimSpace(git(src, "rev-parse", "HEAD")) | |
| 691 | ||
| 692 | dir := RepoDir(root, repo.OwnerName, repo.Name) | |
| 693 | os.MkdirAll(filepath.Dir(dir), 0o755) | |
| 694 | git(root, "clone", "-q", "--bare", src, dir) | |
| 695 | ||
| 696 | // A fork's merge request head, built untrusted and green. | |
| 697 | QueueMRBuilds(st, root, "https://x.test", repo, uid, 1, first) | |
| 698 | b, ok, err := st.ClaimBuild([]int64{repo.ID}, true) | |
| 699 | if err != nil || !ok || b.Trusted { | |
| 700 | t.Fatalf("claim: ok=%v trusted=%v err=%v", ok, b.Trusted, err) | |
| 701 | } | |
| 702 | if err := st.FinishBuild(b.ID, "success"); err != nil { | |
| 703 | t.Fatal(err) | |
| 704 | } | |
| 705 | ||
| 706 | // The same commit lands on main, then a commit with the same tree. | |
| 707 | QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", "", first, time.Now()) | |
| 708 | QueueBranchBuilds(st, root, "https://x.test", repo, uid, "main", first, second, time.Now()) | |
| 709 | pending, _ := st.ListBuilds(repo.ID, store.BuildFilter{Status: "pending"}, 10) | |
| 710 | if len(pending) != 2 { | |
| 711 | t.Fatalf("queued %d builds, want 2 (one per commit): %+v", len(pending), pending) | |
| 712 | } | |
| 713 | for _, p := range pending { | |
| 714 | if !p.Trusted { | |
| 715 | t.Errorf("build %d queued untrusted on a branch push", p.Number) | |
| 716 | } | |
| 717 | } | |
| 718 | } | |
internal/store/builds.go +13 −8
| @@ -450,26 +450,31 @@ func (s *Store) CancelBuild(id int64) error { | ||
| 450 | 450 | return nil |
| 451 | 451 | } |
| 452 | 452 | |
| 453 | // SuccessBuildFor finds a passed build of the commit for the job, on any | |
| 454 | // ref: what a cancelled duplicate can point back at. | |
| 455 | // SuccessBuildForTree is SuccessBuildFor keyed by tree rather than | |
| 456 | // commit: a rebase that changes nothing in the tree has already been | |
| 457 | // built (#177). An empty tree never matches. | |
| 458 | func (s *Store) SuccessBuildForTree(repoID int64, tree, job string) (Build, bool, error) { | |
| 453 | // SuccessBuildForTree finds a passed build of the job for a tree rather | |
| 454 | // than a commit: a rebase that changes nothing in the tree has already | |
| 455 | // been built (#177). Only a trusted build on the image the job names | |
| 456 | // counts: a fork's result, or one from an image the job has left, does | |
| 457 | // not stand for the repository's own (#258). A job naming no image | |
| 458 | // matches builds that named none, whichever default the runner used; | |
| 459 | // the CI wiki page says so. An empty tree never matches. | |
| 460 | func (s *Store) SuccessBuildForTree(repoID int64, tree, job, image string) (Build, bool, error) { | |
| 459 | 461 | if tree == "" { |
| 460 | 462 | return Build{}, false, nil |
| 461 | 463 | } |
| 462 | 464 | b, err := scanBuild(s.DB.QueryRow(buildSelect+ |
| 463 | " WHERE repo_id = ? AND tree = ? AND job = ? AND status = 'success' ORDER BY number DESC LIMIT 1", repoID, tree, job)) | |
| 465 | " WHERE repo_id = ? AND tree = ? AND job = ? AND image = ? AND trusted = 1 AND status = 'success'"+ | |
| 466 | " ORDER BY number DESC LIMIT 1", repoID, tree, job, image)) | |
| 464 | 467 | if errors.Is(err, sql.ErrNoRows) { |
| 465 | 468 | return Build{}, false, nil |
| 466 | 469 | } |
| 467 | 470 | return b, err == nil, err |
| 468 | 471 | } |
| 469 | 472 | |
| 473 | // SuccessBuildFor finds a passed trusted build of the commit for the job, | |
| 474 | // on any ref: what a cancelled duplicate can point back at. | |
| 470 | 475 | func (s *Store) SuccessBuildFor(repoID int64, sha, job string) (Build, bool, error) { |
| 471 | 476 | b, err := scanBuild(s.DB.QueryRow(buildSelect+ |
| 472 | " WHERE repo_id = ? AND sha = ? AND job = ? AND status = 'success' ORDER BY number DESC LIMIT 1", repoID, sha, job)) | |
| 477 | " WHERE repo_id = ? AND sha = ? AND job = ? AND trusted = 1 AND status = 'success' ORDER BY number DESC LIMIT 1", repoID, sha, job)) | |
| 473 | 478 | if errors.Is(err, sql.ErrNoRows) { |
| 474 | 479 | return Build{}, false, nil |
| 475 | 480 | } |
internal/store/builds_test.go +42 −3
| @@ -238,17 +238,56 @@ func TestSuccessBuildForTree(t *testing.T) { | ||
| 238 | 238 | if err := s.FinishBuild(b["unit"].ID, "success"); err != nil { |
| 239 | 239 | t.Fatal(err) |
| 240 | 240 | } |
| 241 | if prev, ok, _ := s.SuccessBuildForTree(repoID, "tree1", "unit"); !ok || prev.SHA != "aaa" { | |
| 241 | if prev, ok, _ := s.SuccessBuildForTree(repoID, "tree1", "unit", ""); !ok || prev.SHA != "aaa" { | |
| 242 | 242 | t.Fatalf("success not found by tree: ok=%v prev=%+v", ok, prev) |
| 243 | 243 | } |
| 244 | if _, ok, _ := s.SuccessBuildForTree(repoID, "tree1", "other"); ok { | |
| 244 | if _, ok, _ := s.SuccessBuildForTree(repoID, "tree1", "other", ""); ok { | |
| 245 | 245 | t.Error("matched a different job") |
| 246 | 246 | } |
| 247 | if _, ok, _ := s.SuccessBuildForTree(repoID, "", "unit"); ok { | |
| 247 | if _, ok, _ := s.SuccessBuildForTree(repoID, "", "unit", ""); ok { | |
| 248 | 248 | t.Error("an empty tree matched") |
| 249 | 249 | } |
| 250 | 250 | } |
| 251 | 251 | |
| 252 | // A result stands for another commit only when it came from a trusted | |
| 253 | // build on the same image: a fork's green build, or one on an image the | |
| 254 | // job has since left, proves nothing about the repository's own (#258). | |
| 255 | func TestSuccessReuseNeedsTrustAndImage(t *testing.T) { | |
| 256 | s := open(t) | |
| 257 | if err := s.MigrateUp(); err != nil { | |
| 258 | t.Fatal(err) | |
| 259 | } | |
| 260 | uid, _ := s.CreateUser("cmc", true) | |
| 261 | repoID, _ := s.CreateRepo("user", uid, "app", "public") | |
| 262 | for _, b := range []struct { | |
| 263 | sha, image string | |
| 264 | trusted bool | |
| 265 | }{ | |
| 266 | {"aaa", "", false}, | |
| 267 | {"bbb", "localhost/old:1", true}, | |
| 268 | } { | |
| 269 | if _, err := s.CreateBuild(repoID, "unit", b.sha, "main", `["true"]`, b.image, "tree1", b.trusted); err != nil { | |
| 270 | t.Fatal(err) | |
| 271 | } | |
| 272 | claimed, ok, err := s.ClaimBuild([]int64{repoID}, true) | |
| 273 | if err != nil || !ok { | |
| 274 | t.Fatalf("claim: ok=%v err=%v", ok, err) | |
| 275 | } | |
| 276 | if err := s.FinishBuild(claimed.ID, "success"); err != nil { | |
| 277 | t.Fatal(err) | |
| 278 | } | |
| 279 | } | |
| 280 | if prev, ok, _ := s.SuccessBuildForTree(repoID, "tree1", "unit", ""); ok { | |
| 281 | t.Fatalf("reused build %d: untrusted, or on another image", prev.Number) | |
| 282 | } | |
| 283 | if prev, ok, _ := s.SuccessBuildForTree(repoID, "tree1", "unit", "localhost/old:1"); !ok || prev.SHA != "bbb" { | |
| 284 | t.Fatalf("trusted build on the same image not found: ok=%v prev=%+v", ok, prev) | |
| 285 | } | |
| 286 | if _, ok, _ := s.SuccessBuildFor(repoID, "aaa", "unit"); ok { | |
| 287 | t.Error("an untrusted success stood for its commit") | |
| 288 | } | |
| 289 | } | |
| 290 | ||
| 252 | 291 | // A running build whose log stream ended is reaped after StaleLogGrace, |
| 253 | 292 | // well before the deadline; one whose stream is still open is not (#179). |
| 254 | 293 | func TestReapStaleBuildsAfterLogClosed(t *testing.T) { |