Skip to content

ci: move the npm release into GitHub Actions - #137

Open
venables wants to merge 3 commits into
mainfrom
ci/changesets-release-workflows
Open

ci: move the npm release into GitHub Actions#137
venables wants to merge 3 commits into
mainfrom
ci/changesets-release-workflows

Conversation

@venables

@venables venables commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Release workflow keeps a version-bump PR open on main
  • Publish workflow ships to npm on manual dispatch
  • Publish waits for approval on a new npm-publish environment
  • Pre-flight script lists package versions that npm does not have
  • Tags become annotated tag objects through the GitHub API
  • One GitHub release for each version, not for each package
  • bin/release calls the correct clean script and stops on error
  • RELEASING.md explains the flow and the one-time setup

Problem

The release ran only from a laptop. pnpm run publish:packages needed an
interactive npm session, and account 2FA is set to auth-and-writes. The last
release stopped halfway. Three packages went to npm and five did not. A second
run fixed it, but nothing checked the tree before the publish, and nothing
recorded who approved the release.

bin/release also called pnpm run Clean. The script is clean, so the command
failed with ERR_PNPM_NO_SCRIPT. The file had no set -e, so the release
continued and skipped the clean step.

Solution

The release now runs in two phases, as changesets intends.

release.yaml runs on each push to main. It applies changeset version and
opens a version-bump PR on the changeset-release/main branch. The PR uses the
GitHub App identity that audit-fix.yaml already uses, so the check workflow
runs on it. The job stops early when no changesets are pending.

publish.yaml is the button. You dispatch it from main after the version PR
merges. The verify job refuses a dispatch from any other branch, lists the
package versions that npm does not have, refuses to continue when that list is
empty, and then runs the full check suite and pnpm audit signatures. The
publish job needs an approval on the npm-publish environment before it writes
to the registry.

npm authentication uses a granular access token, not OIDC trusted publishing.
changeset publish calls pnpm publish in a pnpm workspace, and pnpm 11
supports neither trusted publishing nor --provenance. It supports only --otp.
OIDC would need the workflow to pack tarballs and publish them with npm
directly, which bypasses the changesets publish path. The token is written to
~/.npmrc as the literal text ${NPM_TOKEN}. npm and pnpm expand it when they
read the file, so the secret never goes to disk.

The publish job creates tags through the GitHub API instead of git push. No
push-capable git credential stays in the checkout. The tags are annotated tag
objects, which matches every tag this repository has published.

Testing

  • actionlint passes on all four workflow files.
  • pnpm run check:format and pnpm run knip pass.
  • node .github/scripts/unpublished-packages.mjs runs against the live registry.
    It correctly reports all eight packages at 0.11.0 as published, and writes
    count, packages, and version to GITHUB_OUTPUT.
  • Confirmed that pnpm expands ${NPM_TOKEN} from a user-level ~/.npmrc.
  • Confirmed that pnpm audit signatures works on this workspace.
  • Confirmed that the git/ref/tags/ API accepts scoped tag names such as
    @agentcommercekit/vc@0.10.1.

The workflows cannot run end to end until the one-time setup is complete. See
RELEASING.md:

  1. Create the npm-publish environment. Add required reviewers. Limit the
    deployment branches to main.
  2. Add NPM_TOKEN as an environment secret on npm-publish. It must be an
    automation-class granular token, because a token that prompts for a one-time
    password cannot work unattended.

AI usage

Claude Code (Opus 5) wrote the workflows, the pre-flight script, and the
documentation in this PR. I directed the design and reviewed each file.

Summary by CodeRabbit

  • New Features

    • Added automated release workflows for versioning, package validation, approval, publishing, tagging, and GitHub release creation.
    • Added detection and reporting for packages that have not yet been published.
  • Documentation

    • Documented the Changesets-based release process, publishing requirements, fallback procedures, and contribution expectations.
    • Improved the release command’s guidance and argument handling.
  • Bug Fixes

    • Corrected the release cleanup command and enabled stricter error handling.

venables and others added 3 commits August 4, 2026 18:35
Move the release off a laptop and into CI, in two phases:

