Skip to content

CLI Release Guide

This guide covers the npm release system for apps/cli, published as @lenserfight/cli.

The release path is intentionally narrow:

  • Source package: apps/cli/package.json
  • Build output: dist/apps/cli
  • Nx project: cli
  • npm package: @lenserfight/cli
  • Git tag pattern: cli@<version>
  • Release workflow: .github/workflows/release-cli.yml
  • Package validation workflow: .github/workflows/cli-package.yml

Install Surfaces

Every release must support:

bash
npm install -g @lenserfight/cli
npx lenserfight --version
npm install --save-dev @lenserfight/cli

The package exposes both lenserfight and lf bins. The production artifact is a bundled CommonJS Node executable with a shebang and Node >=22.

Versioning

Releases use SemVer and Nx Release conventional-commit analysis.

Supported channels:

Channelnpm dist-tagVersion shapeBranches
Stablelatest1.2.3main
Betabeta1.2.3-beta.1main, development, release/*
RCrc1.2.3-rc.1main, release/*
Nightlynightly1.2.3-nightly.1main, development

Use Conventional Commits:

  • fix(cli): ... -> patch
  • feat(cli): ... -> minor
  • feat(cli)!: ... or BREAKING CHANGE: -> major
  • docs(cli): ..., test(cli): ..., chore(cli): ... -> changelog context, usually no release by themselves

Major releases require allow_major=true in the release workflow. This prevents accidental major bumps from a mistaken commit footer.

Release Preview

Run a dry release before every publish:

  1. Open Actions -> Release CLI.
  2. Select the channel and version specifier.
  3. Keep dry_run=true.
  4. Run the workflow from the intended branch.

The preview runs tests, builds the CLI, validates the tarball contents, smoke-installs the package, previews Nx version/changelog output, runs npm publish --dry-run, and uploads artifacts.

Publish

After the dry run is clean:

  1. Re-run Release CLI with the same channel.
  2. Set dry_run=false.
  3. Use first_release=true only for the first public CLI release.
  4. For stable releases, approve the npm-production environment.

The workflow:

  1. Installs with pnpm install --frozen-lockfile.
  2. Runs pnpm nx run cli:test --ci.
  3. Runs pnpm nx run cli:smoke-install.
  4. Versions apps/cli/package.json through Nx Release.
  5. Generates apps/cli/CHANGELOG.md.
  6. Rebuilds, validates, and smoke-installs the versioned package.
  7. Generates a CycloneDX SBOM.
  8. Commits the version/changelog and tags cli@<version>.
  9. Publishes dist/apps/cli to npm with --provenance.
  10. Verifies the package from npm with npm view and npm exec.
  11. Creates a GitHub Release with the tarball, SBOM, and validation reports.

Required Secrets and Environments

Prefer npm Trusted Publishing for lenserfight. Configure npm to trust this repository and the release-cli.yml workflow, then remove the fallback token once the trusted flow is confirmed.

Fallback secret:

SecretScopeNotes
NPM_TOKENnpm-production and npm-prerelease environmentsnpm automation token with publish permission for lenserfight

Environment policy:

  • npm-production: restricted to maintainers, required approval for stable releases.
  • npm-prerelease: maintainers only, approval optional but recommended.
  • GITHUB_TOKEN: provided by GitHub, limited to contents: write and id-token: write.

Rotate NPM_TOKEN after maintainer changes, suspected exposure, or failed release attempts with unclear logs. Never echo npm config or tokens in workflow steps.

Package Gates

pnpm nx run cli:validate-package fails when:

  • main.js is missing, lacks a shebang, or is not executable.
  • package.json is private, missing bin aliases, missing Node >=22, or has an invalid version.
  • --version does not match the package version.
  • --help does not start successfully.
  • The npm tarball contains sources, maps, lockfiles, configs, secrets, certificates, local paths, internal tool folders, or unexpected files.
  • The package exceeds the size guardrails.

pnpm nx run cli:smoke-install verifies:

  • npm exec --package <tarball> lenserfight --version
  • local project install and lf --version
  • global install and lenserfight --version

The CI matrix runs these checks on Linux, macOS, and Windows.

Rollback

npm packages are immutable. Rollback means moving traffic away from a bad version.

  1. Identify the previous known-good version:
bash
npm view lenserfight versions --json
npm dist-tag ls lenserfight
  1. Move the affected tag:
bash
npm dist-tag add lenserfight@<previous-version> latest

For prereleases, replace latest with beta, rc, or nightly.

  1. Deprecate the bad version:
bash
npm deprecate lenserfight@<bad-version> "Use <previous-version>; this release was withdrawn."
  1. Update the GitHub Release with the rollback note.
  2. Open a hotfix branch from main, fix forward, run a dry release, then publish a new patch.

Do not delete git tags unless the package was never published. If a tag points to a published npm version, keep it for provenance and auditability.

Troubleshooting

SymptomAction
--version is staleConfirm apps/cli/src/main.ts reads apps/cli/package.json; rebuild with pnpm nx run cli:build.
npm publish says version existsThe package already published. Bump version or move the dist-tag only.
Provenance failsCheck id-token: write, npm trusted publishing settings, and the NPM_TOKEN fallback.
Package validation blocks a fileInspect npm pack --dry-run --json dist/apps/cli; update files in apps/cli/package.json only if the file is intentionally public.
Windows smoke test failsCheck bin shims and avoid POSIX-only assumptions in release scripts.

Future Distribution

The current system is ready for:

  • scoped package split, for example @lenserfight/cli
  • plugin packages released with separate Nx project tags
  • Homebrew formula generation from GitHub Releases
  • Docker images built from the npm tarball
  • standalone binaries added as extra release assets
  • enterprise/private registry publishing through a separate release environment