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 subject | Bump |
|---|---|
feat(orm): eager load via query IR | minor |
fix(next): type page() as Promise | patch |
perf, refactor, or any other type | patch |
feat(core)!: or a BREAKING CHANGE: footer | major |
docs, chore, style, test, ci, build | none |
| Anything that is not a conventional subject | patch |
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:
- Decides the version, or exits cleanly when no commit earns a bump.
bun run release:bump <version>across all eleven publishable manifests, plusbun install.bun run test:unit,bun run typecheck,bun run release:packagainst the bumped tree. Nothing is tagged or uploaded until these pass. A red release is worse than a late one.- Commits
chore(release): <version>, pushes it tomain, and pushes thev<version>tag. - Cuts a GitHub Release with generated notes, spanning the previous
v*tag to this one. - Publishes to npmjs on the
latestdist-tag. - Publishes to GitHub Packages on the
latestdist-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:
- npmjs: run
publish.ymlwithdry_runfalse andtaglatest. It publishes whatever version the manifests onmaincurrently hold, which is the version the tag names. - 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.
- Sign in at https://www.npmjs.com/login. Create an account if you do not have one.
- Open https://www.npmjs.com/org/create.
- Name the org
avelonjs. - Pick the free plan (unlimited public packages).
2. Make an automation token
- Open https://www.npmjs.com/settings/~/tokens.
- Generate a granular access token.
- Set this and nothing else:
- Permissions: Read and write
- Organization:
avelonjs - Packages: all packages in that org / the
@avelonjsscope - Expiration: 90 days is fine
- Two-factor: off / bypass. A token that still requires 2FA returns
EOTPand never uploads. This is the usual failure. Delete that token and make another.
- 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 see | What it means | What you do |
|---|---|---|
NPM_TOKEN is not set | The secret is missing or named wrong | Repeat step 3. The name is NPM_TOKEN. |
missing authentication | Bun did not get NPM_CONFIG_TOKEN | Re-run after this branch is on main. Do not hand-edit .npmrc. |
403 / no permission for @avelonjs | Org or token scope is wrong | Confirm the org is avelonjs and the token can write that scope. |
| 2FA / OTP prompt in the log | You created a token that requires 2FA | Make a new token with 2FA bypass. Delete the old one. |
cannot publish over existing version | That version is already on the registry | Stop. Do not republish. The next merge cuts the next number. |
| The run says no releasable commits | Every commit was docs or chore | Nothing 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:
| Package | What it is |
|---|---|
@avelonjs/core | Frozen contracts, errors, and capability types |
@avelonjs/orm | Model layer and QueryIR builder |
@avelonjs/cli | reeve CLI and TUI |
@avelonjs/next | Next.js adapter |
@avelonjs/bailiff | Architecture linter |
@avelonjs/assay | Test harness |
@avelonjs/supabase | Supabase drivers |
@avelonjs/postgres | Postgres database driver |
@avelonjs/neon | Neon drivers (database, auth, S3, queue) |
@avelonjs/resend | Resend mail driver |
@avelonjs/conformance | Contract suites and fakes |
These stay private. You do not publish them:
| Package | Why |
|---|---|
avelon (repo root) | Workspace root |
@avelonjs/docs-site | Site builder for this repository |
@avelonjs/s4 | Integration slice |
@avelonjs/gazette | Example 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:
- Copies the root
LICENSEinto the package if needed. - Runs
bun pm pack --destination <temp> --ignore-scriptsfrom the package directory. Bun writes<name>-<version>.tgz. It rejects--filenametogether with--destination. - Prints every path in the tarball.
- Fails if
LICENSEorREADME.mdis missing, ifsrc/is missing, iftests/,fixtures/,tsconfig, or*.test.tsleaked, or if the packedpackage.jsonstill containsworkspace:.
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:
| Command | What it does to workspace:* |
|---|---|
bun pm pack --destination <dir> --ignore-scripts | Writes @avelonjs/core as 0.0.0 (the dependency's package.json version) |
npm publish --access public --tag <tag> --ignore-scripts | Same 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.
- Put an npm automation token in the repository secret
NPM_TOKEN. Do not commit it. Do not put it in.npmrc. - Open the Publish workflow in GitHub Actions and run it.
- Leave
dry_runattruethe first time. That only runsbun run release:pack. - When you mean it, set
dry_runtofalseand choose a tag (next,dev, orlatest). - The workflow then runs
bun scripts/release/publish.tswithNPM_TOKEN,NPM_CONFIG_TOKEN(same value; Bun 1.3.14 reads this name), andRELEASE_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.
- Open Publish GitHub Packages.
- First click: leave
dry_runtrue. That only packs. - Second click: set
dry_runto false. That uploads. - Or push a tag named
github-packages-v*, for examplegithub-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
.envor.env.*- A service role key
- A real project ref
NPM_TOKENor 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.