flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/work/src/statuses.rs

347 lines13,891 bytesCodeBlame

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//!
4//! The default branch's protection names the checks that must pass
5//! (`RepoSettings::required_checks`): a required check that failed, is
6//! still running or has not reported refuses the merge, for everyone and
7//! for the merge queue. Where g1t sees an agent's pull request through,
8//! any check that failed sends the agent back to fix it, with what the
9//! failing jobs printed; once it is out of revisions, only a required
10//! check holds the pull request for a person.
GitHub Actions on g1t, part two: running workflows11
12use g1t_contracts::events::ChecksEvent;
13use g1t_contracts::time::rfc3339;
Fast pages, required checks on the branch, self-hosted runners, honest incidents14use g1t_contracts::work::{
15 CommitStatus, RequiredCheck, RequiredState, SeenCheck, SeenChecksArgs, SetCommitStatusArgs, check_name,
16 required_checks,
17};
GitHub Actions on g1t, part two: running workflows18use g1t_contracts::{FailureCode, Outcome};
19use g1t_kit::now_ms;
20use serde::Deserialize;
21use worker::Result;
22
23use crate::Work;
24
25#[derive(Deserialize)]
26struct StatusRow {
27 context: String,
28 state: String,
29 description: Option<String>,
30 target_url: Option<String>,
31 updated_at: String,
32}
33
34impl From<StatusRow> for CommitStatus {
35 fn from(row: StatusRow) -> Self {
36 CommitStatus {
37 context: row.context,
38 state: row.state,
39 description: row.description,
40 target_url: row.target_url,
41 updated_at: row.updated_at,
42 }
43 }
44}
45
46#[derive(Deserialize)]
47struct HeadRow {
48 id: String,
49 number: u32,
50}
51
Fast pages, required checks on the branch, self-hosted runners, honest incidents52/// What a commit's checks say: every status still running and every one
53/// that failed, by context, and where each required check stands.
54#[derive(Clone, Debug, Default, PartialEq)]
GitHub Actions on g1t, part two: running workflows55pub(crate) struct WorkflowFacts {
56 pub(crate) pending: Vec<String>,
57 pub(crate) failed: Vec<String>,
Fast pages, required checks on the branch, self-hosted runners, honest incidents58 pub(crate) required: Vec<RequiredCheck>,
GitHub Actions on g1t, part two: running workflows59}
60
61impl WorkflowFacts {
Fast pages, required checks on the branch, self-hosted runners, honest incidents62 /// `required` names the checks the default branch's protection requires.
63 pub(crate) fn of(statuses: &[CommitStatus], required: &[String]) -> WorkflowFacts {
GitHub Actions on g1t, part two: running workflows64 WorkflowFacts {
65 pending: statuses.iter().filter(|s| s.state == "pending").map(|s| s.context.clone()).collect(),
66 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 incidents67 required: required_checks(required, statuses),
GitHub Actions on g1t, part two: running workflows68 }
69 }
70
Fast pages, required checks on the branch, self-hosted runners, honest incidents71 fn required_in(&self, state: RequiredState) -> Vec<String> {
72 self.required.iter().filter(|check| check.state == state).map(|check| check.name.clone()).collect()
73 }
74
75 /// The required checks that failed, by name.
76 pub(crate) fn required_failed(&self) -> Vec<String> {
77 self.required_in(RequiredState::Failure)
78 }
79
80 /// The required checks nothing has reported on the commit yet.
81 pub(crate) fn expected(&self) -> Vec<String> {
82 self.required_in(RequiredState::Expected)
83 }
84
85 /// Why a merge has to wait, if it does: a required check that failed,
86 /// is still running, or has not reported. Other checks never hold it.
GitHub Actions on g1t, part two: running workflows87 pub(crate) fn refusal(&self) -> Option<String> {
Fast pages, required checks on the branch, self-hosted runners, honest incidents88 let failed = self.required_failed();
89 if !failed.is_empty() {
90 return Some(format!("The required {} {} failed.", checks_word(&failed), list(&failed)));
GitHub Actions on g1t, part two: running workflows91 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents92 let running = self.required_in(RequiredState::Pending);
93 if !running.is_empty() {
94 let verb = if running.len() == 1 { "is" } else { "are" };
95 return Some(format!("The required {} {} {verb} still running.", checks_word(&running), list(&running)));
GitHub Actions on g1t, part two: running workflows96 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents97 let expected = self.expected();
98 if !expected.is_empty() {
99 let verb = if expected.len() == 1 { "has" } else { "have" };
100 return Some(format!(
101 "The required {} {} {verb} not reported on this commit yet.",
102 checks_word(&expected),
103 list(&expected)
104 ));
105 }
GitHub Actions on g1t, part two: running workflows106 None
107 }
108}
109
Fast pages, required checks on the branch, self-hosted runners, honest incidents110fn checks_word(names: &[String]) -> &'static str {
111 if names.len() == 1 { "check" } else { "checks" }
112}
113
114/// The check names in `(context, last reported)` rows, most recent first:
115/// each name once, with the events it was reported for.
116pub(crate) fn seen(rows: Vec<(String, String)>) -> Vec<SeenCheck> {
117 let mut rows = rows;
118 rows.sort_by(|a, b| b.1.cmp(&a.1));
119 let mut out: Vec<SeenCheck> = Vec::new();
120 for (context, at) in rows {
121 let (name, event) = check_name(&context);
122 match out.iter_mut().find(|seen| seen.name.eq_ignore_ascii_case(name)) {
123 Some(seen) => {
124 if let Some(event) = event
125 && !seen.events.iter().any(|known| known == event)
126 {
127 seen.events.push(event.to_owned());
128 }
129 }
130 None => out.push(SeenCheck {
131 name: name.to_owned(),
132 events: event.map(|event| vec![event.to_owned()]).unwrap_or_default(),
133 last_seen: at,
134 }),
135 }
136 }
137 out
138}
139
140/// How far back `seen_checks` looks, and the most contexts it reads.
141const SEEN_DAYS: u64 = 30;
142const SEEN_LIMIT: u32 = 200;
143
144#[derive(Deserialize)]
145struct SeenRow {
146 context: String,
147 at: String,
148}
149
GitHub Actions on g1t, part two: running workflows150pub(crate) fn list(names: &[String]) -> String {
151 match names {
152 [] => String::new(),
153 [one] => one.clone(),
154 [rest @ .., last] => format!("{} and {last}", rest.join(", ")),
155 }
156}
157
158impl Work {
Fast pages, required checks on the branch, self-hosted runners, honest incidents159 /// Where a commit's checks stand, against the repository's required ones.
160 pub(crate) async fn facts(&self, repo_id: &str, sha: Option<&str>) -> Result<WorkflowFacts> {
161 let (statuses, settings) =
162 futures_util::future::try_join(self.statuses(repo_id, sha), self.settings(repo_id)).await?;
163 Ok(WorkflowFacts::of(&statuses, &settings.required_checks))
164 }
165
166 /// The check names reported on a repository's commits lately, for
167 /// choosing which to require.
168 pub(crate) async fn seen_checks(&self, a: SeenChecksArgs) -> Result<Outcome<Vec<SeenCheck>>> {
169 let repo = match self.repo(&a.repo, &a.viewer).await? {
170 Outcome::Ok(repo) => repo,
171 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
172 };
173 let since = rfc3339(now_ms().saturating_sub(SEEN_DAYS * 24 * 60 * 60 * 1000));
174 let rows = self
175 .db
176 .prepare(
177 "SELECT context, MAX(updated_at) AS at FROM commit_statuses
178 WHERE repo_id = ? AND updated_at >= ? GROUP BY context ORDER BY at DESC LIMIT ?",
179 )
180 .bind(&[repo.id.as_str().into(), since.into(), SEEN_LIMIT.into()])?
181 .all()
182 .await?
183 .results::<SeenRow>()?;
184 Ok(Outcome::Ok(seen(rows.into_iter().map(|row| (row.context, row.at)).collect())))
185 }
186
GitHub Actions on g1t, part two: running workflows187 pub(crate) async fn statuses(&self, repo_id: &str, sha: Option<&str>) -> Result<Vec<CommitStatus>> {
188 let Some(sha) = sha else { return Ok(Vec::new()) };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily189 if let Some(found) = self.prefetched_repo(repo_id).filter(|found| found.head.as_deref() == Some(sha)) {
190 return Ok(found
191 .rows::<StatusRow>(crate::prefetch::Slot::Statuses)?
192 .into_iter()
193 .map(CommitStatus::from)
194 .collect());
195 }
GitHub Actions on g1t, part two: running workflows196 Ok(self
197 .db
198 .prepare("SELECT context, state, description, target_url, updated_at FROM commit_statuses WHERE repo_id = ? AND sha = ? ORDER BY context")
199 .bind(&[repo_id.into(), sha.into()])?
200 .all()
201 .await?
202 .results::<StatusRow>()?
203 .into_iter()
204 .map(CommitStatus::from)
205 .collect())
206 }
207
208 pub(crate) async fn set_commit_status(&self, a: SetCommitStatusArgs) -> Result<Outcome<bool>> {
209 if !matches!(a.state.as_str(), "pending" | "success" | "failure" | "error") {
210 return Ok(Outcome::fail(FailureCode::Invalid, "`state` is pending, success, failure or error."));
211 }
212 self.db
213 .prepare(
214 "INSERT INTO commit_statuses (repo_id, sha, context, state, description, target_url, updated_at)
215 VALUES (?, ?, ?, ?, ?, ?, ?)
216 ON CONFLICT (repo_id, sha, context) DO UPDATE SET
217 state = excluded.state, description = excluded.description,
218 target_url = excluded.target_url, updated_at = excluded.updated_at",
219 )
220 .bind(&[
221 a.repo_id.as_str().into(),
222 a.sha.as_str().into(),
223 a.context.as_str().into(),
224 a.state.as_str().into(),
225 a.description.as_deref().map_or(worker::wasm_bindgen::JsValue::NULL, Into::into),
226 a.target_url.as_deref().map_or(worker::wasm_bindgen::JsValue::NULL, Into::into),
227 rfc3339(now_ms()).into(),
228 ])?
229 .run()
230 .await?;
231 if a.state == "pending" {
232 return Ok(Outcome::Ok(true));
233 }
234 // Once every workflow on a pull request's head has finished, its
235 // lifecycle moves on, as it does when its checks finish.
Fast pages, required checks on the branch, self-hosted runners, honest incidents236 let facts = self.facts(&a.repo_id, Some(&a.sha)).await?;
GitHub Actions on g1t, part two: running workflows237 if !facts.pending.is_empty() {
238 return Ok(Outcome::Ok(true));
239 }
Sidebar: the panels really slide240 // A merge queue state waiting on its merge_group workflows.
Fast pages, required checks on the branch, self-hosted runners, honest incidents241 self.merge_group_finished(&a.repo_id, &a.sha, &facts).await?;
GitHub Actions on g1t, part two: running workflows242 let heads = self
243 .db
244 .prepare("SELECT id, number FROM pulls WHERE repo_id = ? AND head_commit = ? AND status IN ('draft', 'open')")
245 .bind(&[a.repo_id.as_str().into(), a.sha.as_str().into()])?
246 .all()
247 .await?
248 .results::<HeadRow>()?;
249 for head in heads {
A stalled pull request picks back up when its workflows pass250 // A pull request g1t stopped on picks back up once what stopped
251 // it passes: its workflows, and its checks if it has any. The
252 // lifecycle then decides again, within its usual limits.
253 if facts.failed.is_empty() {
254 let resumed = self
255 .db
256 .prepare(
257 "UPDATE pulls SET stalled = NULL WHERE id = ? AND managed = 1 AND stalled IS NOT NULL
258 AND (check_status IS NULL OR check_status = 'passed') RETURNING id AS value",
259 )
260 .bind(&[head.id.as_str().into()])?
261 .first::<crate::rows::ValueRow>(None)
262 .await?;
263 if resumed.is_some() {
264 self.note(
265 &a.repo_id,
266 head.number,
267 (crate::lifecycle::POLICY_ACTOR_ID, crate::lifecycle::POLICY_ACTOR_NAME),
268 "picked this back up: its workflows pass now",
269 )
270 .await?;
271 }
272 }
GitHub Actions on g1t, part two: running workflows273 self.publish_as(
274 "checks.completed",
275 &a.repo_id,
276 None,
277 ChecksEvent {
278 pull_id: head.id,
279 repo_id: a.repo_id.clone(),
280 number: head.number,
281 status: if facts.failed.is_empty() { "passed" } else { "failed" },
282 commit: a.sha.clone(),
283 },
284 )
285 .await?;
286 }
287 Ok(Outcome::Ok(true))
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 fn status(context: &str, state: &str) -> CommitStatus {
296 CommitStatus {
297 context: context.into(),
298 state: state.into(),
299 description: None,
300 target_url: None,
301 updated_at: String::new(),
302 }
303 }
304
Fast pages, required checks on the branch, self-hosted runners, honest incidents305 fn names(list: &[&str]) -> Vec<String> {
306 list.iter().map(|name| (*name).to_owned()).collect()
307 }
308
309 #[test]
310 fn only_required_checks_hold_a_merge() {
311 let statuses = [status("CI / push", "pending"), status("Lint / pull_request", "failure"), status("Docs", "success")];
312 // Nothing required: nothing holds it, whatever failed.
313 let free = WorkflowFacts::of(&statuses, &[]);
314 assert_eq!(free.pending, ["CI / push"]);
315 assert_eq!(free.failed, ["Lint / pull_request"]);
316 assert!(free.refusal().is_none());
317 // Failures come before waiting.
318 let both = WorkflowFacts::of(&statuses, &names(&["CI", "Lint"]));
319 assert_eq!(both.refusal().unwrap(), "The required check Lint failed.");
320 let waiting = WorkflowFacts::of(&[status("A / pull_request", "pending"), status("B", "pending")], &names(&["A", "B"]));
321 assert_eq!(waiting.refusal().unwrap(), "The required checks A and B are still running.");
322 assert!(WorkflowFacts::of(&[status("Docs", "success")], &names(&["Docs"])).refusal().is_none());
323 }
324
325 #[test]
326 fn a_required_check_nothing_reported_holds_a_merge() {
327 let facts = WorkflowFacts::of(&[status("CI / pull_request", "success")], &names(&["CI", "Deploy"]));
328 assert_eq!(facts.expected(), ["Deploy"]);
329 assert_eq!(facts.refusal().unwrap(), "The required check Deploy has not reported on this commit yet.");
330 }
331
GitHub Actions on g1t, part two: running workflows332 #[test]
Fast pages, required checks on the branch, self-hosted runners, honest incidents333 fn seen_checks_are_named_once_with_their_events() {
334 let rows = vec![
335 ("CI / push".to_owned(), "2026-10-01T00:00:00Z".to_owned()),
336 ("CI / pull_request".to_owned(), "2026-10-03T00:00:00Z".to_owned()),
337 ("g1t / deploy".to_owned(), "2026-10-02T00:00:00Z".to_owned()),
338 ];
339 let seen = seen(rows);
340 assert_eq!(seen.len(), 2);
341 assert_eq!(seen[0].name, "CI");
342 assert_eq!(seen[0].events, ["pull_request", "push"]);
343 assert_eq!(seen[0].last_seen, "2026-10-03T00:00:00Z");
344 assert_eq!(seen[1].name, "g1t / deploy");
345 assert!(seen[1].events.is_empty());
GitHub Actions on g1t, part two: running workflows346 }
347}