Skip to content

Commit 481cf99

Browse files
dmealingclaude
andcommitted
fix(release): the tag gate matched a version prefix, so it stopped checking at 1.0
`finish-release.mjs` gate 6 refuses to cut a tag the site deploy would not resolve. It found the deploy's answer with `git tag -l 'v0.*'`. That was true until the 1.0 cut and then quietly stopped being true: it resolves `v0.25.0` now and will forever, because the 0.x line is frozen. A prefix match does not fail when it goes stale — it selects the newest tag of a dead line and passes — so the gate would have gone green at every future release while checking nothing. This is the same defect that kept metaobjects.dev serving 0.25.0 after the 1.0 cut with every deploy step green; it was fixed in the site repo and survived here, in the script that cuts the tag the site resolves. It now mirrors what deploy.yml actually does: walk `v*` newest-first and take the first tag whose `cli/package.json` is versioned AS the tag. That is the property that DEFINES the npm line (v1.0.0 ships cli 1.0.0; the JVM-only v7.20.12 ships cli 0.20.11), it is what keeps the v7.x line out without naming a digit, and there is nothing to maintain at the next major. **The tests could not have caught this, and that is the more useful finding.** Every gate-6 fixture tagged its versions on the 0.x line, where the prefix and the intrinsic property agree — so both selectors passed all four cases and the suite was blind to the bug it shipped with. Worse, `makeRepo` cut every requested tag at the single fixture commit, so no tag agreed with its own tree; under a faithful selector nothing resolved and two "is refused" cases would have wrongly passed. So the fixture is now faithful: each tag gets its own commit stamping `cli/package.json` at that tag's version, with a `tagCli` override for a tag that must NOT read as the npm line — which lets the JVM-line test assert the real reason v7.20.12 is skipped (its tree disagrees) instead of the reason that expired. Two cases are added on the 1.x line, and they are the ones that discriminate: reverting the selector to `v0.*` turns `cutting v1.0.1 over v0.25.0,v1.1.0 is refused` red and leaves the other 39 green, which is the proof that the gate now covers the defect rather than merely passing. The fixture's hardcoded `maven: 7.<minor>.<patch>` is derived too — it is correct only on the 0.x line and would have failed the 1.x cases for a reason that is not gate 6's. Alongside: the README's language rows named a registry version for TypeScript only, so a reader learned where to get one port of five; all of them now carry their registry and version. The release-docs checklist pointed at a "parity status as of <ver>" marker that no longer exists in `extending-with-providers.md`, and says so rather than sending the next cut looking for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent 49131e3 commit 481cf99

5 files changed

Lines changed: 93 additions & 21 deletions

