Skip to content

Commit 606b85e

Browse files
authored
fix(ci): distinguish review-signal queue timeout (#3823)
1 parent 5962132 commit 606b85e

3 files changed

Lines changed: 50 additions & 5 deletions

File tree

.github/workflows/pr-review-signal.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,11 @@ jobs:
137137
# the rest -- so there is nothing to optimise and no
138138
# ceiling: queue depth grows with how many PRs are open, which is
139139
# when this gate is under load. A budget cannot bound that, so it
140-
# WILL breach again; the gate then says "within the poll budget"
141-
# rather than reporting a bare absence, and the remedy is a re-run,
142-
# because nothing failed.
140+
# WILL breach again. A breach while the rollup is still moving is an
141+
# explicit LANE_PUBLICATION_TIMEOUT advisory, not MISSING_LANES: five
142+
# runs in #3810 crossed 2400 s and published every named lane later.
143+
# The remedy is a re-run; the independent dirty-PR scan remains the
144+
# eventual failing signal for lanes that genuinely never appear.
143145
#
144146
# Measuring from run creation is the CONSERVATIVE direction: this
145147
# job's own deadline starts later still, after its pickup and

scripts/check-pr-review-signal.mjs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
* has re-reviewed the newest push is transient GitHub state, not a fact
140140
* about the diff.
141141
*
142-
* FAIL-CLOSED, EVERY PATH. `gh` missing, `gh` erroring, unparseable JSON, an
142+
* FAIL-CLOSED ON READABLE FACTS. `gh` missing, `gh` erroring, unparseable JSON, an
143143
* empty rollup, a head SHA that will not resolve, a reviewer that passed with no
144144
* description, a job name this parser cannot expand, a reviews walk that did not
145145
* complete, a review whose `commit_id` is unreadable -- each exits non-zero with
@@ -191,7 +191,11 @@ const DEFAULT_CONFIG = join(SCRIPTS_DIR, 'pr-review-signal.config.json');
191191
* makes `now() >= deadline` false forever: the poll would spin until the job's
192192
* own job timeout killed it, printing nothing at all. That is the exact
193193
* "no output, no verdict" shape this gate exists to reject, so an unreadable
194-
* duration is an error rather than a silently infinite one. Zero and negatives
194+
* duration is an error rather than a silently infinite one. Exhausting that
195+
* duration while the rollup is still moving is reported as an explicit
196+
* LANE_PUBLICATION_TIMEOUT advisory: absence has not become evidence yet, and
197+
* the independent push/scheduled dirty-PR scan remains the eventual backstop.
198+
* Zero and negatives
195199
* go the same way: a zero budget is a gate that never waits, and a zero poll
196200
* interval is a busy loop against the API.
197201
*
@@ -653,6 +657,24 @@ export function evaluate({
653657
'normal for a fork and is reported without failing:',
654658
);
655659
for (const n of missing) lines.push(` - ${n}`);
660+
} else if (timedOut) {
661+
// A moving rollup at the deadline is UNKNOWN, not MISSING. Five live runs
662+
// in #3810 crossed the 2400 s budget while Build packages + WASM was still
663+
// queued; every named lane appeared later and largely passed. Rendering
664+
// that queue condition as MISSING_LANES makes a red check indistinguishable
665+
// from settled absence. The dirty-PR scan independently rechecks actual
666+
// missing lanes on main pushes and schedule, so this is reported loudly
667+
// without pretending the code failed.
668+
lines.push(
669+
`⚠️ LANE_PUBLICATION_TIMEOUT: ${missing.length} of ${required.length} required lane(s) ` +
670+
'had not appeared before the poll budget expired, while the rollup was still unsettled. ' +
671+
'This is an unknown queue state, not evidence that the lanes will never run:',
672+
);
673+
for (const n of missing) lines.push(` - ${n}`);
674+
lines.push(
675+
' Re-run this signal for an immediate answer. The independent silent-PR scan will fail ' +
676+
'on settled missing lanes without turning hosted-runner delay into a code failure.',
677+
);
656678
} else {
657679
ok = false;
658680
lines.push(

scripts/check-pr-review-signal.test.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { dirname, join } from 'node:path';
2020
import { fileURLToPath } from 'node:url';
2121

2222
import { expandJobNames, pullRequestBranchFilterKeys } from './lib/pr-review-signal.mjs';
23+
import { evaluate } from './check-pr-review-signal.mjs';
2324

2425
const HERE = dirname(fileURLToPath(import.meta.url));
2526
const REPO_ROOT = join(HERE, '..');
@@ -98,6 +99,26 @@ const FATAL = () => ['--config', cfgWith({ reviewVerdictSeverity: 'fail' }, 'fat
9899
const LANE = (name, state = 'success') => ({ name, state });
99100
const HEALTHY = ['Typecheck', 'Lint', 'Node tests'];
100101

102+
test('#3810: a poll-budget breach is visibly unknown, not a false MISSING_LANES failure', () => {
103+
const cfg = JSON.parse(readFileSync(CONFIG, 'utf8'));
104+
const result = evaluate({
105+
required: HEALTHY,
106+
aliases: new Map(),
107+
lanes: [LANE('Typecheck', 'in_progress')],
108+
reviewChecks: [],
109+
reviews: [],
110+
headSha: ANY_HEAD,
111+
headCommittedAt: ANY_HEAD_COMMITTED_AT,
112+
isFork: false,
113+
cfg,
114+
timedOut: true,
115+
baseRefName: 'main',
116+
});
117+
assert.equal(result.ok, true);
118+
assert.match(result.lines.join('\n'), /LANE_PUBLICATION_TIMEOUT/);
119+
assert.doesNotMatch(result.lines.join('\n'), /MISSING_LANES/);
120+
});
121+
101122
// -------------------------------------------------------------- happy path
102123

103124
test('GREEN: every required lane present and no reviewer claims a verdict it lacks', () => {

0 commit comments

Comments
 (0)