From 80d004bb3f4878b3ff6e61053d98bc427c560257 Mon Sep 17 00:00:00 2001 From: Ada Lovelace Date: Tue, 3 Nov 2026 09:22:44 +0000 Subject: [PATCH] docs: npm package release guide (package.json + package-lock.json via --exec) Mirror the Python/Rust guides: bump package.json with release.sh --bump-file, sync package-lock.json with --exec "npm install --package-lock-only --ignore-scripts --silent", tag-push workflow with tag check, npm ci/test, trusted publishing, and gh release create. tests/test_npm_package_docs.py runs it with real npm. --- README.md | 2 + docs/index.html | 1 + docs/llms.txt | 5 + docs/npm-package.html | 354 +++++++++++++++++++++++++ docs/python-package.html | 2 +- docs/rust-crate.html | 2 +- docs/sitemap.xml | 6 + docs/standard-version-alternative.html | 2 +- tests/test_npm_package_docs.py | 189 +++++++++++++ 9 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 docs/npm-package.html create mode 100644 tests/test_npm_package_docs.py diff --git a/README.md b/README.md index 47307f3..463d196 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,8 @@ Releasing a Python package? [Release a Python package from conventional commits] Releasing a Rust crate? [Release a Rust crate from conventional commits](https://loki-inu.github.io/relnote/rust-crate.html) bumps `version` in `Cargo.toml`, syncs `Cargo.lock` with `release.sh --exec "cargo update --workspace"`, prepends CHANGELOG.md, and tags; a tag-push workflow checks the tag against `Cargo.toml`, publishes with `cargo publish --locked` through crates.io trusted publishing, and creates the GitHub Release with relnote notes. +Releasing an npm package? [Release an npm package from conventional commits](https://loki-inu.github.io/relnote/npm-package.html) bumps `version` in `package.json`, syncs `package-lock.json` with `release.sh --exec "npm install --package-lock-only --ignore-scripts --silent"`, prepends CHANGELOG.md, and tags; a tag-push workflow checks the tag against `package.json`, runs `npm ci` / `npm test`, publishes with npm trusted publishing, and creates the GitHub Release with relnote notes. + ```sh ./release.sh --dry-run --bump-file package.json ./release.sh --bump-file package.json && git push --follow-tags diff --git a/docs/index.html b/docs/index.html index 1a849a0..5e3b9ae 100644 --- a/docs/index.html +++ b/docs/index.html @@ -247,6 +247,7 @@

relnote

· release-please alternative (no release PR) · Python package releases (pyproject.toml, PyPI) · Rust crate releases (Cargo.toml, Cargo.lock, crates.io) + · npm package releases (package.json, package-lock.json, npmjs) · Windows and PowerShell diff --git a/docs/llms.txt b/docs/llms.txt index dc027f1..a2a4038 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -32,6 +32,10 @@ Locally: examples/release.sh --bump-file pyproject.toml (first version = "..." l Locally: examples/release.sh --bump-file Cargo.toml --exec "cargo update --workspace" (v0.3.7+; --exec runs after the bump and CHANGELOG.md, before the commit, and tracked files it changes such as Cargo.lock are committed; NEXT_VERSION is exported) → next version, Cargo.toml + Cargo.lock bump, CHANGELOG.md section, chore(release) commit, annotated tag. Without the lock sync, cargo publish --locked fails (Cargo.lock needs updating but --locked was passed). Tag-push workflow: check v$version == tag via cargo metadata --no-deps | jq, cargo test --locked, rust-lang/crates-io-auth-action@v1 (trusted publishing; first version published by hand), cargo publish --locked, then gh release create with the loki-inu/relnote action's notes. Shared workspace version: [workspace.package] version in the root Cargo.toml. Guide: https://loki-inu.github.io/relnote/rust-crate.html +## npm packages (package.json, package-lock.json, npmjs) + +Locally: examples/release.sh --bump-file package.json --exec "npm install --package-lock-only --ignore-scripts --silent" (v0.3.7+; --exec runs after the bump and CHANGELOG.md, before the commit, and tracked files it changes such as package-lock.json are committed; NEXT_VERSION is exported) → next version, package.json + package-lock.json bump, CHANGELOG.md section, chore(release) commit, annotated tag. Without the lock sync, package-lock.json keeps the old version under packages[""].version and the next npm install dirties the tree. Tag-push workflow: check v$version == tag via node -p require('./package.json').version, npm ci, npm test, npm publish with trusted publishing (environment release, id-token: write; first version published by hand), then gh release create with the loki-inu/relnote action's notes. Workspaces: bump the root or use --tag-prefix / --path per package as on the monorepo page. Yarn Berry: --exec "yarn install --mode=update-lockfile"; pnpm: --exec "pnpm install --lockfile-only". Guide: https://loki-inu.github.io/relnote/npm-package.html + ## Compared to nearby tools - git-cliff: keep it for templated CHANGELOG.md rebuilt from history and version bump. relnote prints a Release body, can prepend one release to CHANGELOG.md, and needs no config. @@ -70,6 +74,7 @@ gh relnote prints the notes; gh relnote create TAG [gh release create flags] [-- - release-please alternative without a release PR (workflow_dispatch: bump version file, CHANGELOG.md, chore(release) commit, tag, gh release create; tested example): https://loki-inu.github.io/relnote/release-please-alternative.html - Release a Python package (pyproject.toml bump, CHANGELOG.md, tag, PyPI trusted publishing, GitHub Release; tested example): https://loki-inu.github.io/relnote/python-package.html - Release a Rust crate (Cargo.toml + Cargo.lock bump, CHANGELOG.md, tag, crates.io trusted publishing, GitHub Release; tested with real cargo): https://loki-inu.github.io/relnote/rust-crate.html +- Release an npm package (package.json + package-lock.json bump, CHANGELOG.md, tag, npm trusted publishing, GitHub Release; tested with real npm): https://loki-inu.github.io/relnote/npm-package.html - Release notes from squash-merged PR titles (feat: x (#41); PR-title check; CHANGELOG.md PR links from v0.3.5): https://loki-inu.github.io/relnote/squash-merge.html - Windows and PowerShell (Scoop, --output vs > redirection, UTF-8/BOM, line endings): https://loki-inu.github.io/relnote/windows.html - Compare (git-cliff / gh --generate-notes / swapnanil/relnotes): https://loki-inu.github.io/relnote/compare.html diff --git a/docs/npm-package.html b/docs/npm-package.html new file mode 100644 index 0000000..15084e6 --- /dev/null +++ b/docs/npm-package.html @@ -0,0 +1,354 @@ + + + + + + + Release an npm package from conventional commits — relnote + + + + + + + + + + + + + + + + + +
+

relnote docs · GitHub Action · CHANGELOG.md · Python packages · Rust crates · standard-version alternative

+

Release an npm package from conventional commits

+

Bump version in package.json, bring package-lock.json along, prepend a CHANGELOG.md section, and tag in one local command. Pushing the tag runs tests, publishes to the npm registry with trusted publishing, and creates a GitHub Release with grouped notes. No semantic-release, no release bot, no config file.

+ + +

1. The version lives in package.json

+
{
+  "name": "demo",
+  "version": "1.2.0",
+  "description": "A small example package",
+  "main": "index.js",
+  "scripts": {
+    "test": "node --test"
+  },
+  "license": "MIT",
+  "files": [
+    "index.js"
+  ],
+  "engines": {
+    "node": ">=18"
+  }
+}
+

Commit package-lock.json (npm's default for apps and most libraries). It also records your package's version under packages[""].version, which is why the release step below updates it.

+ +

2. Cut the release locally

+

Three commits since v1.2.0:

+
git log --oneline v1.2.0..HEAD
+docs: document the --json flag
+fix: handle empty input without panicking
+feat(cli): add --json output
+

Install relnote (pipx install git+https://github.com/loki-inu/relnote.git, brew install loki-inu/tap/relnote, or the single file), then run the release script pinned to a version:

+
curl -fsSLO https://raw.githubusercontent.com/loki-inu/relnote/v0.3.7/examples/release.sh
+bash release.sh --bump-file package.json --exec "npm install --package-lock-only --ignore-scripts --silent"
+
next: v1.3.0
+changelog: CHANGELOG.md
+bump: package.json -> 1.3.0
+exec: npm install --package-lock-only --ignore-scripts --silent
+committed: chore(release): v1.3.0
+tagged: v1.3.0
+next step: git push --follow-tags
+

A feat made it a minor bump. --exec runs after the bump and before the commit; npm install --package-lock-only --ignore-scripts --silent rewrites only package-lock.json (no node_modules, no lifecycle scripts), and release.sh commits it with the rest:

+
git show --name-only --format= v1.3.0
+CHANGELOG.md
+package-lock.json
+package.json
+

Without it, package-lock.json still says 1.2.0 while package.json says 1.3.0, so the next npm install dirties the tree with a lockfile-only change. The new CHANGELOG.md section:

+
## [v1.3.0] - 2026-11-03
+
+### Features
+
+- **cli:** add --json output (`4e39256`) — Ada Lovelace
+
+### Fixes
+
+- handle empty input without panicking (`a8cc6d8`) — Ada Lovelace
+
+### Other
+
+- **docs:** document the --json flag (`83256ad`) — Ada Lovelace
+
+**Full changelog:** https://github.com/you/demo/compare/v1.2.0...v1.3.0
+

Output from a test repository built in relnote's CI with real npm (tests/test_npm_package_docs.py); your hashes and date differ. Use --dry-run first to see the version and notes without changing anything. Then:

+
git push --follow-tags
+ +

3. Publish and release on the tag

+

Save as .github/workflows/release.yml. Publish the first version by hand (npm publish with an access token); then, on npmjs.com, add a trusted publisher for this repository with workflow release.yml and environment release, and create that environment in the repository settings. Trusted publishing needs npm CLI 11.5.1+ and Node 22.14+ on the runner.

+
name: Release
+on:
+  push:
+    tags: ["v*"]
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    environment: release
+    permissions:
+      contents: write
+      id-token: write
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      - uses: actions/setup-node@v4
+        with:
+          node-version: "22"
+          registry-url: https://registry.npmjs.org
+      - name: Tag matches package.json
+        run: |
+          version=$(node -p "require('./package.json').version")
+          if [ "v$version" != "$GITHUB_REF_NAME" ]; then
+            echo "::error::tag $GITHUB_REF_NAME but package.json says $version"
+            exit 1
+          fi
+      - run: npm ci
+      - run: npm test
+      - run: npm publish --access public
+      - id: relnote
+        uses: loki-inu/relnote@v0.3.7
+      - env:
+          GH_TOKEN: ${{ github.token }}
+          GH_REPO: ${{ github.repository }}
+          NOTES: ${{ steps.relnote.outputs.notes }}
+        run: |
+          printf '%s\n' "$NOTES" > notes.md
+          gh release create "$GITHUB_REF_NAME" --verify-tag --notes-file notes.md
+

The first step stops the run if the tag and package.json disagree. npm ci installs from the lockfile committed with the tag. The GitHub Release is created only after the registry accepted the package. Its body is the action's notes output, passed through env: because commit subjects can contain backticks:

+
## What's changed
+
+### Features
+
+- **cli:** add --json output (`4e39256`) — Ada Lovelace
+
+### Fixes
+
+- handle empty input without panicking (`a8cc6d8`) — Ada Lovelace
+
+### Other
+
+- **docs:** document the --json flag (`83256ad`) — Ada Lovelace
+
+---
+
+Range: `v1.2.0` → `v1.3.0`.
+
+**Full changelog:** https://github.com/you/demo/compare/v1.2.0...v1.3.0
+

The chore(release): v1.3.0 commit is left out of the notes by default.

+ +

FAQ

+

Why is package-lock.json in the release commit?

+

package-lock.json records your package's own version under packages[""].version. release.sh --bump-file package.json only rewrites package.json, so without a follow-up the lockfile still shows the old version and the next npm install dirties the tree. The --exec "npm install --package-lock-only --ignore-scripts --silent" step refreshes only the lockfile (no node_modules install, no lifecycle scripts), and release.sh commits every tracked file the command changed.

+

Does it work with npm workspaces?

+

If the workspace root is the published package, bump the root package.json and run the --exec from the root so the single package-lock.json updates. For packages versioned separately under packages/, use one tag prefix per package (release.sh --tag-prefix demo-core/v --bump-file packages/demo-core/package.json --exec "npm install --package-lock-only --ignore-scripts --silent" -- --path packages/demo-core) as on the monorepo page, and publish with npm publish -w.

+

Why trusted publishing instead of an npm token?

+

npm trusted publishing exchanges the job's OpenID Connect token for a short-lived publish credential, so there is no long-lived NPM_TOKEN in the repository. Provenance attestations are included automatically. Add a trusted publisher on npmjs.com for this repository with workflow release.yml and environment release before relying on it; publish the first version by hand with npm publish and a normal access token if the package does not exist yet.

+

What if the tag and package.json disagree?

+

The check step fails before npm ci runs, with an error naming both values. An npm version cannot be reused once published, so the check is worth keeping.

+

Can I dry-run the whole thing?

+

Locally, release.sh --dry-run prints the next version, the files it would bump, the --exec commands, and the notes, and changes nothing. npm publish --dry-run packs the tarball without uploading.

+

What about yarn or pnpm?

+

The bump still uses --bump-file package.json. For Yarn Berry, swap the --exec for yarn install --mode=update-lockfile; for pnpm, use pnpm install --lockfile-only. Keep the matching lockfile committed and use the package manager's ci-equivalent in the workflow (yarn install --immutable, pnpm install --frozen-lockfile).

+

Do I need semantic-release?

+

No. semantic-release is a good fit when every merge to main should publish from CI with plugins. This page is for people who want to cut the version, changelog, and tag locally from commit messages, then let a tag-push workflow publish and open the GitHub Release.

+ + +
+ + diff --git a/docs/python-package.html b/docs/python-package.html index 50c705a..3ac98e3 100644 --- a/docs/python-package.html +++ b/docs/python-package.html @@ -222,7 +222,7 @@
-

relnote docs · GitHub Action · CHANGELOG.md · release-please alternative · standard-version alternative · Rust crates

+

relnote docs · GitHub Action · CHANGELOG.md · release-please alternative · standard-version alternative · Rust crates · npm packages

Release a Python package from conventional commits

Bump version in pyproject.toml, prepend a CHANGELOG.md section, and tag in one local command. Pushing the tag builds the sdist and wheel, uploads them to PyPI with trusted publishing, and creates a GitHub Release with grouped notes. No Node, no release bot, no config file; relnote itself is stdlib Python.