- release.yaml keeps a version-bump PR open on main as changesets land,
  applying `changeset version` and opening the PR under the existing
  GitHub App identity so it triggers the check workflow.
- publish.yaml is the release button: dispatch from main, re-run the
  full check suite, then publish behind an approval on the `npm-publish`
  environment.

npm auth is a granular token rather than OIDC trusted publishing:
`changeset publish` shells out to `pnpm publish` in a pnpm workspace,
and pnpm 11 supports neither trusted publishing nor --provenance. The
token is expanded from the environment when npm reads ~/.npmrc, so it is
never written to disk.

Tags are created as annotated tag objects through the GitHub API, which
matches the tags published so far and avoids persisting a push-capable
git credential in the checkout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`pnpm run Clean` fails with ERR_PNPM_NO_SCRIPT -- the script is `clean`.
With no `set -e` the failure was swallowed and the release continued
without cleaning. Fix the name, add `set -eu`, and forward arguments so
a one-time password can be passed for account-level 2FA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add RELEASING.md covering the two-phase changesets flow, the one-time
NPM_TOKEN and environment setup, why OIDC trusted publishing is not
available on this toolchain, and the local fallback. Point AGENTS.md and
the contributing steps at it, and tell contributors to add a changeset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Release automation

Layer / File(s) Summary
Package publication status
.github/scripts/unpublished-packages.mjs
Discovers public workspace packages, checks npm versions, reports unpublished packages, and writes GitHub Actions outputs.
Changeset version PR
.github/workflows/release.yaml
Detects pending changesets, versions packages when needed, and opens or updates the version pull request.
Publish verification
.github/workflows/publish.yaml
Validates the repository and branch, rejects empty releases, runs checks and audits, and exposes release metadata.
Publishing and GitHub release
.github/workflows/publish.yaml
Publishes packages after environment approval, creates annotated tags and a GitHub release, and records a summary.
Release process guidance
AGENTS.md, CONTRIBUTING.md, RELEASING.md, bin/release
Documents Changesets, CI releases, required configuration, fallback publishing, and script argument forwarding.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: moving npm releases from a laptop-based process into GitHub Actions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/changesets-release-workflows

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/unpublished-packages.mjs:
- Around line 70-80: Update .github/scripts/unpublished-packages.mjs lines 70-80
to derive the release version from the unpublished package records, verify that
all records share one version, and fail when multiple versions are present; do
not use the umbrella package version. Update .github/workflows/publish.yaml
lines 208-234 to generate release metadata from this verified published-package
version, with no direct change required to other sites.

In @.github/workflows/publish.yaml:
- Around line 68-77: Update the publish workflow around the “Refuse if there is
nothing to publish” gate to record the expected release state before running
changeset publish. When unpublished count is zero, check whether the
corresponding tags and GitHub release exist; skip npm publication and continue
with repair when either is missing, and reject only when npm, tags, and the
GitHub release are all complete.
🪄 Autofix

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 Plus

Run ID: ead8de9c-1b3a-4214-a3e8-ee26edb1efd3

📥 Commits

Reviewing files that changed from the base of the PR and between 0b8fdaa and cf1c22f.

📒 Files selected for processing (7)
  • .github/scripts/unpublished-packages.mjs
  • .github/workflows/publish.yaml
  • .github/workflows/release.yaml
  • AGENTS.md
  • CONTRIBUTING.md
  • RELEASING.md
  • bin/release

Comment on lines +70 to +80
// Every package is version-locked by the `linked` group in
// .changeset/config.json, so the umbrella package's version names the release.
const umbrella = results.find((result) => result.name === "agentcommercekit")

