Repository navigation
Commit d1f8ce8
ci(pr-automation): run the ADR-0087 and major-bump steps past a designed Check Changeset red (#20976)
Fixes #20784
Clause-②: no
## What changes
`.github/workflows/pr-automation.yml`, job `changeset-check` (`Check
Changeset`). The three steps after `Reject an empty-frontmatter
changeset added by this PR` now take:
```yaml
if: >-
!cancelled()
&& steps.labels.outputs.skip != 'true'
&& steps.labels_settled.outputs.skip != 'true'
&& steps.diffbase.outputs.merge_base != ''
# the major guard alone adds: && steps.allow_major.outputs.allow != 'true'
```
- `Require an ADR-0087 disposition on a declared-breaking changeset`
(`check-adr-0087-registration.mjs`)
- `Re-read this PR's allow-major label live` (id `allow_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 changeset` prints 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:
- each of the three steps begins its `if:` with `!cancelled() &&` and
has no `||`;
- each of them requires `steps.diffbase.outputs.merge_base != ''`;
- no other step of the job names a status function;
- nothing in the workflow uses `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
changeset` fail 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, so `merge_base` is set, `labels.skip` is
`false`, and `labels_settled.skip` is `false` or 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`), where `steps.diffbase.outputs.merge_base` is 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 against `origin/main` with
exit 0:
```text
node scripts/check-adr-0087-registration.mjs --base "" -> exit 0 "this PR adds no declared-breaking changeset"
node scripts/check-changeset-no-major.mjs --base "" -> exit 0 "Diffing HEAD from 3693a1b (merge base with origin/main)"
node scripts/check-empty-changeset.mjs --base "" -> exit 0 (same fallback)
```
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 that `release.yml` and `cut-rc.yml` already use.
This is not the dispatch's stop condition: `merge_base` is 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 failed `pnpm install` cannot
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-functions`
does not fit: its own scope note limits it to job-level `if:`.
`check-changeset-no-major.mjs`'s wiring battery pins the guard step's
`run:` and `env:`, 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`) and `git diff HEAD`
is empty.
| Leg | Mutation | Self-test result |
|---|---|---|
| a | ADR-0087 step without `!cancelled()` | exit 1, 2 failures: the
leading conjunct, and the closed set (found steps 13 and 14 instead of
12 to 14) |
| b | major guard with `base_error == ''` in place of the `merge_base`
read | exit 1, 1 failure |
| c | allow-major read with `always()` | exit 1, 2 failures: the leading
conjunct, and no `always()` |
| d | `Require a changeset` also given `!cancelled()` | exit 1, 1
failure: the closed set (found steps 10 and 12 to 14) |
| e | whole workflow as it was at the base commit, i.e. without this
change (trap-guarded swap; blob proven equal to HEAD after restore) |
exit 1, 7 failures: 3 leading conjunct, 3 `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 `--base` set to this branch's head.
| Scenario | empty-changeset | ADR-0087 | no-major |
|---|---|---|---|
| F1: the shape of PR #20780. It edits a pending note and adds its own
`minor` changeset that carries a **BREAKING** banner and no disposition.
| exit 1 (foreign-changeset refusal) | exit 1 (no disposition) | exit 0
|
| F2: correction only. It edits a pending note from `minor` to `major`
and adds nothing. The count step would report 0, so route 0 turns
`Require a changeset` red. | exit 1 | exit 1 (turned breaking, no
disposition) | exit 1 (majored in place) |
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 --ran` reports: "52 derived famil(ies) accounted for, 52
run, 0 NOT-MEASURED". Among them:
```text
exit 0 node scripts/check-empty-changeset.mjs --self-test (170 assertions)
exit 0 pnpm check:changeset-gate-self-tests (empty-changeset, adr-0087 and no-major self-tests; no-major 339 assertions)
exit 0 pnpm check:workflow-status-functions
exit 0 pnpm check:workflow-step-name-quoting
exit 0 node scripts/check-self-test-workflow-commands.mjs (+ --self-test)
exit 0 node scripts/check-step-collectors.mjs (+ --self-test)
exit 0 pnpm check:nul-bytes
exit 0 pnpm check:pm-dispatch-gates (1976 cases; 1243 s on this box)
```
**Narrowed lint.** eslint's population is
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, and
`.github/workflows/pr-automation.yml` falls outside it. eslint itself
reports "File ignored because no matching configuration was supplied".
The `--format json` output has 2 entries:
`scripts/check-empty-changeset.mjs` with 0 errors and 0 warnings, and
the ignored YAML. Nothing in the config enables type-aware linting (no
`parserOptions.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
- **Live proof on GitHub is NOT MEASURED in this PR.** A `pull_request`
run 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 #20780
would then need a new event such as a push or a `main` merge. A re-run
is not enough: `rerun_failed_jobs` replays the original merge ref.
- **This PR's own `Check Changeset` run carries `skip-changeset`.** That
only exercises the exempt direction: the three steps must stay skipped
when either label read exempts the PR.
- **Boundary, not widened:** `Reject an empty-frontmatter changeset
added by this PR` keeps the implicit `success()`. On a correction-only
PR it is still skipped behind route 0, and route 0's own text says so.
- **Dormant, not filed:** the three changeset scripts treat an explicit
empty `--base ""` as absent and fall back to `origin/main` with exit 0.
With the `merge_base` conjunct, CI never reaches that path. Carrier:
none.
- **Base behind `origin/main`:** this branch sits on `3693a1b50`. No
upstream commit since then touches either changed file or the three
changeset scripts, and `merge-tree` is clean. CI judges the merge ref.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d67b942 commit d1f8ce8
2 files changed
Lines changed: 143 additions & 17 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
739 | 739 | | |
740 | 740 | | |
741 | 741 | | |
742 | | - | |
743 | | - | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
744 | 745 | | |
745 | 746 | | |
746 | 747 | | |
| |||
929 | 930 | | |
930 | 931 | | |
931 | 932 | | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
932 | 970 | | |
933 | 971 | | |
934 | | - | |
| 972 | + | |
| 973 | + | |
935 | 974 | | |
| 975 | + | |
936 | 976 | | |
937 | 977 | | |
938 | 978 | | |
| |||
1005 | 1045 | | |
1006 | 1046 | | |
1007 | 1047 | | |
1008 | | - | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
1009 | 1053 | | |
1010 | | - | |
| 1054 | + | |
| 1055 | + | |
1011 | 1056 | | |
| 1057 | + | |
1012 | 1058 | | |
1013 | 1059 | | |
1014 | 1060 | | |
| |||
1037 | 1083 | | |
1038 | 1084 | | |
1039 | 1085 | | |
1040 | | - | |
1041 | | - | |
1042 | | - | |
1043 | | - | |
1044 | | - | |
1045 | | - | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
1046 | 1090 | | |
1047 | | - | |
1048 | | - | |
1049 | | - | |
1050 | | - | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
1051 | 1104 | | |
1052 | 1105 | | |
1053 | 1106 | | |
| |||
1067 | 1120 | | |
1068 | 1121 | | |
1069 | 1122 | | |
1070 | | - | |
| 1123 | + | |
| 1124 | + | |
1071 | 1125 | | |
| 1126 | + | |
1072 | 1127 | | |
1073 | 1128 | | |
1074 | 1129 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
690 | 690 | | |
691 | 691 | | |
692 | 692 | | |
693 | | - | |
| 693 | + | |
694 | 694 | | |
695 | 695 | | |
696 | 696 | | |
| |||
1745 | 1745 | | |
1746 | 1746 | | |
1747 | 1747 | | |
| 1748 | + | |
| 1749 | + | |
| 1750 | + | |
| 1751 | + | |
| 1752 | + | |
| 1753 | + | |
| 1754 | + | |
| 1755 | + | |
| 1756 | + | |
| 1757 | + | |
| 1758 | + | |
| 1759 | + | |
| 1760 | + | |
| 1761 | + | |
| 1762 | + | |
| 1763 | + | |
| 1764 | + | |
| 1765 | + | |
| 1766 | + | |
| 1767 | + | |
| 1768 | + | |
| 1769 | + | |
| 1770 | + | |
| 1771 | + | |
| 1772 | + | |
| 1773 | + | |
| 1774 | + | |
| 1775 | + | |
| 1776 | + | |
| 1777 | + | |
| 1778 | + | |
| 1779 | + | |
| 1780 | + | |
| 1781 | + | |
| 1782 | + | |
| 1783 | + | |
| 1784 | + | |
| 1785 | + | |
| 1786 | + | |
| 1787 | + | |
| 1788 | + | |
| 1789 | + | |
| 1790 | + | |
| 1791 | + | |
| 1792 | + | |
| 1793 | + | |
| 1794 | + | |
| 1795 | + | |
| 1796 | + | |
| 1797 | + | |
| 1798 | + | |
| 1799 | + | |
| 1800 | + | |
| 1801 | + | |
| 1802 | + | |
| 1803 | + | |
| 1804 | + | |
| 1805 | + | |
| 1806 | + | |
| 1807 | + | |
| 1808 | + | |
| 1809 | + | |
| 1810 | + | |
| 1811 | + | |
| 1812 | + | |
| 1813 | + | |
| 1814 | + | |
| 1815 | + | |
| 1816 | + | |
| 1817 | + | |
| 1818 | + | |
1748 | 1819 | | |
1749 | 1820 | | |
1750 | 1821 | | |
| |||
0 commit comments