Skip to content

Add changelog and release workflow for npm publishing#6

Open
justin808 wants to merge 4 commits intomainfrom
jg-codex/release-prep-docs-scripts
Open

Add changelog and release workflow for npm publishing#6
justin808 wants to merge 4 commits intomainfrom
jg-codex/release-prep-docs-scripts

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Mar 15, 2026

Summary

  • add CHANGELOG.md with initial v1.0.0 release notes
  • add maintainer guide at docs/releasing.md
  • add scripts/release.sh publish script with dry-run support
  • add npm scripts: release and release:dry-run
  • add release links to README

Validation

  • npm test
  • npm run build
  • ./scripts/release.sh --help

Note

Low Risk
Low risk: adds release documentation and a maintainer-facing publish script; no runtime/library logic changes aside from new npm scripts.

Overview
Adds a top-level CHANGELOG.md (with initial v1.0.0 notes) and a maintainer release guide at docs/releasing.md.

Introduces scripts/release.sh and wires it into package.json as npm run release / release:dry-run, enforcing clean main, matching changelog/version, running tests/build (unless skipped), publishing to npm (optional dist-tag / dry-run), and tagging/pushing v<version>; README now links to the new release docs.

Written by Cursor Bugbot for commit 5860cc8. Configure here.

Summary by CodeRabbit

  • Documentation
    • Added a comprehensive CHANGELOG (v1.0.0 + Unreleased), README release section, and a detailed release guide.
  • New Features
    • Released a standalone config-diff CLI with rich output formats, plugin-aware comparison, rule-matching improvements, path normalization, and contextual config docs.
  • Tests
    • Added comprehensive unit and integration test coverage.
  • Chores
    • Added release automation (scripts, config, dry-run support), npm release scripts, a CI release workflow, and a release tooling dev dependency.

@coderabbitai
Copy link

coderabbitai bot commented Mar 15, 2026

Walkthrough

Adds release infrastructure: comprehensive CHANGELOG with v1.0.0, README release links, a maintainer releasing guide, release scripts and config, a new Bash release orchestration script, and a GitHub Actions workflow to publish releases on pushes to main.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Adds Unreleased placeholder and detailed v1.0.0 release notes describing the extracted semantic webpack/rspack config diff engine, CLI name pack-config-diff, diff features, formatters, plugin-aware options, tests, and build notes.
Docs & README
README.md, docs/releasing.md
Inserts Release sections into README.md (two locations) and adds docs/releasing.md with a full maintainer release guide: prerequisites, stable/prerelease protocols, dry-run/local verification, and post-release checks.
Release scripts & config
package.json, scripts/release.sh, .release-it.json
Adds release and release:dry-run npm scripts, release-it devDependency, a new scripts/release.sh implementing argument parsing, git/state validation, changelog/version extraction, tag/dist-tag logic, test/build gating, and invokes release-it; adds .release-it.json release configuration and hooks.
CI Workflow
.github/workflows/release-on-main.yml
Adds GitHub Actions workflow to run the release script on pushes to main, sets up Node.js, installs dependencies, and supplies required secrets (GITHUB_TOKEN, NPM_TOKEN, NODE_AUTH_TOKEN).

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Developer
    participant Script as ./scripts/release.sh
    participant Git as Git (local & remote)
    participant CI as CI / Tests & Build
    participant NPM as npm Registry
    participant GH as GitHub

    Dev->>Script: run (--dry-run / --npm-tag / --skip-tests)
    Script->>Git: verify branch == main & working tree clean
    Script->>Script: extract target version from CHANGELOG & package.json
    Script->>Git: ensure no existing tag v{target} (local & remote)
    alt tests/build enabled
        Script->>CI: run tests & build
        CI-->>Script: result
    end
    Script->>NPM: npm publish (--tag X) [--dry-run]
    alt not dry-run
        Script->>Git: git tag v{target} & push
        Script->>GH: create GitHub release with changelog notes
    end
    Script-->>Dev: print summary & exit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibbled the changelog, hopped on main,

scripts at the ready, no messy refrain,
A dry-run wiggle, then tags take flight,
Publish bells ringing into the night,
V1.0.0 — carrots crunchy and bright.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding CHANGELOG.md and release automation infrastructure for npm publishing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jg-codex/release-prep-docs-scripts
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

esac
done

if ! git diff --quiet || ! git diff --cached --quiet; then
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The working-tree check misses untracked files. git diff only reports changes to tracked files, so a dirty tree with new, uncommitted files will pass this guard silently.

