ci(pr-automation): run the ADR-0087 and major-bump steps past a designed Check Changeset red - #20976
Merged
objectstack-fleet[bot] merged 2 commits intoOct 1, 2026
Conversation
…d red The Check Changeset job's DELIBERATE CORRECTION class is red by design (route 0 on `Require a changeset`, and the empty-changeset step's foreign-changeset refusal). Under GitHub's implicit success() that red skipped the ADR-0087 disposition step, the live allow-major read and the major guard, so the PRs most likely to carry a breaking changeset never got either verdict in CI. The three steps now name `!cancelled()` (never always()) and require `steps.diffbase.outputs.merge_base != ''`: run past a red they would otherwise also run past the unusable-base red, where every changeset script reads `--base ""` as no base and answers against origin/main at exit 0. The route-0 prose that said the steps after it are skipped is corrected to say which one still is. Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…ast a designed red The consumer battery now asserts that the ADR-0087 disposition step, the live allow-major read and the major guard each lead their `if:` with `!cancelled() &&` (no `||`), each require `steps.diffbase.outputs.merge_base != ''`, that exactly those three steps of the job name a status function, and that nothing in the workflow uses always(). The battery's floor moves 23 -> 34 with the eleven new cases. Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
objectstack-fleet
Bot
deleted the
claude/issue-20784-changeset-steps-past-correction
branch
October 1, 2026 03:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #20784
Clause-②: no
What changes
.github/workflows/pr-automation.yml, jobchangeset-check(Check Changeset). The three steps afterReject an empty-frontmatter changeset added by this PRnow take:Require an ADR-0087 disposition on a declared-breaking changeset(check-adr-0087-registration.mjs)Re-read this PR's allow-major label live(idallow_major; it only feeds the guard below)Guard against accidental major bumps (launch window)(check-changeset-no-major.mjs, the level axis)There is no
always(), so a cancelled run still stops. The job stays red on the DELIBERATE CORRECTION class. It now also shows the ADR-0087 and level-axis verdicts on that same run.The route-0 text that
Require a changesetprints said "the steps after it are skipped". This change makes that false, so the text now says which step is still skipped and that the two verdict steps after it run.The pin is the file's existing shape pin:
scripts/check-empty-changeset.mjs --self-test, battery "The consumer: this gate's own CI step". It gains 11 cases, and the battery floor moves from 23 to 34. It asserts four things:if:with!cancelled() &&and has no||;steps.diffbase.outputs.merge_base != '';always().No gate-script logic changed. Only the self-test block and its floor did.
The three hypotheses, measured before building
H1: which steps follow the empty-changeset step. There are exactly three, the ones listed above. All three should run past its red. None of them is a consequence of that red: two are independent verdicts on the same diff, and the allow-major read is an input to one of them.
A second designed red hides the same three steps. On a correction-only PR (it adds no changeset of its own), route 0 makes
Require a changesetfail by design. Both later gates diff with--diff-filter=AMR, so such a PR still gives them something to judge. The triage direction's literal form,!cancelled() && LABEL_CONDITIONS, runs the steps past both reds, and this PR keeps that reach. Fixture F2 below shows the verdicts that were being hidden.H2: the outputs the later steps read. No later step reads an output of the empty-changeset step, which has no
id. When that step is red, every step before it succeeded, somerge_baseis set,labels.skipisfalse, andlabels_settled.skipisfalseor unwritten (never'true'). The later steps see correct inputs.The literal form has a wider reach than that, though. It would also run the three steps past the unusable-base red (
Require a usable diff base), wheresteps.diffbase.outputs.merge_baseis the empty string. I measured all three changeset scripts on this tree with an empty base. Each one treats it as "no base" and answers againstorigin/mainwith exit 0:That verdict would be about a base this job had already refused to trust. So each step also requires
merge_base != ''. This is the same step-level!cancelled()plus "the output that proves the input exists" pattern thatrelease.ymlandcut-rc.ymlalready use.This is not the dispatch's stop condition:
merge_baseis not an output of the empty-changeset step. It is the minimal guard the literal form needs. No other input needs a guard. The two scripts import only node builtins and repo-local modules, so a failedpnpm installcannot change their verdict. An unwritten label output is not'true', which is the enforcing direction.H3: where the shape pin lives. It lives in
scripts/check-empty-changeset.mjs --self-test, in the battery "The consumer: this gate's own CI step". That battery already pins this job's step conditions: the settling read, both label guards on every step that can fail, and the allow-major read.check-workflow-status-functionsdoes not fit: its own scope note limits it to job-levelif:.check-changeset-no-major.mjs's wiring battery pins the guard step'srun:andenv:, and it passes unchanged.Evidence
Ablation. Each leg ran once, on the committed tree, through
scripts/ablation-replace.mjs. After every leg the tool proved the file was restored: its blob equals HEAD (e0b36ccdfa02) andgit diff HEADis empty.!cancelled()base_error == ''in place of themerge_basereadalways()always()Require a changesetalso given!cancelled()merge_base, and the closed set (found none)The first attempt at leg b did nothing. Its replacement text was a substring of its anchor, so the tool refused before running anything. The leg was re-run with a replacement that can be told apart.
Fixture run. This used a shared clone in scratch whose commits were never pushed. Each script ran with
--baseset to this branch's head.minorchangeset that carries a BREAKING banner and no disposition.minortomajorand adds nothing. The count step would report 0, so route 0 turnsRequire a changesetred.Under the old conditions, none of the ADR-0087 or no-major verdicts in F1 or F2 would have run in CI.
Gates at HEAD 4888e25
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 52 commands from this diff. All 52 were run and every one exited 0.dispatch-gates --ranreports: "52 derived famil(ies) accounted for, 52 run, 0 NOT-MEASURED". Among them:Narrowed lint. eslint's population is
**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, and.github/workflows/pr-automation.ymlfalls outside it. eslint itself reports "File ignored because no matching configuration was supplied". The--format jsonoutput has 2 entries:scripts/check-empty-changeset.mjswith 0 errors and 0 warnings, and the ignored YAML. Nothing in the config enables type-aware linting (noparserOptions.project, no typed rules), so this diff cannot change the verdict on any file it does not touch.No changeset: this diff changes workflow wiring and a root
scripts/self-test, and neither publishes anything.Acceptance notes
pull_requestrun takes its workflow from the PR's own merge ref. A correction-class PR will show both verdicts on one run only after this lands. PR fix(service-automation)!: the toggle door switches packaged flows only; a customer flow is refused, naming its status switch (#20726) #20780 would then need a new event such as a push or amainmerge. A re-run is not enough:rerun_failed_jobsreplays the original merge ref.Check Changesetrun carriesskip-changeset. That only exercises the exempt direction: the three steps must stay skipped when either label read exempts the PR.Reject an empty-frontmatter changeset added by this PRkeeps the implicitsuccess(). On a correction-only PR it is still skipped behind route 0, and route 0's own text says so.--base ""as absent and fall back toorigin/mainwith exit 0. With themerge_baseconjunct, CI never reaches that path. Carrier: none.origin/main: this branch sits on3693a1b50. No upstream commit since then touches either changed file or the three changeset scripts, andmerge-treeis clean. CI judges the merge ref.Generated by Claude Code