File tree

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Equal weight — all four ship per-language today across the five ports (TS / C#
1717

1818
## Status
1919

20-
_Last refreshed 2026-09-08._
20+
_Last refreshed 2026-09-09._
2121

2222
**1.0 gating — the quiet period is RETIRED (2026-09-06).** `docs/1.0-readiness.md` §G3 no
2323
longer asks for "one coordinated release with no metamodel-breaking change." It measured a
@@ -37,7 +37,8 @@ in ADR-0035 **Amendment 3**. Do not reintroduce a waiting gate in any form.
3737
**Where the versions are.** `latest` on every registry is **`1.0.0`** (npm / PyPI / NuGet) ·
3838
**`8.0.0`** (Maven Central, aligned `minor.patch` on its historical major `7` — the Maven
3939
major is always **npm major + 7**, so 1.0.0 is 8.0.0). **1.0 is CUT**: the `1.0.0-rc.5`
40-
through `rc.8` candidates are superseded and their npm `next` tag is retired.
40+
through `rc.8` candidates are superseded, and npm's `next` tag was REPOINTED onto `1.0.0` —
41+
not deleted, because `dist-tag rm` 403s for every token we hold (`docs/RELEASING.md` §4).
4142
`metamodelVersion` reads **`1.0`**, frozen — C4 landed and G4 shipped it. Per-release detail
4243
lives in **`CHANGELOG.md`** — it is the log, and this file does not duplicate it.
4344

‎README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,11 @@ first-week wedge plan — and `meta init` picks up from there.
9393

9494
| Language | Status | Quickstart | Source |
9595
|---|---|---|---|
96-
| TypeScript | Published to npm at `1.0.0` (the `@metaobjectsdev/*` packages) | [`docs/ports/typescript.md`](docs/ports/typescript.md) | [`server/typescript/`](server/typescript/) · [`client/web/`](client/web/) |
97-
| Java | Loader + OMDB + render + Maven plugin all shipped; full conformance green | [`docs/ports/java.md`](docs/ports/java.md) | [`server/java/`](server/java/) |
98-
| Kotlin | Codegen tier on top of Java — 14 generators (entity, Exposed table, relations, repository, payload, output-parser, output-prompt, render-helper, extractor, filter-allowlist, validator, Spring config, storedProc, Spring controller); 24 / 24 persistence-conformance | [`docs/ports/kotlin.md`](docs/ports/kotlin.md) | [`server/java/codegen-kotlin/`](server/java/codegen-kotlin/) · [`server/java/metadata-ktx/`](server/java/metadata-ktx/) |
99-
| C# | Loader + conformance + EF Core codegen + render engine + `dotnet meta` CLI all shipped | [`docs/ports/csharp.md`](docs/ports/csharp.md) | [`server/csharp/`](server/csharp/) |
100-
| Python | Loader + conformance + render + entity-model codegen + ObjectManager runtime shipped; schema migrations are TS-owned (ADR-0015) | [`docs/ports/python.md`](docs/ports/python.md) | [`server/python/`](server/python/) |
96+
| TypeScript | **npm `1.0.0`** — the `@metaobjectsdev/*` packages | [`docs/ports/typescript.md`](docs/ports/typescript.md) | [`server/typescript/`](server/typescript/) · [`client/web/`](client/web/) |
97+
| Java | **Maven Central `8.0.0`** (`com.metaobjects:*` — the JVM major is npm major + 7) — loader + OMDB + render + Maven plugin all shipped; full conformance green | [`docs/ports/java.md`](docs/ports/java.md) | [`server/java/`](server/java/) |
98+
| Kotlin | **Maven Central `8.0.0`** — codegen tier on top of Java: 14 generators (entity, Exposed table, relations, repository, payload, output-parser, output-prompt, render-helper, extractor, filter-allowlist, validator, Spring config, storedProc, Spring controller); 24 / 24 persistence-conformance | [`docs/ports/kotlin.md`](docs/ports/kotlin.md) | [`server/java/codegen-kotlin/`](server/java/codegen-kotlin/) · [`server/java/metadata-ktx/`](server/java/metadata-ktx/) |
99+
| C# | **NuGet `1.0.0`** — loader + conformance + EF Core codegen + render engine + `dotnet meta` CLI all shipped | [`docs/ports/csharp.md`](docs/ports/csharp.md) | [`server/csharp/`](server/csharp/) |
100+
| Python | **PyPI `1.0.0`** — loader + conformance + render + entity-model codegen + ObjectManager runtime shipped; schema migrations are TS-owned (ADR-0015) | [`docs/ports/python.md`](docs/ports/python.md) | [`server/python/`](server/python/) |
101101

102102
## Capability matrix
103103

‎docs/RELEASING-docs-checklist.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ by exactly one release.
3131
- [ ] `docs/ports/java.md` — Maven `<version>` + artifact list.
3232
- [ ] `docs/ports/kotlin.md` — Maven `<version>`.
3333
- [ ] `docs/ports/python.md` — PyPI version + `pip install`.
34-
- [ ] `docs/features/extending-with-providers.md` — the "parity status as of `<ver>`" marker.
34+
- [ ] `docs/features/extending-with-providers.md` — **no version edit any more.** Its
35+
"Cross-port parity status" table reads `Shipped` for all five ports with no version
36+
scope, so there is no "as of `<ver>`" marker left to refresh (checked at the 1.0 cut).
37+
Re-add one here only if parity ever becomes version-scoped again.
3538
- [ ] `docs/RELEASING.md` / `docs/RELEASING-java.md` — the "currently `<ver>`" notes.
3639
- [ ] `.github/workflows/publish-*.yml` — **no version edits by design.** All four
3740
publish workflows read the version from the committed manifest

‎scripts/finish-release.mjs‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,10 +314,24 @@ for (const rel of ["docs/llms/llms.txt", "docs/llms/llms-full.txt"]) {
314314
ok("the llms mirrors state this release");
315315

316316
// ── 6. the tag the DEPLOY will resolve is the one being cut ──────────────────
317-
// The repo carries two tag lines and `sort -V` over a bare `v*` returns v7.20.12, which
318-
// has no examples/showcase at all. This mirrors deploy.yml's own filter, so a change to
319-
// either is caught here rather than on a deploy nobody is watching.
320-
const resolved = out(`git tag -l 'v0.*' | grep -E '^v0\\.[0-9]+\\.[0-9]+$' | sort -V | tail -1 || true`);
317+
// MIRRORS deploy.yml's own selector, so a change to either is caught here rather than on
318+
// a deploy nobody is watching. The repo carries two tag lines and `sort -V` over a bare
319+
// `v*` returns v7.20.12 — the JVM-only line, which has no examples/showcase at all.
320+
//
321+
// It matches deploy.yml by testing the property that DEFINES the npm line — a release tag
322+
// ships a `cli/package.json` versioned as the tag (v1.0.0 ships cli 1.0.0; v7.20.12 ships
323+
// cli 0.20.11) — and NOT a version prefix. This gate used to filter `v0.*`, which was true
324+
// until it wasn't: a prefix match does not fail when it goes stale, it silently selects the
325+
// newest tag of a frozen line and passes green forever. After the 1.0 cut it would have
326+
// resolved v0.25.0 at every future release, so the check could never fire again — the same
327+
// defect that kept the live site on 0.25.0 with every deploy step green. Nothing here needs
328+
// maintaining at the next major.
329+
const resolved = out(`git tag -l 'v*' | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+$' | sort -rV || true`)
330+
.split("\n").map((t) => t.trim()).filter(Boolean)
331+
.find((t) => {
332+
const pkg = out(`git show ${t}:server/typescript/packages/cli/package.json 2>/dev/null || true`);
333+
try { return JSON.parse(pkg).version === t.slice(1); } catch { return false; }
334+
}) ?? "";
321335
const wouldResolve = [resolved, tag].filter(Boolean).sort((a, b) =>
322336
a.localeCompare(b, undefined, { numeric: true })).at(-1);
323337
if (wouldResolve !== tag) {

‎scripts/finish-release.test.ts‎

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,13 @@ type Spec = {
7171
manifest?: Partial<Coords>;
7272
/** Whole-file body for both mirrors. Defaults to one consistent with `payload`. */
7373
llms?: string;
74-
/** Extra tags to create locally before the run (gate 6's tag line). */
74+
/** Extra tags to create locally before the run (gate 6's tag line). Each is cut on its
75+
* own commit stamping `cli/package.json` at the tag's own version, because that
76+
* agreement is what identifies the npm release line. */
7577
tags?: string[];
78+
/** Per-tag override of the stamped `cli` version, for a tag that must NOT read as the
79+
* npm line (the JVM `v7.20.12` ships cli `0.20.11`). */
80+
tagCli?: Record<string, string>;
7681
/** Files the tree deliberately does NOT carry (gate 4 / gate 5). */
7782
omit?: string[];
7883
/** Verbatim overrides, applied last. */
@@ -164,7 +169,32 @@ function makeRepo(spec: Spec = {}): Fixture {
164169
git(work, "commit", "--quiet", "-m", "fixture");
165170
git(work, "remote", "add", "origin", origin);
166171
git(work, "push", "--quiet", "-u", "origin", "main");
167-
for (const t of spec.tags ?? []) git(work, "tag", t);
172+
// A prior release tag must name a tree that AGREES with it. The site deploy — and the
173+
// gate that mirrors it — identifies the npm release line by the intrinsic property that
174+
// a tag `vX.Y.Z` ships `cli/package.json` at X.Y.Z, NOT by a version prefix (a prefix
175+
// goes stale silently at a major: `v0.*` kept resolving v0.25.0 after the 1.0 cut and
176+
// passed green forever). Tagging every version at one commit made every tag disagree
177+
// with itself, so the fixture gives each its own commit and returns to the release tree.
178+
// `tagCli` overrides the stamped version for a tag that must NOT count as the npm line —
179+
// the real `v7.20.12` is the JVM line and ships cli `0.20.11`, which is why it is skipped.
180+
const cliRel = "server/typescript/packages/cli/package.json";
181+
const cliPath = join(work, cliRel);
182+
const releaseCli = readFileSync(cliPath, "utf8");
183+
const tags = spec.tags ?? [];
184+
for (const t of tags) {
185+
const stamped = spec.tagCli?.[t] ?? t.replace(/^v/, "");
186+
writeFileSync(cliPath,
187+
`${JSON.stringify({ name: "@metaobjectsdev/cli", version: stamped }, null, 2)}\n`);
188+
// --allow-empty: a tag naming the version being cut (gate 2's local-only case) stamps
189+
// a value the tree already has, and "nothing to commit" is an error, not a no-op.
190+
git(work, "commit", "--quiet", "-a", "--allow-empty", "-m", `prior release ${t}`);
191+
git(work, "tag", t);
192+
}
193+
if (tags.length > 0) {
194+
writeFileSync(cliPath, releaseCli);
195+
git(work, "commit", "--quiet", "-a", "--allow-empty", "-m", "back to the release tree");
196+
git(work, "push", "--quiet", "origin", "main");
197+
}
168198
if (spec.breakOrigin === true) rmSync(origin, { recursive: true, force: true });
169199

170200
return {
@@ -458,21 +488,39 @@ describe("finish-release gate 5: the llms mirrors state this release", () => {
458488
});
459489

460490
describe("finish-release gate 6: the tag the deploy will resolve", () => {
461-
// The deploy resolves `git tag -l 'v0.*' | grep -E '^v0\.[0-9]+\.[0-9]+$' | sort -V |
462-
// tail -1`. This mirrors that filter, so a change to either is caught here rather than
463-
// on a deploy nobody is watching — and the comparison is NUMERIC, which a plain string
464-
// sort gets wrong at every ten-fold boundary.
491+
// The deploy walks `git tag -l 'v*'` newest-first and takes the first tag whose
492+
// `cli/package.json` is versioned AS the tag. This mirrors that selector, so a change to
493+
// either is caught here rather than on a deploy nobody is watching — and the comparison
494+
// is NUMERIC, which a plain string sort gets wrong at every ten-fold boundary.
495+
//
496+
// It deliberately does NOT mirror a version prefix. Both sides used to filter `v0.*`,
497+
// which was true until the 1.0 cut and then silently resolved v0.25.0 forever: a prefix
498+
// match selects a stale answer instead of failing, so the gate would have passed green
499+
// at every future release while checking nothing.
465500
const cases: Array<{ existing: string[]; cutting: string; passes: boolean; why: string }> = [
466501
{ existing: ["v0.9.0"], cutting: "0.10.0", passes: true, why: "0.10.0 is newer than 0.9.0" },
467502
{ existing: ["v0.24.9"], cutting: "0.24.10", passes: true, why: "0.24.10 is newer than 0.24.9" },
468503
{ existing: ["v0.10.0"], cutting: "0.9.0", passes: false, why: "0.9.0 is OLDER than 0.10.0" },
469504
{ existing: ["v0.24.10"], cutting: "0.24.9", passes: false, why: "0.24.9 is OLDER than 0.24.10" },
505+
// The two cases that a `v0.*` prefix filter gets WRONG, and the reason this suite could
506+
// not catch the bug it shipped with: every case above lives on the 0.x line, where the
507+
// prefix and the intrinsic property agree, so both selectors pass all of them. Once a
508+
// 1.x tag exists the prefix stops seeing the newest release at all — it resolves the
509+
// frozen v0.25.0 and waves through a tag the deploy will never resolve.
510+
{ existing: ["v0.25.0", "v1.1.0"], cutting: "1.0.1", passes: false,
511+
why: "v1.1.0 outranks it — a v0.* filter would resolve v0.25.0 and pass" },
512+
{ existing: ["v0.25.0", "v1.0.0"], cutting: "1.0.1", passes: true,
513+
why: "v1.0.0 is the newest on the npm line and 1.0.1 is newer still" },
470514
];
471515
for (const c of cases) {
472516
test(`cutting v${c.cutting} over ${c.existing.join(",")} ${c.passes ? "passes" : "is refused"} — ${c.why}`, () => {
517+
// The Maven major is npm major + 7, DERIVED — the literal `7.` this line used to
518+
// carry is correct only while the npm line sits on 0.x, and the 1.x cases below
519+
// would have failed the manifest-agreement gate for a reason that is not gate 6's.
520+
const [maj, ...rest] = c.cutting.split(".");
473521
const coords: Coords = {
474522
npm: c.cutting, pypi: c.cutting, nuget: c.cutting,
475-
maven: `7.${c.cutting.split(".").slice(1).join(".")}`,
523+
maven: `${Number(maj) + 7}.${rest.join(".")}`,
476524
};
477525
const r = makeRepo({ payload: coords, manifest: coords, llms: llms(coords), tags: c.existing })
478526
.run(c.cutting, "--check");
@@ -487,9 +535,15 @@ describe("finish-release gate 6: the tag the deploy will resolve", () => {
487535
}
488536

489537
// The repository carries two tag lines and a bare `v*` sort returns v7.20.12, a tree
490-
// with no examples/showcase at all. The grep is what keeps the JVM line out.
538+
// with no examples/showcase at all. What keeps the JVM line out is that its tree
539+
// DISAGREES with it: the real v7.20.12 ships cli 0.20.11, not 7.20.12. That is a
540+
// property of the tag itself, so it needs nothing maintained at the next major — unlike
541+
// the prefix filter this replaced, which named the npm line by a digit that expired.
491542
test("the JVM tag line does not count as the newest tag", () => {
492-
const r = makeRepo({ tags: ["v7.20.12", "v0.24.5"] }).run(RELEASE, "--check");
543+
const r = makeRepo({
544+
tags: ["v7.20.12", "v0.24.5"],
545+
tagCli: { "v7.20.12": "0.20.11" },
546+
}).run(RELEASE, "--check");
493547
expect(r.out).toContain(`the site deploy will resolve v${RELEASE}`);
494548
expect(r.code).toBe(0);
495549
});

0 commit comments

Comments
 (0)