Commit 6421e2fd3b
Verified · cmc
Layout: unified · split
internal/control/build.go +46 −13
| @@ -101,9 +101,13 @@ func init() { | |||
| 101 | Examples: []string{"runner log 431"}, | 101 | Examples: []string{"runner log 431"}, |
| 102 | ReadsStdin: true, Run: runRunnerLog}) | 102 | ReadsStdin: true, Run: runRunnerLog}) |
| 103 | register(Command{Path: []string{"runner", "done"}, | 103 | register(Command{Path: []string{"runner", "done"}, |
| 104 | Summary: "finish a build", | 104 | Summary: "finish a build", |
| 105 | Usage: "runner done <build-id> success|failure", | 105 | Usage: "runner done <build-id> success|failure [--step <n>] [--reason <text>]", |
| 106 | Examples: []string{"runner done 431 success"}, | 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 | Run: runRunnerDone}) | 111 | Run: runRunnerDone}) |
| 108 | } | 112 | } |
| 109 | 113 | ||
| @@ -675,12 +679,18 @@ func runRunnerDone(c *Ctx, args []string) int { | |||
| 675 | if code >= 0 { | 679 | if code >= 0 { |
| 676 | return code | 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 | return c.usage() | 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 | if err != nil { | 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 | b, err := c.Store.BuildByID(id) | 695 | b, err := c.Store.BuildByID(id) |
| 686 | if err != nil { | 696 | if err != nil { |
| @@ -699,7 +709,20 @@ func runRunnerDone(c *Ctx, args []string) int { | |||
| 699 | fmt.Fprintf(w, "build %d was cancelled\n", b.Number) | 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 | return c.fail(protocol.ExitFailure, "finishing build %d: %v", id, err) | 726 | return c.fail(protocol.ExitFailure, "finishing build %d: %v", id, err) |
| 704 | } | 727 | } |
| 705 | c.Store.RunnerDone(key.ID) | 728 | c.Store.RunnerDone(key.ID) |
| @@ -708,15 +731,15 @@ func runRunnerDone(c *Ctx, args []string) int { | |||
| 708 | return c.fail(protocol.ExitFailure, "%v", err) | 731 | return c.fail(protocol.ExitFailure, "%v", err) |
| 709 | } | 732 | } |
| 710 | url := fmt.Sprintf("%s/%s/builds/%d", c.Cfg.Server.SiteURL, repo.Path(), b.Number) | 733 | url := fmt.Sprintf("%s/%s/builds/%d", c.Cfg.Server.SiteURL, repo.Path(), b.Number) |
| 711 | desc := "build " + args[1] | 734 | desc := "build " + outcome |
| 712 | if err := c.Store.SetCommitStatus(repo.ID, b.SHA, "ci/"+b.Job, args[1], desc, url, c.User.ID); err != nil { | 735 | if err := c.Store.SetCommitStatus(repo.ID, b.SHA, "ci/"+b.Job, outcome, desc, url, c.User.ID); err != nil { |
| 713 | return c.fail(protocol.ExitFailure, "%v", err) | 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 | fmt.Sprintf(`{"number":%d,"job":%q,"sha":%q}`, b.Number, b.Job, b.SHA)) | 739 | fmt.Sprintf(`{"number":%d,"job":%q,"sha":%q}`, b.Number, b.Job, b.SHA)) |
| 717 | // A red build mails the repo's notify targets with the log tail — a | 740 | // A red build mails the repo's notify targets with the log tail — a |
| 718 | // failed scheduled job must not wait to be noticed. | 741 | // failed scheduled job must not wait to be noticed. |
| 719 | if args[1] == "failure" { | 742 | if outcome == "failure" { |
| 720 | if targets, err := c.Store.RepoNotifyTargets(repo); err == nil { | 743 | if targets, err := c.Store.RepoNotifyTargets(repo); err == nil { |
| 721 | tail := "" | 744 | tail := "" |
| 722 | if log, err := c.Store.BuildLog(id); err == nil && len(log) > 0 { | 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 | path: fmt.Sprintf("%s/builds/%d", repo.Path(), b.Number)}) | 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) { | 758 | return c.emit(map[string]any{"build": b.Number, "status": outcome}, func(w io.Writer) { |
| 736 | fmt.Fprintf(w, "build %d %s\n", b.Number, args[1]) | 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 | // QueueBranchBuilds reads .gitbay/ci.yml at sha and creates one pending | 773 | // QueueBranchBuilds reads .gitbay/ci.yml at sha and creates one pending |
| 741 | // build per push job, with a pending commit status the runner resolves. | 774 | // build per push job, with a pending commit status the runner resolves. |
| 742 | // A broken config surfaces as a failed "ci/config" status, not silence. | 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 | } | ||