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.
- Claude-Session:
- https://claude.ai/code/session_01PnwTRSNaC7vEHTAuBzjhrn
7 files+108−90/7 viewed
| 106 | 106 | must report it, such as workflows or deployments. | |
| 107 | 107 | ||
| 108 | 108 | A required check is met by a status of that name on the pull request's head, | |
| 109 | − | whatever event reported it: | |
| 109 | + | whatever event reported it, or by a [g1t Actions](/guides/actions/) job | |
| 110 | + | of that name, as on GitHub. A job's check is named as GitHub names it: | |
| 111 | + | the job's `name:` (or its id), with a matrix combination's values after it | |
| 112 | + | in brackets, such as `test` or `build (ubuntu-latest, 22)`, and | |
| 113 | + | `<calling job> / <job>` for a job of a called workflow. A branch rule | |
| 114 | + | copied from GitHub that requires `test` is met by the `test` job, and one | |
| 115 | + | that requires the workflow's name, such as `CI`, is met by the whole run. | |
| 116 | + | Type a job's name to require it: the list offers the names of statuses. | |
| 117 | + | Each job's latest attempt counts. One that was skipped passes the check, | |
| 118 | + | and one that was cancelled fails it. | |
| 110 | 119 | ||
| 111 | 120 | | What reported it | What the merge does | | |
| 112 | 121 | | --- | --- | |
| 483 | 483 | out | |
| 484 | 484 | } | |
| 485 | 485 | ||
| 486 | + | /// The latest run of each workflow on `sha` (`runs` newest first, as the | |
| 487 | + | /// actions service lists them), each of its jobs as the status its check | |
| 488 | + | /// run stands as: named as the check run is (the job's name, such as | |
| 489 | + | /// `test` or `build (ubuntu-latest)`), so a required check named for a job | |
| 490 | + | /// is met by it, as on GitHub. A job still waiting or going is pending; one | |
| 491 | + | /// that passed, was skipped or was neutral passed; any other ending failed. | |
| 492 | + | pub fn job_statuses(runs: &[(WorkflowRun, Vec<Job>)], sha: &str) -> Vec<CommitStatus> { | |
| 493 | + | latest_runs(runs) | |
| 494 | + | .into_iter() | |
| 495 | + | .filter(|(run, _)| run.sha == sha) | |
| 496 | + | .flat_map(|(run, jobs)| { | |
| 497 | + | jobs.iter().map(move |job| { | |
| 498 | + | let state = match (job.status.as_str(), job.conclusion.as_deref()) { | |
| 499 | + | ("completed", Some("success" | "skipped" | "neutral")) => "success", | |
| 500 | + | ("completed", _) => "failure", | |
| 501 | + | _ => "pending", | |
| 502 | + | }; | |
| 503 | + | CommitStatus { | |
| 504 | + | context: job.name.clone(), | |
| 505 | + | state: state.to_owned(), | |
| 506 | + | description: job.reason.clone(), | |
| 507 | + | target_url: None, | |
| 508 | + | updated_at: job.finished_at.clone().or_else(|| job.started_at.clone()).unwrap_or_else(|| run.created_at.clone()), | |
| 509 | + | source: Some("actions".to_owned()), | |
| 510 | + | check_run_id: Some(job.id.clone()), | |
| 511 | + | } | |
| 512 | + | }) | |
| 513 | + | }) | |
| 514 | + | .collect() | |
| 515 | + | } | |
| 516 | + | ||
| 486 | 517 | // --- A commit's checks, added up ----------------------------------------- | |
| 487 | 518 | ||
| 488 | 519 | /// Every check on one commit, as its list shows them: each check run, and | |
| ⋯ | |||
| 1025 | 1056 | assert_eq!(check.output.annotations_count, 1); | |
| 1026 | 1057 | assert_eq!(job_annotations(&job)[0].annotation_level, "failure"); | |
| 1027 | 1058 | let waiting = Job { status: "waiting".into(), conclusion: None, ..job.clone() }; | |
| 1028 | − | let suite = run_check_suite(&workflow_run, &[job, waiting]); | |
| 1059 | + | let suite = run_check_suite(&workflow_run, &[job.clone(), waiting.clone()]); | |
| 1029 | 1060 | assert_eq!((suite.status.as_str(), suite.head_branch.as_deref()), ("in_progress", Some("main"))); | |
| 1030 | 1061 | assert_eq!(suite.name.as_deref(), Some("CI")); | |
| 1062 | + | ||
| 1063 | + | // Each job stands as a status named as GitHub names its check run, | |
| 1064 | + | // from the latest attempt of each workflow only. | |
| 1065 | + | let sha = "b".repeat(40); | |
| 1066 | + | let skipped = Job { id: "job_3".into(), name: "Lint".into(), conclusion: Some("skipped".into()), ..job.clone() }; | |
| 1067 | + | let cancelled = Job { id: "job_4".into(), name: "Build (ubuntu-latest)".into(), conclusion: Some("cancelled".into()), ..job.clone() }; | |
| 1068 | + | let older = WorkflowRun { id: "run_0".into(), attempt: 0, ..workflow_run.clone() }; | |
| 1069 | + | let other = WorkflowRun { sha: "c".repeat(40), ..workflow_run.clone() }; | |
| 1070 | + | let statuses = job_statuses( | |
| 1071 | + | &[ | |
| 1072 | + | (workflow_run.clone(), vec![job.clone(), waiting, skipped, cancelled]), | |
| 1073 | + | (older, vec![Job { conclusion: Some("failure".into()), ..job.clone() }]), | |
| 1074 | + | (other, vec![job.clone()]), | |
| 1075 | + | ], | |
| 1076 | + | &sha, | |
| 1077 | + | ); | |
| 1078 | + | let states: Vec<(&str, &str)> = statuses.iter().map(|s| (s.context.as_str(), s.state.as_str())).collect(); | |
| 1079 | + | assert_eq!(states, [("Test", "success"), ("Test", "pending"), ("Lint", "success"), ("Build (ubuntu-latest)", "failure")]); | |
| 1080 | + | assert_eq!(statuses[0].check_run_id.as_deref(), Some("job_1")); | |
| 1081 | + | assert_eq!(statuses[0].source.as_deref(), Some("actions")); | |
| 1031 | 1082 | } | |
| 1032 | 1083 | ||
| 1033 | 1084 | #[test] | |
| 274 | 274 | } | |
| 275 | 275 | ||
| 276 | 276 | /// g1t Actions' runs and jobs, by commits, by a run or by a job. | |
| 277 | − | async fn actions_runs(&self, args: ActionsChecksArgs) -> Result<Vec<RunDetail>> { | |
| 277 | + | pub(crate) async fn actions_runs(&self, args: ActionsChecksArgs) -> Result<Vec<RunDetail>> { | |
| 278 | 278 | // Without the actions service a commit's other checks still show. | |
| 279 | 279 | Ok(g1t_kit::call(&self.actions, "check_runs", &args).await.unwrap_or_default()) | |
| 280 | 280 | } |
| 1618 | 1618 | ) | |
| 1619 | 1619 | .await?; | |
| 1620 | 1620 | let code_owners = self.pull_code_owners(&pull, &comments, &settings).await?; | |
| 1621 | + | let required = if settings.required_checks.is_empty() { | |
| 1622 | + | Vec::new() | |
| 1623 | + | } else { | |
| 1624 | + | let checked = self.with_job_checks(&repo.id, pull.head_commit.as_deref(), statuses.clone()).await?; | |
| 1625 | + | required_checks(&settings.required_checks, &checked) | |
| 1626 | + | }; | |
| 1621 | 1627 | Ok(Outcome::Ok(PullDetail { | |
| 1622 | − | required_checks: required_checks(&settings.required_checks, &statuses), | |
| 1628 | + | required_checks: required, | |
| 1623 | 1629 | rules: gate.map(|gate| rulesets::merge_rules(&gate.judged, &gate.requirements, pull.base_branch(&repo.default_branch) == repo.default_branch)), | |
| 1624 | 1630 | code_owners, | |
| 1625 | 1631 | comments, |
| 568 | 568 | .is_none_or(|revised| review.finished_at.as_str() >= revised) | |
| 569 | 569 | }); | |
| 570 | 570 | let settings = self.settings_for(pull).await?; | |
| 571 | − | let workflows = WorkflowFacts::of( | |
| 572 | − | &self.statuses(&pull.repo_id, pull.head_commit.as_deref()).await?, | |
| 573 | − | &settings.required_checks, | |
| 574 | − | ); | |
| 571 | + | let statuses = self.statuses(&pull.repo_id, pull.head_commit.as_deref()).await?; | |
| 572 | + | let workflows = self.workflow_facts(&pull.repo_id, pull.head_commit.as_deref(), &statuses, &settings.required_checks).await?; | |
| 575 | 573 | // Once the agent has finished the change: how sure g1t is of it, and | |
| 576 | 574 | // whether that holds it for a person. | |
| 577 | 575 | let (confidence, low_confidence) = if pull.status == PullStatus::Draft { |
| 829 | 829 | }, | |
| 830 | 830 | )?; | |
| 831 | 831 | let reviews = latest_reviews(verdicts); | |
| 832 | + | // A required check named for a g1t Actions job is met by that job. | |
| 833 | + | let requires_checks = | |
| 834 | + | applicable.iter().any(|ruleset| ruleset.rules.iter().any(|entry| matches!(&entry.rule, Rule::RequiredStatusChecks(_)))); | |
| 835 | + | let statuses = if requires_checks { self.with_job_checks(&repo.id, pull.head_commit.as_deref(), statuses).await? } else { statuses }; | |
| 832 | 836 | let facts = MergeFacts { | |
| 833 | 837 | git_ref: git_ref.clone(), | |
| 834 | 838 | agent_change: agent, |
| 139 | 139 | pub(crate) async fn facts(&self, repo_id: &str, sha: Option<&str>) -> Result<WorkflowFacts> { | |
| 140 | 140 | let (statuses, settings) = | |
| 141 | 141 | futures_util::future::try_join(self.statuses(repo_id, sha), self.settings_by_id(repo_id)).await?; | |
| 142 | − | Ok(WorkflowFacts::of(&statuses, &settings.required_checks)) | |
| 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) | |
| 143 | 174 | } | |
| 144 | 175 | ||
| 145 | 176 | /// The check names reported on a repository's commits lately, for |