internal/store/builds.go
149 lines · 4189 bytes
1package store
2
3import (
4 "database/sql"
5 "errors"
6)
7
8// Build is one CI job execution for one commit.
9type Build struct {
10 ID int64
11 RepoID int64
12 Number int64
13 Job string
14 SHA string
15 Ref string
16 Steps string // JSON array of shell commands
17 Status string // pending|running|success|failure
18 CreatedAt string
19 StartedAt string
20 FinishedAt string
21}
22
23// MaxBuildLog caps a build's stored log; appends past it are dropped.
24const MaxBuildLog = 2 << 20
25
26// CreateBuild allocates the per-repo build number in the same transaction
27// as the insert, like issue and MR numbers.
28func (s *Store) CreateBuild(repoID int64, job, sha, ref, stepsJSON string) (int64, error) {
29 tx, err := s.DB.Begin()
30 if err != nil {
31 return 0, err
32 }
33 defer tx.Rollback()
34 if _, err := tx.Exec("UPDATE repos SET build_counter = build_counter + 1 WHERE id = ?", repoID); err != nil {
35 return 0, err
36 }
37 var n int64
38 if err := tx.QueryRow("SELECT build_counter FROM repos WHERE id = ?", repoID).Scan(&n); err != nil {
39 return 0, err
40 }
41 if _, err := tx.Exec(
42 "INSERT INTO builds (repo_id, number, job, sha, ref, steps) VALUES (?, ?, ?, ?, ?, ?)",
43 repoID, n, job, sha, ref, stepsJSON); err != nil {
44 return 0, err
45 }
46 return n, tx.Commit()
47}
48
49const buildSelect = `
50 SELECT id, repo_id, number, job, sha, ref, steps, status, created_at, started_at, finished_at
51 FROM builds`
52
53func scanBuild(row interface{ Scan(...any) error }) (Build, error) {
54 var b Build
55 err := row.Scan(&b.ID, &b.RepoID, &b.Number, &b.Job, &b.SHA, &b.Ref, &b.Steps,
56 &b.Status, &b.CreatedAt, &b.StartedAt, &b.FinishedAt)
57 return b, err
58}
59
60// ClaimBuild atomically hands the oldest pending build to a runner.
61func (s *Store) ClaimBuild() (Build, bool, error) {
62 tx, err := s.DB.Begin()
63 if err != nil {
64 return Build{}, false, err
65 }
66 defer tx.Rollback()
67 var id int64
68 err = tx.QueryRow("SELECT id FROM builds WHERE status = 'pending' ORDER BY id LIMIT 1").Scan(&id)
69 if errors.Is(err, sql.ErrNoRows) {
70 return Build{}, false, nil
71 }
72 if err != nil {
73 return Build{}, false, err
74 }
75 if _, err := tx.Exec(
76 "UPDATE builds SET status = 'running', started_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?", id); err != nil {
77 return Build{}, false, err
78 }
79 b, err := scanBuild(tx.QueryRow(buildSelect+" WHERE id = ?", id))
80 if err != nil {
81 return Build{}, false, err
82 }
83 return b, true, tx.Commit()
84}
85
86// AppendBuildLog adds a chunk to the build's log, dropping bytes past the cap.
87func (s *Store) AppendBuildLog(id int64, chunk []byte) error {
88 _, err := s.DB.Exec(`
89 UPDATE builds SET log = log || ?
90 WHERE id = ? AND length(log) < ?`, chunk, id, MaxBuildLog)
91 return err
92}
93
94// FinishBuild records the outcome of a running build.
95func (s *Store) FinishBuild(id int64, status string) error {
96 res, err := s.DB.Exec(`
97 UPDATE builds SET status = ?, finished_at = strftime('%Y-%m-%dT%H:%M:%SZ','now')
98 WHERE id = ? AND status = 'running'`, status, id)
99 if err != nil {
100 return err
101 }
102 if n, _ := res.RowsAffected(); n == 0 {
103 return ErrNotFound
104 }
105 return nil
106}
107
108func (s *Store) BuildByID(id int64) (Build, error) {
109 b, err := scanBuild(s.DB.QueryRow(buildSelect+" WHERE id = ?", id))
110 if errors.Is(err, sql.ErrNoRows) {
111 return b, ErrNotFound
112 }
113 return b, err
114}
115
116func (s *Store) BuildByNumber(repoID, number int64) (Build, error) {
117 b, err := scanBuild(s.DB.QueryRow(buildSelect+" WHERE repo_id = ? AND number = ?", repoID, number))
118 if errors.Is(err, sql.ErrNoRows) {
119 return b, ErrNotFound
120 }
121 return b, err
122}
123
124func (s *Store) ListBuilds(repoID int64, limit int) ([]Build, error) {
125 rows, err := s.DB.Query(buildSelect+" WHERE repo_id = ? ORDER BY number DESC LIMIT ?", repoID, limit)
126 if err != nil {
127 return nil, err
128 }
129 defer rows.Close()
130 var out []Build
131 for rows.Next() {
132 b, err := scanBuild(rows)
133 if err != nil {
134 return nil, err
135 }
136 out = append(out, b)
137 }
138 return out, rows.Err()
139}
140
141// BuildLog returns the stored log bytes.
142func (s *Store) BuildLog(id int64) ([]byte, error) {
143 var log []byte
144 err := s.DB.QueryRow("SELECT log FROM builds WHERE id = ?", id).Scan(&log)
145 if errors.Is(err, sql.ErrNoRows) {
146 return nil, ErrNotFound
147 }
148 return log, err
149}