internal/store/statuses.go
190 lines · 6074 bytes
1package store
2
3import (
4 "strings"
5 "time"
6)
7
8type CommitStatus struct {
9 Context string
10 State string // pending | success | failure | error | skipped
11 Description string
12 TargetURL string
13 Creator string
14 UpdatedAt string
15}
16
17// SetCommitStatus upserts the latest state for one context on one commit.
18// A zero creatorID records no creator (system actions like the scheduler).
19func (s *Store) SetCommitStatus(repoID int64, sha, context, state, description, targetURL string, creatorID int64) error {
20 var creator any
21 if creatorID != 0 {
22 creator = creatorID
23 }
24 _, err := s.DB.Exec(`
25 INSERT INTO commit_statuses (repo_id, commit_sha, context, state, description, target_url, creator_id)
26 VALUES (?, ?, ?, ?, ?, ?, ?)
27 ON CONFLICT (repo_id, commit_sha, context) DO UPDATE SET
28 state = excluded.state, description = excluded.description,
29 target_url = excluded.target_url, creator_id = excluded.creator_id,
30 updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')`,
31 repoID, sha, context, state, description, targetURL, creator)
32 return err
33}
34
35// ListCommitStatuses returns the latest status per context for a commit.
36func (s *Store) ListCommitStatuses(repoID int64, sha string) ([]CommitStatus, error) {
37 rows, err := s.DB.Query(`
38 SELECT cs.context, cs.state, cs.description, cs.target_url, COALESCE(u.username, ''), cs.updated_at
39 FROM commit_statuses cs LEFT JOIN users u ON u.id = cs.creator_id
40 WHERE cs.repo_id = ? AND cs.commit_sha = ? ORDER BY cs.context`, repoID, sha)
41 if err != nil {
42 return nil, err
43 }
44 defer rows.Close()
45 var out []CommitStatus
46 for rows.Next() {
47 var c CommitStatus
48 if err := rows.Scan(&c.Context, &c.State, &c.Description, &c.TargetURL, &c.Creator, &c.UpdatedAt); err != nil {
49 return nil, err
50 }
51 out = append(out, c)
52 }
53 return out, rows.Err()
54}
55
56// CommitStatusesFor is ListCommitStatuses for every commit on a list
57// page in one query, keyed by commit.
58func (s *Store) CommitStatusesFor(repoID int64, shas []string) (map[string][]CommitStatus, error) {
59 out := map[string][]CommitStatus{}
60 if len(shas) == 0 {
61 return out, nil
62 }
63 args := []any{repoID}
64 for _, sha := range shas {
65 args = append(args, sha)
66 }
67 rows, err := s.DB.Query(`
68 SELECT cs.commit_sha, cs.context, cs.state, cs.description, cs.target_url, COALESCE(u.username, ''), cs.updated_at
69 FROM commit_statuses cs LEFT JOIN users u ON u.id = cs.creator_id
70 WHERE cs.repo_id = ? AND cs.commit_sha IN (?`+strings.Repeat(",?", len(shas)-1)+`)
71 ORDER BY cs.context`, args...)
72 if err != nil {
73 return nil, err
74 }
75 defer rows.Close()
76 for rows.Next() {
77 var sha string
78 var c CommitStatus
79 if err := rows.Scan(&sha, &c.Context, &c.State, &c.Description, &c.TargetURL, &c.Creator, &c.UpdatedAt); err != nil {
80 return nil, err
81 }
82 out[sha] = append(out[sha], c)
83 }
84 return out, rows.Err()
85}
86
87// RepoHasStatuses reports whether anything has ever reported a status in
88// this repository. It is how require_checks tells a repository whose
89// checks come from outside — `status set`, with no .gitbay/ci.yml — from
90// one that has no checks at all.
91func (s *Store) RepoHasStatuses(repoID int64) (bool, error) {
92 var n int
93 err := s.DB.QueryRow(`SELECT EXISTS(SELECT 1 FROM commit_statuses WHERE repo_id = ?)`, repoID).Scan(&n)
94 return n == 1, err
95}
96
97// CombinedStatus reduces per-context states to one: error/failure dominate,
98// then pending, then success; "" when no statuses exist.
99// CombinedStatusFor returns the combined state for each of several commits
100// in one query. The log lists fifty commits at a time; asking per commit
101// turns one page into fifty round trips.
102func (s *Store) CombinedStatusFor(repoID int64, shas []string) (map[string]string, error) {
103 out := map[string]string{}
104 if len(shas) == 0 {
105 return out, nil
106 }
107 args := make([]any, 0, len(shas)+1)
108 args = append(args, repoID)
109 for _, sha := range shas {
110 args = append(args, sha)
111 }
112 q := `SELECT commit_sha, state FROM commit_statuses WHERE repo_id = ? AND commit_sha IN (?` +
113 strings.Repeat(",?", len(shas)-1) + `)`
114 rows, err := s.DB.Query(q, args...)
115 if err != nil {
116 return nil, err
117 }
118 defer rows.Close()
119 for rows.Next() {
120 var sha, state string
121 if err := rows.Scan(&sha, &state); err != nil {
122 return nil, err
123 }
124 // Same precedence as CombinedStatus, folded in as rows arrive.
125 switch {
126 case out[sha] == "failure":
127 case state == "error" || state == "failure":
128 out[sha] = "failure"
129 case state == "pending":
130 out[sha] = "pending"
131 case out[sha] == "":
132 out[sha] = "success"
133 }
134 }
135 return out, rows.Err()
136}
137
138func CombinedStatus(statuses []CommitStatus) string {
139 if len(statuses) == 0 {
140 return ""
141 }
142 combined := "success"
143 for _, s := range statuses {
144 switch s.State {
145 case "error", "failure":
146 return "failure"
147 case "pending":
148 combined = "pending"
149 }
150 }
151 return combined
152}
153
154// Check is a commit status paired with the build behind it. Only ci/<job>
155// statuses have one; anything posted through `status set` reports just the
156// state and when it last changed.
157type Check struct {
158 CommitStatus
159 Build int64 // build number, 0 when the check is not CI's
160 Duration time.Duration // 0 until that build has finished
161}
162
163// ChecksForCommit lists a commit's checks with their timing, and reduces
164// them to one combined state. Two queries at most, whatever the surface.
165func (s *Store) ChecksForCommit(repoID int64, sha string) ([]Check, string, error) {
166 statuses, err := s.ListCommitStatuses(repoID, sha)
167 if err != nil {
168 return nil, "", err
169 }
170 var builds map[string]Build
171 for _, st := range statuses {
172 if strings.HasPrefix(st.Context, "ci/") {
173 if builds, err = s.BuildsForCommit(repoID, sha); err != nil {
174 return nil, "", err
175 }
176 break
177 }
178 }
179 checks := make([]Check, 0, len(statuses))
180 for _, st := range statuses {
181 c := Check{CommitStatus: st}
182 if job, ok := strings.CutPrefix(st.Context, "ci/"); ok {
183 if b, ok := builds[job]; ok {
184 c.Build, c.Duration = b.Number, b.Elapsed()
185 }
186 }
187 checks = append(checks, c)
188 }
189 return checks, CombinedStatus(statuses), nil
190}