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 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.15.9):
| 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.