Skip to content

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

Commit

Merge branch 'worktree-agent-aa617b6b7205068c2' into ship-fixes

# Conflicts: # apps/web/app/routes/repo/pull.tsx # services/work/src/lib.rs # services/work/src/lifecycle.rs

syntaqxcommitted Parents321b50ecf2c743Browse files
6 files+355−290/6 viewed
+11−1
522522
523523 A pull request that only has fallen behind its target, without conflicts,
524524 still merges: merging brings it up to date first, unless the repository
525−requires pull requests to be up to date.
525+requires pull requests to be up to date. Bringing it up to date makes a new
526+commit, so when the target branch requires checks, the merge box says
527+**Merging once checks pass** and the pull request lands when the required
528+checks pass on that commit, with nothing more to press. If one of them fails
529+there, it is not merged: the pull request says which check failed, and you
530+push a fix and merge again. With no required checks, it lands as soon as it
531+is up to date.
532+
533+If the repository does not answer in time while merging, the merge box says
534+g1t couldn't reach the repository. Merge again: if the first merge went
535+through meanwhile, merging again records it rather than merging twice.
526536
527537 ## Keeping it up to date
528538
+27−4
3838 type RepoSettings,
3939 type SessionEntry,
4040 type Viewer,
41+ fail,
4142 pullComparison,
4243 workOwner,
4344 } from "@g1t/contracts";
516517 }
517518 const result =
518519 action === "merge"
519− ? await work.mergePull(user, path, number, mergeOptions(form))
520+ ? // A merge that did not answer in time (lib/deadline.ts) or broke
521+ // off says so in the merge box, not as an error page: trying again
522+ // is safe, as a merge that went through meanwhile is recognised.
523+ await work
524+ .mergePull(user, path, number, mergeOptions(form))
525+ .catch(() => fail("unavailable", "g1t couldn't reach the repository to merge this. Try again in a moment."))
520526 : action === "unqueue"
521527 ? await work.removeFromQueue(user, path, number)
522528 : action === "close"
688694 );
689695 }
690696
697+/** The required checks a caught-up merge still waits for: "CI passes", "CI and Deploy pass". */
698+function awaitedChecks(checks: { name: string; state: string }[]): string {
699+ const names = checks.filter((check) => check.state !== "success").map((check) => check.name);
700+ if (names.length === 0) return "the required checks pass";
701+ if (names.length === 1) return `the required check ${names[0]} passes`;
702+ return `the required checks ${names.slice(0, -1).join(", ")} and ${names[names.length - 1]} pass`;
703+}
704+
691705 export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
692706 const {
693707 pull,
12671281 </StatusRow>
12681282 )}
12691283 {landing ? (
1284+ // Brought up to date first, then landed once the
1285+ // required checks pass on that new commit.
12701286 <StatusRow
12711287 icon={<Loader size={16} className="animate-spin text-accent" />}
1272− title="Merging"
1288+ title={!behind && unchecked ? "Merging once checks pass" : "Merging"}
12731289 >
1274− {defaultBranch} has moved, so it is being brought up to date first. It
1275− lands as soon as that is done.
1290+ {behind
1291+ ? `${defaultBranch} has moved, so it is being brought up to date first. ${
1292+ requiredChecks.length > 0
1293+ ? "It lands once the required checks pass on that commit."
1294+ : "It lands as soon as that is done."
1295+ }`
1296+ : unchecked
1297+ ? `Brought up to date with ${defaultBranch}. It lands once ${awaitedChecks(requiredChecks)} on that commit.`
1298+ : `Brought up to date with ${defaultBranch}. Landing now.`}
12761299 </StatusRow>
12771300 ) : conflicting ? (
12781301 <ConflictsSection
+60−0
6464 read_answer(method, &text)
6565 }
6666
67+/// Calls `method` like [`call`], waiting at most `within` for the answer:
68+/// `None` when none came in time. Nothing on Workers ends a subrequest that
69+/// stops answering, so a caller that must answer someone (a click on Merge)
70+/// bounds the calls it makes this way and says what happened instead of
71+/// hanging until its own caller gives up. The call is not cancelled: what
72+/// it was doing may still finish, so only a call that is safe to make
73+/// again (or whose late result is harmless) should be bounded.
74+pub async fn call_within<A: Serialize, R: DeserializeOwned>(
75+ service: &Fetcher,
76+ method: &str,
77+ arguments: &A,
78+ within: std::time::Duration,
79+) -> Result<Option<R>> {
80+ match first_of(call(service, method, arguments), worker::Delay::from(within)).await {
81+ Some(answer) => answer.map(Some),
82+ None => {
83+ worker::console_error!("{method} did not answer within {} s", within.as_secs());
84+ Ok(None)
85+ }
86+ }
87+}
88+
89+/// `work`'s output if it finishes before `deadline` does, `None` otherwise.
90+pub async fn first_of<W: std::future::Future, D: std::future::Future>(work: W, deadline: D) -> Option<W::Output> {
91+ use std::task::Poll;
92+ let mut work = Box::pin(work);
93+ let mut deadline = Box::pin(deadline);
94+ std::future::poll_fn(|cx| {
95+ if let Poll::Ready(output) = work.as_mut().poll(cx) {
96+ return Poll::Ready(Some(output));
97+ }
98+ if deadline.as_mut().poll(cx).is_ready() {
99+ return Poll::Ready(None);
100+ }
101+ Poll::Pending
102+ })
103+ .await
104+}
105+
106+#[cfg(test)]
107+mod deadline_tests {
108+ use super::first_of;
109+ use std::future::{pending, ready};
110+ use std::task::{Context, Poll, Waker};
111+
112+ fn now<F: std::future::Future>(future: F) -> Poll<F::Output> {
113+ let mut future = Box::pin(future);
114+ future.as_mut().poll(&mut Context::from_waker(Waker::noop()))
115+ }
116+
117+ #[test]
118+ fn an_answer_in_time_is_passed_through_and_a_late_one_is_not_waited_for() {
119+ assert_eq!(now(first_of(ready(7), pending::<()>())), Poll::Ready(Some(7)));
120+ assert_eq!(now(first_of(pending::<u8>(), ready(()))), Poll::Ready(None));
121+ // Both ready: the answer wins.
122+ assert_eq!(now(first_of(ready(1), ready(()))), Poll::Ready(Some(1)));
123+ assert_eq!(now(first_of(pending::<u8>(), pending::<()>())), Poll::Pending);
124+ }
125+}
126+
67127 /// A method's answer, read from its body.
68128 pub fn read_answer<R: DeserializeOwned>(method: &str, text: &str) -> Result<R> {
69129 serde_json::from_str(text).map_err(|error| {
+26−7
22872287 check!(allowed(Some(&a.actor), &repo, Capability::Merge));
22882288 self.record_merge_evaluations(&repo, &pull, &gate).await;
22892289 // Brought up to date here, without a sandbox: the merge is
2290− // recorded first, and lands when the push that catches it up
2291− // arrives (lifecycle.rs `land_if_requested`).
2292− self.request_landing(&pull, &a.actor, a.keep_issue_open, a.delete_branch)
2293− .await?;
2294− let updated: Outcome<PullBranchUpdate> = g1t_kit::call(
2290+ // recorded first, and lands once the push that catches it up
2291+ // arrives and the required checks pass on that commit
2292+ // (lifecycle.rs `land_if_requested`, `land_when_checked`).
2293+ // Merging past failing checks, where that is allowed, carries
2294+ // over: it then lands without waiting for them.
2295+ self.request_landing(
2296+ &pull,
2297+ &a.actor,
2298+ a.keep_issue_open,
2299+ a.delete_branch,
2300+ a.ignore_checks && settings.allow_ignoring_checks,
2301+ )
2302+ .await?;
2303+ // Bounded: a repository that does not answer gets a clear
2304+ // failure back to whoever clicked, not a request that hangs
2305+ // until the site gives up on it.
2306+ let updated: Option<Outcome<PullBranchUpdate>> = g1t_kit::call_within(
22952307 &self.repos,
22962308 "update_pull_branch",
22972309 &UpdatePullBranchArgs {
23052317 method: if requirements.linear_history { UpdateMethod::Rebase } else { UpdateMethod::Merge },
23062318 expected_head: pull.head_commit.clone(),
23072319 },
2320+ lifecycle::REPOS_WRITE_WITHIN,
23082321 )
23092322 .await?;
2323+ // A push that still arrives later only brings it up to date.
2324+ let updated = updated.unwrap_or_else(lifecycle::repository_unreachable);
23102325 match updated {
23112326 Outcome::Ok(PullBranchUpdate::Updated { .. }) => return Ok(Outcome::Ok(pull)),
23122327 // Moved again in between, or already caught up: land it now.
23412356
23422357 // Whether the actor may write to the repository is decided by repos.
23432358 let head = pull.head_commit.clone();
2344− let landed: Outcome<Landed> = g1t_kit::call(
2359+ // Bounded like the catch-up above. Landing the same checked commit
2360+ // again is recognised as done (repos `land`), so trying again after
2361+ // a land that finished late records it.
2362+ let landed: Option<Outcome<Landed>> = g1t_kit::call_within(
23452363 &self.repos,
23462364 "land",
23472365 &LandArgs {
23582376 author: merging::squash_author(&pull, method),
23592377 sign_off: settings.require_web_commit_sign_off,
23602378 },
2379+ lifecycle::REPOS_WRITE_WITHIN,
23612380 )
23622381 .await?;
2363− let landed = check!(landed);
2382+ let landed = check!(landed.unwrap_or_else(lifecycle::repository_unreachable));
23642383 self.record_merge_evaluations(&repo, &pull, &gate).await;
23652384 let merged = self.record_merge(&repo, pull, &a.actor, a.keep_issue_open, landed).await?;
23662385 // Its branch, where the merger or the repository says it goes.
+224−16
88 //! branch's rules ask for is met, attributed to `g1t` (`POLICY_ACTOR_ID`).
99 //!
1010 //! A merge asked for while a pull request is behind is recorded here
11−//! (`request_landing`) and lands once the push that brings it up to date
12−//! arrives (`land_if_requested`).
11+//! (`request_landing`). Once the push that brings it up to date arrives
12+//! (`land_if_requested`), it lands when the checks the branch requires
13+//! pass on that new commit, as they must for any merge (`land_when_checked`,
14+//! also called as each of the commit's checks finishes).
1315
1416 use g1t_contracts::repos::{GetByIdArgs, Repo, RepoPath};
1517 use g1t_contracts::time::rfc3339;
3133 /// How long a claimed step is waited for before it may be taken again.
3234 const CATCH_UP_MINUTES: u64 = 30;
3335 const MERGE_MINUTES: u64 = 2;
36+/// How long a merge that was brought up to date waits for the required
37+/// checks on the new commit; after that it is no longer pending.
38+const CHECKS_MINUTES: u64 = 180;
39+/// The step under way while it does (`working_on`).
40+const WAITING_FOR_CHECKS: &str = "checks";
41+
42+/// How long a merge waits on the repository service for one write (the
43+/// catch-up push, the landing): long enough for a large merge, short
44+/// enough that the person who clicked hears back well before the site
45+/// stops waiting on the merge (apps/web/app/lib/deadline.ts, 60 s).
46+pub(crate) const REPOS_WRITE_WITHIN: std::time::Duration = std::time::Duration::from_secs(25);
47+
48+/// What a merge says when the repository service did not answer in time.
49+pub(crate) fn repository_unreachable<T>() -> Outcome<T> {
50+ Outcome::fail(
51+ FailureCode::Unavailable,
52+ "g1t couldn't reach the repository to merge this. Try again in a moment.",
53+ )
54+}
55+
56+/// What becomes of a merge that was brought up to date, by the checks the
57+/// branch requires on the commit that did it.
58+#[derive(Debug, PartialEq)]
59+pub(crate) enum Landing {
60+ /// None are required, or all of them passed.
61+ Now,
62+ /// Some are still running or have not reported yet.
63+ Wait,
64+ /// One failed: a person has to push a fix.
65+ Stall(String),
66+}
67+
68+/// Whether a merge that was brought up to date lands now, by its new
69+/// commit's checks. The same checks hold it as hold any merge
70+/// (`merge_gate`): landing it before they report would only be refused.
71+pub(crate) fn landing_step(facts: &WorkflowFacts) -> Landing {
72+ let failed = facts.required_failed();
73+ if !failed.is_empty() {
74+ let one = failed.len() == 1;
75+ return Landing::Stall(format!(
76+ "It was brought up to date, but the required {} {} failed on that commit, so it was not merged. Push a fix, then merge again.",
77+ if one { "check" } else { "checks" },
78+ statuses::list(&failed),
79+ ));
80+ }
81+ if facts.required.iter().any(|check| check.state != RequiredState::Success) {
82+ return Landing::Wait;
83+ }
84+ Landing::Now
85+}
86+
3487 /// Who a merge made by a repository's settings is attributed to. Not an
3588 /// account: `g1t` cannot be registered.
3689 pub(crate) const POLICY_ACTOR_ID: &str = "g1t_policy";
59112 /// when absent.
60113 #[serde(default)]
61114 delete_branch: Option<bool>,
115+ /// The merger asked to merge past the checks, and the repository
116+ /// allows that: it lands without waiting for them.
117+ #[serde(default)]
118+ ignore_checks: bool,
62119 }
63120
64121 #[derive(Deserialize)]
66123 land_requested: Option<String>,
67124 land_requested_at: Option<String>,
68125 stalled: Option<String>,
126+ #[serde(default)]
127+ working_on: Option<String>,
69128 }
70129
71130 /// What should happen next, if it is g1t's turn.
163222 return wait(Stage::CatchingUp, "Bringing it up to date with the branch it will land on.");
164223 }
165224 Some("merge") => return wait(Stage::Ready, "Merging."),
225+ Some(WAITING_FOR_CHECKS) => {
226+ return wait(
227+ Stage::Checking,
228+ "Brought up to date. It lands once the required checks pass on that commit.",
229+ );
230+ }
166231 _ => {}
167232 }
168233 if let Some(name) = &facts.person_request {
526591 actor: &User,
527592 keep_issue_open: bool,
528593 delete_branch: Option<bool>,
594+ ignore_checks: bool,
529595 ) -> Result<()> {
530596 let now = now_ms();
531597 let request = serde_json::to_string(&LandRequest {
532598 actor: actor.clone(),
533599 keep_issue_open,
534600 delete_branch,
601+ ignore_checks,
535602 })?;
536603 self.db
537604 .prepare(
561628 /// enough to still stand.
562629 async fn land_request(&self, pull_id: &str) -> Result<Option<LandRequest>> {
563630 let row = self.land_row(pull_id).await?;
564− let oldest = rfc3339(now_ms().saturating_sub(CATCH_UP_MINUTES * 60 * 1000));
631+ let now = now_ms();
565632 Ok(row
566633 .filter(|row| {
634+ // Waiting for checks on the caught-up commit stands longer
635+ // than waiting for the push.
636+ let minutes = if row.working_on.as_deref() == Some(WAITING_FOR_CHECKS) {
637+ CHECKS_MINUTES
638+ } else {
639+ CATCH_UP_MINUTES
640+ };
641+ let oldest = rfc3339(now.saturating_sub(minutes * 60 * 1000));
567642 row.land_requested_at
568643 .as_deref()
569644 .is_some_and(|at| at >= oldest.as_str())
586661 return found.first::<LandRow>(Slot::Pull);
587662 }
588663 self.db
589− .prepare("SELECT land_requested, land_requested_at, stalled FROM pulls WHERE id = ?")
664+ .prepare("SELECT land_requested, land_requested_at, stalled, working_on FROM pulls WHERE id = ?")
590665 .bind(&[pull_id.into()])?
591666 .first::<LandRow>(None)
592667 .await
593668 }
594669
595670 /// Forgets a merge that was waiting on a pull request, and the step
596− /// that was bringing it up to date.
671+ /// that was bringing it up to date or waiting for its checks.
597672 pub(crate) async fn forget_landing(&self, pull_id: &str) -> Result<()> {
598− self.db
673+ self.take_landing(pull_id).await?;
674+ Ok(())
675+ }
676+
677+ /// Forgets a merge that was waiting on a pull request and returns it,
678+ /// in one statement, so that of two callers deciding to land it at
679+ /// once (two checks finishing together) only one does.
680+ async fn take_landing(&self, pull_id: &str) -> Result<Option<LandRequest>> {
681+ // SET sees the row as it was: the CASEs read the old `working_on`.
682+ let taken = self
683+ .db
599684 .prepare(
600685 "UPDATE pulls SET land_requested = NULL, land_requested_at = NULL,
601− working_on = CASE WHEN working_on = 'catch_up' THEN NULL ELSE working_on END,
602− working_until = CASE WHEN working_on = 'catch_up' THEN NULL ELSE working_until END
603− WHERE id = ?",
686+ working_on = CASE WHEN working_on IN ('catch_up', 'checks') THEN NULL ELSE working_on END,
687+ working_until = CASE WHEN working_on IN ('catch_up', 'checks') THEN NULL ELSE working_until END
688+ WHERE id = ? AND land_requested IS NOT NULL
689+ RETURNING land_requested AS value",
604690 )
605691 .bind(&[pull_id.into()])?
692+ .first::<ValueRow>(None)
693+ .await?;
694+ Ok(taken.and_then(|row| serde_json::from_str(&row.value).ok()))
695+ }
696+
697+ /// Notes that a merge which was brought up to date waits for the
698+ /// required checks on its new commit, from now: on the first look at
699+ /// this commit (a later push clears the step and starts it again).
700+ async fn wait_for_checks(&self, pull_id: &str) -> Result<()> {
701+ let now = now_ms();
702+ self.db
703+ .prepare(
704+ "UPDATE pulls SET working_on = 'checks', working_until = ?1, land_requested_at = ?2
705+ WHERE id = ?3 AND land_requested IS NOT NULL AND status = 'open'
706+ AND (working_on IS NULL OR working_on <> 'checks')",
707+ )
708+ .bind(&[
709+ rfc3339(now + CHECKS_MINUTES * 60 * 1000).into(),
710+ rfc3339(now).into(),
711+ pull_id.into(),
712+ ])?
606713 .run()
607714 .await?;
608715 Ok(())
609716 }
610717
611− /// Lands a pull request whose head has just moved, if a merge of it was
612− /// waiting for exactly that. What it was caught up to was already
613− /// checked and reviewed apart from the merge, so the checks are not
614− /// waited for again; a repository that wants them rerun turns on
615− /// "require up to date", and then nothing is landed this way.
718+ /// Moves on a merge that was waiting for a pull request's head to move
719+ /// (the push that brought it up to date): it lands once the checks the
720+ /// branch requires pass on that commit ([`Work::land_when_checked`]).
721+ /// The commit is new, so they are waited for, as for any merge: a
722+ /// ruleset that requires them refuses a merge that skips them.
616723 pub(crate) async fn land_if_requested(&self, pull_id: &str) -> Result<()> {
724+ if self.land_request(pull_id).await?.is_none() {
725+ return Ok(());
726+ }
727+ self.land_when_checked(pull_id).await
728+ }
729+
730+ /// Lands a pull request that was brought up to date for a merge, once
731+ /// the checks the branch requires pass on its head: at once when none
732+ /// are required, never when one failed (it stops and says which), and
733+ /// otherwise when the last of them reports (statuses.rs `store_status`
734+ /// calls this as the commit's checks finish).
735+ pub(crate) async fn land_when_checked(&self, pull_id: &str) -> Result<()> {
617736 let Some(request) = self.land_request(pull_id).await? else {
618737 return Ok(());
619738 };
620− self.forget_landing(pull_id).await?;
621739 let Some(pull) = self.pull_by_id(pull_id).await? else {
622740 return Ok(());
623741 };
742+ if pull.status != PullStatus::Open {
743+ return self.forget_landing(pull_id).await;
744+ }
624745 let repo: Outcome<Repo> = g1t_kit::call(
625746 &self.repos,
626747 "get_by_id",
631752 )
632753 .await?;
633754 let Outcome::Ok(repo) = crate::retired::unless_archived(repo) else {
755+ return self.forget_landing(pull_id).await;
756+ };
757+ let step = if request.ignore_checks {
758+ Landing::Now
759+ } else {
760+ let settings = self.settings_on(&repo, &pull).await?;
761+ let statuses = self.statuses(&pull.repo_id, pull.head_commit.as_deref()).await?;
762+ let facts = self
763+ .workflow_facts(&pull.repo_id, pull.head_commit.as_deref(), &statuses, &settings.required_checks)
764+ .await?;
765+ landing_step(&facts)
766+ };
767+ match step {
768+ Landing::Wait => return self.wait_for_checks(pull_id).await,
769+ Landing::Stall(reason) => {
770+ self.stall(StallArgs {
771+ pull_id: pull.id,
772+ reason,
773+ by: None,
774+ })
775+ .await?;
776+ return Ok(());
777+ }
778+ Landing::Now => {}
779+ }
780+ // Taken by one caller: two checks finishing together land it once.
781+ let Some(request) = self.take_landing(pull_id).await? else {
634782 return Ok(());
635783 };
636784 let merged = self
643791 number: pull.number,
644792 summary: String::new(),
645793 keep_issue_open: request.keep_issue_open,
646− ignore_checks: true,
794+ ignore_checks: request.ignore_checks,
647795 bypass_rules: false,
648796 // Only a fast-forward waits to be brought up to date.
649797 merge_method: Some(g1t_contracts::rules::MergeMethod::FastForward),
8701018 }
8711019
8721020 #[test]
1021+ fn a_merge_brought_up_to_date_lands_once_its_required_checks_pass_on_the_new_commit() {
1022+ // The catch-up's merge commit, just pushed: nothing has reported yet.
1023+ assert_eq!(landing_step(&checks(&[], &["CI"])), Landing::Wait);
1024+ assert_eq!(landing_step(&checks(&[("CI / pull_request", "pending")], &["CI"])), Landing::Wait);
1025+ assert_eq!(landing_step(&checks(&[("CI / pull_request", "success")], &["CI"])), Landing::Now);
1026+ // Every required one, not just the first to report.
1027+ assert_eq!(
1028+ landing_step(&checks(&[("CI / pull_request", "success")], &["CI", "Deploy"])),
1029+ Landing::Wait
1030+ );
1031+ // Nothing required: it lands at once, whatever else runs or failed.
1032+ assert_eq!(landing_step(&checks(&[], &[])), Landing::Now);
1033+ assert_eq!(landing_step(&checks(&[("Lint / pull_request", "failure")], &[])), Landing::Now);
1034+ assert_eq!(
1035+ landing_step(&checks(&[("CI / pull_request", "success"), ("Lint / pull_request", "failure")], &["CI"])),
1036+ Landing::Now
1037+ );
1038+ // A required one that failed stops it, saying which.
1039+ let Landing::Stall(reason) = landing_step(&checks(&[("CI / pull_request", "failure")], &["CI"])) else {
1040+ panic!("a failing required check must stop it");
1041+ };
1042+ assert_eq!(
1043+ reason,
1044+ "It was brought up to date, but the required check CI failed on that commit, so it was not merged. Push a fix, then merge again."
1045+ );
1046+ let Landing::Stall(reason) =
1047+ landing_step(&checks(&[("CI / push", "error"), ("Deploy", "failure")], &["CI", "Deploy"]))
1048+ else {
1049+ panic!("failing required checks must stop it");
1050+ };
1051+ assert!(reason.contains("required checks CI and Deploy failed"), "{reason}");
1052+ }
1053+
1054+ #[test]
1055+ fn a_merge_that_cannot_reach_the_repository_says_so() {
1056+ let Outcome::Fail(failure) = repository_unreachable::<()>() else {
1057+ panic!("it must fail");
1058+ };
1059+ assert_eq!(failure.code, FailureCode::Unavailable);
1060+ assert!(failure.message.contains("couldn't reach the repository"));
1061+ assert!(failure.message.contains("Try again"));
1062+ // Well inside the site's own 60 s wait on a merge, catch-up and landing both.
1063+ assert!(REPOS_WRITE_WITHIN.as_secs() * 2 < 60);
1064+ }
1065+
1066+ #[test]
1067+ fn a_landing_asked_for_before_it_carried_ignore_checks_still_reads() {
1068+ let old: LandRequest = serde_json::from_value(serde_json::json!({
1069+ "actor": serde_json::to_value(User::default()).unwrap(),
1070+ "keepIssueOpen": true,
1071+ }))
1072+ .unwrap();
1073+ assert!(old.keep_issue_open && !old.ignore_checks);
1074+ }
1075+
1076+ #[test]
8731077 fn drafts_steps_under_way_and_stops_wait() {
1078+ assert_eq!(
1079+ outcome(Facts { working_on: Some(WAITING_FOR_CHECKS.into()), auto_merge: true, ..facts() }),
1080+ (Stage::Checking, "wait")
1081+ );
8741082 assert_eq!(outcome(Facts { draft: true, ..facts() }), (Stage::Working, "wait"));
8751083 assert_eq!(outcome(Facts { working_on: Some("catch_up".into()), auto_merge: true, ..facts() }), (Stage::CatchingUp, "wait"));
8761084 assert_eq!(outcome(Facts { working_on: Some("merge".into()), auto_merge: true, ..facts() }), (Stage::Ready, "wait"));
+7−1
439439 &a.repo_id,
440440 None,
441441 ChecksEvent {
442− pull_id: head.id,
442+ pull_id: head.id.clone(),
443443 repo_id: a.repo_id.clone(),
444444 number: head.number,
445445 status: if facts.failed.is_empty() { "passed" } else { "failed" },
447447 },
448448 )
449449 .await?;
450+ // A merge that brought it up to date and waits for its required
451+ // checks on this commit lands, or stops on one that failed. The
452+ // status is kept either way.
453+ if let Err(error) = self.land_when_checked(&head.id).await {
454+ worker::console_error!("landing {} after its checks: {error}", head.id);
455+ }
450456 }
451457 Ok(Outcome::Ok(true))
452458 }

Comments

Sign in to comment.