Commit 6421e2fd3b
Verified · cmc
Layout: unified · split
internal/control/build.go +46 −13
| @@ -101,9 +101,13 @@ func init() { | ||
| 101 | 101 | Examples: []string{"runner log 431"}, |
| 102 | 102 | ReadsStdin: true, Run: runRunnerLog}) |
| 103 | 103 | register(Command{Path: []string{"runner", "done"}, |
| 104 | Summary: "finish a build", | |
| 105 | Usage: "runner done <build-id> success|failure", | |
| 106 | Examples: []string{"runner done 431 success"}, | |
| 104 | Summary: "finish a build", | |
| 105 | Usage: "runner done <build-id> success|failure [--step <n>] [--reason <text>]", | |
| 106 | Flags: []Flag{ | |
| 107 | {"--step", "<n>", "the 1-based step a failed build stopped at", ""}, | |
| 108 | {"--reason", "<text>", "how it failed, one line", ""}, | |
| 109 | }, | |
| 110 | Examples: []string{"runner done 431 success", "runner done 431 failure --step 3 --reason 'exit 1'"}, | |
| 107 | 111 | Run: runRunnerDone}) |
| 108 | 112 | } |
| 109 | 113 | |
| @@ -675,12 +679,18 @@ func runRunnerDone(c *Ctx, args []string) int { | ||
| 675 | 679 | if code >= 0 { |
| 676 | 680 | return code |
| 677 | 681 | } |
| 678 | if len(args) != 2 || (args[1] != "success" && args[1] != "failure") { | |
| 682 | f, err := parseFlags(args, flagSpec{Values: []string{"--step", "--reason"}, MaxPos: 2, | |
| 683 | Usage: "runner done <build-id> success|failure [--step <n>] [--reason <text>]"}) | |
| 684 | if err != nil { | |
| 685 | return c.fail(protocol.ExitUsage, "%v", err) | |
| 686 | } | |
| 687 | if len(f.Pos) != 2 || (f.Pos[1] != "success" && f.Pos[1] != "failure") { | |
| 679 | 688 | return c.usage() |
| 680 | 689 | } |
| 681 | id, err := strconv.ParseInt(args[0], 10, 64) | |
| 690 | outcome := f.Pos[1] | |
| 691 | id, err := strconv.ParseInt(f.Pos[0], 10, 64) | |
| 682 | 692 | if err != nil { |
| 683 | return c.fail(protocol.ExitUsage, "bad build id %q", args[0]) | |
| 693 | return c.fail(protocol.ExitUsage, "bad build id %q", f.Pos[0]) | |
| 684 | 694 | } |
| 685 | 695 | b, err := c.Store.BuildByID(id) |
| 686 | 696 | if err != nil { |
| @@ -699,7 +709,20 @@ func runRunnerDone(c *Ctx, args []string) int { | ||
| 699 | 709 | fmt.Fprintf(w, "build %d was cancelled\n", b.Number) |
| 700 | 710 | }) |
| 701 | 711 | } |
| 702 | if err := c.Store.FinishBuild(id, args[1]); err != nil { | |
| 712 | if outcome == "failure" { | |
| 713 | // A step the job does not have is recorded as none rather than | |
| 714 | // refused: refusing would lose the outcome over a detail (#266). | |
| 715 | var steps []string | |
| 716 | json.Unmarshal([]byte(b.Steps), &steps) | |
| 717 | step, _ := strconv.Atoi(f.Value("--step")) | |
| 718 | if step < 0 || step > len(steps) { | |
| 719 | step = 0 | |
| 720 | } | |
| 721 | if err := c.Store.SetBuildFailure(id, step, failureReason(f.Value("--reason"))); err != nil && !errors.Is(err, store.ErrNotFound) { | |
| 722 | return c.fail(protocol.ExitFailure, "recording build %d's failure: %v", id, err) | |
| 723 | } | |
| 724 | } | |
| 725 | if err := c.Store.FinishBuild(id, outcome); err != nil { | |
| 703 | 726 | return c.fail(protocol.ExitFailure, "finishing build %d: %v", id, err) |
| 704 | 727 | } |
| 705 | 728 | c.Store.RunnerDone(key.ID) |
| @@ -708,15 +731,15 @@ func runRunnerDone(c *Ctx, args []string) int { | ||
| 708 | 731 | return c.fail(protocol.ExitFailure, "%v", err) |
| 709 | 732 | } |
| 710 | 733 | url := fmt.Sprintf("%s/%s/builds/%d", c.Cfg.Server.SiteURL, repo.Path(), b.Number) |
| 711 | desc := "build " + args[1] | |
| 712 | if err := c.Store.SetCommitStatus(repo.ID, b.SHA, "ci/"+b.Job, args[1], desc, url, c.User.ID); err != nil { | |
| 734 | desc := "build " + outcome | |
| 735 | if err := c.Store.SetCommitStatus(repo.ID, b.SHA, "ci/"+b.Job, outcome, desc, url, c.User.ID); err != nil { | |
| 713 | 736 | return c.fail(protocol.ExitFailure, "%v", err) |
| 714 | 737 | } |
| 715 | c.Store.RecordEvent(repo.ID, c.User.ID, "build."+args[1], | |
| 738 | c.Store.RecordEvent(repo.ID, c.User.ID, "build."+outcome, | |
| 716 | 739 | fmt.Sprintf(`{"number":%d,"job":%q,"sha":%q}`, b.Number, b.Job, b.SHA)) |
| 717 | 740 | // A red build mails the repo's notify targets with the log tail — a |
| 718 | 741 | // failed scheduled job must not wait to be noticed. |
| 719 | if args[1] == "failure" { | |
| 742 | if outcome == "failure" { | |
| 720 | 743 | if targets, err := c.Store.RepoNotifyTargets(repo); err == nil { |
| 721 | 744 | tail := "" |
| 722 | 745 | if log, err := c.Store.BuildLog(id); err == nil && len(log) > 0 { |
| @@ -732,11 +755,21 @@ func runRunnerDone(c *Ctx, args []string) int { | ||
| 732 | 755 | path: fmt.Sprintf("%s/builds/%d", repo.Path(), b.Number)}) |
| 733 | 756 | } |
| 734 | 757 | } |
| 735 | return c.emit(map[string]any{"build": b.Number, "status": args[1]}, func(w io.Writer) { | |
| 736 | fmt.Fprintf(w, "build %d %s\n", b.Number, args[1]) | |
| 758 | return c.emit(map[string]any{"build": b.Number, "status": outcome}, func(w io.Writer) { | |
| 759 | fmt.Fprintf(w, "build %d %s\n", b.Number, outcome) | |
| 737 | 760 | }) |
| 738 | 761 | } |
| 739 | 762 | |
| 763 | // failureReason keeps a runner's reason to one line of at most 200 | |
| 764 | // bytes: it is shown on the build page and by build show. | |
| 765 | func failureReason(s string) string { | |
| 766 | s = strings.Join(strings.Fields(s), " ") | |
| 767 | if len(s) > 200 { | |
| 768 | s = s[:200] | |
| 769 | } | |
| 770 | return strings.ToValidUTF8(s, "") | |
| 771 | } | |
| 772 | ||
| 740 | 773 | // QueueBranchBuilds reads .gitbay/ci.yml at sha and creates one pending |
| 741 | 774 | // build per push job, with a pending commit status the runner resolves. |
| 742 | 775 | // A broken config surfaces as a failed "ci/config" status, not silence. |
internal/control/runnernext_test.go +41
| @@ -295,3 +295,44 @@ func TestRunnerNextCarriesPublicSSH(t *testing.T) { | ||
| 295 | 295 | } |
| 296 | 296 | } |
| 297 | 297 | } |
| 298 | ||
| 299 | // runner done records the failed step and a one-line reason (#266). | |
| 300 | func TestRunnerDoneRecordsFailedStep(t *testing.T) { | |
| 301 | st, repo, uid, root, baseSHA, _ := setupOrphanRepo(t) | |
| 302 | n, err := st.CreateBuild(repo.ID, "unit", baseSHA, "main", `["go build ./...","go test ./..."]`, "", "", true) | |
| 303 | if err != nil { | |
| 304 | t.Fatal(err) | |
| 305 | } | |
| 306 | b, ok, err := st.ClaimBuild([]int64{repo.ID}, false) | |
| 307 | if err != nil || !ok { | |
| 308 | t.Fatalf("claim: ok=%v err=%v", ok, err) | |
| 309 | } | |
| 310 | c, out := runnerCtx(st, uid, root) | |
| 311 | if code := runRunnerDone(c, []string{fmt.Sprint(b.ID), "failure", "--step", "2", "--reason", "exit 1\n"}); code != protocol.ExitOK { | |
| 312 | t.Fatalf("runner done: exit %d\n%s", code, out.String()) | |
| 313 | } | |
| 314 | got, _ := st.BuildByNumber(repo.ID, n) | |
| 315 | if got.Status != "failure" || got.FailedStep != 2 || got.FailedReason != "exit 1" { | |
| 316 | t.Fatalf("status %s step %d reason %q", got.Status, got.FailedStep, got.FailedReason) | |
| 317 | } | |
| 318 | } | |
| 319 | ||
| 320 | // A report with no flags — an older runner — or with a step past the | |
| 321 | // job's still finishes the build; the step is then recorded as 0. | |
| 322 | func TestRunnerDoneToleratesMissingOrBadStep(t *testing.T) { | |
| 323 | st, repo, uid, root, baseSHA, _ := setupOrphanRepo(t) | |
| 324 | for _, extra := range [][]string{nil, {"--step", "9"}} { | |
| 325 | n, _ := st.CreateBuild(repo.ID, "unit", baseSHA, "main", `["true"]`, "", "", true) | |
| 326 | b, ok, err := st.ClaimBuild([]int64{repo.ID}, false) | |
| 327 | if err != nil || !ok { | |
| 328 | t.Fatalf("claim: ok=%v err=%v", ok, err) | |
| 329 | } | |
| 330 | c, out := runnerCtx(st, uid, root) | |
| 331 | if code := runRunnerDone(c, append([]string{fmt.Sprint(b.ID), "failure"}, extra...)); code != protocol.ExitOK { | |
| 332 | t.Fatalf("runner done %v: exit %d\n%s", extra, code, out.String()) | |
| 333 | } | |
| 334 | if got, _ := st.BuildByNumber(repo.ID, n); got.Status != "failure" || got.FailedStep != 0 { | |
| 335 | t.Fatalf("%v: status %s step %d", extra, got.Status, got.FailedStep) | |
| 336 | } | |
| 337 | } | |
| 338 | } | |