Skip to content

Commit f6fcf7b

Browse files
Rhinomcdclaude
andcommitted
fix(server): only a failing gate check owes the checks-finished wake
An advisory failure alongside a required check rerun sent checks-finished with an empty failing list. Cover required gates, partial failures, and the wake message. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent ff41395 commit f6fcf7b

2 files changed

Lines changed: 73 additions & 5 deletions

File tree

‎apps/server/src/orchestration-v2/pullRequestWatch.test.ts‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { assert, describe, it } from "@effect/vitest";
88
import {
99
PULL_REQUEST_WATCH_WAKE_LIMIT,
1010
evaluatePullRequestWatch as evaluate,
11+
pullRequestWatchMessage,
1112
} from "./pullRequestWatch.ts";
1213

1314
// These cases read the head once or at one moment, so the no-checks notice never comes due.
@@ -226,6 +227,76 @@ describe("evaluatePullRequestWatch", () => {
226227
{ kind: "checks-failed", failed: [check("e2e", "failure")] },
227228
]);
228229
assert.deepEqual(evaluatePullRequestWatch(failure.next, broke, noRemarks).changes, []);
230+
231+
// A failure while another owed check still runs keeps that one owed.
232+
const unitBroke = detail({
233+
checks: [sonar, check("unit", "failure"), check("e2e", "pending")],
234+
});
235+
const second = evaluatePullRequestWatch(first.next, unitBroke, noRemarks);
236+
assert.deepEqual(second.changes, [
237+
{ kind: "checks-failed", failed: [check("unit", "failure")] },
238+
]);
239+
const settled = detail({ checks: [sonar, check("unit", "failure"), check("e2e", "success")] });
240+
const last = evaluatePullRequestWatch(second.next, settled, noRemarks);
241+
assert.deepEqual(last.changes, [
242+
{ kind: "checks-finished", failing: [sonar, check("unit", "failure")] },
243+
]);
244+
});
245+
246+
it("wakes once when the other required checks finish after a required one failed", () => {
247+
const required = (name: string, status: PullRequestCheck["status"]) => ({
248+
...check(name, status),
249+
required: true,
250+
});
251+
const early = detail({
252+
checks: [
253+
required("lint", "failure"),
254+
required("unit", "pending"),
255+
check("bot", "failure"),
256+
check("e2e", "pending"),
257+
],
258+
});
259+
const first = evaluatePullRequestWatch(watch(), early, noRemarks);
260+
assert.equal(first.changes.length, 1);
261+
assert.deepEqual(first.next.owedChecks, ["unit"]);
262+
263+
const done = detail({
264+
checks: [
265+
required("lint", "failure"),
266+
required("unit", "success"),
267+
check("bot", "failure"),
268+
check("e2e", "pending"),
269+
],
270+
});
271+
const final = evaluatePullRequestWatch(first.next, done, noRemarks);
272+
assert.deepEqual(final.changes, [
273+
{ kind: "checks-finished", failing: [required("lint", "failure")] },
274+
]);
275+
assert.deepEqual(evaluatePullRequestWatch(final.next, done, noRemarks).changes, []);
276+
const message = pullRequestWatchMessage({
277+
number: 7,
278+
url: "https://github.com/o/r/pull/7",
279+
baseBranch: "main",
280+
headSha: "aaaaaaaaaa",
281+
report: final,
282+
});
283+
assert.deepEqual(message.notification.outcome, "failed");
284+
assert.equal(message.notification.summary, "#7: checks finished");
285+
assert.include(
286+
message.text,
287+
"- The checks still running when one failed have finished on aaaaaaa. Still failing:\n - lint https://ci.example/lint",
288+
);
289+
290+
// A rerun of the failed required check is not the gate finishing, even with an advisory failure.
291+
const rerun = detail({
292+
checks: [
293+
required("lint", "pending"),
294+
required("unit", "success"),
295+
check("bot", "failure"),
296+
check("e2e", "pending"),
297+
],
298+
});
299+
assert.deepEqual(evaluatePullRequestWatch(first.next, rerun, noRemarks).changes, []);
229300
});
230301

231302
it("clears old-head owed checks when a new head is pushed", () => {

‎apps/server/src/orchestration-v2/pullRequestWatch.ts‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,14 @@ export function evaluatePullRequestWatch(
8686
} else if (settled && owed.length > 0) {
8787
changes.push({ kind: "checks-passed", count: detail.checks.length, required: false });
8888
owedChecks = [];
89-
} else if (!passedNow && failed.length > 0) {
89+
} else if (gate.some(isFailedCheck)) {
9090
// Gate checks still running when one failed are owed one wake when they finish, since
9191
// the gate can no longer pass and so never sends the checks-passed wake.
9292
const running = gate.filter((check) => check.status === "pending").map((check) => check.name);
9393
owedChecks =
9494
newlyFailed.length > 0 ? running : owedBefore.filter((name) => running.includes(name));
9595
if (newlyFailed.length === 0 && owedBefore.length > 0 && owedChecks.length === 0) {
96-
changes.push({
97-
kind: "checks-finished",
98-
failing: failed.filter((check) => gateNames.includes(check.name)),
99-
});
96+
changes.push({ kind: "checks-finished", failing: gate.filter(isFailedCheck) });
10097
}
10198
}
10299
passed = passedNow;

0 commit comments

Comments
 (0)