Skip to main content
nexlem

Nexlem Release Process

This document is the single source of truth for cutting a Nexlem release. It covers the changesets workflow, 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.


Overview

The release flow has three steps:

1. changeset add          # record what changed and bump type
       ↓
2. changeset version      # apply version bump → CHANGELOG.md + package.json
       ↓
3. changeset publish      # bun publish → npm registry

All three steps use bunx @changesets/cli — see Known Pitfalls for the correct binary name.


1. Adding a Changeset

Before merging a PR that contains a user-visible change, add a changeset describing what changed and whether it is a patch, minor, or major bump:

bunx @changesets/cli add

The CLI will prompt for:

  • Bump type: patch (bug fix), minor (new feature, backward-compatible), or major (breaking change)
  • Summary: a one-line description that will appear in CHANGELOG.md

This creates a file under .changeset/<random-hash>.md. Commit that file alongside the PR.

When to add a changeset:

  • Bug fixes visible to consumers → patch
  • New nexlem-sdk query handler or new /nexlem skill → minor
  • Breaking change to a stable-tier interface → major (and follow the deprecation policy in docs/STABILITY.md)
  • Internal-only changes (tests, CI, docs) → no changeset required

2. Cutting a Release

When ready to release, apply all pending changesets to produce the version bump and CHANGELOG entry:

bunx @changesets/cli version

This command:

  1. Reads all .changeset/*.md files
  2. Determines the highest bump type (patch / minor / major)
  3. Updates package.json version field
  4. Appends a new version block to CHANGELOG.md
  5. Deletes the consumed .changeset/*.md files

After running changeset version:

  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:

bunx @changesets/cli publish

This runs bun publish for each non-private package in the repository.

Critical — private: true packages are skipped entirely: If package.json contains "private": true, changeset publish will print "No unpublished projects to publish" and exit without publishing anything. This is the expected behavior; it is not an error.

The private: false flip and the actual npm publish to the registry are deferred to v2.3, alongside the license decision. The Phase 48 release rehearsal exercised bun publish --dry-run via a temporary, immediately-reverted flip to private: false, but the repository remains private: true. Do not flip private: false permanently until the v2.3 publish is ready.


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.


Known Pitfalls

Always use bunx @changesets/cli, not bunx changeset

There is no npm package named changeset. The correct scoped package name is @changesets/cli.

# WRONG — fails with "could not determine executable to run for package changeset"
bunx changeset add
bunx changeset version
 
# CORRECT
bunx @changesets/cli add
bunx @changesets/cli version
bunx @changesets/cli publish

changeset publish skips private: true packages

If package.json has "private": true, changeset publish exits without publishing. This is expected behavior. Flip private: false before attempting to publish to the registry.

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) or bunx @changesets/cli publish to ensure the gate runs.


Edit this page on GitHub