Use git status --porcelain instead:

Suggested change
if ! git diff --quiet || ! git diff --cached --quiet; then
if [[ -n "$(git status --porcelain)" ]]; then

Comment on lines +59 to +63
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$CURRENT_BRANCH" != "main" ]]; then
echo "Error: release must be run from main (current: $CURRENT_BRANCH)." >&2
exit 1
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch check confirms you're on main, but it doesn't verify that local main is up-to-date with origin/main. A maintainer could inadvertently release from a stale local branch that is missing recent commits.

Consider adding a sync check after the branch check:

Suggested change
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$CURRENT_BRANCH" != "main" ]]; then
echo "Error: release must be run from main (current: $CURRENT_BRANCH)." >&2
exit 1
fi
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$CURRENT_BRANCH" != "main" ]]; then
echo "Error: release must be run from main (current: $CURRENT_BRANCH)." >&2
exit 1
fi
git fetch origin main --quiet
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/main)
if [[ "$LOCAL" != "$REMOTE" ]]; then
echo "Error: local main is not in sync with origin/main. Run 'git pull' first." >&2
exit 1
fi

Comment on lines +86 to +93
if [[ "$SKIP_TESTS" == false ]]; then
echo "Running tests..."
npm test
echo "Building dist..."
npm run build
else
echo "Skipping tests/build checks."
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package.json already has "prepublishOnly": "npm run build", so npm publish will trigger a second build automatically. The explicit npm run build here means the build runs twice on every release.

You can drop the redundant explicit build call and rely on prepublishOnly:

Suggested change
if [[ "$SKIP_TESTS" == false ]]; then
echo "Running tests..."
npm test
echo "Building dist..."
npm run build
else
echo "Skipping tests/build checks."
fi
if [[ "$SKIP_TESTS" == false ]]; then
echo "Running tests..."
npm test
else
echo "Skipping tests."
fi


### Build

- Added `prepare` script so installs from GitHub build `dist/` automatically.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [Unreleased] and [v1.0.0] headings use Keep a Changelog's link-reference style but there are no link definitions at the bottom of the file. Most renderers and changelog tooling (e.g. git-cliff, standard-version) expect comparison links here.

Add these at the end of the file:

[Unreleased]: https://github.com/shakacode/pack-config-diff/compare/v1.0.0...HEAD
[v1.0.0]: https://github.com/shakacode/pack-config-diff/releases/tag/v1.0.0

@claude
Copy link

claude bot commented Mar 15, 2026

Review: Add changelog and release workflow for npm publishing

Overall this is a solid, well-structured release workflow. The script covers the important safety rails (clean tree, branch check, tag de-dup, changelog validation) and the docs are clear. A few things worth fixing before merging:

Issues

  1. Untracked files bypass the clean-tree check (scripts/release.sh line 54) — git diff only sees tracked files, so new uncommitted files slip through. Swap to git status --porcelain. (inline comment added)

  2. No check that local main is synced with origin/main (lines 59–63) — a maintainer on a stale local branch could release missing commits. Adding a git fetch + SHA comparison prevents this. (inline comment added)

  3. Build runs twice on publish (lines 86–93) — prepublishOnly in package.json already runs npm run build when npm publish fires, so the explicit npm run build here is redundant. Dropping it halves the build time on release. (inline comment added)

  4. Missing Keep a Changelog link references (CHANGELOG.md EOF) — the [Unreleased] and [v1.0.0] headings are link-reference syntax, but no definitions appear at the bottom. Standard tooling and GitHub's renderer expect comparison URLs there. (inline comment added)

Minor / Nits

  • --skip-tests also skips the build step, but both the flag name and help text say "tests." The help text at least says "test/build checks" — the flag itself (--skip-tests) is a bit misleading; --skip-checks would be more accurate if you ever revisit.
  • The annotated tag message (-m "v$VERSION") is minimal. A richer message (e.g. the first line of the changelog section) gives a nicer experience on git tag -l -n and in GitHub's Releases UI, but this is purely cosmetic.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5860cc875e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

esac
done

if ! git diff --quiet || ! git diff --cached --quiet; then

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject untracked files before publishing

The clean-tree guard only checks tracked/staged changes via git diff --quiet and git diff --cached --quiet, so untracked files are treated as clean and the release continues. That can leak stale artifacts into a publish (for example, extra files left in dist/) even though the script reports the working tree is clean. This is introduced by the new check and should use a status command that also covers untracked files.

Useful? React with 👍 / 👎.

