Skip to main content
nexlem

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

This 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 changelog

This 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:

  1. Review CHANGELOG.md — edit the generated entry for clarity if needed.
  2. Review package.json — confirm the version bump is correct.
  3. Commit both files: git commit -m "chore: release vX.Y.Z".
  4. Tag the release: git tag vX.Y.Z.
  5. Push the commit and tag: git push && git push --tags.

3. Publishing

After tagging, publish to the npm registry:

bun publish

bun 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):

StepCommandWhat it catches
TypeScriptbunx tsc --noEmitType errors across the entire codebase
Lintbunx biome check src/Style and lint violations in src/
Testsbun vitest runAny failing unit or integration test
Clean tree (unstaged)git diff --exit-codeUncommitted changes to tracked files
Clean tree (staged)git diff --cached --exit-codeStaged but not committed changes
Bin permissionschmod +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 prepublishOnly

Or 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.mdGitHub Release
AudienceDevelopers, scriptsEnd users
Detail levelDetailed, per-commit bulletsHigh-level, top 3-5 changes
LocationCommitted to repoGitHub Releases UI (not committed)
FormatKeep-a-ChangelogFree-form markdown

How to create a GitHub release:

  1. Go to the repository's Releases page on GitHub
  2. Click "Draft a new release"
  3. Select the tag you pushed (vX.Y.Z)
  4. Set the title to vX.Y.Z
  5. Write the release notes (see format below)
  6. 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.


Edit this page on GitHub