Skip to content

Commit 86552f2

Browse files
ci(release): gate the release PR on objectui pin freshness (#3340) (#4944)
The frontend enters the changesets pipeline exactly once: when .objectui-sha moves and bump-objectui.sh writes the @objectstack/console changeset for the range it crossed. Everything objectui merged AFTER the pin is outside that range by construction — no changeset, no changelog, no release page — and a complete-looking release record is indistinguishable from a complete one. Cutting v16 that way lost four frontend changes, two of them minor features, while objectui main sat 4 commits and 21 pending changesets ahead of the pin. scripts/check-objectui-pin-fresh.mjs is red when the pin is not objectui main (or a named --ref), and names both the commits ahead and the .changeset/*.md files declared after the pin. git ls-remote alone decides the verdict, so the three GitHub API calls only itemize an already-established lag: a rate-limited or unreachable API degrades the report — loudly — and can never turn red into green. An unreachable remote is verdict `unreadable`, never a pass. Wired as the `Console Pin Freshness` job in a new workflow. It runs on every PR so the context always reports and can therefore be required in branch protection, but blocks only on the changesets Version Packages / release PR: a pin lagging between bumps is the normal state of an ordinary code PR, and failing every PR over it would train everyone to ignore the check. Not to be confused with ci.yml's Console Pin Gate (#4290), which proves the pinned SHA still BUILDS; this one proves the pin is still CURRENT. Either can be green while the other is red. The distinction is spelled out in the script header, in both workflows' output text and in docs/releases-maintenance.md. --self-test covers: a lagging pin is red and lists the changesets by name; a current pin is green; pending unbundled changesets are judged on their own; a diverged/behind pin is red; an unreadable API still yields red with the reason printed; a network failure never renders as FRESH in either mode. Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 71bb07f commit 86552f2

5 files changed

Lines changed: 994 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
ci(release): add the objectui pin-freshness gate (#3340 P0). `scripts/check-objectui-pin-fresh.mjs` fails when `.objectui-sha` is not objectui `main` (or a named `--ref`), naming the commits ahead and the `.changeset/*.md` files declared after the pin — the blind spot that dropped four frontend changes, two of them `minor` features, from the v16 release page. Wired as `Console Pin Freshness` in `.github/workflows/objectui-pin-freshness.yml`: it runs on every PR so the context can be required in branch protection, but blocks only on the Version Packages / release PR. Distinct from ci.yml's `Console Pin Gate` (#4290), which proves the pin still *builds* rather than that it is still *current*. Tooling and CI only; releases nothing.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: Console Pin Freshness
2+
3+
# Is `.objectui-sha` still CURRENT? (#3340 P0)
4+
#
5+
# ⚠️ NOT ci.yml's "Console Pin Gate" (#4290). The names are close and the
6+
# questions are opposite ends of the same fact:
7+
#
8+
# Console Pin Gate (#4290) "does the PINNED SHA still BUILD?" — clones
9+
# objectui at the pin and builds the SPA.
10+
# Console Pin Freshness "is the PIN still CURRENT?" — compares the pin
11+
# (this workflow) against objectui `main`.
12+
#
13+
# A two-month-old pin builds perfectly (Pin Gate green) while hiding two months
14+
# of frontend releases from the release record (this gate red). Keep both.
15+
#
16+
# WHERE IT BLOCKS
17+
# ---------------
18+
# On the changesets **Version Packages / release PR** only. Between pin bumps an
19+
# ordinary code PR sits behind objectui almost always — that is the normal state
20+
# of the repo, not a defect, and failing every PR over it would train everyone to
21+
# ignore this check. So the job runs everywhere and blocks only on the release
22+
# lane, where a lagging pin silently drops frontend changes from the release
23+
# record (#3340: four changes, two of them `minor` features, lost from v16).
24+
#
25+
# WHY THE JOB IS NEVER SKIPPED
26+
# ----------------------------
27+
# It carries no job-level `if:` and no paths filter on purpose. A check that
28+
# does not run reports nothing, and a *required* context that reports nothing
29+
# leaves every PR stuck "Expected — waiting for status". Advisory mode is
30+
# expressed in the EXIT CODE, not by skipping: the report is printed in full
31+
# either way, so a green run on an ordinary PR still shows how far the pin has
32+
# drifted.
33+
#
34+
# REQUIRED-CHECK ENFORCEMENT IS NOT SELF-DECLARED
35+
# -----------------------------------------------
36+
# A workflow cannot make itself required. A maintainer must add the
37+
# `Console Pin Freshness` context to the branch-protection rule for `main`
38+
# (Settings → Branches → main → Require status checks to pass). Until then this
39+
# workflow REPORTS on the release PR without blocking the merge button.
40+
41+
on:
42+
pull_request:
43+
branches: [main]
44+
workflow_dispatch:
45+
46+
concurrency:
47+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
48+
cancel-in-progress: true
49+
50+
jobs:
51+
# Job name == the branch-protection context. Keep it stable: renaming it
52+
# silently detaches the required check (the #3622 lesson ci.yml records).
53+
pin-freshness:
54+
name: Console Pin Freshness
55+
runs-on: ubuntu-latest
56+
timeout-minutes: 10
57+
permissions:
58+
contents: read
59+
60+
steps:
61+
- name: Checkout repository
62+
uses: actions/checkout@v7
63+
64+
- name: Setup Node.js
65+
uses: actions/setup-node@v7
66+
with:
67+
node-version: '22'
68+
69+
# "A change to the guard runs the guard" — the rule this repo applies to
70+
# every other scripts/ gate. No install: the script is dependency-free.
71+
- name: Self-test the gate
72+
run: node scripts/check-objectui-pin-fresh.mjs --self-test
73+
74+
- name: Check objectui pin freshness
75+
env:
76+
# Only ITEMIZES an already-established lag (`git ls-remote` decides the
77+
# verdict), but the token keeps the API off the 60/hr anonymous limit
78+
# so the report names the commits and changesets instead of degrading.
79+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80+
# Read through env, never inlined into the shell: a PR title is
81+
# attacker-controlled text.
82+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
83+
PR_TITLE: ${{ github.event.pull_request.title }}
84+
EVENT: ${{ github.event_name }}
85+
run: |
86+
# The changesets action opens the version PR from `changeset-release/<base>`
87+
# with the title configured in release.yml. Either identifies the lane;
88+
# both are checked so a future rename of one does not silently disarm
89+
# the gate.
90+
if [ "$EVENT" != "pull_request" ] \
91+
|| [ "$HEAD_REF" = "changeset-release/main" ] \
92+
|| [ "$PR_TITLE" = "chore: version packages" ]; then
93+
echo "::notice::Release lane — the objectui pin-freshness gate BLOCKS here (#3340)."
94+
node scripts/check-objectui-pin-fresh.mjs
95+
else
96+
echo "::notice::Not the Version Packages PR — pin freshness is reported but does not block (a pin lagging between bumps is normal). It blocks on the release PR."
97+
node scripts/check-objectui-pin-fresh.mjs --advisory
98+
fi

docs/releases-maintenance.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,41 @@ changesets also embed companion frontend notes inline ("Companion objectui PR
102102
ships…", renderer notes), which are enough to write an accurate Console section on
103103
their own.
104104

105+
### Pin freshness — the gate on the release PR (#3340)
106+
107+
Everything above reads the range `OLD_PIN..NEW_PIN`. That is exact, and it is also the
108+
whole blind spot: anything objectui merged **after** the current pin is outside every
109+
range, so it reaches no changeset, no changelog and no release page — and a
110+
complete-*looking* release record is indistinguishable from a complete one. Cutting v16
111+
that way lost four frontend changes, two of them `minor` features, while objectui `main`
112+
sat 4 commits and 21 pending changesets ahead of the pin.
113+
114+
`scripts/check-objectui-pin-fresh.mjs` (`pnpm check:objectui-pin-fresh`) closes it. It is
115+
red when the pin is not objectui `main` (or the `--ref` you name), and it lists the
116+
commits ahead plus the `.changeset/*.md` files that exist at `main` and not at the pin.
117+
118+
```bash
119+
pnpm check:objectui-pin-fresh # enforcing
120+
node scripts/check-objectui-pin-fresh.mjs --advisory # report only
121+
node scripts/check-objectui-pin-fresh.mjs --ref v17.0.0 --json
122+
```
123+
124+
- **Where it blocks:** the changesets **Version Packages / release PR**, via
125+
`.github/workflows/objectui-pin-freshness.yml`. The job runs on every PR — so the
126+
context always reports and can be a branch-protection *required* check — but passes
127+
`--advisory` outside the release lane, because a pin lagging between bumps is the
128+
normal state of an ordinary code PR.
129+
- **It is not the Console Pin Gate.** `ci.yml`'s **Console Pin Gate** (#4290) proves the
130+
pinned SHA still **builds**; this one proves the pin is still **current**. Either can
131+
be green while the other is red; neither replaces the other.
132+
- **Network failure is never green.** `git ls-remote` alone decides the verdict, so the
133+
GitHub API (which only itemizes an already-established lag) can be rate-limited or
134+
down without turning red into green — the degradation is printed, not swallowed. An
135+
unreachable remote is reported as `unreadable` and exits non-zero.
136+
- **Fix when it fires:** `scripts/bump-objectui.sh` to move the pin (which writes the
137+
`@objectstack/console` changeset for the crossed range), then re-source the Console
138+
section with `scripts/objectui-range.mjs`.
139+
105140
## Drift guard
106141

107142
`scripts/check-release-notes.mjs` (run in CI as `pnpm check:release-notes`) fails the

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"check:startup-registry-verdict": "node scripts/check-startup-registry-verdict.mjs --self-test && node scripts/check-startup-registry-verdict.mjs",
5050
"check:console-sha": "node scripts/check-console-sha.mjs",
5151
"check:objectui-changeset": "node scripts/objectui-changeset-digest.mjs --self-test && node scripts/objectui-range.mjs --self-test",
52+
"check:objectui-pin-fresh": "node scripts/check-objectui-pin-fresh.mjs --self-test && node scripts/check-objectui-pin-fresh.mjs",
5253
"check:release-notes": "node scripts/check-release-notes.mjs",
5354
"check:node-version": "node scripts/check-node-version.mjs",
5455
"check:published-files": "node scripts/check-published-files.mjs --self-test && node scripts/check-published-files.mjs",

0 commit comments

Comments
 (0)