Skip to content
365 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1//! Statuses on commits: what workflow runs (and other tools, such as
2//! deployments) say about a pull request's head. These are its checks.
3//!
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge4//! The rules of the branch a pull request merges into name the checks that
5//! must pass (rulesets.rs; `RepoSettings::required_checks` as they stack):
6//! a required check that failed, is still running or has not reported
7//! refuses the merge, for everyone and for the merge queue (`g1t_rules`
8//! says so). Where g1t sees an agent's pull request through,
Fast pages, required checks on the branch, self-hosted runners, honest incidents9//! any check that failed sends the agent back to fix it, with what the
10//! failing jobs printed; once it is out of revisions, only a required
11//! check holds the pull request for a person.
Icons are cached at the edge12
13use g1t_contracts::events::ChecksEvent;
14use g1t_contracts::time::rfc3339;
Fast pages, required checks on the branch, self-hosted runners, honest incidents15use g1t_contracts::work::{
16 CommitStatus, RequiredCheck, RequiredState, SeenCheck, SeenChecksArgs, SetCommitStatusArgs, check_name,
17 required_checks,
18};
Icons are cached at the edge19use g1t_contracts::{FailureCode, Outcome};
20use g1t_kit::now_ms;
21use serde::Deserialize;
22use worker::Result;
23
24use crate::Work;
25
26#[derive(Deserialize)]
27struct StatusRow {
28 context: String,
29 state: String,
30 description: Option<String>,
31 target_url: Option<String>,
32 updated_at: String,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge33 #[serde(default)]
34 source: Option<String>,
Merge checks: statuses and check runs on every commit35 #[serde(default)]
36 check_run_id: Option<String>,
Icons are cached at the edge37}
38
39impl From<StatusRow> for CommitStatus {
40 fn from(row: StatusRow) -> Self {
41 CommitStatus {
42 context: row.context,
43 state: row.state,
44 description: row.description,
45 target_url: row.target_url,
46 updated_at: row.updated_at,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge47 source: row.source,
Merge checks: statuses and check runs on every commit48 check_run_id: row.check_run_id,
Icons are cached at the edge49 }
50 }
51}
52
53#[derive(Deserialize)]
54struct HeadRow {
55 id: String,
56 number: u32,
57}
58
Fast pages, required checks on the branch, self-hosted runners, honest incidents59/// What a commit's checks say: every status still running and every one
60/// that failed, by context, and where each required check stands.
61#[derive(Clone, Debug, Default, PartialEq)]
Icons are cached at the edge62pub(crate) struct WorkflowFacts {
63 pub(crate) pending: Vec<String>,
64 pub(crate) failed: Vec<String>,
Fast pages, required checks on the branch, self-hosted runners, honest incidents65 pub(crate) required: Vec<RequiredCheck>,
Icons are cached at the edge66}
67
68impl WorkflowFacts {
Fast pages, required checks on the branch, self-hosted runners, honest incidents69 /// `required` names the checks the default branch's protection requires.
70 pub(crate) fn of(statuses: &[CommitStatus], required: &[String]) -> WorkflowFacts {
Icons are cached at the edge71 WorkflowFacts {
72 pending: statuses.iter().filter(|s| s.state == "pending").map(|s| s.context.clone()).collect(),
73 failed: statuses.iter().filter(|s| s.state == "failure" || s.state == "error").map(|s| s.context.clone()).collect(),
Fast pages, required checks on the branch, self-hosted runners, honest incidents74 required: required_checks(required, statuses),
Icons are cached at the edge75 }
76 }
77
Fast pages, required checks on the branch, self-hosted runners, honest incidents78 fn required_in(&self, state: RequiredState) -> Vec<String> {
79 self.required.iter().filter(|check| check.state == state).map(|check| check.name.clone()).collect()
80 }
81
82 /// The required checks that failed, by name.
83 pub(crate) fn required_failed(&self) -> Vec<String> {
84 self.required_in(RequiredState::Failure)
85 }
86
87 /// The required checks nothing has reported on the commit yet.
88 pub(crate) fn expected(&self) -> Vec<String> {
89 self.required_in(RequiredState::Expected)
90 }
91}
92
93/// The check names in `(context, last reported)` rows, most recent first:
94/// each name once, with the events it was reported for.
95pub(crate) fn seen(rows: Vec<(String, String)>) -> Vec<SeenCheck> {
96 let mut rows = rows;
97 rows.sort_by(|a, b| b.1.cmp(&a.1));
98 let mut out: Vec<SeenCheck> = Vec::new();
99 for (context, at) in rows {
100 let (name, event) = check_name(&context);
101 match out.iter_mut().find(|seen| seen.name.eq_ignore_ascii_case(name)) {
102 Some(seen) => {
103 if let Some(event) = event
104 && !seen.events.iter().any(|known| known == event)
105 {
106 seen.events.push(event.to_owned());
107 }
108 }
109 None => out.push(SeenCheck {
110 name: name.to_owned(),
111 events: event.map(|event| vec![event.to_owned()]).unwrap_or_default(),
112 last_seen: at,
113 }),
114 }
115 }
116 out
117}
118
119/// How far back `seen_checks` looks, and the most contexts it reads.
120const SEEN_DAYS: u64 = 30;
121const SEEN_LIMIT: u32 = 200;
122
123#[derive(Deserialize)]
124struct SeenRow {
125 context: String,
126 at: String,
127}
128
Icons are cached at the edge129pub(crate) fn list(names: &[String]) -> String {
130 match names {
131 [] => String::new(),
132 [one] => one.clone(),
133 [rest @ .., last] => format!("{} and {last}", rest.join(", ")),
134 }
135}
136
137impl Work {
Fast pages, required checks on the branch, self-hosted runners, honest incidents138 /// Where a commit's checks stand, against the repository's required ones.
139 pub(crate) async fn facts(&self, repo_id: &str, sha: Option<&str>) -> Result<WorkflowFacts> {
140 let (statuses, settings) =
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge141 futures_util::future::try_join(self.statuses(repo_id, sha), self.settings_by_id(repo_id)).await?;
A required check named for a workflow job, as GitHub's branch rules name them, is met by that job. Each g1t Actions job on a pull request's head counts as a check of its name (`test`, `build (ubuntu-latest, 22)`, or `deploy / build` for a called workflow's job), from the latest attempt of its workflow: skipped passes, cancelled fails, and a job still going holds the merge. So a ruleset or required check copied from GitHub needs no renaming, and one that names the workflow, such as `CI`, is met by the whole run as before. This holds in the merge box, for the merge queue and for agents' merges; jobs count only toward required checks, so a `continue-on-error` job that fails doesn't send an agent back. The pull requests guide says so.142 self.workflow_facts(repo_id, sha, &statuses, &settings.required_checks).await
143 }
144
145 /// What `statuses`, a commit's, say, with each required check also met
146 /// by the g1t Actions jobs of its name ([`Work::with_job_checks`]). Jobs
147 /// count only toward required checks: whether workflows are running or
148 /// failed is their runs' statuses' to say.
149 pub(crate) async fn workflow_facts(
150 &self,
151 repo_id: &str,
152 sha: Option<&str>,
153 statuses: &[CommitStatus],
154 required: &[String],
155 ) -> Result<WorkflowFacts> {
156 let mut facts = WorkflowFacts::of(statuses, required);
157 if !required.is_empty() {
158 facts.required = required_checks(required, &self.with_job_checks(repo_id, sha, statuses.to_vec()).await?);
159 }
160 Ok(facts)
161 }
162
163 /// `statuses` and, for each g1t Actions job on `sha`, the status its
164 /// check run stands as (`g1t_contracts::checks::job_statuses`), so a
165 /// required check named for a job, as GitHub's are, is met by it.
166 pub(crate) async fn with_job_checks(&self, repo_id: &str, sha: Option<&str>, mut statuses: Vec<CommitStatus>) -> Result<Vec<CommitStatus>> {
167 let Some(sha) = sha else { return Ok(statuses) };
168 let details = self
169 .actions_runs(g1t_contracts::checks::ActionsChecksArgs { repo_id: repo_id.to_owned(), shas: vec![sha.to_owned()], ..Default::default() })
170 .await?;
171 let runs: Vec<_> = details.into_iter().map(|detail| (detail.run, detail.jobs)).collect();
172 statuses.extend(g1t_contracts::checks::job_statuses(&runs, sha));
173 Ok(statuses)
Fast pages, required checks on the branch, self-hosted runners, honest incidents174 }
175
176 /// The check names reported on a repository's commits lately, for
177 /// choosing which to require.
178 pub(crate) async fn seen_checks(&self, a: SeenChecksArgs) -> Result<Outcome<Vec<SeenCheck>>> {
179 let repo = match self.repo(&a.repo, &a.viewer).await? {
180 Outcome::Ok(repo) => repo,
181 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
182 };
183 let since = rfc3339(now_ms().saturating_sub(SEEN_DAYS * 24 * 60 * 60 * 1000));
184 let rows = self
185 .db
186 .prepare(
187 "SELECT context, MAX(updated_at) AS at FROM commit_statuses
188 WHERE repo_id = ? AND updated_at >= ? GROUP BY context ORDER BY at DESC LIMIT ?",
189 )
190 .bind(&[repo.id.as_str().into(), since.into(), SEEN_LIMIT.into()])?
191 .all()
192 .await?
193 .results::<SeenRow>()?;
194 Ok(Outcome::Ok(seen(rows.into_iter().map(|row| (row.context, row.at)).collect())))
195 }
196
Icons are cached at the edge197 pub(crate) async fn statuses(&self, repo_id: &str, sha: Option<&str>) -> Result<Vec<CommitStatus>> {
198 let Some(sha) = sha else { return Ok(Vec::new()) };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily199 if let Some(found) = self.prefetched_repo(repo_id).filter(|found| found.head.as_deref() == Some(sha)) {
200 return Ok(found
201 .rows::<StatusRow>(crate::prefetch::Slot::Statuses)?
202 .into_iter()
203 .map(CommitStatus::from)
204 .collect());
205 }
Icons are cached at the edge206 Ok(self
207 .db
Merge checks: statuses and check runs on every commit208 .prepare("SELECT context, state, description, target_url, updated_at, source, check_run_id FROM commit_statuses WHERE repo_id = ? AND sha = ? ORDER BY context")
Icons are cached at the edge209 .bind(&[repo_id.into(), sha.into()])?
210 .all()
211 .await?
212 .results::<StatusRow>()?
213 .into_iter()
214 .map(CommitStatus::from)
215 .collect())
216 }
217
218 pub(crate) async fn set_commit_status(&self, a: SetCommitStatusArgs) -> Result<Outcome<bool>> {
Merge checks: statuses and check runs on every commit219 self.store_status(a, None).await
220 }
221
222 /// Sets a status, standing for check run `check_run_id` if it does,
223 /// and moves on what was waiting for the commit's checks.
224 pub(crate) async fn store_status(&self, a: SetCommitStatusArgs, check_run_id: Option<&str>) -> Result<Outcome<bool>> {
Icons are cached at the edge225 if !matches!(a.state.as_str(), "pending" | "success" | "failure" | "error") {
226 return Ok(Outcome::fail(FailureCode::Invalid, "`state` is pending, success, failure or error."));
227 }
228 self.db
229 .prepare(
Merge checks: statuses and check runs on every commit230 "INSERT INTO commit_statuses (repo_id, sha, context, state, description, target_url, updated_at, source, check_run_id)
231 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
Icons are cached at the edge232 ON CONFLICT (repo_id, sha, context) DO UPDATE SET
233 state = excluded.state, description = excluded.description,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge234 target_url = excluded.target_url, updated_at = excluded.updated_at,
Merge checks: statuses and check runs on every commit235 source = excluded.source, check_run_id = excluded.check_run_id",
Icons are cached at the edge236 )
237 .bind(&[
238 a.repo_id.as_str().into(),
239 a.sha.as_str().into(),
240 a.context.as_str().into(),
241 a.state.as_str().into(),
242 a.description.as_deref().map_or(worker::wasm_bindgen::JsValue::NULL, Into::into),
243 a.target_url.as_deref().map_or(worker::wasm_bindgen::JsValue::NULL, Into::into),
244 rfc3339(now_ms()).into(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge245 a.source.as_deref().map_or(worker::wasm_bindgen::JsValue::NULL, Into::into),
Merge checks: statuses and check runs on every commit246 check_run_id.map_or(worker::wasm_bindgen::JsValue::NULL, Into::into),
Icons are cached at the edge247 ])?
248 .run()
249 .await?;
250 if a.state == "pending" {
251 return Ok(Outcome::Ok(true));
252 }
253 // Once every workflow on a pull request's head has finished, its
254 // lifecycle moves on, as it does when its checks finish.
Fast pages, required checks on the branch, self-hosted runners, honest incidents255 let facts = self.facts(&a.repo_id, Some(&a.sha)).await?;
Icons are cached at the edge256 if !facts.pending.is_empty() {
257 return Ok(Outcome::Ok(true));
258 }
259 // A merge queue state waiting on its merge_group workflows.
Fast pages, required checks on the branch, self-hosted runners, honest incidents260 self.merge_group_finished(&a.repo_id, &a.sha, &facts).await?;
Icons are cached at the edge261 let heads = self
262 .db
263 .prepare("SELECT id, number FROM pulls WHERE repo_id = ? AND head_commit = ? AND status IN ('draft', 'open')")
264 .bind(&[a.repo_id.as_str().into(), a.sha.as_str().into()])?
265 .all()
266 .await?
267 .results::<HeadRow>()?;
268 for head in heads {
269 // A pull request g1t stopped on picks back up once what stopped
270 // it passes: its workflows, and its checks if it has any. The
271 // lifecycle then decides again, within its usual limits.
272 if facts.failed.is_empty() {
273 let resumed = self
274 .db
275 .prepare(
276 "UPDATE pulls SET stalled = NULL WHERE id = ? AND managed = 1 AND stalled IS NOT NULL
277 AND (check_status IS NULL OR check_status = 'passed') RETURNING id AS value",
278 )
279 .bind(&[head.id.as_str().into()])?
280 .first::<crate::rows::ValueRow>(None)
281 .await?;
282 if resumed.is_some() {
Events: review requests, assignments, stops and deployments are published283 self.announce_resumed(&head.id, None).await?;
Icons are cached at the edge284 self.note(
285 &a.repo_id,
286 head.number,
287 (crate::lifecycle::POLICY_ACTOR_ID, crate::lifecycle::POLICY_ACTOR_NAME),
288 "picked this back up: its workflows pass now",
289 )
290 .await?;
291 }
292 }
293 self.publish_as(
294 "checks.completed",
295 &a.repo_id,
296 None,
297 ChecksEvent {
298 pull_id: head.id,
299 repo_id: a.repo_id.clone(),
300 number: head.number,
301 status: if facts.failed.is_empty() { "passed" } else { "failed" },
302 commit: a.sha.clone(),
303 },
304 )
305 .await?;
306 }
307 Ok(Outcome::Ok(true))
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 fn status(context: &str, state: &str) -> CommitStatus {
316 CommitStatus {
317 context: context.into(),
318 state: state.into(),
319 description: None,
320 target_url: None,
321 updated_at: String::new(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge322 source: None,
Merge checks: statuses and check runs on every commit323 check_run_id: None,
Icons are cached at the edge324 }
325 }
326
Fast pages, required checks on the branch, self-hosted runners, honest incidents327 fn names(list: &[&str]) -> Vec<String> {
328 list.iter().map(|name| (*name).to_owned()).collect()
329 }
330
331 #[test]
332 fn only_required_checks_hold_a_merge() {
333 let statuses = [status("CI / push", "pending"), status("Lint / pull_request", "failure"), status("Docs", "success")];
334 // Nothing required: nothing holds it, whatever failed.
335 let free = WorkflowFacts::of(&statuses, &[]);
336 assert_eq!(free.pending, ["CI / push"]);
337 assert_eq!(free.failed, ["Lint / pull_request"]);
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge338 assert!(free.required_failed().is_empty() && free.expected().is_empty());
Fast pages, required checks on the branch, self-hosted runners, honest incidents339 let both = WorkflowFacts::of(&statuses, &names(&["CI", "Lint"]));
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge340 assert_eq!(both.required_failed(), ["Lint"]);
341 assert!(WorkflowFacts::of(&[status("Docs", "success")], &names(&["Docs"])).required_failed().is_empty());
Fast pages, required checks on the branch, self-hosted runners, honest incidents342 }
343
344 #[test]
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge345 fn a_required_check_nothing_reported_is_expected() {
Fast pages, required checks on the branch, self-hosted runners, honest incidents346 let facts = WorkflowFacts::of(&[status("CI / pull_request", "success")], &names(&["CI", "Deploy"]));
347 assert_eq!(facts.expected(), ["Deploy"]);
348 }
349
Icons are cached at the edge350 #[test]
Fast pages, required checks on the branch, self-hosted runners, honest incidents351 fn seen_checks_are_named_once_with_their_events() {
352 let rows = vec![
353 ("CI / push".to_owned(), "2026-10-01T00:00:00Z".to_owned()),
354 ("CI / pull_request".to_owned(), "2026-10-03T00:00:00Z".to_owned()),
355 ("g1t / deploy".to_owned(), "2026-10-02T00:00:00Z".to_owned()),
356 ];
357 let seen = seen(rows);
358 assert_eq!(seen.len(), 2);
359 assert_eq!(seen[0].name, "CI");
360 assert_eq!(seen[0].events, ["pull_request", "push"]);
361 assert_eq!(seen[0].last_seen, "2026-10-03T00:00:00Z");
362 assert_eq!(seen[1].name, "g1t / deploy");
363 assert!(seen[1].events.is_empty());
Icons are cached at the edge364 }
365}

This file's history is long; its oldest lines are credited to the oldest commit read.