You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
echo "::error::This event carries no base branch, so the changeset diff base cannot be computed. A gate that cannot read its input has verified nothing, so this is a failure rather than a pass (#4690)."
286
-
exit 1
302
+
echo "::warning::This event carries no base branch, so the changeset diff base cannot be computed. Adjudicated below, once the skip-changeset window has settled."
303
+
echo 'base_error=This event carries no base branch, so the changeset diff base cannot be computed.' >> "$GITHUB_OUTPUT"
304
+
exit 0
287
305
fi
288
306
if ! git rev-parse --verify --quiet "refs/remotes/origin/$BASE_REF^{commit}" >/dev/null; then
# `if !` rather than a bare assignment on purpose: these steps run under
293
311
# `bash -e` (no `shell:` key anywhere in this file), where a failing
294
312
# command substitution kills the step with no message at all. The gate
295
-
# is allowed to fail here -- it is NOT allowed to fail unexplained.
313
+
# is allowed to fail over this -- it is NOT allowed to fail unexplained.
296
314
if ! MERGE_BASE=$(git merge-base "refs/remotes/origin/$BASE_REF" HEAD); then
297
-
echo "::error::Could not compute merge-base(origin/$BASE_REF, HEAD), so the changeset diff has no trustworthy starting point. Failing rather than falling back to the frozen base.sha, which is the #6129 defect itself."
298
-
exit 1
315
+
echo "::warning::Could not compute merge-base(origin/$BASE_REF, HEAD), so the changeset diff has no trustworthy starting point. Adjudicated below, once the skip-changeset window has settled."
316
+
echo "base_error=Could not compute merge-base(origin/$BASE_REF, HEAD), so the changeset diff has no trustworthy starting point." >> "$GITHUB_OUTPUT"
317
+
exit 0
299
318
fi
319
+
# Written explicitly rather than left unset. An unwritten output already
320
+
# reads as the empty string, but every downstream `if:` here spells the
321
+
# success case as `base_error == ''`, and a contract that load-bearing
322
+
# should be visible in the shell that establishes it.
323
+
echo 'base_error=' >> "$GITHUB_OUTPUT"
300
324
echo "merge_base=$MERGE_BASE" >> "$GITHUB_OUTPUT"
301
325
# The drift is printed, not just corrected. #6129 was invisible for as
302
326
# long as it was because nothing in the log ever said which commit the
@@ -306,18 +330,37 @@ jobs:
306
330
echo "Diff base: $MERGE_BASE (merge-base of origin/$BASE_REF and HEAD)"
307
331
echo "Frozen payload base.sha: $PINNED_BASE_SHA -- $BASE_REF has moved $DRIFT commit(s) since it was frozen, and that drift is exactly what this gate used to count as this PR's own."
308
332
333
+
# The three toolchain steps and the counting step below all carry the
334
+
# `base_error == ''` conjunct as well (#6434). For the counting step it is
335
+
# CORRECTNESS: handed an empty `$MERGE_BASE`, `git diff --diff-filter=A ""
336
+
# HEAD` fails, but it fails inside a pipeline whose last command is `tr`, so
337
+
# the step would report `added=0` -- a fabricated count that reads exactly
338
+
# like "this PR forgot its changeset" and would send the verdict step to the
339
+
# wrong error message entirely. For the three toolchain steps it is COST and
340
+
# legibility: an unusable base can no longer be adjudicated until the label
341
+
# window settles, and there is no reason to buy a Node toolchain and a
342
+
# `--frozen-lockfile` install first for a run that will either be exempted or
343
+
# failed without ever compiling anything. It also keeps the failure signature
344
+
# single: on this path the only red is the adjudication step, never an
345
+
# install that happened to flake on the same shaky network.
309
346
- name: Setup Node.js
310
-
if: steps.labels.outputs.skip != 'true'
347
+
if: >-
348
+
steps.labels.outputs.skip != 'true'
349
+
&& steps.diffbase.outputs.base_error == ''
311
350
uses: actions/setup-node@v7
312
351
with:
313
352
node-version: '22'
314
353
315
354
- name: Enable Corepack
316
-
if: steps.labels.outputs.skip != 'true'
355
+
if: >-
356
+
steps.labels.outputs.skip != 'true'
357
+
&& steps.diffbase.outputs.base_error == ''
317
358
run: corepack enable
318
359
319
360
- name: Install dependencies
320
-
if: steps.labels.outputs.skip != 'true'
361
+
if: >-
362
+
steps.labels.outputs.skip != 'true'
363
+
&& steps.diffbase.outputs.base_error == ''
321
364
run: pnpm install --frozen-lockfile
322
365
323
366
# COUNTING ONLY -- the verdict is two steps down (#6378). The split is not
echo "::error::$BASE_ERROR Failing rather than falling back to the frozen base.sha, which is the #6129 defect itself: a gate that cannot read its input has verified nothing, so this is a failure rather than a pass (#4690). Neither live label read found 'skip-changeset' on this PR, so the changeset gate does apply to it and cannot be evaluated. If this PR releases nothing, apply the label and re-run; if the base branch is genuinely fetchable, this is infrastructure and a re-run will clear it."
551
+
exit 1
552
+
467
553
# The VERDICT. Everything it needs was decided above; this step only
468
554
# announces it, which is what makes the failure message a single block of
469
555
# prose rather than something interleaved with counting and polling.
Copy file name to clipboardExpand all lines: scripts/check-empty-changeset.mjs
+74-9Lines changed: 74 additions & 9 deletions
Original file line number
Diff line number
Diff line change
@@ -733,6 +733,20 @@ function selfTest() {
733
733
settle,
734
734
'consumer: the settling label read must be conditioned on `steps.changeset_count.outputs.added == \'0\'` -- the wait #6378 introduces is charged ONLY to a PR headed for red, and un-conditioning it taxes every run instead',
735
735
);
736
+
// The condition above is a substring test, so it would go on passing if a
737
+
// later edit bolted an unrelated `|| <anything>` onto it and quietly taxed
738
+
// every run again. #6434 legitimately adds ONE disjunct -- the unusable diff
739
+
// base, which is the other way a PR arrives at red and which the count
740
+
// cannot speak for, because the counting step is skipped on that path. So
741
+
// the whole condition is pinned, not just its first term: exactly these two
"consumer: the settling read's condition must be exactly `no fast-path label AND (added == '0' OR base_error != '')` -- both disjuncts are ways of being headed for RED, which is the only thing that may buy the #6378 wait",
749
+
);
736
750
737
751
// Both reads, one matcher. A divergence (say a substring `grep -q` on one
738
752
// path) would be a gate that exempts on one read and enforces on the
@@ -744,20 +758,56 @@ function selfTest() {
744
758
`consumer: exactly two live \`grep -qxF 'skip-changeset'\` reads are expected (the fast path and the settling read); found ${matchers.length}`,
745
759
);
746
760
747
-
// Every step that can FAIL a PR over the changeset rule must honour both
748
-
// reads. Scoped to those steps by what they run, not by name: a step that
749
-
// shells out to a `check-*.mjs` gate, or that emits the "no changeset"
750
-
// error. `Resolve the diff base` is deliberately outside this set -- it
751
-
// exits 1 over an unusable git base, which is not a changeset verdict and
752
-
// was never label-exempt (recorded, not implied).
761
+
// Every step of this job that can FAIL a PR must honour both reads. Scoped
762
+
// by what a step RUNS, never by its name: it shells out to a `check-*.mjs`
763
+
// gate, or it contains a literal `exit 1` outside a comment.
764
+
//
765
+
// #6434 widened this boundary, and the widening is the point rather than an
766
+
// accident of it. The predicate used to be "runs a `check-*.mjs` OR emits
767
+
// the no-changeset error", which described the four steps that existed and
768
+
// nothing else. `Resolve the diff base` sat outside it carrying two `exit
769
+
// 1`s of its own -- deliberately, on the argument that "the git base is
770
+
// unavailable" is not a changeset verdict and so was never label-exempt.
771
+
// That argument is sound about the VERDICT and wrong about its ADDRESS: the
772
+
// step ran before the settling read, so on a PR whose `skip-changeset` label
773
+
// landed in the ordinary +10..45s window it could red a run the job was
774
+
// about to exempt. #6434 moved that verdict to `Require a usable diff base`,
775
+
// which honours both reads, and the boundary here moved with it.
776
+
//
777
+
// Naming `exit 1` instead of one specific error string is what makes the
778
+
// rule outlive the steps it was written for: the gap #6434 closed existed
779
+
// precisely because the old predicate could not see a failing step it had
780
+
// not been told about, and the next one added here would have been invisible
781
+
// the same way. Residual, stated rather than implied: a step that fails by
782
+
// running a command that returns non-zero, with no literal `exit 1` and no
783
+
// `check-*.mjs`, is still outside this set. That is a smaller hole than the
`consumer: expected 4 changeset-verdict steps in the Check Changeset job, found ${chunks.length} -- a new one that this rule cannot see is a new way to red an exempt PR`,
791
+
chunks.length===5,
792
+
`consumer: expected 5 failable steps in the Check Changeset job, found ${chunks.length} -- a new one that this rule cannot see is a new way to red an exempt PR`,
793
+
);
794
+
// The step whose relocation #6434 IS. Pinned in the negative as well as the
795
+
// positive: base resolution reports into an output and the verdict is taken
796
+
// downstream, so re-introducing an `exit 1` here would restore the fast-path
797
+
// -only failure this card exists to remove. The count above would catch that
798
+
// as a 6th failable step; this says which one and why, so the next reader
799
+
// gets the reason and not just an arithmetic mismatch.
'consumer: `Resolve the diff base` must not fail on the spot -- it runs BEFORE the settling read, so its verdict would be taken on the fast path alone and would red a PR whose skip-changeset label was still in flight (#6434). It reports `base_error` and the adjudication step below decides.',
'consumer: `Resolve the diff base` must report an unusable base as a `base_error` output -- dropping it silently would leave the downstream adjudication permanently un-triggerable, i.e. a gate that passes because it never runs (#4690)',
assert(adjudication!==undefined,'consumer: the unusable-diff-base verdict step must exist (#6434) -- without it `base_error` is written and never read, which is a gate deleted rather than relocated');
"consumer: the unusable-diff-base verdict must fire on `base_error != ''` and exit 1 -- #6434 relocates a failure past the settling read, it does not forgive one. A PR the label reads did not exempt is still failed over a base that could not be resolved (#4690).",
845
+
);
781
846
assert(
782
847
!/continue-on-error/.test(yaml),
783
848
'consumer: no step in pr-automation.yml may carry `continue-on-error` -- that would turn this gate into a warning, which is the one outcome #6378 rules out',
0 commit comments