Who runs releases: the agent (Claude) does — end to end, for every registry. The credentials are on the maintainer's machine; there is no "hand it to the human to publish" step. Locations:
- npm →
~/.npmrc(automation token). Publish withbun publish.- PyPI → token in
~/Work/Keys/pypi.txt. Publish manually withuv publish— the OIDC Trusted Publishing workflow is misconfigured ([#36]), so the keyless path below does NOT work yet; use the manual procedure.- Maven Central →
~/.m2/settings.xml(server idscentral+gpg-credentials) + the local GPG signing key.mvn -Prelease deployfromserver/java(autoPublish — no manual staging promotion).- NuGet → keyless OIDC via the
publish-csharp.ymlworkflow:gh workflow run publish-csharp.yml(or push acsharp-v<version>tag).Run network commands via the sandbox-disabled shell. Single shared patch number (standing policy since 0.20.13): every release cuts ALL FOUR registries at the same
minor.patch— npm / PyPI / NuGet on0.<m>.<p>, Maven on7.<m>.<p>(only the major differs, for historical continuity). A registry with no changed product file publishes a version-parity bump (identical content at the new version) rather than sitting the release out. The rest of this doc is the per-registry procedure.
Run this first, before the CHANGELOG is even written:
git fetch origin -q
for b in $(git branch -a --no-merged origin/main --format='%(refname)' | grep -v HEAD); do
d=$(git log -1 --format=%ct "$b" 2>/dev/null); [ -z "$d" ] && continue
age=$(( ( $(date +%s) - d ) / 86400 ))
[ "$age" -le 14 ] && printf "%3sd %s %s\n" "$age" "$(git log -1 --format=%h "$b")" "${b#refs/}"
done | sort -nThe recency filter is load-bearing: a bare git branch -a --no-merged returns ~100 branches here
(abandoned spikes, worktree-agent-* artifacts, old release branches). A wall that size gets
skimmed, which defeats the check — so bound it to what could plausibly belong in this cut.
Decide explicitly for every branch the scan returns: merge it into this cut, or state why it waits.
Note that no-mistakes/* branches are usually gate replays of work already on main — confirm
rather than assume. Do not skip past the list.
Why this is step 0 and not a nicety: 0.21.1 shipped without a timestampMode fix that was
already written and sitting on an unmerged branch. Because versions are immutable on all four
registries, the only correction was a full coordinated 0.21.2 within the hour — four publishes, an
~11-minute Maven deploy, and a second round of adopter notification, to ship work that already
existed. The single shared patch number makes an omission cost four publishes, not one.
How to publish the @metaobjectsdev/* TypeScript packages. Read the Golden rules first —
each one cost a broken/burned release to learn.
After any release, walk
RELEASING-docs-checklist.md— it lists every doc + website (this repo, metaobjects.dev, metaobjects.com) whose version references must be refreshed. A version bump is not done until that list is walked.
The publish-candidate packages (versioned in lockstep unless a package gets an
isolated patch). Enumerate the set each release — do not trust this count (it
was 13 at 0.11.5); the lockstep set is "every non-private package at the
previous version":
node scripts/publish-set.mjs --check # the set + its tier order, and the invariants
scripts/publish-set.mjsis the single source of truth for the set and its order. Both publish paths read it —scripts/release.mjsand.github/workflows/publish-npm.yml— because they used to answer the question separately and drifted: the workflow's hardcoded list of 13 directories omitted@metaobjectsdev/docs-site, which@metaobjectsdev/clidepends on at runtime, so a release cut through the workflow would have published aclipinning adocs-siteversion nobody published (npm i @metaobjectsdev/cli→ETARGET). It also asserts what the tier table below asserts in prose: every member has a declared tier, nothing publishes before its own dependencies, and the set is closed over its sibling deps. Runs in thegateslane; the table stays as the human-readable statement of intent.
Mind the gap in that rule. A non-
privatepackage on its OWN version line is at neither the previous lockstep version norprivate, so it matches neither branch and gets silently skipped by every release. That is exactly what happened to@metaobjectsdev/angular+@metaobjectsdev/codegen-ts-angular(their own0.6.xline): never published, while the README,CLAUDE.md, four port docs and a full recipe described them as installable.scripts/check-publish-intent.shnow fails the build on any non-private package that is neither at the lockstep version nor declared source-only, so the decision has to be made out loud. It runs in thegateslane; run it yourself before a cut.For the Angular pair the decision has been made: they stay source-only on purpose (ADR-0048). A release cut must NOT sweep them into lockstep or publish them under any dist-tag; they join the tier table only when the ADR's promotion bar is met.
| Tier | Packages |
|---|---|
| 0 | metadata, render |
| 1 | codegen-ts, runtime-ts, migrate-ts, sdk, docs-site, runtime-web |
| 2 | codegen-ts-react, codegen-ts-tanstack, react |
| 3 | tanstack |
| 4 | cli, ai-runtime (leaves — nothing depends on them; publish last) |
Publish in tier order so a dependent never lands before its dependency. forge and
conformance are private: true and must never be published (bun refuses them).
-
Publish with
bun publish, nevernpm publish. The packages depend on each other viaworkspace:*; only bun rewrites that to the concrete version in the published tarball.npm publishships the literal string"workspace:*"and breaks every consumer. -
After ANY version bump, regenerate the lockfile:
rm bun.lock && bun install.bun publishresolvesworkspace:*frombun.lock, not the livepackage.json. A plainbun installreports "no changes" and keeps the stale member versions — so packages publish with sibling deps pinned to the previous version (uninstallable). Then verify by inspecting a packed tarball, not just that it packs:cd server/typescript/packages/cli && bun pm pack --destination /tmp/p tar -xzOf /tmp/p/*.tgz package/package.json | grep '@metaobjectsdev' # must show the version you're releasing
-
Runtime imports must be
dependencies, notdevDependencies. The in-workspace test suite can't catch a misclassified dep (devDeps are installed there). Only a clean external install does. -
Always smoke-test a real external install before promoting to
latest— in both npm and pnpm (pnpm's strict, non-nestednode_modulesexposes resolution bugs npm/bun hide). Install the cli into a throwaway dir, runmeta --version,meta init,meta gen. -
npm versions are immutable, and a burned one never comes back. You can never re-publish a version. Unpublish is not a reliable escape hatch: it is refused (
E405) once anything depends on the version, and deprecating it does not free the number.@metaobjectsdev/metadata@0.24.0-rc.1is burned that way and no other package in the set carries it — so a lockstep RC at0.24.0-rc.1would publish thirteen packages and then fail irreversibly on the fourteenth.scripts/release.mjsnow preflights the target version against every package in the set (it used to check only the cli), andbun run prereleaseskips burned numbers when choosing an iteration.
The version number's only mechanical meaning today is npm's caret rule. For
0.y.z, ^0.19.3 resolves >=0.19.3 <0.20.0, so: PATCH is auto-adopted by every
consumer on a routine npm update; MINOR requires a deliberate bump of their range.
That — not semver §8 — is the contract you are versioning against pre-1.0.
Litmus test. Can a consumer on ^prev run npm update && meta gen and (1) still
typecheck their owned generators + hand-written imports, and (2) get output that is
byte-identical or only corrects previously-wrong output? Yes to both → PATCH.
Otherwise → MINOR.
Do NOT reach for MINOR merely because "generated output changed." Scaffold-and-own
(ADR-0034) does not firewall consumers from engine-output changes — the copied
generators are thin compositions; the render* primitives + defaults live in the
package and re-propagate on the next meta gen. So the axis is API/default vs.
bytes, not "did output change."
| Change class | Example | Pre-1.0 | Post-1.0 |
|---|---|---|---|
| Public API additive (new export / optional param / CLI flag) | new render* primitive |
PATCH (MINOR if it headlines a feature release) | MINOR |
| Public API breaking (required param, removed/renamed export, changed CLI semantics) | relativeModuleSpecifier +required param |
MINOR | MAJOR |
| Output change = pure bugfix (wrong output corrected; correct output byte-identical) | 0.19.3 payload naming, 0.19.4 TPH stamping | PATCH | PATCH |
| Output change alters shape/default of correct output (renamed generated export, changed default, dropped artifact) | extStyle "none"→"js" default flip |
MINOR + a "Generated-output change" changelog flag + an opt-out where feasible | MAJOR if consumer code referencing the output breaks; else MINOR |
| New opt-in codegen feature, default output byte-identical | new generator defaulting off | PATCH | MINOR |
| New attribute on an existing type/subtype | @intValueMap, @lenient, @maxTokens |
PATCH | MINOR |
| New subtype of an existing type | field.uri, index.lookup, attr.intMap |
PATCH when inert (see the vocabulary rule below); MINOR when it changes existing metadata's meaning/output, narrows something previously permitted, or headlines a feature | MINOR |
| New top-level metadata type | requirement.*, index.*, api.* |
MINOR | MINOR |
| Breaking metamodel-vocabulary change (retire an attr/subtype/type; narrow what is permitted) | FR-037 @readOnly, FR-038 @verifiedBy, ADR-0052 @promptStyle re-homing |
MINOR (pre-1.0 MINOR is the breaking slot) | metamodelVersion MAJOR, package MINOR (ADR-0035 Am. 2) |
| Wire-contract / conformance behavior change of already-valid deployments | FR-036 enforcement | MINOR, loud notice (pre-1.0 MINOR is the breaking slot) | metamodelVersion MAJOR (Metamodel 2.0) + package MINOR — see the two-contracts rule below |
| Wire behavior fixed to match the documented/conformance contract | 0.19.1 @min clamp |
PATCH | PATCH |
| No changed product file in a port | PyPI/NuGet/Maven at 0.20.14 | Version-parity bump at the shared patch number — publish identical content at the new version; never skip a registry (single-shared-patch policy, standing since 0.20.13) | same |
Package 1.0 does not freeze the metamodel. After the cut, the project versions two contracts on two numbers:
- Package version (npm/PyPI/NuGet
1.x, Maven8.x) promises the SOFTWARE surface — exports, CLI flags, generated-code shape, runtime helpers. A break here is2.0.0/9.0.0. metamodelVersion("1.0"at the cut; the current value is the first key of the byte-gatedexpected-registry.json) promises the METADATA contract — registered vocabulary, canonical/interchange format, wire contract. A break here moves ITS major, and does not force a package major.
That severance is the whole point: under the pre-amendment rule one vocabulary retirement
dragged npm to 2.0.0 and Maven to 9.0.0, so the package majors became a running count
of metamodel edits. Measured cadence at the time of the amendment: 19 minor lines in 87
days.
The gate: node scripts/check-metamodel-version.mjs (runs in the gates lane, so
scripts/ci-local.sh and hosted CI both enforce it). It diffs
expected-registry.json — already the byte-exact bill of materials every port is gated
against — against its content at the last release tag, classifies every difference,
and fails if the declared version did not move by at least the amount the change
requires. Same shape as buf breaking --against '.git#tag=…' / oasdiff.
| Change | Required move |
|---|---|
a type/subtype removed; an attr removed; an attr made required, retyped or re-arrayed; an enum member removed or an open attr closed; a child rule removed, its min raised or its max lowered; a default subtype changed |
major (pre-1.0: minor — see below) |
| a type/subtype added; an optional attr added; an enum member added; a child rule added or relaxed; a default subtype added | minor |
prose only (description / rules / whenToUse) |
none — but read the warning |
Pre-1.0 a breaking change moves the MINOR, for the same reason the package line
works that way while it is 0.x: 0.y makes no compatibility claim there is anything to
break. At 1.0 the major becomes real.
What the gate cannot see, and says so. A rule can change with NO machine-readable
footprint. #210 is the proof: retiring assembly origins from object.value was a
breaking metamodel change whose only manifest edit was a rules PROSE string. So prose
changes are reported as a WARNING with a direct question — did the rule change, or only
its wording? — rather than classified, because a typo fix and a semantics change are
indistinguishable here and failing on every wording edit trains people to ignore the
gate. Answering that question is a human step in every release.
When you cut a release that moves metamodelVersion:
node scripts/check-metamodel-version.mjs --set <version>— it writes the manifest and all four port constants in one go (Kotlin emits through the JVM's). A partial edit is caught byregistry-conformance, but only in the lane for the port you forgot, so do not hand-edit. Then re-run the corpus in every port.- The changelog entry MUST say the metamodel version moved, and to what. Post-1.0 the
caret rule is no longer a gate (
^1.0.0accepts1.1.0), so the changelog is the adopter's only signal until the deferred loader check exists. - Ship a migration guide under
docs/features/migrations/, as every breaking metamodel change already does.
--explain prints the full classified diff and always exits 0; --against <ref> picks a
different baseline.
Design + deferral triggers:
docs/superpowers/specs/2026-08-20-two-contracts-versioning-design.md.
Cadence is a separate lever, and it is free. Nothing forces one release per merged change; batching a fortnight of work into one coordinated cut removes most of the number pressure without touching policy at all.
Adding registry vocabulary does NOT, by itself, force a MINOR. The rule used to read
"PATCH (MINOR if it adds registry vocabulary — cross-port conformance surface)", and that was
wrong on its own terms: expected-registry.json is an internal gate. Every port
byte-matching one manifest is how we stop the five ports drifting from each other — it says
nothing about whether an adopter's project changes. Treating an internal gate's churn as an
adopter-facing event is what burned the minors: 0.22.0 and 0.23.0 were both cut MINOR for
changes that a project declaring no requirement.* nodes could not observe at all, which each
changelog says out loud in its own opening paragraph. Sort vocabulary by what it can do to a
consumer, which splits three ways:
- A new ATTRIBUTE is a PATCH. You get it only by authoring it. Every existing document loads unchanged and emits byte-identical output, so there is nothing for a consumer to adopt deliberately.
- A new TOP-LEVEL TYPE is a MINOR. A type is a new modeling concept with its own children,
validation and (usually) tooling surface —
requirement.*brought its ownverifypass and summary output. That is a thing a consumer newly depends on, and it deserves a deliberate range bump. - A new SUBTYPE goes either way, and the test is whether it is INERT. PATCH when nothing but authoring it can reach it: no existing valid document changes meaning or output, nothing previously permitted is narrowed, nothing reserved is consumed. MINOR when any of those fails — a subtype that closes a wildcard, promotes a reserved-not-registered member (ADR-0007 Amendment 2 / ADR-0040), or shifts what the recommended shape for an existing field is — or when you deliberately want it behind a range bump because it headlines a release. Most subtypes carry a native type and behavior (that is ADR-0037's very test for making something a subtype), so read them carefully; but "it appears in the registry manifest" is not the deciding fact.
Do not invert the caret rule. "Pre-1.0 ^0.22.x resolves <0.23.0, so a consumer adopts
a MINOR deliberately" is a reason to choose MINOR when you want that gate. It is not a
reason additive vocabulary must be MINOR. The gate exists to be used on purpose, not by
reflex — and a minor spent on a change nobody can observe is a gate you no longer have when
something real needs it.
The extStyle 0.20.0 case, for calibration: it was correctly MINOR — but for the
API break (relativeModuleSpecifier gained a required param — a public export) and
the default flip (churns every existing project's diff on regen), NOT because "output
changed." Had it kept "none" as the default and only scaffolded "js" for new
projects, PATCH would have been defensible.
Changelog convention (required for output-changing releases). Every entry in
classes (bugfix-output) and (shape/default-output) must carry the phrase
"Generated-output change — regenerate to pick it up; three-way merge preserves hand
edits." The real risk of the (correct) PATCH cadence is a consumer seeing an
unexplained meta gen diff after npm update; the changelog flag + the planned
gen-state engine-version stamp (see the tracking issue) are how you keep every such
diff explained.
- The JS/TS workspace root is the repo root (
/package.json), globbingserver/typescript/packages/*+client/web/packages/*. This is what makesworkspace:*resolve uniformly at publish time — don't move it. - npm auth as an owner of the
metaobjectsdevorg. The account has 2FA auth-and-writes, so for an unattended publish use a Granular/Automation token with the bypass-2FA option, scoped read+write to@metaobjectsdev, in~/.npmrc(//registry.npmjs.org/:_authToken=...). Revoke it after the release. (Without it, everybun publishprompts for an OTP.) bun publishdoes not applypublishConfigfield overrides (bin/main/exports) — onlyaccess/tag(oven-sh/bun#19205). So fields likebinmust be correct at the top level, not swapped viapublishConfig.
Run everything from the repo root unless noted. Bump the publish-candidate set only (not the private root, not forge/conformance) — enumerate it (see "What gets published").
bun publish does NOT rebuild, dist/ is gitignored, and main points at dist/,
so a stale dist publishes code without your change. tsc also leaves orphaned
.js for deleted sources. Clean-rebuild before publishing:
bun run clean && bun run buildSpot-check dist reflects the change (a deleted source's .js is gone, new code present).
Most changes do not need this. To try an unreleased change against a downstream project, publish a PRE-RELEASE to the private registry instead —
docs/features/prerelease.md,bun run prerelease. It is reversible, invisible to the public registries, and costs no version number.A public RC is for the one case a private registry cannot cover: dependencies or package layout changed, so the thing being tested IS a real external install from the real registry — a misclassified
dependencies/devDependenciesentry, a peer range, a new package name, anexportsmap. Rule 4 below only means something against npmjs.org.Remember what it costs: an RC version is permanent. Once anything depends on it,
npm unpublishis refused outright and deprecating it does not free the number.
# bump the candidate set to <version>-rc.N (sed the "version" field in each publish-candidate package.json)
rm bun.lock && bun install # CRITICAL — re-pins workspace versions
# verify a packed tarball's deps show <version>-rc.N (rule 2)
# publish each package in tier order:
( cd <pkg-dir> && bun publish --tag=next )Note: the first-ever publish of a brand-new package name sets latest even with --tag=next;
move it after the smoke test (or accept it points at the RC until you promote).
cd $(mktemp -d) && npm init -y >/dev/null
npm i @metaobjectsdev/cli@next --prefer-online # or pnpm in a pnpm project
npx meta --version && npx meta init && npx meta genFix anything that surfaces, bump to -rc.(N+1), repeat. (rc.1 missed the lockfile regen; rc.2
missed a runtime dep; rc.3 was clean — expect iterations.)
Before promoting to latest, run the cross-language persistence suite against real Postgres
(spins up one ephemeral testcontainer per scenario, exercises each port's codegen + runtime
end-to-end). It is not part of bun test / dotnet test because it requires a docker daemon:
scripts/integration-test.sh # all runners (typescript + c# + java)
scripts/integration-test.sh ts # just typescript
scripts/integration-test.sh csharp # just c#
scripts/integration-test.sh java # just javaThe corpus lives at fixtures/persistence-conformance/.
A red run here has caught real cross-port divergence (view-DDL identifier quoting, column-naming
strategy mismatches) that the unit suites missed.
Before bun publish: confirm the local-ci run for the release commit is green.
Its ts-slow lane now carries the real-Postgres migrate gate. Publishing is irreversible on
all four registries, and the v* tag is pushed after bun publish — so the tag-triggered
integration-tests run can never be the pre-publish gate. This is the last gate that can
precede the irreversible step.
gh run list --workflow local-ci.yml --limit 1 --json headSha,conclusion# bump the candidate set to the final <version>
rm bun.lock && bun install
# verify packed deps (rule 2); commit "chore(release): <version>"
( cd <pkg-dir> && bun publish ) # default tag = latest, tier order
git tag v<version> && git push origin main --tags# deprecate any broken/superseded RCs
npm deprecate '@metaobjectsdev/<pkg>@<bad-version>' "superseded; use <version>"
# point latest off a bad version if needed, and drop the now-stale next tag
npm dist-tag rm @metaobjectsdev/<pkg> nextThen verify the registry: npm view @metaobjectsdev/<pkg> dist-tags (or curl the registry to
bypass npm CLI cache, which lags right after publish).
If only one package changed (e.g. a cli bugfix), bump just that package, rm bun.lock && bun install, verify, and bun publish it — the others stay at their current version. Tag scoped
(e.g. cli-v0.5.1).
How to publish the MetaObjects* C# packages to nuget.org. We use Trusted Publishing
(OIDC from GitHub Actions) — no long-lived API key, no signing certificate. Read the
Gotchas first.
Four packages, version-locked at the C# port version (currently 0.23.2):
| Package | Contents |
|---|---|
MetaObjects |
Loader + canonical serializer |
MetaObjects.Render |
Mustache render + payload-VO + verify |
MetaObjects.Codegen |
EF Core + ASP.NET codegen + the runtime filter/dispatch helpers generated code references |
MetaObjects.Cli |
The dotnet meta .NET tool (gen / verify; agent-docs is a redirect stub to the Node meta CLI) |
Shared package metadata lives in server/csharp/Directory.Build.props;
per-package PackageId/Title/Description live in each .csproj. Test/integration projects set
IsPackable=false and never publish. There are no inter-package version-rewrite concerns like npm's
workspace:* — ProjectReferences become NuGet dependencies pinned to the same Version.
The workflow .github/workflows/publish-csharp.yml packs
the four projects, exchanges a GitHub OIDC token for a short-lived (~1 hour) nuget.org key via
NuGet/login@v1, then dotnet nuget pushes. Trigger it manually (Actions → publish-csharp → Run
workflow, with an optional version override) or by pushing a csharp-v* tag.
Create a Trusted Publishing policy (nuget.org → your username → Trusted Publishing → Create).
For this repo (github.com/metaobjectsdev/metaobjects) enter exactly:
| Field | Value |
|---|---|
| Policy Name | metaobjects-csharp-publish (any name) |
| Package Owner | metaobjects (the org) |
| Repository Owner | metaobjectsdev |
| Repository | metaobjects |
| Workflow File | publish-csharp.yml (filename only — not the .github/workflows/ path) |
| Environment | (leave empty) |
Then add a GitHub repo secret (Settings → Secrets and variables → Actions):
| Secret | Value |
|---|---|
NUGET_USER |
Your nuget.org username (the profile name at nuget.org/profiles/<username>) — NOT your doug@dougmealing.com login email |
Because this repo is public, the policy activates immediately. (The "pending for 7 days" status the nuget.org docs mention only applies to private repos, where NuGet waits for a first publish to lock the repo/owner IDs against resurrection attacks.)
NuGet/login'suser:is the nuget.org username (profile name), never the email. Email silently fails the token exchange. We pass it via theNUGET_USERsecret.- NuGet versions are immutable (like npm). You cannot re-push a version — only unlist or
deprecate. So validate the packed
.nupkglocally before triggering the workflow. - Bump the version in
Directory.Build.props(<Version>), not per-project. The workflow can also override per-run via theversiondispatch input (-p:Version=). - The temp key is single-use and ~1 h. The workflow requests it immediately before push — don't
move the
NuGet/loginstep earlier. - The policy is bound to the org + repo + workflow filename. Renaming
publish-csharp.yml, or the policy owner leaving/locking themetaobjectsorg, makes the policy inactive until fixed. - Source Link + symbols are on (
PublishRepositoryUrl,EmbedUntrackedSources,snupkg); CI setsContinuousIntegrationBuildfor deterministic builds. No action needed — just don't strip them.
- Pick the publish commit on
main(a stable, merged tip — not a mid-refactor branch). Ensure the packaging config +publish-csharp.ymlare on it. Set<Version>inDirectory.Build.props. - Validate locally (catches immutable-version mistakes before they're permanent):
cd server/csharp # NOTE: pack ONE project per invocation. Passing all four to a single `dotnet pack` # fails on modern SDKs (verified on 8.0.129) with `MSBUILD : error MSB1008: Only one # project can be specified` — MSBuild treats the extra paths as switches. for p in MetaObjects/MetaObjects.csproj MetaObjects.Render/MetaObjects.Render.csproj \ MetaObjects.Codegen/MetaObjects.Codegen.csproj MetaObjects.Cli/MetaObjects.Cli.csproj; do dotnet pack "$p" -c Release -o /tmp/mo-nupkg || echo "PACK FAILED: $p" done # inspect a nuspec — version, license, readme, deps: unzip -p /tmp/mo-nupkg/MetaObjects.Render.0.11.1.nupkg MetaObjects.Render.nuspec | grep -iE '<id>|<version>|<license|<readme>|<dependenc' # optional: install the tool from the local dir and smoke-test it dotnet tool install --global --add-source /tmp/mo-nupkg MetaObjects.Cli && dotnet meta --help
- Run persistence conformance if the runtime/codegen changed:
scripts/integration-test.sh csharp. - Publish: GitHub → Actions → publish-csharp → Run workflow (or push a
csharp-v<version>tag). - Verify on nuget.org: all four packages listed and owned by the
metaobjectsorg (indexing/validation takes a few minutes).
How to publish the metaobjects Python package to PyPI via Trusted Publishing
(OIDC from GitHub Actions) — no API token.
One package, metaobjects (version in server/python/pyproject.toml,
currently 0.23.2), as an sdist + a universal py3-none-any wheel (pure Python).
The workflow .github/workflows/publish-python.yml
builds with uv and publishes via pypa/gh-action-pypi-publish using OIDC.
Trigger it manually (Actions → publish-python → Run workflow) or with a python-v* tag.
Project metaobjects → Settings → Publishing → Add a new GitHub publisher:
| Field | Value |
|---|---|
| Owner | metaobjectsdev |
| Repository | metaobjects |
| Workflow | publish-python.yml |
| Environment | (leave empty) |
(The initial 0.9.0 was published from a local uv publish; this workflow makes
subsequent releases keyless.)
-
PyPI versions are immutable (like npm/NuGet). You can't re-upload a version — only yank. Validate locally first (below).
-
Bump the version in
pyproject.toml([project].version). -
The wheel is pure-Python/universal (
py3-none-any) — one wheel serves every platform.(No agent-context content is vendored into the wheel anymore — scaffolding moved to the Node
meta agent-docsCLI, sohatch_build.pyis a no-op. Don't re-add aforce-includeof../../agent-context.)
- Bump
[project].versioninserver/python/pyproject.toml. - Validate locally (versions are immutable):
cd server/python rm -rf dist && uv build --out-dir dist # must produce BOTH .tar.gz and .whl uvx twine check dist/* # metadata + README render
- Publish — MANUAL (
uv publish), not the OIDC workflow. Trusted Publishing is misconfigured ([#36] —publish-python.ymlfails withinvalid-publisher), so publish from the local build with the token in~/Work/Keys/pypi.txt:(When #36 is fixed, switch to Actions → publish-python → Run workflow / acd server/python # after the `uv build` above produced dist/ # the token is line 4 of the key file, AFTER the "secret: " label — strip it or you get a 403: UV_PUBLISH_TOKEN="$(sed -n '4p' ~/Work/Keys/pypi.txt | sed 's/^secret:[[:space:]]*//')" uv publish dist/*
python-v<version>tag.) - Verify:
curl -s https://pypi.org/pypi/metaobjects/json | python3 -c "import sys,json;print(json.load(sys.stdin)['info']['version'])".
The 18 com.metaobjects:* modules ship to Maven Central via the Sonatype Central Portal,
versioned on the 7.x line (currently 7.23.2) in the parent + module poms. Signed with the
maintainer's GPG key.
- Bump the version in all poms — parent + reactor modules and the two
reactor-EXCLUDED integration-test modules (
server/java/integration-tests/pom.xml,server/java/integration-tests-kotlin/pom.xml). Use the tree-widegrep, NOTmvn versions:set:versions:setonly walks the reactor and silently leaves the excluded modules behind, so their<parent><version>lags and the next tag failsrelease-gate (java|kotlin)with "Non-resolvable parent POM".(Verify everygrep -rl 7.7.8 --include=pom.xml server/java | xargs sed -i 's/7\.7\.8/7.7.9/g'
<version>7.4.0</version>is the project version, not a third-party dep.) Then assert the excluded modules are in sync:scripts/check-pom-versions.sh(also enforced on every push by.githooks/pre-pushand byscripts/ci-local.sh). - Validate locally:
cd server/java && mvn -q clean install -DskipTests(or with tests /scripts/integration-test.sh javaif runtime changed). - Deploy:
mvn -Prelease deployfromserver/java. Thecentral-publishing-maven-plugin(<publishingServerId>central</publishingServerId>,<autoPublish>true</autoPublish>) uploads the signed bundle and auto-releases — no manual staging → release promotion. Auth + the GPG passphrase come from~/.m2/settings.xml(server idscentral+gpg-credentials); the GPG secret key must be present (gpg --list-secret-keys). The release profile activates GPG signing- the javadoc/sources jars Central requires.
- Verify: the modules appear at
https://central.sonatype.com//https://repo1.maven.org/maven2/com/metaobjects/(indexing takes minutes).
Gotchas: Maven Central versions are immutable (like the others);
groupIdownership is already verified forcom.metaobjects; a missing GPG key or expired Central token fails the deploy with an auth error, not a clear message.
central-publishing-maven-plugin 0.6.0crashes COSMETICALLY (UnrecognizedPropertyException: "warnings"while parsing Sonatype's response) —mvnexits non-zero, but the bundle DID publish (all modules go live on Central via autoPublish). Do not blindly re-run (versions are immutable) — first VERIFY:for m in metadata om omdb-ktx render codegen-spring spring-boot-starter; do curl -s -o /dev/null -w "%{http_code} metaobjects-$m\n" \ "https://repo1.maven.org/maven2/com/metaobjects/metaobjects-$m/<version>/metaobjects-$m-<version>.pom"; doneIf all are
200, the release is out — the error was just the response parse. Bump the plugin to ≥0.7.0to stop the crash on the next release.
The minor.patch IS unified across languages (standing policy since 0.20.13): npm, PyPI and
NuGet share 0.<m>.<p>, and Maven Central ships the same minor.patch on its historical major 7.
Every release bumps all four registries, with version-parity bumps where a port has no changed
file. The cross-language behavior contract
is the conformance corpus + fixtures/conformance/CAPABILITIES.json:
each release states which capabilities/conformance level it satisfies, and that manifest — not a
shared version — is the coordination point. (Generated code runs without any MetaObjects runtime, so
a language only publishes the libraries it actually ships: runtime helpers, and codegen where it exists.)
This repo is public. Before committing release changes, ensure no local paths or private/consumer
names leak (the .githooks/pre-commit guard enforces this — activate with
git config core.hooksPath .githooks). See CLAUDE.md → Public repository hygiene.