Commit b4e14d4383

b4e14d43830b28ba188f893db6f7b015bbb56729

parent: dcd93804bc

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 07:39 UTC

store: failed step and reason on a build

Ref #266

Layout: unified · split

internal/store/builds.go +24 −2
@@ -30,6 +30,11 @@ type Build struct {
3030 // Trusted is false for a merge request head fetched from another
3131 // repository: its steps run without the target's secrets.
3232 Trusted bool
33 // FailedStep is the 1-based step a failed build stopped at, 0 when it
34 // stopped before its first step or did not fail. FailedReason is the
35 // runner's one line: "exit 1", "build timed out after 45m0s".
36 FailedStep int
37 FailedReason string
3338}
3439
3540// MaxBuildLog caps a build's stored log; appends past it are dropped.
@@ -65,14 +70,16 @@ func (s *Store) CreateBuild(repoID int64, job, sha, ref, stepsJSON, image, tree
6570}
6671
6772const buildSelect = `
68 SELECT id, repo_id, number, job, sha, ref, steps, image, tree, status, created_at, started_at, finished_at, log_closed_at, trusted
73 SELECT id, repo_id, number, job, sha, ref, steps, image, tree, status, created_at, started_at, finished_at, log_closed_at, trusted,
74 failed_step, failed_reason
6975 FROM builds`
7076
7177func scanBuild(row interface{ Scan(...any) error }) (Build, error) {
7278 var b Build
7379 var trusted int
7480 err := row.Scan(&b.ID, &b.RepoID, &b.Number, &b.Job, &b.SHA, &b.Ref, &b.Steps, &b.Image, &b.Tree,
75 &b.Status, &b.CreatedAt, &b.StartedAt, &b.FinishedAt, &b.LogClosedAt, &trusted)
81 &b.Status, &b.CreatedAt, &b.StartedAt, &b.FinishedAt, &b.LogClosedAt, &trusted,
82 &b.FailedStep, &b.FailedReason)
7683 b.Trusted = trusted != 0
7784 return b, err
7885}
@@ -263,6 +270,21 @@ func (s *Store) FinishBuild(id int64, status string) error {
263270 return nil
264271}
265272
273// SetBuildFailure records where a running build failed. The runner
274// reports it with the outcome; it is written first, so a reader woken
275// by the finish sees both.
276func (s *Store) SetBuildFailure(id int64, step int, reason string) error {
277 res, err := s.DB.Exec(`UPDATE builds SET failed_step = ?, failed_reason = ?
278 WHERE id = ? AND status = 'running'`, step, reason, id)
279 if err != nil {
280 return err
281 }
282 if n, _ := res.RowsAffected(); n == 0 {
283 return ErrNotFound
284 }
285 return nil
286}
287
266288func (s *Store) BuildByID(id int64) (Build, error) {
267289 b, err := scanBuild(s.DB.QueryRow(buildSelect+" WHERE id = ?", id))
268290 if errors.Is(err, sql.ErrNoRows) {
internal/store/builds_test.go +35
@@ -1,6 +1,7 @@
11package store
22
33import (
4 "errors"
45 "strings"
56 "testing"
67)
@@ -566,3 +567,37 @@ func TestBuildLogFrom(t *testing.T) {
566567 t.Fatalf("missing build: %v", err)
567568 }
568569}
570
571// Where a failed build stopped is recorded while it runs, before the
572// outcome, and a finished build is not rewritten (#266).
573func TestSetBuildFailure(t *testing.T) {
574 s := open(t)
575 if err := s.MigrateUp(); err != nil {
576 t.Fatal(err)
577 }
578 uid, _ := s.CreateUser("cmc", true)
579 repoID, _ := s.CreateRepo("user", uid, "app", "public")
580 if _, err := s.CreateBuild(repoID, "unit", "abc", "main", `["true","false"]`, "", "", true); err != nil {
581 t.Fatal(err)
582 }
583 b, ok, err := s.ClaimBuild(nil, false)
584 if err != nil || !ok {
585 t.Fatalf("claim: ok=%v err=%v", ok, err)
586 }
587 if err := s.SetBuildFailure(b.ID, 2, "exit 1"); err != nil {
588 t.Fatal(err)
589 }
590 if err := s.FinishBuild(b.ID, "failure"); err != nil {
591 t.Fatal(err)
592 }
593 got, err := s.BuildByID(b.ID)
594 if err != nil {
595 t.Fatal(err)
596 }
597 if got.FailedStep != 2 || got.FailedReason != "exit 1" {
598 t.Fatalf("failed step %d reason %q", got.FailedStep, got.FailedReason)
599 }
600 if err := s.SetBuildFailure(b.ID, 1, "late"); !errors.Is(err, ErrNotFound) {
601 t.Fatalf("rewrote a finished build: %v", err)
602 }
603}
internal/store/migrations/0065_build_failure.down.sql added +2
@@ -0,0 +1,2 @@
1ALTER TABLE builds DROP COLUMN failed_reason;
2ALTER TABLE builds DROP COLUMN failed_step;
internal/store/migrations/0065_build_failure.up.sql added +4
@@ -0,0 +1,4 @@
1-- Where a failed build stopped: the 1-based step, 0 when it stopped
2-- before any step or did not fail, and the runner's one-line reason.
3ALTER TABLE builds ADD COLUMN failed_step INTEGER NOT NULL DEFAULT 0;
4ALTER TABLE builds ADD COLUMN failed_reason TEXT NOT NULL DEFAULT '';