exit 1
fi

if git ls-remote --exit-code --tags origin "refs/tags/v$VERSION" >/dev/null 2>&1; then

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail fast on remote tag lookup errors

The remote-tag check treats every non-zero git ls-remote --exit-code status as “tag not found” because it is used directly as an if condition. Per git ls-remote -h, --exit-code returns 2 when no matching refs are found, but auth/network/remote errors return different non-zero codes; those should abort the release. As written, an unreachable or unauthorized origin can still proceed to npm publish, then fail at final tag push, leaving npm published without the expected git tag.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
CHANGELOG.md (1)

5-5: Consider linking to Keep a Changelog and Semantic Versioning.

Adding links helps contributors understand the conventions being followed.

📝 Optional: Add reference links
-The format is based on Keep a Changelog and this project follows Semantic Versioning.
+The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project follows [Semantic Versioning](https://semver.org/).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 5, Update the sentence "The format is based on Keep a
Changelog and this project follows Semantic Versioning." to include hyperlinks
to the referenced standards (Keep a Changelog and Semantic Versioning) so
contributors can quickly access them; specifically, modify the line in
CHANGELOG.md that contains that sentence to embed or append the URLs for Keep a
Changelog and SemVer (e.g., link text "Keep a Changelog" ->
https://keepachangelog.com and "Semantic Versioning" -> https://semver.org).
scripts/release.sh (2)

73-81: Consider fetching before checking remote tags.

The remote tag check queries origin directly via git ls-remote, which should be current. However, in fast-moving environments, a git fetch --tags before the check could prevent race conditions where another maintainer just pushed a tag.

This is a minor edge case and the current implementation is functional.

🔧 Optional: Add git fetch before tag checks
+echo "Fetching latest tags from origin..."
+git fetch --tags --quiet
+
 if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
   echo "Error: git tag v$VERSION already exists locally." >&2
   exit 1
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.sh` around lines 73 - 81, Add a tags fetch before the remote
tag existence check to avoid races: run a `git fetch --tags` (or `git fetch
--tags origin`) prior to calling `git ls-remote --exit-code --tags origin
"refs/tags/v$VERSION"` so the script uses up-to-date remote tags; keep the local
check with `git rev-parse -q --verify "refs/tags/v$VERSION"` and the variable
`VERSION` unchanged.

106-114: Minor: Tag push failure leaves local tag.

If git push origin "v$VERSION" fails after git tag succeeds, the local tag remains. On retry, the script will error with "tag already exists locally." The user would need to manually delete the local tag with git tag -d "v$VERSION" before retrying.

This is acceptable behavior given set -e ensures failures are loud, and documenting this edge case in the releasing guide could help.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.sh` around lines 106 - 114, Wrap the git push step so that if
git push origin "v$VERSION" fails you delete the just-created local tag to avoid
left-over local tags; specifically after git tag -a "v$VERSION" -m "v$VERSION"
run git push origin "v$VERSION" and on its non-zero exit path run git tag -d
"v$VERSION" and then exit with the push failure code (or propagate the error).
Use the existing VERSION variable and the exact tag commands shown (git tag -a
"v$VERSION" and git push origin "v$VERSION") so the script cleans up the local
tag when push fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@CHANGELOG.md`:
- Line 5: Update the sentence "The format is based on Keep a Changelog and this
project follows Semantic Versioning." to include hyperlinks to the referenced
standards (Keep a Changelog and Semantic Versioning) so contributors can quickly
access them; specifically, modify the line in CHANGELOG.md that contains that
sentence to embed or append the URLs for Keep a Changelog and SemVer (e.g., link
text "Keep a Changelog" -> https://keepachangelog.com and "Semantic Versioning"
-> https://semver.org).

In `@scripts/release.sh`:
- Around line 73-81: Add a tags fetch before the remote tag existence check to
avoid races: run a `git fetch --tags` (or `git fetch --tags origin`) prior to
calling `git ls-remote --exit-code --tags origin "refs/tags/v$VERSION"` so the
script uses up-to-date remote tags; keep the local check with `git rev-parse -q
--verify "refs/tags/v$VERSION"` and the variable `VERSION` unchanged.
- Around line 106-114: Wrap the git push step so that if git push origin
"v$VERSION" fails you delete the just-created local tag to avoid left-over local
tags; specifically after git tag -a "v$VERSION" -m "v$VERSION" run git push
origin "v$VERSION" and on its non-zero exit path run git tag -d "v$VERSION" and
then exit with the push failure code (or propagate the error). Use the existing
VERSION variable and the exact tag commands shown (git tag -a "v$VERSION" and
git push origin "v$VERSION") so the script cleans up the local tag when push
fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8653c2ac-9137-4f96-b603-231f9dd9b770

📥 Commits

Reviewing files that changed from the base of the PR and between 6bb09f4 and 5860cc8.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • README.md
  • docs/releasing.md
  • package.json
  • scripts/release.sh

Comment on lines +93 to +98
fi

PUBLISH_ARGS=(publish --access public)
if [[ -n "$NPM_TAG" ]]; then
PUBLISH_ARGS+=(--tag "$NPM_TAG")
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Prerelease versions can silently publish to latest

If VERSION contains a prerelease identifier (e.g. 1.0.0-rc.0), npm publish without --tag defaults to the latest dist-tag — exactly what the docs warn against.

The script should auto-detect prerelease semver and either enforce --npm-tag or default to next:

Suggested change
fi
PUBLISH_ARGS=(publish --access public)
if [[ -n "$NPM_TAG" ]]; then
PUBLISH_ARGS+=(--tag "$NPM_TAG")
fi
PUBLISH_ARGS=(publish --access public)
# Safety: detect prerelease versions (contain "-") and require an explicit npm tag.
if [[ "$VERSION" == *-* ]] && [[ -z "$NPM_TAG" ]]; then
echo "Error: prerelease version '$VERSION' detected. Pass --npm-tag (e.g. --npm-tag next) to avoid publishing to 'latest'." >&2
exit 1
fi
if [[ -n "$NPM_TAG" ]]; then
PUBLISH_ARGS+=(--tag "$NPM_TAG")
fi
if [[ "$DRY_RUN" == true ]]; then
PUBLISH_ARGS+=(--dry-run)
fi

Comment on lines +85 to +91

if [[ "$SKIP_TESTS" == false ]]; then
echo "Running tests..."
npm test
echo "Building dist..."
npm run build
else
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant double build

npm run build is called explicitly here, but package.json has "prepublishOnly": "npm run build", which means npm publish (line 103) will trigger a second build automatically.

Either:

  • Remove the explicit npm run build here and rely on prepublishOnly (preferred — single source of truth), or
  • Remove prepublishOnly from package.json and keep only the explicit call here.

Also note: --skip-tests currently advertises skipping both tests and build, but the build will still run via prepublishOnly regardless.

Comment on lines +67 to +71

if ! grep -Fq "$CHANGELOG_HEADING" CHANGELOG.md; then
echo "Error: CHANGELOG.md is missing heading: $CHANGELOG_HEADING" >&2
exit 1
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing check: local main may be out of sync with origin/main

This confirms the local branch name is main but doesn't verify it's up-to-date with the remote. A release could be cut from a main that has unpushed commits, or that is behind origin/main.

Consider adding:

Suggested change
if ! grep -Fq "$CHANGELOG_HEADING" CHANGELOG.md; then
echo "Error: CHANGELOG.md is missing heading: $CHANGELOG_HEADING" >&2
exit 1
fi
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$CURRENT_BRANCH" != "main" ]]; then
echo "Error: release must be run from main (current: $CURRENT_BRANCH)." >&2
exit 1
fi
# Ensure local main is in sync with origin/main.
git fetch origin main --quiet
LOCAL_SHA="$(git rev-parse HEAD)"
REMOTE_SHA="$(git rev-parse origin/main)"
if [[ "$LOCAL_SHA" != "$REMOTE_SHA" ]]; then
echo "Error: local main is not in sync with origin/main. Pull or push first." >&2
exit 1
fi

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/releasing.md`:
- Around line 147-148: The doc uses two spellings for the same term; replace the
instance of "pre-release" in the sentence that mentions `--npm-tag next` with
the consistent form "prerelease" (so the line reads "Use `--npm-tag next` for
prerelease channels."), and scan the rest of docs to ensure only "prerelease" is
used instead of "pre-release".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9da60ad9-7de9-423f-b218-dfc05c5e530d

📥 Commits

Reviewing files that changed from the base of the PR and between 5860cc8 and b2a23bf.

📒 Files selected for processing (2)
  • README.md
  • docs/releasing.md

Comment on lines +147 to +148
- Use `--npm-tag next` for pre-release channels.
- `--skip-tests` should be used only in exceptional cases.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use one term consistently: “prerelease” vs “pre-release”.

Line 147 uses “pre-release” while the rest of the doc predominantly uses “prerelease”. Standardize to one form for consistency.

Suggested doc tweak
-- Use `--npm-tag next` for pre-release channels.
+- Use `--npm-tag next` for prerelease channels.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Use `--npm-tag next` for pre-release channels.
- `--skip-tests` should be used only in exceptional cases.
- Use `--npm-tag next` for prerelease channels.
- `--skip-tests` should be used only in exceptional cases.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~147-~147: Do not mix variants of the same word (‘pre-release’ and ‘prerelease’) within a single text.
Context: ...``` Notes: - Use --npm-tag next for pre-release channels. - `--skip-tests` should be us...

(EN_WORD_COHERENCY)


[style] ~148-~148: To form a complete sentence, be sure to include a subject.
Context: ... pre-release channels. - --skip-tests should be used only in exceptional cases. ## ...

(MISSING_IT_THERE)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/releasing.md` around lines 147 - 148, The doc uses two spellings for the
same term; replace the instance of "pre-release" in the sentence that mentions
`--npm-tag next` with the consistent form "prerelease" (so the line reads "Use
`--npm-tag next` for prerelease channels."), and scan the rest of docs to ensure
only "prerelease" is used instead of "pre-release".

exit 0
fi

if ! grep -Fq "## [v$TARGET_VERSION]" CHANGELOG.md; then
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is redundant. TARGET_VERSION was already extracted from the exact same pattern on line 65 — if that grep succeeded, this one will always succeed too. The only way TARGET_VERSION could be set without this heading existing is if the sed replacement somehow produced output from a different line, which can't happen given the regex. Safe to remove lines 78–81.

exit 0
fi

if [[ -z "${NPM_TOKEN:-}" ]]; then
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NPM_TOKEN check is too late — tests and the full build have already run before reaching this point. If NPM_TOKEN isn't configured, you've wasted CI minutes on a release that was never going to publish. Move this check (and any other preconditions) to the top of the script, before SKIP_TESTS and before running tests.

Comment on lines +151 to +152
git tag -a "v$TARGET_VERSION" -m "v$TARGET_VERSION"
git push origin "v$TARGET_VERSION"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent failure state: if npm publish (line 149) succeeds but git push origin "v$TARGET_VERSION" fails (network blip, permission error, etc.), the package is live on npm with no corresponding git tag. A subsequent re-run will then fail at the tag existence check (lines 83–86), leaving you stuck.

Consider pushing the tag before publishing so a failed push can be safely retried. Alternatively, add a trap ERR that prints remediation instructions when the tag push fails post-publish.

Comment on lines +155 to +160
RELEASE_NOTES_FILE="$(mktemp)"
awk -v version="$TARGET_VERSION" '
$0 ~ "^## \\[v"version"\\]" { in_section=1; next }
in_section && $0 ~ "^## \\[v" { in_section=0 }
in_section { print }
' CHANGELOG.md > "$RELEASE_NOTES_FILE"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here:

  1. Temp file leak: mktemp output is never cleaned up. Add a trap at the top of this block:

    Suggested change
    RELEASE_NOTES_FILE="$(mktemp)"
    awk -v version="$TARGET_VERSION" '
    $0 ~ "^## \\[v"version"\\]" { in_section=1; next }
    in_section && $0 ~ "^## \\[v" { in_section=0 }
    in_section { print }
    ' CHANGELOG.md > "$RELEASE_NOTES_FILE"
    RELEASE_NOTES_FILE="$(mktemp)"
    trap 'rm -f "$RELEASE_NOTES_FILE"' EXIT
    awk -v version="$TARGET_VERSION" '
    $0 ~ "^## \\[v"version"\\]" { in_section=1; next }
    in_section && $0 ~ "^## \\[v" { in_section=0 }
    in_section { print }
    ' CHANGELOG.md > "$RELEASE_NOTES_FILE"
  2. Regex vs literal in awk: version is used in a regex context ($0 ~ "^## \\[v"version"\\]"), so dots in 1.0.0 are treated as "any character". Use index() or escape the dots:

    $0 ~ "^## \\[v"version"\\]" { ... }

    Replace dots with \\. in the version string before passing it to awk, or use index($0, "## [v"version"]") == 1 for a literal match.

Comment on lines +163 to +165
gh release create "v$TARGET_VERSION" \
--title "v$TARGET_VERSION" \
--notes-file "$RELEASE_NOTES_FILE"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prerelease versions (1.2.3-rc.0, 1.2.3-beta.0) should be created with the --prerelease flag so GitHub marks them correctly and they don't appear as the "latest" release:

Suggested change
gh release create "v$TARGET_VERSION" \
--title "v$TARGET_VERSION" \
--notes-file "$RELEASE_NOTES_FILE"
gh release create "v$TARGET_VERSION" \
--title "v$TARGET_VERSION" \
$([[ "$TARGET_VERSION" == *-* ]] && echo "--prerelease") \
--notes-file "$RELEASE_NOTES_FILE"

Comment on lines +22 to +27
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actions are pinned to mutable major-version tags (@v4), not immutable commit SHAs. A compromised or accidentally broken actions/checkout@v4 or actions/setup-node@v4 release would affect this workflow with no notice. Since this workflow has contents: write and publishes to npm, the blast radius of a supply-chain issue here is significant.

Pin to commit SHAs, e.g.:

uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0

@claude
Copy link

claude bot commented Mar 15, 2026

Review: Release Workflow & Scripts

The overall approach is solid — changelog-driven releases with idempotency and dry-run support are the right patterns. A few issues worth addressing before merging:

Blocking

Inconsistent post-publish state (scripts/release.sh lines 151–152)
If npm publish succeeds but git push origin "v$TARGET_VERSION" fails, the package is live on npm with no git tag. A re-run will then fail at the tag existence check, leaving the release in a permanently broken state. Consider pushing the tag before publishing, or adding a trap ERR that prints manual recovery steps.

Branch protection may block CI push to main (.github/workflows/release-on-main.yml)
The script does git push origin main (line 144 of release.sh) to commit the version bump. If the repo has branch protection requiring PRs or status checks, this push will fail. This isn't visible from the workflow file alone — worth documenting the required bypass rule, or switching to a workflow that opens a PR for the version bump instead.

Non-blocking but worth fixing

  • NPM_TOKEN check too late (line 126): happens after tests run; should be one of the first checks so CI doesn't waste time on a release that can't publish.
  • Redundant CHANGELOG check (line 78): the grep -Fq is a no-op since TARGET_VERSION was already extracted from that exact pattern on line 65.
  • Temp file leak + awk regex (lines 155–160): mktemp output is never cleaned up (add trap 'rm -f "$RELEASE_NOTES_FILE"' EXIT); dots in the version string (e.g. 1.0.0) are treated as regex wildcards in the awk pattern — use index() for a literal match instead.
  • Missing --prerelease flag (lines 163–165): GitHub releases for *-rc.* / *-beta.* versions should pass --prerelease to gh release create, otherwise they appear as the "latest" stable release.
  • Action SHAs not pinned (workflow lines 22–27): actions/checkout@v4 and actions/setup-node@v4 use mutable tags. Given this workflow has contents: write and publishes to npm, pinning to commit SHAs is strongly recommended.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/release.sh`:
- Around line 73-76: The release flow currently commits/pushes the package.json
version bump before running npm publish (see checks using TARGET_VERSION and
CURRENT_VERSION), which can leave main bumped if publish fails; change the
sequence so npm publish is executed first (using the current working tree or a
temporary pack), and only after a successful publish perform the commit/push and
git tag steps for the version bump (i.e., move the publish step to run before
the code that creates the bump commit and calls git push/tag), and ensure any
temporary changes are cleaned up if publish fails so subsequent runs can retry.
- Around line 54-57: The release pre-check IF block only tests tracked/staged
changes using "git diff --quiet" and "git diff --cached --quiet" and therefore
misses untracked files; update the check to also detect untracked files (e.g.,
run "git status --porcelain" or "git ls-files --others --exclude-standard") and
fail if any untracked entries are present so the script's clean-working-tree
condition is strict; modify the conditional that contains the git diff checks to
include this untracked-files test and ensure the same error message/exit path is
used when untracked files are found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2aa448e2-2a10-48a8-84fb-0ec5d004f346

📥 Commits

Reviewing files that changed from the base of the PR and between b2a23bf and 821fdd0.

📒 Files selected for processing (4)
  • .github/workflows/release-on-main.yml
  • README.md
  • docs/releasing.md
  • scripts/release.sh

Comment on lines +54 to +57
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: git working tree must be clean before release." >&2
exit 1
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

“Clean working tree” check misses untracked files.

Current checks only cover tracked/staged diffs. Untracked files still pass, which weakens the release precondition.

Suggested fix
-if ! git diff --quiet || ! git diff --cached --quiet; then
+if [[ -n "$(git status --porcelain)" ]]; then
   echo "Error: git working tree must be clean before release." >&2
   exit 1
 fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: git working tree must be clean before release." >&2
exit 1
fi
if [[ -n "$(git status --porcelain)" ]]; then
echo "Error: git working tree must be clean before release." >&2
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.sh` around lines 54 - 57, The release pre-check IF block only
tests tracked/staged changes using "git diff --quiet" and "git diff --cached
--quiet" and therefore misses untracked files; update the check to also detect
untracked files (e.g., run "git status --porcelain" or "git ls-files --others
--exclude-standard") and fail if any untracked entries are present so the
script's clean-working-tree condition is strict; modify the conditional that
contains the git diff checks to include this untracked-files test and ensure the
same error message/exit path is used when untracked files are found.

runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: action tags (@v4) are mutable — a tag can be moved to a different commit without notice. Pin to a full commit SHA to prevent supply-chain attacks:

Suggested change
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Same for actions/setup-node@v4 below.


CURRENT_VERSION="$(node -p "require('./package.json').version")"

if [[ "$TARGET_VERSION" == "$CURRENT_VERSION" ]]; then
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard only prevents a no-op re-release, but it won't catch a downgrade. If a maintainer accidentally puts v0.9.0 at the top of CHANGELOG while package.json is already at 1.0.0, the script would happily proceed to publish v0.9.0 to npm.

Consider adding a semver ordering check before this early-exit, e.g. using npx semver or a simple string comparison guard:

# After extracting TARGET_VERSION and CURRENT_VERSION:
if [[ "$TARGET_VERSION" == "$CURRENT_VERSION" ]]; then
  echo "No release needed: package.json already at v$CURRENT_VERSION."
  exit 0
fi

# Guard against accidental downgrades
if npx --yes semver --range ">$CURRENT_VERSION" "$TARGET_VERSION" >/dev/null 2>&1; then
  : # target is newer, proceed
else
  echo "Error: changelog version v$TARGET_VERSION is not newer than current v$CURRENT_VERSION." >&2
  exit 1
fi

exit 1
fi

CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script verifies the branch name but doesn't check whether local main is up-to-date with origin/main. If CI checks out the commit, runs tests, then a second push lands — the release commit could be pushed onto a stale base and conflict mid-flight.

Add a remote sync guard:

git fetch origin main --quiet
if ! git merge-base --is-ancestor origin/main HEAD; then
  echo "Error: local main is behind origin/main. Pull first." >&2
  exit 1
fi

echo "Skipping tests/build checks (SKIP_RELEASE_TESTS=true)."
fi

echo "Running: npx release-it ${RELEASE_IT_ARGS[*]}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer the locally installed binary over npx to ensure the exact version from package-lock.json is used and avoid any npx resolution surprises:

Suggested change
echo "Running: npx release-it ${RELEASE_IT_ARGS[*]}"
./node_modules/.bin/release-it "${RELEASE_IT_ARGS[@]}"

@@ -0,0 +1,20 @@
{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding "requireUpstream": true to the git section. Without it, release-it won't verify that the local branch tracks a remote and is up-to-date, which complements the clean-tree check but doesn't replace it.

Suggested change
{
{
"git": {
"requireCleanWorkingDir": true,
"requireUpstream": true,
"requireBranch": "main",

"@types/jest": "^29.0.0",
"@types/js-yaml": "^4.0.0",
"jest": "^29.0.0",
"release-it": "^17.11.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding release-it as a dev dependency pulls in ~250 transitive packages (octokit, puppeteer-adjacent browser-compat libs, etc.) as visible in the lockfile diff (+3,795 lines). This meaningfully inflates npm install times for contributors.

A lighter alternative is to keep release-it out of package.json entirely and rely on npx release-it@17 directly in scripts/release.sh for the CI/release path only, or install it globally in the release workflow step. That way regular contributor installs stay lean.

branches:
- main

permissions:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contents: write permission is correct for tagging and creating GitHub releases, but it's set at the workflow level, granting it to all jobs (currently just one). Consider using job-level permissions to keep the scope minimal and make the intent explicit:

Suggested change
permissions:
permissions:
contents: read
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write

@claude
Copy link

claude bot commented Mar 15, 2026

Review: Release Infrastructure

The overall approach is solid — changelog-driven releases via release-it, a wrapper shell script with good guards (clean tree, branch check, duplicate tag check), and a CI workflow that auto-publishes on merge to main. The [skip ci] commit message convention is correctly used to prevent the release commit from re-triggering the workflow.

Issues to address

Security

  • Action tags (actions/checkout@v4, actions/setup-node@v4) should be pinned to full commit SHAs. Mutable tags are a supply-chain risk — see inline comment on the workflow.
  • Workflow-level contents: write should be scoped to the job level to minimize blast radius — inline comment added.

Correctness / reliability

  • scripts/release.sh doesn't guard against accidental version downgrades — if the CHANGELOG top entry is an older version than package.json, the script proceeds happily. See inline on line 73.
  • No check that local main is in sync with origin/main before releasing — a stale branch could cause a mid-release push conflict (inline on line 59).
  • release-it should be called via ./node_modules/.bin/release-it rather than npx to guarantee the lockfile-pinned version is used (inline on line 132).
  • .release-it.json is missing "requireUpstream": true, which would have release-it itself enforce upstream sync as a second layer of defence (inline on .release-it.json).

Dependency footprint

  • Adding release-it to devDependencies pulls in ~250 transitive packages (+3,795 lines to package-lock.json). Since release is a maintainer-only operation, consider keeping it out of the project's install graph (e.g. npx release-it@17 in the workflow or a global install step). See inline on package.json.

What looks good

  • set -euo pipefail in the shell script — correct error handling.
  • Prerelease detection via *-* glob for auto-selecting the next dist-tag.
  • concurrency: cancel-in-progress: false — correct, queues rather than drops concurrent release runs.
  • Dual token exposure (NPM_TOKEN + NODE_AUTH_TOKEN) is the right pattern for actions/setup-node + manual publish.
  • docs/releasing.md is thorough and clearly describes the flow for future maintainers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
.github/workflows/release-on-main.yml (1)

8-9: Consider adding id-token: write for npm provenance.

If you plan to enable npm provenance (recommended for supply chain security), you'll need to add id-token: write permission and configure provenance: true in your npm publish settings. This is optional but increasingly a best practice.

Optional enhancement for npm provenance
 permissions:
   contents: write
+  id-token: write

Then in .release-it.json:

"npm": {
  "publish": true,
  "publishArgs": ["--provenance"]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-on-main.yml around lines 8 - 9, The workflow
currently only grants contents: write; to enable npm provenance add the
id-token: write permission in the permissions block (i.e., include id-token:
write alongside contents: write) and, if enabling provenance, update your
release tooling configuration (.release-it.json) to set npm.publish to true and
include the publishArgs ["--provenance"] (or set provenance: true where
applicable) so the GitHub Actions id token can be used for provenance-enabled
npm publishing.
scripts/release.sh (1)

78-81: Redundant changelog validation.

This check is redundant since TARGET_VERSION on line 65 was extracted from the same pattern. If the grep on line 65 succeeded, this check will always pass. However, it's harmless defensive coding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.sh` around lines 78 - 81, The redundant changelog validation
duplicates the earlier grep that extracted TARGET_VERSION; remove the repeated
block that checks for "## [v$TARGET_VERSION]" (the if ! grep -Fq ...
CHANGELOG.md; then ... fi) from scripts/release.sh to avoid unnecessary
duplication, or if you want to keep it as defensive coding replace it with a
short comment explaining why it's retained; locate the check referencing
TARGET_VERSION and grep -Fq and delete or comment it accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/release-on-main.yml:
- Around line 8-9: The workflow currently only grants contents: write; to enable
npm provenance add the id-token: write permission in the permissions block
(i.e., include id-token: write alongside contents: write) and, if enabling
provenance, update your release tooling configuration (.release-it.json) to set
npm.publish to true and include the publishArgs ["--provenance"] (or set
provenance: true where applicable) so the GitHub Actions id token can be used
for provenance-enabled npm publishing.

In `@scripts/release.sh`:
- Around line 78-81: The redundant changelog validation duplicates the earlier
grep that extracted TARGET_VERSION; remove the repeated block that checks for
"## [v$TARGET_VERSION]" (the if ! grep -Fq ... CHANGELOG.md; then ... fi) from
scripts/release.sh to avoid unnecessary duplication, or if you want to keep it
as defensive coding replace it with a short comment explaining why it's
retained; locate the check referencing TARGET_VERSION and grep -Fq and delete or
comment it accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf4dca74-675f-40fa-9aee-bffe79574abd

📥 Commits

Reviewing files that changed from the base of the PR and between 821fdd0 and 2402e7d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • .github/workflows/release-on-main.yml
  • .release-it.json
  • README.md
  • docs/releasing.md
  • package.json
  • scripts/release.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant