Commit 44533deae4
Verified · cmc ci/build: success ci/test: success
.gitbay/wiki/API.org +5 −3
| @@ -110,9 +110,11 @@ worst of them. Statuses appear on commit pages, MR pages, and | ||
| 110 | 110 | =mr show=, each with =updated_at=; a =ci/<job>= status also carries |
| 111 | 111 | =duration=, read from the build behind it, once that build has |
| 112 | 112 | finished. With =repo settings require-checks <repo> on=, every status |
| 113 | on the MR head must be green, and a head whose =.gitbay/ci.yml= has a | |
| 114 | job a push would run must carry some; a repository with no CI | |
| 115 | configuration, or only scheduled and tag jobs, merges. Each | |
| 113 | on the MR head must be green, and a head something was going to report | |
| 114 | on must carry some: a =.gitbay/ci.yml= with a job a push runs, or a | |
| 115 | repository that has recorded a status before, which is what reporting | |
| 116 | from outside through =status set= looks like. A repository where | |
| 117 | nothing has ever reported merges. Each | |
| 116 | 118 | report also emits a =status= event to webhooks. |
| 117 | 119 | |
| 118 | 120 | * Webhooks |
internal/control/checksgate_test.go +22 −1
| @@ -11,8 +11,13 @@ import ( | ||
| 11 | 11 | |
| 12 | 12 | // gatesForHead builds a repository with require_checks on, a bare dir |
| 13 | 13 | // holding the given .gitbay/ci.yml (empty string for none), and one MR |
| 14 | // whose head carries no statuses at all. | |
| 14 | // whose head carries no statuses at all. seed records a status on the | |
| 15 | // base commit, standing for a repository that reports from outside. | |
| 15 | 16 | func gatesForHead(t *testing.T, ciYML string) GatesOut { |
| 17 | return gatesForHeadSeeded(t, ciYML, false) | |
| 18 | } | |
| 19 | ||
| 20 | func gatesForHeadSeeded(t *testing.T, ciYML string, seed bool) GatesOut { | |
| 16 | 21 | t.Helper() |
| 17 | 22 | st, repo, uid := newQueueTestRepo(t) |
| 18 | 23 | if _, err := st.UpdateRepoSettings(repo.ID, func(set *store.RepoSettings) { set.RequireChecks = true }); err != nil { |
| @@ -54,6 +59,12 @@ func gatesForHead(t *testing.T, ciYML string) GatesOut { | ||
| 54 | 59 | t.Fatal(err) |
| 55 | 60 | } |
| 56 | 61 | |
| 62 | if seed { | |
| 63 | if err := st.SetCommitStatus(repo.ID, targetSHA, "lint", "success", "", "", uid); err != nil { | |
| 64 | t.Fatal(err) | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 57 | 68 | g, err := MergeGates(st, repo, mr, dir, targetSHA, headSHA) |
| 58 | 69 | if err != nil { |
| 59 | 70 | t.Fatal(err) |
| @@ -97,3 +108,13 @@ func TestRequireChecksRefusesSilentPushJob(t *testing.T) { | ||
| 97 | 108 | t.Fatalf("allowed a head whose push job reported nothing: %v", g.Unmet) |
| 98 | 109 | } |
| 99 | 110 | } |
| 111 | ||
| 112 | // A repository whose checks come from outside — `status set`, no | |
| 113 | // .gitbay/ci.yml — looks like one with no CI at all. Having reported | |
| 114 | // before is what says a report was coming, so a silent head there is | |
| 115 | // still refused. | |
| 116 | func TestRequireChecksRefusesSilentHeadInReportingRepo(t *testing.T) { | |
| 117 | if g := gatesForHeadSeeded(t, "", true); !checksUnmet(g) { | |
| 118 | t.Fatalf("allowed a silent head in a repository that reports statuses: %v", g.Unmet) | |
| 119 | } | |
| 120 | } | |
internal/control/mr.go +20 −8
| @@ -1244,13 +1244,25 @@ func (c *Ctx) reviewGates(repo store.Repo, mr store.MR, dir, targetSHA, headSHA | ||
| 1244 | 1244 | return -1 |
| 1245 | 1245 | } |
| 1246 | 1246 | |
| 1247 | // checksExpected reports whether anything was going to report a status | |
| 1248 | // on this head. A repository with no CI configuration and no history of | |
| 1249 | // statuses can never satisfy require_checks, and refusing its merges | |
| 1250 | // leaves no remedy but turning the setting off. Two things say a report | |
| 1251 | // was coming: a .gitbay/ci.yml at the head with a job a push runs, and a | |
| 1252 | // status having ever been recorded in the repository, which is how a | |
| 1253 | // repository reporting from outside through `status set` looks. | |
| 1254 | func checksExpected(st *store.Store, repoID int64, dir, headSHA string) bool { | |
| 1255 | if seen, err := st.RepoHasStatuses(repoID); err != nil || seen { | |
| 1256 | return true | |
| 1257 | } | |
| 1258 | return headRunsJobs(dir, headSHA) | |
| 1259 | } | |
| 1260 | ||
| 1247 | 1261 | // headRunsJobs reports whether a push of this head would have queued or |
| 1248 | // skipped a job, and so left it a status. A repository with no CI | |
| 1249 | // configuration, or one whose jobs all wait on a schedule or a tag, can | |
| 1250 | // never satisfy require_checks, and refusing its merges leaves no remedy | |
| 1251 | // but turning the setting off. A configuration that will not parse | |
| 1252 | // counts as running jobs: the push recorded a ci/config failure for it, | |
| 1253 | // so the head is not silent and this is not the branch that decides. | |
| 1262 | // skipped a job, and so left it a status. A configuration that will not | |
| 1263 | // parse counts as running jobs: the push recorded a ci/config failure | |
| 1264 | // for it, so the head is not silent and this is not the branch that | |
| 1265 | // decides. | |
| 1254 | 1266 | func headRunsJobs(dir, headSHA string) bool { |
| 1255 | 1267 | raw, err := gitutil.ReadBlob(dir, headSHA, ci.ConfigPath, 1<<16) |
| 1256 | 1268 | if err != nil { |
| @@ -1286,7 +1298,7 @@ func MergeGates(st *store.Store, repo store.Repo, mr store.MR, dir, targetSHA, h | ||
| 1286 | 1298 | } |
| 1287 | 1299 | |
| 1288 | 1300 | // Checks: with require_checks, every status the head carries must be |
| 1289 | // green, and a head whose CI would report must carry some. | |
| 1301 | // green, and a head something was going to report on must carry some. | |
| 1290 | 1302 | statuses, err := st.ListCommitStatuses(repo.ID, headSHA) |
| 1291 | 1303 | if err != nil { |
| 1292 | 1304 | return g, err |
| @@ -1296,7 +1308,7 @@ func MergeGates(st *store.Store, repo store.Repo, mr store.MR, dir, targetSHA, h | ||
| 1296 | 1308 | switch g.Checks { |
| 1297 | 1309 | case "success": |
| 1298 | 1310 | case "": |
| 1299 | if headRunsJobs(dir, headSHA) { | |
| 1311 | if checksExpected(st, repo.ID, dir, headSHA) { | |
| 1300 | 1312 | g.Unmet = append(g.Unmet, fmt.Sprintf("%s requires green checks and none were reported on %.10s", repo.Path(), headSHA)) |
| 1301 | 1313 | } |
| 1302 | 1314 | default: |
internal/store/statuses.go +10
| @@ -53,6 +53,16 @@ func (s *Store) ListCommitStatuses(repoID int64, sha string) ([]CommitStatus, er | ||
| 53 | 53 | return out, rows.Err() |
| 54 | 54 | } |
| 55 | 55 | |
| 56 | // RepoHasStatuses reports whether anything has ever reported a status in | |
| 57 | // this repository. It is how require_checks tells a repository whose | |
| 58 | // checks come from outside — `status set`, with no .gitbay/ci.yml — from | |
| 59 | // one that has no checks at all. | |
| 60 | func (s *Store) RepoHasStatuses(repoID int64) (bool, error) { | |
| 61 | var n int | |
| 62 | err := s.DB.QueryRow(`SELECT EXISTS(SELECT 1 FROM commit_statuses WHERE repo_id = ?)`, repoID).Scan(&n) | |
| 63 | return n == 1, err | |
| 64 | } | |
| 65 | ||
| 56 | 66 | // CombinedStatus reduces per-context states to one: error/failure dominate, |
| 57 | 67 | // then pending, then success; "" when no statuses exist. |
| 58 | 68 | // CombinedStatusFor returns the combined state for each of several commits |