Nexlem Release Process
This document is the single source of truth for cutting a Nexlem release. It covers the
git-cliff changelog workflow, the bun pm pkg set version bump, the prepublishOnly
quality gate, and the GitHub release-notes process.
Deprecation policy: The tier-based deprecation contract for consumers lives in
docs/STABILITY.md, not here. Consult that document before demoting or removing any classified interface.
Why this changed
The former release tooling, based on @changesets/cli, could not version the root
@nexlem/framework package in this monorepo (the root package is not a workspace member,
which caused a "not in the workspace" error). It was also over-engineered for a single
published package. git-cliff + bun pm pkg set version operate at the repo root with no
workspace coupling and are a better fit for this project.
Overview
The release flow has three steps:
1. bun pm pkg set version=X.Y.Z # semver bump — no git side-effects
↓
2. bun run changelog # regenerate CHANGELOG.md from git history
↓
3. bun publish # npm registry (separate manual step)
1. Bumping the Version
When ready to release, bump the version field in package.json with no git side-effects:
bun pm pkg set version=X.Y.ZThis atomically rewrites the "version" field in package.json and performs no git
operations — no tag, no commit. The version bump is a plain file edit; commit it yourself
as part of the release commit.
Note: Do not use bun pm version — on Bun 1.3.x it creates a git tag by default
and has no --no-git-tag-version flag.
2. Generating the Changelog
After bumping the version, regenerate CHANGELOG.md from the git log:
bun run changelogThis runs git-cliff -o CHANGELOG.md, which reads cliff.toml at the repo root and
emits a Keep-a-Changelog-formatted changelog from conventional commits + git tags.
Manual splice option: If you only need to prepend a new release section (e.g. when
preserving existing hand-edited entries), you can manually splice the new block into
CHANGELOG.md above the previous release heading instead of regenerating the full file.
The [2.3.0] section in the current CHANGELOG.md was added this way.
After generating the changelog:
- Review
CHANGELOG.md— edit the generated entry for clarity if needed. - Review
package.json— confirm the version bump is correct. - Commit both files:
git commit -m "chore: release vX.Y.Z". - Tag the release:
git tag vX.Y.Z. - Push the commit and tag:
git push && git push --tags.
3. Publishing
After tagging, publish to the npm registry:
bun publishbun publish /path/to/tarball skips prepublishOnly:
Running bun publish with a pre-built tarball path (bun publish ./package.tgz) does NOT
fire lifecycle scripts including prepublishOnly. Always use bun publish (no argument)
to ensure the gate runs.
4. The prepublishOnly Gate
The prepublishOnly script in package.json runs automatically when bun publish packs the
package inline. It is a hard gate — if any step fails, the publish is aborted.
The gate checks (in order):
| Step | Command | What it catches |
|---|---|---|
| TypeScript | bunx tsc --noEmit | Type errors across the entire codebase |
| Lint | bunx biome check src/ | Style and lint violations in src/ |
| Tests | bun vitest run | Any failing unit or integration test |
| Clean tree (unstaged) | git diff --exit-code | Uncommitted changes to tracked files |
| Clean tree (staged) | git diff --cached --exit-code | Staged but not committed changes |
| Bin permissions | chmod +x bin/* | Ensures binaries are executable in the tarball |
Do not bypass the gate. Running bun publish --ignore-scripts bypasses prepublishOnly
and ships an unvalidated package. That flag is not documented here as a valid workflow step
because it is not one.
bun publish --dry-run exits 1 on Bun v1.3.14: When running bun publish --dry-run with
no npm auth token configured, all six prepublishOnly gate steps run and pass, but Bun then
contacts the npm registry to verify credentials and exits 1 with error: missing authentication.
This is expected behavior in a dry-run rehearsal context. In the real publish flow, the auth
token is set in the environment and the command exits 0 after successful publish.
Running the gate manually (without publishing):
bun run prepublishOnlyOr run individual steps:
bunx tsc --noEmit
bunx biome check src/
bun vitest run
git diff --exit-code && git diff --cached --exit-code
chmod +x bin/*5. GitHub Release Notes
GitHub release notes are distinct from CHANGELOG.md:
| CHANGELOG.md | GitHub Release | |
|---|---|---|
| Audience | Developers, scripts | End users |
| Detail level | Detailed, per-commit bullets | High-level, top 3-5 changes |
| Location | Committed to repo | GitHub Releases UI (not committed) |
| Format | Keep-a-Changelog | Free-form markdown |
How to create a GitHub release:
- Go to the repository's Releases page on GitHub
- Click "Draft a new release"
- Select the tag you pushed (
vX.Y.Z) - Set the title to
vX.Y.Z - Write the release notes (see format below)
- Attach the tarball if distributing pre-publish
Alternatively, use the GitHub CLI:
gh release create vX.Y.Z --title "vX.Y.Z" --notes "$(cat release-notes.md)"Release notes format:
## What's New in vX.Y.Z
[One-sentence headline summarizing the most important change]
### Highlights
- **[Feature name]:** [One-sentence description]
- **[Fix name]:** [One-sentence description]
- **[Improvement name]:** [One-sentence description]
### Install
[Install instructions for this version]
Full changelog: [CHANGELOG.md#XYZ](CHANGELOG.md#XYZ)The drafted release notes for the first public release live in
release-notes-v2.2.0.md at the repo root. That file is
passed to gh release create --notes-file release-notes-v2.2.0.md when cutting the GitHub
release.
Related Documents
docs/STABILITY.md— stability contract and deprecation policy for consumersCHANGELOG.md— per-release record of changescliff.toml— git-cliff changelog generation configuration