Repository navigation
Commit ec8f37c
fix(scripts): in Changesets pre mode the doc image and install pins follow the newest GA, not the prerelease (#22134)
Fixes #22131
Clause-②: no
Refs #22085 (the parent; this PR carries its Q3 half). Ruling record:
comment 6049734955 on #22085, quoted as the dispatch carried it:
> **Q3 → B. During the `next` line, the documented image and install
versions follow the newest GA, not the prerelease.** In pre mode
`sync-docs-image-tags` and `check-docs-image-tag` pin the newest GA
(read from the CHANGELOG, the same way the GA is already judged);
outside pre mode nothing changes. ⛔ Not taken: A (self-hosting and
upgrade documents reading `18.0.0-next.N` while npm `latest` and the
published image stay 17.7.0, so a production user copying the documents
is led into the breaking prerelease line).
## What changed
Two files, both under `scripts/`, nothing published:
- `scripts/check-docs-image-tag.mjs` gains `expectedVersion(root)`, the
ONE function that answers which version the doc surfaces must pin.
- No `.changeset/pre.json`: `packages/cli/package.json`'s version
(`VERSION_SOURCE`, kept with its name), as before.
- `.changeset/pre.json` in mode `pre` or `exit`: the newest GA in
`packages/spec/CHANGELOG.md`, overall (not of the CLI's major).
- It reads through the existing readers only: `SPEC_CHANGELOG`,
`gaVersions` and `newestGaOfMajor` from
`check-release-section-coverage.mjs` (unchanged), and `readPre` from
`check-changeset-no-major.mjs` (unchanged). No second GA reader and no
third `pre.json` reader.
- Every pre-mode state it cannot read throws instead of falling back: a
`pre.json` that is present but does not parse, a mode Changesets never
writes, no spec CHANGELOG, or a CHANGELOG with no GA heading.
- `main()` judges against it. The STALE detail, the scope line and the
red footer name the source used. A pre-mode red also prints why the
expectation is not `packages/cli`'s prerelease. Outside pre mode the
gate's output is byte-identical to `main`'s.
- `scripts/sync-docs-image-tags.mjs` gains `syncRepo({ root })`, which
`main()` runs: the target comes from the gate's `expectedVersion()`,
followed by the existing `syncSurfaces()`. The rewriter and the gate
therefore read one value. A pre-mode run logs why the docs did not
follow `packages/cli`.
Not touched: `content/docs/**`, `release.yml`, `cut-rc.yml`, `lint.yml`,
the root `version` script, `.changeset/**`, #21988.
## Premises measured on `origin/main` `8fc50b7647`
1. The root `version` script (`package.json:20`) is `changeset version
&& sync-protocol-version && sync-template-versions &&
sync-docs-image-tags && sync-release-index-currency`. Holds.
2. `VERSION_SOURCE`, `SURFACES` and `PROSE_CLAIMS` are as the card
states, and the rewriter imports them from the gate. Holds.
3. `check-release-section-coverage.mjs` exports `SPEC_CHANGELOG`,
`gaVersions` (ascending, GA-only, end-anchored) and `newestGaOfMajor`.
Holds.
- On a pre-mode tree the answer has to be the newest GA overall. After
the first `next` cut the CHANGELOG's top heading is `## 18.0.0-next.0`
and there is no GA of 18, so `newestGaOfMajor(versions, 18)` is `null`.
- `@objectstack/spec` and `@objectstack/cli` are in one `fixed` group in
`.changeset/config.json`, so the spec CHANGELOG's newest GA is also the
CLI's.
4. Pre mode is `.changeset/pre.json`. **Refined:** Changesets writes it
in two modes, and both are the prerelease line.
- `changeset pre exit` only rewrites the mode to `"exit"`, so the tree
stays on `18.0.0-next.N`.
- The next `changeset version` then computes the GA and deletes
`pre.json`. Measured in `@changesets/apply-release-plan` 8.1.1, which
removes the file when the mode is `exit`; replayed below.
- That `exit` commit reaches `main` before the Version Packages PR does.
Keying on `"pre"` alone would expect `18.0.0-next.N` in that window
while the docs read the GA, which reds this required gate on every PR in
between, the exiting PR included.
- So `exit` counts as pre mode here. The self-tests pin this case.
5. Other consumers: no change in meaning except the rc lane below.
- `release.yml:569` and `cut-rc.yml:605` import `SURFACES` only. Re-run
after the change, the import prints the same 3 paths and exits 0; the
two new imports are side-effect-free, and `check:entry-guard` is green.
- `lint.yml:2649` and `lint.yml:2672` run the two gates. Outside pre
mode the output is identical.
- **Correction to the dispatch's reading of `cut-rc.yml`:** it does not
refuse pre mode. It refuses unless `pre.json` is mode `pre` **with tag
`rc`** (`cut-rc.yml:232-236`). So it never runs on the `next` line, but
it does run in pre mode on an rc line.
- Under this change, an rc cut's version pass leaves the doc surfaces at
the newest GA instead of stamping `X.Y.Z-rc.N`. That follows the
ruling's text ("in pre mode") and its reason, a production reader led
into a prerelease, which applies to an rc as well.
- Its allowlist permits the doc surfaces to change and does not require
it (`git add -A -- … "${DOCS_SURFACES[@]}"`), so a cut that leaves them
unchanged passes. The self-test pins the rc case: CLI `17.8.0-rc.0`,
expected `17.7.0`.
## The pin, replayed
Each run used a throwaway `git worktree add --detach` under the session
scratch directory, then `pnpm install --frozen-lockfile --offline
--ignore-scripts` and `pnpm run version`. All trees were removed
afterwards. PR #22084's two files were copied from its head
`a630de657d`: `.changeset/pre.json` (`{"mode": "pre", "tag": "next"}`)
and `.changeset/22080-v18-line-opens.md`.
**Tree 1: this branch (`a4b1b5106d`) plus PR #22084's two `.changeset`
files.** `pnpm run version` exit 0. `packages/cli` went from `17.7.0` to
`18.0.0-next.0`, and the spec CHANGELOG's top heading is now `##
18.0.0-next.0`, above `## 17.7.0`.
```text
✓ sync-docs-image-tags: all 9 concrete pin(s) across 3 surface(s) already at the newest GA in packages/spec/CHANGELOG.md (pre mode) 17.7.0 — nothing rewritten.
Pre mode: .changeset/pre.json is in mode "pre" (tag "next"), so the documented versions follow the newest GA, 17.7.0, and not packages/cli/package.json's 18.0.0-next.0. ...
$ git diff -- docker/README.md content/docs/deployment/self-hosting.mdx content/docs/upgrading.mdx | wc -l
0
pins on the 3 surfaces: 7 x ghcr.io/objectstack-ai/objectstack:17.7.0, 1 x OS_CLI_VERSION=17.7.0, 1 x @objectstack/cli@17.7.0
$ pnpm check:docs-image-tag # exit 0
check-docs-image-tag: OK (3/3 enumerated surface(s) read, 9 concrete pin(s) compared against the newest GA in packages/spec/CHANGELOG.md (pre mode) 17.7.0, ...)
```
**Tree 1, continued: the exit window and the GA pass.** These are the
states premise 4 is about.
```text
$ pnpm exec changeset pre exit # .changeset/pre.json → {"mode": "exit", "tag": "next"}; packages/cli still 18.0.0-next.0
$ pnpm check:docs-image-tag # exit 0, compared against the newest GA in packages/spec/CHANGELOG.md (pre mode) 17.7.0
$ pnpm run version # exit 0; .changeset/pre.json deleted; packages/cli 18.0.0
✓ sync-docs-image-tags: 9 pin(s) across 3 surface(s) → 18.0.0 (lockstep with packages/cli/package.json).
$ pnpm check:docs-image-tag # exit 0, compared against packages/cli/package.json 18.0.0
```
**Tree 2: this branch on plain `main`, without `pre.json`.** `pnpm run
version` exit 0. `packages/cli` went from `17.7.0` to `17.8.0` (the 58
pending changesets), and the surfaces move to the CLI's version, as they
do today.
```text
✓ sync-docs-image-tags: 9 pin(s) across 3 surface(s) → 17.8.0 (lockstep with packages/cli/package.json).
pins on the 3 surfaces: 7 x ghcr.io/objectstack-ai/objectstack:17.8.0, 1 x OS_CLI_VERSION=17.8.0, 1 x @objectstack/cli@17.8.0
$ pnpm check:docs-image-tag # exit 0, compared against packages/cli/package.json 17.8.0
```
**Control tree: `main` at `8fc50b7647`, without this change, plus PR
#22084's two files.** This is the state the ruling rejects (⛔ A):
```text
✓ sync-docs-image-tags: 9 pin(s) across 3 surface(s) → 18.0.0-next.0 (lockstep with packages/cli/package.json).
pins on the 3 surfaces: 7 x ghcr.io/objectstack-ai/objectstack:18.0.0-next.0, 1 x OS_CLI_VERSION=18.0.0-next.0, 1 x @objectstack/cli@18.0.0-next.0
```
## Self-tests and the ablation
New batteries, each case with its positive control:
- In the gate, `Pre mode: the expectation is the newest GA (#22131)` has
18 cases. The roster floor goes from 11 to 12, and the run has 112
assertions.
- In the rewriter, `Control I: in pre mode the target is the newest GA,
from the gate's one function` has 10 cases and runs `syncRepo()`. The
roster floor goes from 8 to 9, and the run has 45 assertions.
The three Done-when cases, and what each is controlled against:
| Case | Expected result | Positive control |
|---|---|---|
| Pre mode, CLI `18.0.0-next.0`, newest GA `17.7.0` | expects `17.7.0` |
The same tree's CLI reads `18.0.0-next.0`, and the CHANGELOG has no GA
of 18 |
| The same tree without `pre.json` | expects the CLI's `18.0.0-next.0` |
Case 1 answers differently, and `pre.json` is the only difference
between the two trees |
| Pre mode, docs already at the GA | gate green, rewriter writes nothing
(byte-identical, mtime unchanged) | The same docs against the CLI's
version are 3 STALE in the gate and 3 rewrites in the rewriter |
Further cases cover mode `exit` and an rc pre mode, which both expect
the GA. Pins moved onto the prerelease in pre mode are STALE, and the
finding names the pre-mode source. Each of the four unreadable states is
refused, and the rewriter refuses before any write.
**Ablation, at `a4b1b5106d`.** The change was committed first. `node
scripts/ablation-replace.mjs` rewrote the pre-mode test in
`expectedVersion()` (`if (!existsSync(join(root, PRE_STATE))) {` became
`if (true) {`), which is the old logic: `pre.json` ignored.
- The mutation landed: the anchor count went 1 → 0, the marker count
during the mutation was 1, and the blob went `e27045098c80` →
`97307ba4e54a`.
- `node scripts/check-docs-image-tag.mjs --self-test` exited 1 with **13
failures**, every one a new pre-mode case.
- `node scripts/sync-docs-image-tags.mjs --self-test` exited 1 with **6
failures**, every one a new pre-mode case.
- The controls that do not depend on the branch stayed green.
- The restore was proven by blob hash: after the restore the blob is
`e27045098c80`, equal to `HEAD:scripts/check-docs-image-tag.mjs`, and
`git diff HEAD` is empty.
## Gates, at `a4b1b5106d`
`node scripts/pm/dispatch-gates.mjs --commands` was derived from this
worktree over the actual diff (2 paths, +548/-39) and gave 31 commands,
the same 31 the dispatch named. All 31 were run and every one exited 0.
- `pnpm check:pm-dispatch-gates` passed 1976 cases (856s).
- `pnpm check:docs-image-tag` (112 assertions, then OK on the live tree)
and `pnpm check:docs-image-tag-sync` (45 assertions) passed.
- `check:entry-guard`, `check:nul-bytes`, `check:scripts-symbol-anchors`
and `check:declaration-mirrors` passed.
- `dispatch-gates --ran`: 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.
Lint was a proven narrowing to the changed files, not the repo-wide
`pnpm lint`, which CI runs:
- Population, read from eslint's own config: `isPathIgnored` is `false`
for both files.
- Count, from `--format json`: 2 results, 0 errors and 0 warnings, with
`--no-inline-config`.
- Invariance: this repo's eslint config never enables type-aware linting
(no `parserOptions.project`), so this diff cannot change the verdict on
any untouched file.
No changeset is needed. The root package is private and no package's
`files[]` ships `scripts/`. Labelled `skip-changeset`.
## Acceptance notes
None of these blocks this PR, and none is a defect. Each is wording that
this change makes slightly inaccurate, in a file outside this PR's
declared surface. Carrier for each: none.
- `scripts/check-changeset-no-major.mjs`'s entry-guard comment says
"Nothing imports this file today". `check-docs-image-tag.mjs` now
imports `readPre`. The guard already makes the import inert, as
`check:entry-guard` confirms.
- `release.yml`'s post-version comment ("check:docs-image-tag — the 3
doc surfaces against packages/cli's NEW version") and `lint.yml`'s step
name "Docs image tags track packages/cli's version" are true outside pre
mode only. In pre mode the expectation is the newest GA.
- `cut-rc.yml`'s staging comment (around line 497) lists "the 3 doc
surfaces" among a cut's paths. An rc cut now leaves them unchanged
(premise 5).
- dispatch-gates now routes `.changeset/pre.json` to
`check:docs-image-tag`, because the gate spells it. It does not route
`packages/spec/CHANGELOG.md`, which is read only through an import from
a gate module. That file is release-owned and only the version pass
writes it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent fec87e7 commit ec8f37c
2 files changed
Lines changed: 548 additions & 39 deletions
0 commit comments