ci.go
⎇
Raw
1package db
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7)
8
9type CiRun struct {
10 ID int64
11 RepoID int64
12 TriggeredBy *int64
13 TriggerSource string
14 CommitSHA *string
15 CommitBranch *string
16 CommitTag *string
17 Status string
18 VariableOverrides *string
19 StartedAt *string
20 FinishedAt *string
21 CreatedAt string
22 RepoRunID *int64
23 // Joined from users, null when the run was not triggered by a person.
24 TriggeredByUsername *string
25}
26
27type CiStep struct {
28 ID int64
29 RunID int64
30 Name string
31 Status string
32 ExitCode *int64
33 StartedAt *string
34 FinishedAt *string
35 Log string
36}
37
38type CiArtifact struct {
39 ID int64
40 RunID int64
41 Filename string
42 Size int64
43 CreatedAt string
44}
45
46// CiSecret omits the value; only the pipeline runner reads values.
47type CiSecret struct {
48 ID int64
49 Name string
50 Description *string
51 CreatedAt string
52}
53
54const ciRunColumns = `ci_runs.id, ci_runs.repo_id, ci_runs.triggered_by, ci_runs.trigger_source,
55 ci_runs.commit_sha, ci_runs.commit_branch, ci_runs.commit_tag, ci_runs.status,
56 ci_runs.variable_overrides, ci_runs.started_at, ci_runs.finished_at, ci_runs.created_at,
57 ci_runs.repo_run_id, users.username`
58
59func scanCiRun(s rowScanner) (*CiRun, error) {
60 var r CiRun
61 err := s.Scan(&r.ID, &r.RepoID, &r.TriggeredBy, &r.TriggerSource, &r.CommitSHA,
62 &r.CommitBranch, &r.CommitTag, &r.Status, &r.VariableOverrides, &r.StartedAt,
63 &r.FinishedAt, &r.CreatedAt, &r.RepoRunID, &r.TriggeredByUsername)
64 if errors.Is(err, sql.ErrNoRows) {
65 return nil, nil
66 }
67 if err != nil {
68 return nil, err
69 }
70 return &r, nil
71}
72
73// LatestCiRunStatus returns the status of the newest run, or "" when the repo
74// has never run CI.
75func (d *DB) LatestCiRunStatus(ctx context.Context, repoID int64) (string, error) {
76 var status string
77 err := d.QueryRowContext(ctx,
78 `SELECT status FROM ci_runs WHERE repo_id = ? ORDER BY id DESC LIMIT 1`, repoID).Scan(&status)
79 if errors.Is(err, sql.ErrNoRows) {
80 return "", nil
81 }
82 return status, err
83}
84
85func (d *DB) CountCiRuns(ctx context.Context, repoID int64) (int, error) {
86 var n int
87 err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM ci_runs WHERE repo_id = ?`, repoID).Scan(&n)
88 return n, err
89}
90
91func (d *DB) ListCiRuns(ctx context.Context, repoID int64, limit, offset int) ([]CiRun, error) {
92 rows, err := d.QueryContext(ctx,
93 `SELECT `+ciRunColumns+` FROM ci_runs
94 LEFT JOIN users ON users.id = ci_runs.triggered_by
95 WHERE ci_runs.repo_id = ? ORDER BY ci_runs.id DESC LIMIT ? OFFSET ?`,
96 repoID, limit, offset)
97 if err != nil {
98 return nil, err
99 }
100 defer rows.Close()
101 var out []CiRun
102 for rows.Next() {
103 r, err := scanCiRun(rows)
104 if err != nil {
105 return nil, err
106 }
107 out = append(out, *r)
108 }
109 return out, rows.Err()
110}
111
112func (d *DB) CiRunInRepo(ctx context.Context, runID, repoID int64) (*CiRun, error) {
113 return scanCiRun(d.QueryRowContext(ctx,
114 `SELECT `+ciRunColumns+` FROM ci_runs
115 LEFT JOIN users ON users.id = ci_runs.triggered_by
116 WHERE ci_runs.id = ? AND ci_runs.repo_id = ?`, runID, repoID))
117}
118
119// ── steps ────────────────────────────────────────────────────────────────
120
121func (d *DB) ListCiSteps(ctx context.Context, runID int64) ([]CiStep, error) {
122 rows, err := d.QueryContext(ctx,
123 `SELECT id, run_id, name, status, exit_code, started_at, finished_at, log
124 FROM ci_steps WHERE run_id = ? ORDER BY id ASC`, runID)
125 if err != nil {
126 return nil, err
127 }
128 defer rows.Close()
129 var out []CiStep
130 for rows.Next() {
131 var s CiStep
132 if err := rows.Scan(&s.ID, &s.RunID, &s.Name, &s.Status, &s.ExitCode,
133 &s.StartedAt, &s.FinishedAt, &s.Log); err != nil {
134 return nil, err
135 }
136 out = append(out, s)
137 }
138 return out, rows.Err()
139}
140
141// ── artifacts ────────────────────────────────────────────────────────────
142
143func (d *DB) ListCiArtifacts(ctx context.Context, runID int64) ([]CiArtifact, error) {
144 rows, err := d.QueryContext(ctx,
145 `SELECT id, run_id, filename, size, created_at FROM ci_artifacts
146 WHERE run_id = ? ORDER BY id ASC`, runID)
147 if err != nil {
148 return nil, err
149 }
150 defer rows.Close()
151 var out []CiArtifact
152 for rows.Next() {
153 var a CiArtifact
154 if err := rows.Scan(&a.ID, &a.RunID, &a.Filename, &a.Size, &a.CreatedAt); err != nil {
155 return nil, err
156 }
157 out = append(out, a)
158 }
159 return out, rows.Err()
160}
161
162// CiArtifactCounts returns the number of artifacts per run for a list view.
163func (d *DB) CiArtifactCounts(ctx context.Context, runIDs []int64) (map[int64]int, error) {
164 out := map[int64]int{}
165 if len(runIDs) == 0 {
166 return out, nil
167 }
168 rows, err := d.QueryContext(ctx,
169 `SELECT run_id, COUNT(*) FROM ci_artifacts WHERE run_id IN (`+placeholders(len(runIDs))+`)
170 GROUP BY run_id`, int64Args(runIDs)...)
171 if err != nil {
172 return nil, err
173 }
174 defer rows.Close()
175 for rows.Next() {
176 var id int64
177 var n int
178 if err := rows.Scan(&id, &n); err != nil {
179 return nil, err
180 }
181 out[id] = n
182 }
183 return out, rows.Err()
184}
185
186// CiArtifactInRun resolves a download request, checking run and repo in the
187// same query so an artifact from another repo cannot be fetched.
188func (d *DB) CiArtifactInRun(ctx context.Context, artifactID, runID, repoID int64) (*CiArtifact, error) {
189 var a CiArtifact
190 err := d.QueryRowContext(ctx,
191 `SELECT ci_artifacts.id, ci_artifacts.run_id, ci_artifacts.filename, ci_artifacts.size,
192 ci_artifacts.created_at
193 FROM ci_artifacts
194 JOIN ci_runs ON ci_runs.id = ci_artifacts.run_id
195 WHERE ci_artifacts.id = ? AND ci_runs.id = ? AND ci_runs.repo_id = ?`,
196 artifactID, runID, repoID).Scan(&a.ID, &a.RunID, &a.Filename, &a.Size, &a.CreatedAt)
197 if errors.Is(err, sql.ErrNoRows) {
198 return nil, nil
199 }
200 if err != nil {
201 return nil, err
202 }
203 return &a, nil
204}
205
206// ── secrets ──────────────────────────────────────────────────────────────
207
208// ListCiSecrets returns secret metadata for the settings page, without values.
209func (d *DB) ListCiSecrets(ctx context.Context, repoID int64) ([]CiSecret, error) {
210 rows, err := d.QueryContext(ctx,
211 `SELECT id, name, description, created_at FROM ci_secrets WHERE repo_id = ? ORDER BY name ASC`,
212 repoID)
213 if err != nil {
214 return nil, err
215 }
216 defer rows.Close()
217 var out []CiSecret
218 for rows.Next() {
219 var s CiSecret
220 if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt); err != nil {
221 return nil, err
222 }
223 out = append(out, s)
224 }
225 return out, rows.Err()
226}
227
228func (d *DB) UpsertCiSecret(ctx context.Context, repoID int64, name, value string, description *string) error {
229 _, err := d.ExecContext(ctx,
230 `INSERT INTO ci_secrets (repo_id, name, value, description) VALUES (?, ?, ?, ?)
231 ON CONFLICT(repo_id, name) DO UPDATE SET value = excluded.value,
232 description = excluded.description`,
233 repoID, name, value, description)
234 return err
235}
236
237func (d *DB) DeleteCiSecret(ctx context.Context, id, repoID int64) error {
238 _, err := d.ExecContext(ctx, `DELETE FROM ci_secrets WHERE id = ? AND repo_id = ?`, id, repoID)
239 return err
240}
241