Skip to content

Commit 49131e3

Browse files
dmealingclaude
andcommitted
docs(release): the 1.0 closeout — a remedy that 403s, and a checklist that is history
Four documents told a reader to retire the RC's `next` tag with `npm dist-tag rm`, and one of them added that a failure there "needs an interactive-2FA session". Both halves are now measured to be wrong: `rm` returns 403 for the local granular token AND for the CI `NPM_TOKEN` that publishes the set, on all fourteen packages, with `--otp` changing nothing on either. It is npm's 2026-07-31 rule — a bypass-2FA token may publish but may not change package access — so no OTP session was ever going to clear it, and a reader following that advice would have spent the session hunting a credential that was never the problem. `dist-tag add` is not restricted. So the documented remedy is now to REPOINT `next` at the release just promoted, across the set, from the `npm dist-tag` workflow where the publish token already lives. That removes the prerelease install path, which was the harm; the tag existing alongside an identical `latest` is cosmetic. Corrected in `RELEASING.md` §4, the `releasing` skill's Phase 9 and its OIDC note, and `release-verify.mjs`'s hint — which is the one a person reads at the exact moment they hit this. `RELEASING.md` also had a copy-pasteable Maven bump that types `7.7.8 -> 7.7.9` by hand, under a header that says never to type a literal `7.` because at the 1.0 cut it asks Central for a version BELOW the last release. It now derives the current version from the pom and the next from `scripts/maven-coordinate.mjs` — the single derivation the rest of the doc already points at — and echoes both for eyeballing before a sed that touches 17 poms irreversibly. The npm credential line calls the token what it is (granular, bypass-2FA) rather than "automation", since the difference is what §4 now turns on. `docs/1.0-readiness.md` is marked CLOSED as of the cut. It is history rather than a live gate list: the blocking path it named in its own footer — G3d, then G1/G2/G4 — is closed end to end. The 🔶/⬜ items are deliberately NOT tidied to ✅; they were never on that path, and leaving the marks as they stood keeps the record honest about what was true at the cut. The prohibition on reintroducing a waiting gate in any form is restated at the top, where a future reader meets it before the gate list rather than after. The CHANGELOG preamble still described the project as pre-1.0 with breaking MINORs. It now states the two-axis rule that actually governs: `^1.0.0` accepts a MINOR, so a release that moves `metamodelVersion` says so in its entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent 0578d56 commit 49131e3

5 files changed

Lines changed: 80 additions & 24 deletions

File tree

‎.claude/skills/releasing/SKILL.md‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,12 @@ Add the `CHANGELOG.md` entry (Keep-a-Changelog format) if not already committed.
151151

152152
## Phase 9 — Cleanup
153153