if (process.env.GITHUB_OUTPUT) {
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`count=${unpublished.length}`,
`packages=${unpublished.map((result) => result.name).join(" ")}`,
`version=${umbrella?.version ?? ""}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

jq '{fixed, linked}' .changeset/config.json

fd -0 '^package\.json$' packages | while IFS= read -r -d '' manifest; do
  jq -r --arg manifest "$manifest" \
    '[$manifest, .name, .version] | `@tsv`' "$manifest"
done

Repository: agentcommercekit/ack

Length of output: 725


🌐 Web query:

For @changesets/cli2.31.0, does alinkedgroup publish every member on each release? Contrastlinkedandfixed package groups using version-specific documentation.

💡 Result:

For @changesets/cli 2.31.0, a linked group does not publish every member on each release [1][2]. The behavior of linked and fixed package groups is contrasted as follows: Linked Packages: The linked configuration option allows you to specify groups of packages that should share a version [3][1]. Packages within a linked group are only version-bumped and published if they have associated changesets (either direct changes or as a result of being a dependent) [1][2]. When a package in a linked group is released, it is versioned to the highest current version in that group plus the highest bump type from the changesets within the group [1][2]. There is no guarantee that all packages in the group will be published together; only those with active changesets are processed [1][2]. Fixed Packages: The fixed configuration option declares that packages must be version-bumped and published together, regardless of whether individual members have received changes [4][5]. Unlike linked packages, all packages in a fixed group are updated to the same version whenever a release is triggered for the group, ensuring that they remain in sync even if some members have no new changes [4][5]. In summary, linked packages synchronize versioning logic for packages that have changes, while fixed packages enforce a strict, all-or-nothing release requirement for the entire group [1][4].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== script outline =="
ast-grep outline .github/scripts/unpublished-packages.mjs --view compact || true

echo
echo "== relevant script lines =="
cat -n .github/scripts/unpublished-packages.mjs | sed -n '1,140p'

echo
echo "== workflow publish relevant lines =="
cat -n .github/workflows/publish.yaml | sed -n '180,250p'

echo
echo "== changeset files =="
git ls-files '.changeset/*' | sort | while IFS= read -r f; do
  echo "--- $f"
  cat -n "$f"
done

echo
echo "== package change logs/changelog mentions =="
rg -n "CHANGELOG|agentcommercekit|`@agentcommercekit`" -S packages .changeset 2>/dev/null | head -n 80

Repository: agentcommercekit/ack

Length of output: 16114


Use the version of the packages being published.

A Changesets linked group does not publish every member on each release. Packages in a linked group are versioned only when they have active changesets, so the agentcommercekit version can stay stale while a scoped package publishes a newer version.

  • .github/scripts/unpublished-packages.mjs#L70-L80: derive the release version from the unpublished package records and fail if one run contains multiple release versions. Use a fixed group if every package must always publish together.
  • .github/workflows/publish.yaml#L208-234: create release metadata from the verified published-package version, not the umbrella manifest version.
📍 Affects 2 files
  • .github/scripts/unpublished-packages.mjs#L70-L80 (this comment)
  • .github/workflows/publish.yaml#L208-L234
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/unpublished-packages.mjs around lines 70 - 80, Update
.github/scripts/unpublished-packages.mjs lines 70-80 to derive the release
version from the unpublished package records, verify that all records share one
version, and fail when multiple versions are present; do not use the umbrella
package version. Update .github/workflows/publish.yaml lines 208-234 to generate
release metadata from this verified published-package version, with no direct
change required to other sites.

Comment on lines +68 to +77
- name: Refuse if there is nothing to publish
env:
COUNT: ${{ steps.unpublished.outputs.count }}
run: |
set -euo pipefail
if [ "$COUNT" = "0" ]; then
echo "Refusing: every workspace package version is already on npm." >&2
echo "Merge the version PR from the Release workflow first." >&2
exit 1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Allow recovery after npm publication succeeds.

If changeset publish succeeds and a later tag or release call fails, a rerun sees zero unpublished versions after npm registry visibility catches up. This gate then exits before it can create the missing tags or GitHub release.

Track the expected release state before the npm write. If no npm publication remains but tags or the release are absent, skip changeset publish and repair the remote release state. Reject only when both npm and GitHub release state are complete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yaml around lines 68 - 77, Update the publish
workflow around the “Refuse if there is nothing to publish” gate to record the
expected release state before running changeset publish. When unpublished count
is zero, check whether the corresponding tags and GitHub release exist; skip npm
publication and continue with repair when either is missing, and reject only
when npm, tags, and the GitHub release are all complete.

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