Docs

Guide 16

Releases

16. Releases

How you pack and publish @avelonjs/* from this monorepo. npmjs is the public install target. The GitHub repo Packages tab is a different registry. GitHub will not host @avelonjs/* under the yannelli account, so that tab shows @yannelli/* until the repo lives under a GitHub org named avelonjs.

Merging to main cuts the release. .github/workflows/release.yml reads the commits, picks the number, bumps every publishable manifest, tags, cuts the GitHub Release, and uploads to both registries. You do not run release:bump by hand and you do not click a workflow.

The two older paths still exist and are still hand-driven: publish.yml for a workflow_dispatch npmjs upload, and a github-packages-v* tag for GitHub Packages only. Use them to recover from a release that half-finished, not as the normal route.

Automatic releases

What triggers one

A push to main. That is a squash-merged pull request in practice.

How the number is chosen

scripts/release/version.ts reads every commit since the last v* tag and takes the largest bump any one of them earns:

Commit subjectBump
feat(orm): eager load via query IRminor
fix(next): type page() as Promisepatch
perf, refactor, or any other typepatch
feat(core)!: or a BREAKING CHANGE: footermajor
docs, chore, style, test, ci, buildnone
Anything that is not a conventional subjectpatch

That last row is deliberate. A merge that moves nothing is the failure this workflow exists to end, so Fix markdown tables squeezing to unreadable text on mobile docs is treated as a real change even though nobody typed fix(docs-site):. Write conventional subjects and you get the bump you meant. Put [skip release] in a commit message to opt that commit out.

If every commit in the range is docs or chore, nothing releases and the run says so in its summary. That is not a failure.

Before 1.0.0 a breaking change moves the minor, not the major. There is no stable public surface to break yet.

What the run does

In this order, stopping the whole release if any step fails:

  1. Decides the version, or exits cleanly when no commit earns a bump.
  2. bun run release:bump <version> across all eleven publishable manifests, plus bun install.
  3. bun run test:unit, bun run typecheck, bun run release:pack against the bumped tree. Nothing is tagged or uploaded until these pass. A red release is worse than a late one.
  4. Commits chore(release): <version>, pushes it to main, and pushes the v<version> tag.
  5. Cuts a GitHub Release with generated notes, spanning the previous v* tag to this one.
  6. Publishes to npmjs on the latest dist-tag.
  7. Publishes to GitHub Packages on the latest dist-tag.

The bump commit is pushed with GITHUB_TOKEN. GitHub does not start workflow runs from those, so the release cannot trigger itself.

A concurrency group named release holds the job to one at a time. Two merges in the same minute queue rather than race for the tag.

Running it by hand

The workflow is also workflow_dispatch:

  • version — cut this exact number instead of reading the commits. Leave it empty for the normal decision.
  • dry_run — decide, bump, and verify, then stop. Nothing is tagged, released, or uploaded. Defaults to true, so a click without thinking is safe.

Preview the decision locally

bun run release:version

That prints the current version, the tag it is measuring from, every commit with the bump it earns and why, and the version it would cut. It changes nothing.

When a release half-finishes

npm versions are permanent. If step 6 uploaded and step 7 failed, do not re-run the release to fix it: the tag and the npm version already exist. Publish the missing registry by hand from the tag, or fix forward with the next merge.

A run that tags and cuts the Release but uploads nothing leaves a version number that exists in git and on the Releases page but on no registry. The credential check in step 0 is there to stop that before the tag, but a token that is present and then rejected still gets this far. Recover by publishing the version already on disk, rather than by cutting a new one:

  1. npmjs: run publish.yml with dry_run false and tag latest. It publishes whatever version the manifests on main currently hold, which is the version the tag names.
  2. GitHub Packages: push a github-packages-v<version> tag.

Do not delete the tag and re-run the release to "redo" it. The next merge cuts the next number and leaves the stranded one behind; publishing it is what closes the gap.

First publish (do these once)

Do them in order. Copy the commands. Do not invent a second process.

1. Create the npm org

The product is Avelon. The npm scope is @avelonjs. @avelon belongs to someone else.

Skip this step if you already created the avelonjs org.

  1. Sign in at https://www.npmjs.com/login. Create an account if you do not have one.
  2. Open https://www.npmjs.com/org/create.
  3. Name the org avelonjs.
  4. Pick the free plan (unlimited public packages).

2. Make an automation token

  1. Open https://www.npmjs.com/settings/~/tokens.
  2. Generate a granular access token.
  3. Set this and nothing else:
    • Permissions: Read and write
    • Organization: avelonjs
    • Packages: all packages in that org / the @avelonjs scope
    • Expiration: 90 days is fine
    • Two-factor: off / bypass. A token that still requires 2FA returns EOTP and never uploads. This is the usual failure. Delete that token and make another.
  4. Copy the token. It starts with npm_. You will not see it again.

3. Put the token in GitHub

In a terminal, already logged into gh as you:

gh secret set NPM_TOKEN --repo yannelli/avelon

Paste the token when it asks. Press Enter.

Or click: GitHub repo → Settings → Secrets and variables → Actions → New repository secret. Name it exactly NPM_TOKEN. Paste the token.

Do not put the token in .env. Do not put it in .npmrc. Do not commit it.

4. Merge something

That is the whole step. The next push to main runs release.yml, which bumps every publishable manifest together, tags, and uploads. release:bump still exists and still rewrites the eleven manifests plus bun.lock, but the workflow is what calls it now.

Eleven packages publish in this order: core, conformance, bailiff, orm, postgres, neon, resend, next, supabase, assay, cli.

Done when you can open https://www.npmjs.com/package/@avelonjs/core and see the version the run cut.

Run bun run release:version first if you want to know what number that will be.

If something explodes

What you seeWhat it meansWhat you do
NPM_TOKEN is not setThe secret is missing or named wrongRepeat step 3. The name is NPM_TOKEN.
missing authenticationBun did not get NPM_CONFIG_TOKENRe-run after this branch is on main. Do not hand-edit .npmrc.
403 / no permission for @avelonjsOrg or token scope is wrongConfirm the org is avelonjs and the token can write that scope.
2FA / OTP prompt in the logYou created a token that requires 2FAMake a new token with 2FA bypass. Delete the old one.
cannot publish over existing versionThat version is already on the registryStop. Do not republish. The next merge cuts the next number.
The run says no releasable commitsEvery commit was docs or choreNothing to publish. Merge a feat or fix, or dispatch with an explicit version.

What you publish

These packages are public (private is false) and share one version:

PackageWhat it is
@avelonjs/coreFrozen contracts, errors, and capability types
@avelonjs/ormModel layer and QueryIR builder
@avelonjs/clireeve CLI and TUI
@avelonjs/nextNext.js adapter
@avelonjs/bailiffArchitecture linter
@avelonjs/assayTest harness
@avelonjs/supabaseSupabase drivers
@avelonjs/postgresPostgres database driver
@avelonjs/neonNeon drivers (database, auth, S3, queue)
@avelonjs/resendResend mail driver
@avelonjs/conformanceContract suites and fakes

These stay private. You do not publish them:

PackageWhy
avelon (repo root)Workspace root
@avelonjs/docs-siteSite builder for this repository
@avelonjs/s4Integration slice
@avelonjs/gazetteExample app under examples/gazette

TypeScript source, not a compiled dist

The first public line publishes TypeScript source. Package exports point at ./src/index.ts. There is no dist/, no dual CJS/ESM build, and no tsc emit step. That is intentional for this prep, not a stand-in for a later compiled release.

You consume these packages with Bun, which runs TypeScript directly. @avelonjs/core and @avelonjs/orm still must not import bun:*. engines.bun is >=1.3.14 because that is the toolchain this repo pins (docs/11-runtime.md). A compiled dist is a later change. Do not advertise Node-native install until that lands.

Version policy

Every publishable package shares one version. You do not ship @avelonjs/orm@0.2.0 against @avelonjs/core@0.1.0. release:bump rewrites all eleven manifests together, and scripts/release/version.ts reads packages/core/package.json as the one number for the set.

After versions change, bun install has to run so bun.lock matches the on-disk manifests. bun run release:bump <version> does both the rewrite and the install. Bun 1.3.14 rewrites workspace:* from the workspace package versions it can see; a stale lockfile is how you publish the wrong number.

Before 1.0.0, a breaking change moves the minor. 0.1.0 was cut by hand and published to GitHub Packages only, which is why the first automatic release is 0.2.0 rather than a patch on top of it.

An automatic release always uploads on the latest dist-tag. Use publish.yml with an explicit tag when you want a canary on next or dev without moving latest.

License

The repo root LICENSE is MIT, copyright Ryan Yannelli. The pack and publish scripts copy that file into each publishable package directory for the tarball, then remove the copy if it was not already checked in. You do not maintain eleven LICENSE files by hand.

Dry-run pack

From the repo root:

bun run release:pack

That runs scripts/release/pack.ts. For each publishable package it:

  1. Copies the root LICENSE into the package if needed.
  2. Runs bun pm pack --destination <temp> --ignore-scripts from the package directory. Bun writes <name>-<version>.tgz. It rejects --filename together with --destination.
  3. Prints every path in the tarball.
  4. Fails if LICENSE or README.md is missing, if src/ is missing, if tests/, fixtures/, tsconfig, or *.test.ts leaked, or if the packed package.json still contains workspace:.

You do not need NPM_TOKEN for this. CI runs the same command.

Workspace protocol

Workspace packages depend on each other with workspace:*. That protocol is invalid on npm.

You do not rewrite package.json on disk. Bun 1.3.14 does it in the tarball:

CommandWhat it does to workspace:*
bun pm pack --destination <dir> --ignore-scriptsWrites @avelonjs/core as 0.0.0 (the dependency's package.json version)
npm publish --access public --tag <tag> --ignore-scriptsSame rewrite via the packed manifest, then uploads

workspace:^ becomes ^<version> and workspace:~ becomes ~<version>. This repo only uses workspace:*, which becomes the exact version.

scripts/release/pack.ts reads package/package.json out of each tarball and fails if any workspace: spec remains. scripts/release/publish.ts writes a temp .npmrc from NPM_TOKEN and runs this from each package directory, in dependency order:

bunx --bun npm publish --access public --tag "$RELEASE_TAG" --ignore-scripts

Set RELEASE_REGISTRY=github to pack the same way, rewrite the tarball scope to @<github-owner>/*, and upload to https://npm.pkg.github.com with GITHUB_TOKEN. That is the GitHub Packages path.

bun publish in 1.3.14 still opens npm's web OTP flow even with NPM_CONFIG_TOKEN. Do not use it for the first public line.

RELEASE_TAG defaults to next.

Publish through workflow_dispatch

.github/workflows/publish.yml is workflow_dispatch only and does not run on push. It is the manual fallback. The normal path is release.yml, above, which runs on every merge to main. Reach for this one to re-upload to npmjs after a release that half-finished, or to put a canary on the next tag without cutting a release.

  1. Put an npm automation token in the repository secret NPM_TOKEN. Do not commit it. Do not put it in .npmrc.
  2. Open the Publish workflow in GitHub Actions and run it.
  3. Leave dry_run at true the first time. That only runs bun run release:pack.
  4. When you mean it, set dry_run to false and choose a tag (next, dev, or latest).
  5. The workflow then runs bun scripts/release/publish.ts with NPM_TOKEN, NPM_CONFIG_TOKEN (same value; Bun 1.3.14 reads this name), and RELEASE_TAG.

A real publish also needs the avelonjs org and a token that can write @avelonjs/* without 2FA. The script refuses to run without NPM_TOKEN.

Publish order is core, conformance, bailiff, orm, postgres, neon, resend, next, supabase, assay, cli.

GitHub Packages

This fills the Packages tab on the GitHub repo. It is not a substitute for npmjs. Install from npmjs unless you are testing the GitHub registry on purpose.

GitHub Packages only accepts scoped names whose scope matches the GitHub owner. This repo is yannelli/avelon, so the uploaded names are @yannelli/core, @yannelli/orm, and the rest of the publishable set. On-disk manifests stay @avelonjs/*. The publish script rewrites the packed tarball after workspace:* has already become a version number.

No extra secret. .github/workflows/publish-github-packages.yml uses GITHUB_TOKEN with packages:write.

  1. Open Publish GitHub Packages.
  2. First click: leave dry_run true. That only packs.
  3. Second click: set dry_run to false. That uploads.
  4. Or push a tag named github-packages-v*, for example github-packages-v0.1.0. A tag skips the dry run and publishes.

Done when you can open https://github.com/yannelli/avelon/pkgs and see the eleven packages.

To install from that registry instead of npmjs:

@yannelli:registry=https://npm.pkg.github.com

When this repository is transferred to a GitHub org named avelonjs, the rewrite becomes a no-op and the Packages tab shows @avelonjs/*.

What you never commit

  • .env or .env.*
  • A service role key
  • A real project ref
  • NPM_TOKEN or any other registry credential

.gitignore already drops .env. The publish workflow reads NPM_TOKEN from GitHub Actions secrets and only injects it when dry_run is false.

Provenance

bun publish in Bun 1.3.14 has no --provenance flag. This prep does not claim npm provenance. If you later publish with npm publish --provenance, add id-token: write to the workflow and document that change here.

CI

.github/workflows/ci.yml runs on push and pull request: bun install, bun run test:unit, bun run typecheck, bun run release:pack.

.github/workflows/release.yml runs the same three commands against the bumped tree before it tags or uploads anything, so a release never ships a tree CI would have rejected.

test:unit is every *.test.ts under packages/, scripts/, and examples/ except *.live.test.ts. Live driver suites need Postgres and PostgREST, and they fail on purpose when those services are down. Run bun test locally when the stack is up. Do not put that command in CI until the workflow starts those services.