154-
`npm dist-tag rm <pkg> next` and `npm deprecate <pkg>@<version>-rc.1 "superseded; use <version>"` for each package.
154+
`npm deprecate <pkg>@<version>-rc.1 "superseded; use <version>"` for each package.
155+
156+
Retire the RC's `next` tag by **repointing, not deleting**: `npm dist-tag rm` 403s for every
157+
token we hold (local and CI both — npm's bypass-2FA package-access rule, not a missing OTP).
158+
Run Actions → **npm dist-tag** → tag `next`, action `add`, version `<version>`, which does the
159+
whole lockstep set from the publish token. See `docs/RELEASING.md` §4.
155160

156161
## Phase 10 — Propagate versions (docs + websites)
157162

@@ -199,8 +204,10 @@ version FAILS by design — skip what's done, never force.
199204
npm removes **direct publishing from 2FA-bypass tokens around January 2027**
200205
([changelog](https://github.blog/changelog/2026-07-31-restricting-npm-bypass-2fa-granular-access-tokens/));
201206
since 2026-07-31 such tokens already cannot change package access — including
202-
**deleting a dist-tag**, which breaks Phase 9's `npm dist-tag rm` and needs an
203-
interactive-2FA session. The replacement is **OIDC Trusted Publishing** via
207+
**deleting a dist-tag**. Measured 2026-09-10: the local token and the CI `NPM_TOKEN`
208+
both 403 on `DELETE …/dist-tags/next` and `--otp` changes neither, so this is a grant
209+
boundary and no OTP session was going to clear it. Phase 9 repoints with `dist-tag add`
210+
instead, which is not restricted. The replacement is **OIDC Trusted Publishing** via
204211
`publish-npm.yml` (which exists), the same mechanism `publish-csharp.yml` already
205212
uses for NuGet; the remaining work is a trusted-publisher registration per package
206213
on npmjs.com. Caveat: OIDC can't drive `npm dist-tag` yet, so RC→promote still needs

‎CHANGELOG.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
All notable changes to `@metaobjectsdev/*` TypeScript packages are documented
44
here. The format follows [Keep a Changelog](https://keepachangelog.com/), and
5-
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
6-
(pre-1.0; MINOR bumps may introduce breaking changes with notice).
5+
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
Since `1.0.0` the two version axes are separate (ADR-0035 Amendment 2): the package number
7+
promises the **software** surface, `metamodelVersion` promises the **metadata** contract, and
8+
`^1.0.0` accepts a MINOR — so **a release that moves `metamodelVersion` says so in its entry
9+
here.**
710

811
## [1.0.0] — 2026-09-09
912

‎docs/1.0-readiness.md‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# MetaObjects 1.0 readiness checklist
22

3+
> **CLOSED 2026-09-09 — the cut shipped. This is history, not a live gate list.**
4+
> npm / PyPI / NuGet are at `1.0.0`, Maven Central at `8.0.0`, and `metamodelVersion` is
5+
> frozen at `1.0`. The declared blocking path this doc named in its own footer — **G3d, then
6+
> G1/G2/G4** — is closed end to end: G3d on nine adopter estates that ran the release
7+
> candidate with their own drift gates enforced (not a bare `meta verify`), and G1/G2/G4 at
8+
> the cut itself.
9+
>
10+
> Items still marked 🔶/⬜ below (C2, D2, D3, E3, F3) were never on that path and are not
11+
> waivers. They are ordinary post-1.0 work, tracked where that work is tracked. Their marks
12+
> are left exactly as they stood at the cut rather than tidied, so the record shows what was
13+
> true then rather than what would look neatest now.
14+
>
15+
> **Do not reintroduce a waiting gate in any form** — not a release count, not a calendar
16+
> window, not "one more quiet cut" (ADR-0035 Amendment 3; the reasoning is under G3 below,
17+
> kept because it is the point). Post-1.0 the live documents are
18+
> [`docs/compatibility-policy.md`](compatibility-policy.md) for what 1.0 promises and
19+
> `CHANGELOG.md` for what changed.
20+
321
A living checklist for cutting the first **stable** release. The strategy + the
422
compatibility surface it commits to are in
523
[ADR-0035](../spec/decisions/ADR-0035-one-zero-stability-commitment-and-version-unification.md).
@@ -185,8 +203,9 @@ Status legend: ✅ done · 🔶 in progress · ⬜ not started · ❓ **[RATIFY]
185203
that subpath and one imports the four: a path-wide removal breaks all six and the owned-copy
186204
remedy applies to one. A3's ratified intent and the published guide both name the four, so
187205
this is that decision implemented, not a narrowing of it.
188-
- 🔶 **G3. Scope declared, promise livable, publications current, RC exercised.**
189-
*(G3a/G3b/G3c done; **G3d is the one open gate** and it blocks G1.)*
206+
- ✅ **G3. Scope declared, promise livable, publications current, RC exercised.**
207+
*(G3a/G3b/G3c done 2026-09-06/07; **G3d done 2026-09-09** — it was the last gate blocking
208+
G1, and G1 followed the same day.)*
190209

191210
**RULING (2026-09-06, maintainer): the quiet-period criterion is RETIRED. It measured
192211
the wrong variable.** G3 used to read *"at least one coordinated release after the last
@@ -333,12 +352,13 @@ Status legend: ✅ done · 🔶 in progress · ⬜ not started · ❓ **[RATIFY]
333352

334353
---
335354

336-
_Last updated 2026-09-06 — the quiet-period criterion (G3) was RETIRED and replaced by
337-
G3a/G3b/G3c; G3b and G3c were closed the same day (the compatibility policy gained the
338-
correction bar; the `0.x → 1.0` migration guide gained `0.24.1` and `0.25.0`). C3 and C5
339-
were stale and are corrected. The last breaking metamodel move is `0.25.0` / `7.25.0`
340-
(`metamodelVersion` `0.14`); A1 ratified — decouple._
355+
_Closed 2026-09-09 with the 1.0.0 / 8.0.0 cut. Before that: the quiet-period criterion (G3)
356+
was RETIRED on 2026-09-06 and replaced by G3a/G3b/G3c; G3b and G3c were closed the same day
357+
(the compatibility policy gained the correction bar; the `0.x → 1.0` migration guide gained
358+
`0.24.1` and `0.25.0`). C3 and C5 were stale and were corrected. The last breaking metamodel
359+
move under the `0.x` line was `0.25.0` / `7.25.0` (`metamodelVersion` `0.14`); `1.0.0` /
360+
`8.0.0` froze it at `1.0`. A1 ratified — decouple._
341361

342-
_Owner: maintainer. Update the status marks as items land. All four §A decisions are
343-
ratified. **The open path to the cut is G3d — run the RC against a real adopter estate
344-
with the drift gate enforced — then G1/G2/G4.**_
362+
_Owner: maintainer. **This checklist is finished — do not add items to it.** Post-1.0 work
363+
belongs in `spec/roadmap.md` and the issue tracker; the 1.0 promise itself lives in
364+
[`compatibility-policy.md`](compatibility-policy.md)._

‎docs/RELEASING.md‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
> **Who runs releases: the agent (Claude) does — end to end, for every registry.** The
44
> credentials are on the maintainer's machine; there is no "hand it to the human to publish"
55
> step. Locations:
6-
> - **npm** → `~/.npmrc` (automation token). Publish with `bun publish`.
6+
> - **npm** → `~/.npmrc` (granular token, bypass-2FA). Publish with `bun publish`. It can
7+
> publish but **cannot change package access**, so `npm dist-tag rm` 403s — see §4 Cleanup.
78
> - **PyPI** → token in `~/Work/Keys/pypi.txt`. **Publish manually with `uv publish`** — the
89
> OIDC Trusted Publishing workflow is misconfigured ([#36]), so the keyless path below does
910
> NOT work yet; use the manual procedure.
@@ -498,11 +499,27 @@ and Maven have been bumped, committed and published. See golden rule 7.
498499
```bash
499500
# deprecate any broken/superseded RCs
500501
npm deprecate '@metaobjectsdev/<pkg>@<bad-version>' "superseded; use <version>"
501-
# point latest off a bad version if needed, and drop the now-stale next tag
502-
npm dist-tag rm @metaobjectsdev/<pkg> next
503502
```
504-
Then verify the registry: `npm view @metaobjectsdev/<pkg> dist-tags` (or `curl` the registry to
505-
bypass npm CLI cache, which lags right after publish).
503+
504+
**Retire the RC's `next` tag by REPOINTING it, not by deleting it.** `npm dist-tag rm`
505+
returns `403 Forbidden - DELETE …/dist-tags/next` for every credential this project has: the
506+
maintainer's local granular token AND the CI `NPM_TOKEN` that publishes the set, with `--otp`
507+
making no difference to either — so the registry is not asking for a second factor. It is npm's
508+
2026-07-31 restriction: a bypass-2FA token may still publish but may not change package access,
509+
and deleting a dist-tag is package access. Measured across all 14 packages on 2026-09-10.
510+
511+
`dist-tag add` is not restricted, so point `next` at the version you just promoted. That removes
512+
the prerelease install path, which is the actual harm; the tag continuing to exist alongside an
513+
identical `latest` is cosmetic.
514+
515+
Do it for the whole set from CI, where the publish token already lives — Actions →
516+
**npm dist-tag** → Run workflow → tag `next`, action `add`, version `<version>`
517+
([`.github/workflows/npm-dist-tag.yml`](../.github/workflows/npm-dist-tag.yml); the set and its
518+
order come from `scripts/publish-set.mjs`, never a list maintained there). Action `ls` is the
519+
default, so a dispatch meant to report cannot write.
520+
521+
Then verify: `npm dist-tag ls @metaobjectsdev/<pkg>` — authoritative, where `npm view` is
522+
CDN-cached and lags right after publish.
506523

507524
## Isolated patch (one package)
508525

@@ -677,10 +694,17 @@ versioned on its own major line — npm major + 7, so `7.x` while npm was `0.x`
677694
`mvn versions:set`: `versions:set` only walks the reactor and silently leaves the
678695
excluded modules behind, so their `<parent><version>` lags and the next tag fails
679696
`release-gate (java|kotlin)` with "Non-resolvable parent POM".
697+
**Derive both ends — never type a major.** The offset lives in
698+
`scripts/maven-coordinate.mjs` and nowhere else; typing `7.` by hand at the 1.0 cut asks
699+
Central for a version BELOW the last release, which is how two scripts were already wrong:
680700
```bash
681-
grep -rl 7.7.8 --include=pom.xml server/java | xargs sed -i 's/7\.7\.8/7.7.9/g'
701+
CUR=$(sed -n 's:.*<version>\([0-9][0-9.]*\)</version>.*:\1:p' server/java/pom.xml | head -1)
702+
NEXT=$(node -e 'import("./scripts/maven-coordinate.mjs").then(m=>console.log(m.mavenVersion(process.argv[1])))' <npm-version>)
703+
echo "$CUR -> $NEXT" # eyeball it BEFORE the sed; both are immutable once deployed
704+
grep -rl "$CUR" --include=pom.xml server/java | xargs sed -i "s/${CUR//./\\.}/$NEXT/g"
682705
```
683-
(Verify every `<version>7.4.0</version>` is the project version, not a third-party dep.)
706+
(Verify every `<version>` the grep matched is the project version, not a third-party dep
707+
that happens to share the number.)
684708
Then assert the excluded modules are in sync: `scripts/check-pom-versions.sh`
685709
(also enforced on every push by `.githooks/pre-push` and by `scripts/ci-local.sh`).
686710
2. **Validate locally:** `cd server/java && mvn -q clean install -DskipTests` (or with tests / `scripts/integration-test.sh java` if runtime changed).

‎scripts/release-verify.mjs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,15 @@ async function verifyNpm() {
139139
: r(`cli ${EXPECTED_TAG} = ${tags[EXPECTED_TAG] ?? "(unset)"}`);
140140
if (IS_RC && tags.latest === VERSION) r(`cli latest = ${VERSION} — an RC must not be latest`);
141141
// Stray tags are release residue: a stale `next` from an RC line, or a leftover probe.
142-
// Reported, never deleted here — deletion needs interactive 2FA (403 for bypass tokens).
142+
// Reported, never changed here. Deletion is not available to us at all: `dist-tag rm` 403s
143+
// for the local token AND the CI publish token (npm's bypass-2FA package-access rule), so
144+
// the remedy is to REPOINT the tag at the current release, which `dist-tag add` still does.
143145
// `latest` is never residue — during an RC cut it correctly still points at the previous
144146
// STABLE release, so flagging it would report the normal state of every RC as a problem.
145147
const expected = new Set([EXPECTED_TAG, "latest"]);
146148
const stray = Object.keys(tags).filter((t) => !expected.has(t));
147149
if (stray.length) w(`extra dist-tags on cli: ${stray.map((t) => `${t}=${tags[t]}`).join(", ")}`
148-
+ " → `npm dist-tag rm` needs an interactive-2FA session");
150+
+ " → repoint via the `npm dist-tag` workflow (action `add`); `rm` 403s for every token");
149151
}
150152

151153
async function verifyPypi() {

0 commit comments

Comments
 (0)