Skip to content

Latest commit

 

History

History
367 lines (297 loc) · 19.4 KB

File metadata and controls

367 lines (297 loc) · 19.4 KB

Releasing MetaObjects

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 with bun publish.
  • PyPI → token in ~/Work/Keys/pypi.txt. Publish manually with uv 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 ids central + gpg-credentials) + the local GPG signing key. mvn -Prelease deploy from server/java (autoPublish — no manual staging promotion).
  • NuGet → keyless OIDC via the publish-csharp.yml workflow: gh workflow run publish-csharp.yml (or push a csharp-v<version> tag).

Run network commands via the sandbox-disabled shell. Versions are not unified (TS/C#/Python on the 0.x line, Java/Kotlin on the 7.x line). The rest of this doc is the per-registry procedure.

Releasing the TypeScript packages to npm

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.

What gets published

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

Golden rules (the non-obvious ones)

  1. Publish with bun publish, never npm publish. The packages depend on each other via workspace:*; only bun rewrites that to the concrete version in the published tarball. npm publish ships the literal string "workspace:*" and breaks every consumer.

  2. After ANY version bump, regenerate the lockfile: rm bun.lock && bun install. bun publish resolves workspace:* from bun.lock, not the live package.json. A plain bun install reports "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
  3. Runtime imports must be dependencies, not devDependencies. The in-workspace test suite can't catch a misclassified dep (devDeps are installed there). Only a clean external install does.

  4. Always smoke-test a real external install before promoting to latest — in both npm and pnpm (pnpm's strict, non-nested node_modules exposes resolution bugs npm/bun hide). Install the cli into a throwaway dir, run meta --version, meta init, meta gen.

  5. 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.

Prerequisites

  • The JS/TS workspace root is the repo root (/package.json), globbing server/typescript/packages/* + client/web/packages/*. This is what makes workspace:* resolve uniformly at publish time — don't move it.
  • npm auth as an owner of the metaobjectsdev org. 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, every bun publish prompts for an OTP.)
  • bun publish does not apply publishConfig field overrides (bin/main/exports) — only access/tag (oven-sh/bun#19205). So fields like bin must be correct at the top level, not swapped via publishConfig.

Procedure

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").

0. Build fresh — the stale-dist trap

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 build

Spot-check dist reflects the change (a deleted source's .js is gone, new code present).

1. Release candidate → next

# 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).

2. Smoke-test the RC (rule 4)

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 gen

Fix 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.)

2b. Cross-language persistence conformance (on-demand)

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 java

The 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.

3. Promote to latest

# 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

4. Cleanup

# 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> next

Then verify the registry: npm view @metaobjectsdev/<pkg> dist-tags (or curl the registry to bypass npm CLI cache, which lags right after publish).

Isolated patch (one package)

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

Releasing the C# packages to NuGet

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.

What gets published

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.

How we publish: Trusted Publishing (OIDC)

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.

One-time nuget.org setup

Create a Trusted Publishing policy (nuget.org → your username → Trusted PublishingCreate). 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.)

Gotchas (the non-obvious ones)

  1. NuGet/login's user: is the nuget.org username (profile name), never the email. Email silently fails the token exchange. We pass it via the NUGET_USER secret.
  2. NuGet versions are immutable (like npm). You cannot re-push a version — only unlist or deprecate. So validate the packed .nupkg locally before triggering the workflow.
  3. Bump the version in Directory.Build.props (<Version>), not per-project. The workflow can also override per-run via the version dispatch input (-p:Version=).
  4. The temp key is single-use and ~1 h. The workflow requests it immediately before push — don't move the NuGet/login step earlier.
  5. The policy is bound to the org + repo + workflow filename. Renaming publish-csharp.yml, or the policy owner leaving/locking the metaobjects org, makes the policy inactive until fixed.
  6. Source Link + symbols are on (PublishRepositoryUrl, EmbedUntrackedSources, snupkg); CI sets ContinuousIntegrationBuild for deterministic builds. No action needed — just don't strip them.

Procedure

  1. Pick the publish commit on main (a stable, merged tip — not a mid-refactor branch). Ensure the packaging config + publish-csharp.yml are on it. Set <Version> in Directory.Build.props.
  2. 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
  3. Run persistence conformance if the runtime/codegen changed: scripts/integration-test.sh csharp.
  4. Publish: GitHub → Actions → publish-csharp → Run workflow (or push a csharp-v<version> tag).
  5. Verify on nuget.org: all four packages listed and owned by the metaobjects org (indexing/validation takes a few minutes).

Releasing the Python package to PyPI

How to publish the metaobjects Python package to PyPI via Trusted Publishing (OIDC from GitHub Actions) — no API token.

What gets published

One package, metaobjects (version in server/python/pyproject.toml, currently 0.15.11), as an sdist + a universal py3-none-any wheel (pure Python).

How we publish: Trusted Publishing (OIDC)

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.

One-time setup on PyPI

Project metaobjectsSettings → 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.)

Gotchas (the non-obvious ones)

  1. PyPI versions are immutable (like npm/NuGet). You can't re-upload a version — only yank. Validate locally first (below).

  2. Bump the version in pyproject.toml ([project].version).

  3. 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-docs CLI, so hatch_build.py is a no-op. Don't re-add a force-include of ../../agent-context.)

Procedure

  1. Bump [project].version in server/python/pyproject.toml.
  2. 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
  3. Publish — MANUAL (uv publish), not the OIDC workflow. Trusted Publishing is misconfigured ([#36] — publish-python.yml fails with invalid-publisher), so publish from the local build with the token in ~/Work/Keys/pypi.txt:
    cd 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/*
    (When #36 is fixed, switch to Actions → publish-python → Run workflow / a python-v<version> tag.)
  4. Verify: curl -s https://pypi.org/pypi/metaobjects/json | python3 -c "import sys,json;print(json.load(sys.stdin)['info']['version'])".

Releasing the Java/Kotlin modules to Maven Central

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.

Procedure

  1. 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-wide grep, NOT mvn versions:set: versions:set only walks the reactor and silently leaves the excluded modules behind, so their <parent><version> lags and the next tag fails release-gate (java|kotlin) with "Non-resolvable parent POM".
    grep -rl 7.7.8 --include=pom.xml server/java | xargs sed -i 's/7\.7\.8/7.7.9/g'
    (Verify every <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-push and by scripts/ci-local.sh).
  2. Validate locally: cd server/java && mvn -q clean install -DskipTests (or with tests / scripts/integration-test.sh java if runtime changed).
  3. Deploy: mvn -Prelease deploy from server/java. The central-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 ids central + 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.
  4. 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); groupId ownership is already verified for com.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.0 crashes COSMETICALLY (UnrecognizedPropertyException: "warnings" while parsing Sonatype's response) — mvn exits 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"; done

If all are 200, the release is out — the error was just the response parse. Bump the plugin to ≥0.7.0 to 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.)

Public-repo hygiene

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.mdPublic repository hygiene.