Skip to content

Commit 2367c5e

Browse files
dmealingclaude
andcommitted
fix(release-verify): the verifier could not express the release it exists for
`release-verify.mjs` is the one committed place release verification lives, and it would have been WRONG on the 1.0 cut in three ways — each of which reports a correct release as broken, the failure mode that teaches people to run a gate with their eyes closed. 1. The Maven coordinate was hardcoded `7.${minor}.${patch}`. ADR-0035's decoupled cut moves BOTH lines forward one major (npm 1.0.0 = Maven 8.0.0), so for the 1.0 line it produced `7.0.0` — a version BELOW the released 7.25.0 — and would have reported all nine modules missing from Central while they were live. Now `npm major + 7`, which reproduces every historical `0.x → 7.x` mapping unchanged, with `--maven=<version>` if the two lines ever stop moving together. 2. A pre-release was unrepresentable: the argument matcher demanded `X.Y.Z`, so `1.0.0-rc.5` was silently read as "no version given" and the script fell through to its preflight-only branch. It now accepts `-rc.N` and applies the same four per-ecosystem spellings `prerelease.mjs` uses — PyPI normalizes to PEP 440 (`1.0.0rc5`), npm and NuGet take the string verbatim. 3. The dist-tag check asserted `latest === VERSION`, which is the wrong question for an RC (it lives on `next`) and would fail every correct RC cut. It now checks the tag the version SHOULD be on, and adds the opposite assertion that actually matters: an RC must not be `latest`. `latest` is likewise no longer reported as a stray tag during an RC — pointing at the previous stable release is exactly where it belongs. Exercised against the live 1.0.0-rc.5 cut: 14 npm packages on `next`, PyPI 1.0.0rc5, four NuGet packages, nine Maven modules at 8.0.0-rc.5 — all four registries green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent 9dc5642 commit 2367c5e

1 file changed

Lines changed: 36 additions & 7 deletions

File tree

‎scripts/release-verify.mjs‎

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55
// node scripts/release-verify.mjs --preflight # can we publish at all?
66
// node scripts/release-verify.mjs 0.24.0 # is 0.24.0 actually live, everywhere?
7+
// node scripts/release-verify.mjs 1.0.0-rc.5 # ...an RC works too (checked on `next`)
78
// node scripts/release-verify.mjs 0.24.0 --smoke # ...plus a real external install
89
// node scripts/release-verify.mjs 0.24.5 --registries=npm,maven
910
// # ...only the registries this cut published
@@ -23,10 +24,32 @@ import { join, dirname } from "node:path";
2324
import { homedir } from "node:os";
2425

2526
const args = process.argv.slice(2);
26-
const VERSION = args.find((a) => /^\d+\.\d+\.\d+$/.test(a));
27+
const VERSION = args.find((a) => /^\d+\.\d+\.\d+(-rc\.\d+)?$/.test(a));
2728
const PREFLIGHT = args.includes("--preflight");
2829
const SMOKE = args.includes("--smoke");
29-
const MVN = VERSION && `7.${VERSION.split(".").slice(1).join(".")}`;
30+
31+
// ── one version, four spellings ────────────────────────────────────────────────────────
32+
// The canonical string is the npm one (`1.0.0`, or `1.0.0-rc.5`). npm and NuGet take it
33+
// verbatim; PyPI normalizes to PEP 440 (`1.0.0rc5`) and Maven Central sits on its own
34+
// major. These are the same four mappings `scripts/prerelease.mjs` already applies for the
35+
// private registry — kept in step, not shared, because that script derives its base from
36+
// CHANGELOG.md and this one is told the version.
37+
//
38+
// The Maven major is `npm major + 7`: the `0.x` line has always been `7.x`, and ADR-0035's
39+
// decoupled cut moves BOTH forward one major at 1.0 (npm 1.0.0 = Maven 8.0.0). The old
40+
// hardcoded `7.` silently produced `7.0.0` for the 1.0 line — a version BELOW 7.25.0, so
41+
// this script would have reported the 1.0 release missing from Maven Central while it was
42+
// live. Override with `--maven=<version>` if the two lines ever stop moving together.
43+
const parts = VERSION?.match(/^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/);
44+
const IS_RC = Boolean(parts?.[4]);
45+
const PYPI_VERSION = parts && `${parts[1]}.${parts[2]}.${parts[3]}${IS_RC ? `rc${parts[4]}` : ""}`;
46+
const MVN = args.find((a) => a.startsWith("--maven="))?.slice("--maven=".length)
47+
?? (parts && `${Number(parts[1]) + 7}.${parts[2]}.${parts[3]}${IS_RC ? `-rc.${parts[4]}` : ""}`);
48+
49+
// A pre-release is published to `next` and must NOT move `latest` — checking `latest`
50+
// against an RC would fail every correct RC cut, and the mistake it is really guarding
51+
// against (an RC promoted to `latest` by accident) is the opposite condition.
52+
const EXPECTED_TAG = IS_RC ? "next" : "latest";
3053

3154
// ── which registries this cut actually published ───────────────────────────────────────
3255
// Since 0.24.5 a registry publishes ONLY when it has a changed product file, and adopts the
@@ -107,18 +130,24 @@ async function verifyNpm() {
107130
if (ok === NPM_PKGS.length) g(`all ${ok}/${NPM_PKGS.length} packages at ${VERSION}`);
108131

109132
const tags = JSON.parse(out("npm view @metaobjectsdev/cli dist-tags --json"));
110-
tags.latest === VERSION ? g(`cli latest = ${VERSION}`) : r(`cli latest = ${tags.latest}`);
133+
tags[EXPECTED_TAG] === VERSION
134+
? g(`cli ${EXPECTED_TAG} = ${VERSION}`)
135+
: r(`cli ${EXPECTED_TAG} = ${tags[EXPECTED_TAG] ?? "(unset)"}`);
136+
if (IS_RC && tags.latest === VERSION) r(`cli latest = ${VERSION} — an RC must not be latest`);
111137
// Stray tags are release residue: a stale `next` from an RC line, or a leftover probe.
112138
// Reported, never deleted here — deletion needs interactive 2FA (403 for bypass tokens).
113-
const stray = Object.keys(tags).filter((t) => t !== "latest");
139+
// `latest` is never residue — during an RC cut it correctly still points at the previous
140+
// STABLE release, so flagging it would report the normal state of every RC as a problem.
141+
const expected = new Set([EXPECTED_TAG, "latest"]);
142+
const stray = Object.keys(tags).filter((t) => !expected.has(t));
114143
if (stray.length) w(`extra dist-tags on cli: ${stray.map((t) => `${t}=${tags[t]}`).join(", ")}`
115144
+ " → `npm dist-tag rm` needs an interactive-2FA session");
116145
}
117146

118147
async function verifyPypi() {
119-
console.log(`\n── PyPI @ ${VERSION} ──\n`);
120-
const s = await head(`https://pypi.org/pypi/metaobjects/${VERSION}/json`);
121-
s === 200 ? g(`metaobjects ${VERSION}`) : r(`metaobjects ${VERSION} → HTTP ${s}`);
148+
console.log(`\n── PyPI @ ${PYPI_VERSION} ──\n`);
149+
const s = await head(`https://pypi.org/pypi/metaobjects/${PYPI_VERSION}/json`);
150+
s === 200 ? g(`metaobjects ${PYPI_VERSION}`) : r(`metaobjects ${PYPI_VERSION} → HTTP ${s}`);
122151
}
123152

124153
async function verifyNuget() {

0 commit comments

Comments
 (0)