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. Versions are not unified (TS/C#/Python on the
0.xline, Java/Kotlin on the7.xline). The rest of this doc is the per-registry procedure.
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" (a sed/grep over */package.json):
| Tier | Packages |
|---|---|
| 0 | metadata, render |
| 1 | codegen-ts, runtime-ts, migrate-ts, sdk, runtime-web, docs-site |
| 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. You can never re-publish a version, and unpublish is a restricted 72-hour escape hatch. That's why we go RC-first.
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 if it adds registry vocabulary — cross-port conformance surface) | MINOR |
| Wire-contract / conformance behavior change of already-valid deployments | FR-036 enforcement | MINOR, loud notice (pre-1.0 MINOR is the breaking slot) | MAJOR |
| Wire behavior fixed to match the documented/conformance contract | 0.19.1 @min clamp |
PATCH | PATCH |
| Docs / tests / fixtures only (no product file in a port) | 0.19.4 npm scope | No release for that registry (per-registry scoping) | same |
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).
# 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.
# 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.19.3):
| 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 dotnet pack MetaObjects/MetaObjects.csproj MetaObjects.Render/MetaObjects.Render.csproj \ MetaObjects.Codegen/MetaObjects.Codegen.csproj MetaObjects.Cli/MetaObjects.Cli.csproj \ -c Release -o /tmp/mo-nupkg # 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.15.11), 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.7.9) 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.
Versions are not unified across languages — TS, C#, and Python are on the 0.9.x line, the
Java/Kotlin module line is on the 7.2.x track. Don't force one number. The cross-language 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.