Add the Changesets flow for multi-package repositories - #38
Conversation
The tag-derived npm flow owns the single-package case and cannot be stretched further: it reads the bump from all commits since the last release with no way to attribute a commit to a package, so with two packages every change stream bumps both. Changesets scopes each change to the packages it names and cascades bumps through dependents — the one thing tags cannot recreate — so repositories crossing the two-package line get a flow shaped around it. npm-changesets.yml wraps changesets/action in the same posture as the rest of the family: SHA-pinned actions, the npm 12 pin, OIDC trusted publishing with a guard that checks what npm will actually authenticate with — NODE_AUTH_TOKEN and the generated npmrc, never org-scoped secrets — and provenance only when the source repository is public. The npmrc check allows setup-node's literal interpolation placeholder and sees through ini whitespace around the equals sign, both bugs caught in review of the guard's first in-repo incarnation. The caller is one push-to-main trigger with no event routing: merging the version pull request is the release act. First consumers: provider-devtool as it crosses the two-package line, xion.js when it migrates off its bespoke changesets workflow.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Adds a reusable Changesets-based npm publishing workflow for multi-package repositories.
Changes:
- Adds OIDC publishing with pinned actions and API-based release updates.
- Adds workflow and credential-guard tests.
- Documents the new workflow shape in
README.mdandAGENTS.md.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Review summary |
|---|---|
.github/workflows/npm-changesets.yml |
4 critical findings (4, 1, 1, and 1 votes) and 3 moderate findings (4, 4, and 3 votes): credential guards must cover NPM_TOKEN and all npm config layers, run before consumer commands, and isolate GITHUB_TOKEN; enforce non-cancelling concurrency; create draft version PRs; and reconcile API-based commits with repository invariants. |
tests/workflows.test.mjs |
Adds workflow and credential-guard test coverage. |
README.md |
Nit (2 votes): scope the existing tag-flow lifecycle documentation so it does not describe the Changesets flow incorrectly. |
AGENTS.md |
Updates workflow inventory and guidance. |
Suppressed comments (2)
AGENTS.md:46
- This entry documents a flow that creates a
changeset-release/*branch, version commits, and package tags through the GitHub API, while the repository invariant above still says workflows must never create commits or push branches (AGENTS.md:12-13). That leaves the documented contract and thegit-only test misleading; explicitly define and test the Changesets exception before onboarding consumers.
| `npm-changesets.yml` | consumer, on push to main | Changesets version PR, multi-package publish |
tests/workflows.test.mjs:501
- The new
-n "\$\{alternative matches any token interpolation on a line, not just the guard'sif [ -n ...:-} ]check. A futureecho -n "${NPM_TOKEN}"or export/use line would satisfy this assertion, allowing the no-token invariant to regress silently. Match the complete emptiness-check form instead of this substring.
/-n "\$\{|must not carry one|^\s*#|NODE_AUTH_TOKEN\\\}/,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The credential guard moves ahead of Install, matching npm-publish.yml: a credential it would reject must fail the job before any consumer lifecycle or build code has had it in scope. It now also rejects NPM_TOKEN in the environment — changesets/action treats that as an alternate credential — and scans both the generated user config and the repository's own .npmrc for every credential key npm accepts, _auth and _password alongside _authToken. The release job enforces its own per-repository, non-cancelling concurrency group instead of trusting the caller comment: concurrent runs force-update the changeset-release branch, and a run cancelled between publish and tagging leaves registry versions with nothing behind them. The write-back invariant in AGENTS.md now names this flow's GitHub-API version pull request as the one authorized exception, and the README scopes the candidate/dist-tag lifecycle to the tag-derived pair. The header documents the accepted GITHUB_TOKEN exposure in Changesets' version and publish subprocesses — inherent to the model, required by the changelog writers, and one more reason single-package repositories belong on the tag flow. All caught in review.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
README.md:175
README.md:170still claims workflows never create commits or push branches, but this new flow explicitly uses Changesets' GitHub API to create version-PR commits and release tags. Since this paragraph now routes users to the flow, update that statement to mention the documented exception; otherwise the README contradictsAGENTS.mdand the workflow contract.
Repositories publishing multiple interdependent packages use
`npm-changesets.yml`, which wraps Changesets — version pull request on merge,
.github/workflows/npm-changesets.yml:33
- The inner group only serializes jobs once a run reaches this workflow. A caller-level
concurrencywithcancel-in-progress: truecan still cancel the entire reusable-workflow run after publishing but before Changesets creates its tags, leaving registry versions without tags. Since this comment says adding caller concurrency is harmless, change the contract to forbid cancellation or requirecancel-in-progress: false.
# Serialization is enforced here — the release job carries a per-repository,
# non-cancelling concurrency group — so a caller needs no concurrency of its
# own, though adding one is harmless.
The guard's bare .npmrc resolved under the quality policy's working directory, but changesets/action runs the version and publish commands from the repository root — under a non-root quality policy a root .npmrc credential escaped the scan while npm would still load it. The workspace root is now scanned explicitly alongside the step's own directory. Caught in review.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
The test for the previous commit: a credentialed .npmrc at GITHUB_WORKSPACE fails the guard even when the step runs from a different working directory.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Suppressed comments (6)
Previously missed (1) — in code that hasn't changed since the last review.
README.md:178
- This adds a workflow that creates version-PR commits and tags through
commitMode: github-api, so the still-unmodified statement at README.md:170 that workflows never create commits or push branches is now false. Update that statement to document the Changesets API exception, matching the invariant already added to AGENTS.md.
Repositories publishing multiple interdependent packages use
`npm-changesets.yml`, which wraps Changesets — version pull request on merge,
publish when it lands — in the same trusted-publishing posture as the rest of
the family. The tag-derived flows below are for repositories publishing one
package.
.github/workflows/npm-changesets.yml:83
- This flow passes only
quality-policy-pathand never requests or validates an npm policy, so a consumer can run it without.github/npm-policy.jsonc, contrary to the repository contract in AGENTS.md:60-63 that npm flows have one. Either make the policy requirement explicit for this flow or narrow that contract to the tag-derived npm flows; otherwise this new path silently bypasses the documented policy validation.
quality:
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
with:
quality-policy-path: ${{ inputs.quality-policy-path }}
.github/workflows/npm-changesets.yml:168
- This match also treats commented-out
.npmrclines as credentials. For example,# //registry.npmjs.org/:_authToken=exampleis selected by the first grep and is not removed by the second, so npm ignores it but every release run fails. Anchor the match after optional whitespace and exclude#/;comments (or parse the file) before rejecting the config.
if grep -E '(_authToken|_auth|_password)[[:space:]]*=' "$npmrc" |
grep -qvE '_authToken[[:space:]]*=[[:space:]]*(\$\{NODE_AUTH_TOKEN\})?[[:space:]]*$'; then
.github/workflows/npm-changesets.yml:81
- This new entry point pins
required-quality.ymltod169eff..., while every existing internal workflow reference in this repository (for examplenpm-main.yml:11) uses46ae18...for the samev1.4.0release. PerAGENTS.md:638-643, internal pins must move in lockstep; otherwise this flow silently runs a different quality-workflow revision than the other entry points. Use the existing released SHA here, or make a coordinated pin advance.
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
AGENTS.md:222
- The new two-shape section is followed by an unqualified “Each push to main produces
v<next>-rc...” section at AGENTS.md:558-568, which is false for this flow: a Changesets push creates or updates a version PR, and merging that PR publishes packages. Scope the versioning section to the tag-derived flows or add the Changesets lifecycle so onboarding docs do not describe the wrong release act.
- **Multiple interdependent packages** — `npm-changesets.yml`. The tag flow
cannot attribute a commit to a package, so with two packages every change
stream would bump both; Changesets scopes each change to the packages it
names and cascades bumps through dependents. The caller is a single
push-to-main trigger with no routing: merging the version pull request is
the release act, and the same run publishes.
AGENTS.md:222
- The new caller guidance omits the
pull-requests: writepermission required by this flow's release job (npm-changesets.yml:96-102). A caller following the existing npm example with onlycontentsandid-tokenwill reach the action but cannot create or update the version PR; document the required permission set here (or add a dedicated Changesets caller example).
names and cascades bumps through dependents. The caller is a single
push-to-main trigger with no routing: merging the version pull request is
the release act, and the same run publishes.
The second grep loses -q: under the default shell's pipefail, -q exiting at the first hit can SIGPIPE the producer and turn the pipeline's status into 141, which the if reads as no credential — consuming the full stream keeps the exit codes honest. And tokenHelper joins the scanned keys: it hands npm a token through a helper binary, which is a credential path like any other. Both caught in review, both covered by fixtures.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Suppressed comments (6)
Previously missed (2) — in code that hasn't changed since the last review.
AGENTS.md:221
- The new flow is described here, but the consumer-wiring section below still documents only the tag-derived npm entrypoints. It does not show the required single push-to-main caller,
contents: write/pull-requests: write/id-token: writepermissions, or the version/publish inputs, and the blanket npm-policy prerequisite is not reconciled with this workflow. Add a Changesets caller example and clarify that contract in this full reference.
- **Multiple interdependent packages** — `npm-changesets.yml`. The tag flow
cannot attribute a commit to a package, so with two packages every change
stream would bump both; Changesets scopes each change to the packages it
names and cascades bumps through dependents. The caller is a single
push-to-main trigger with no routing: merging the version pull request is
README.md:178
- This new flow uses
commitMode: github-apito create the version-PR commit and release tags, so the existing sentence immediately above this section saying that workflows never create commits or push branches is now false. AGENTS.md qualifies the exception, but the README still presents the opposite contract; please qualify it here too.
Repositories publishing multiple interdependent packages use
`npm-changesets.yml`, which wraps Changesets — version pull request on merge,
publish when it lands — in the same trusted-publishing posture as the rest of
the family. The tag-derived flows below are for repositories publishing one
package.
.github/workflows/npm-changesets.yml:168
- The credential scan matches raw lines without excluding
.npmrccomments. A normal file containing# //registry.npmjs.org/:_authToken=${NPM_TOKEN}is therefore rejected even though npm ignores it, despite this guard intending to check effective credentials. Anchor the match after optional whitespace and exclude#/;comment lines.
# pipeline's status into 141, which reads as "no credential".
# Consuming the full stream keeps the exit codes honest.
.github/workflows/npm-changesets.yml:83
- This entrypoint invokes
required-quality.ymlwithoutnpm-required: true.loadPoliciestherefore treats.github/npm-policy.jsoncas optional, so a Changesets caller can omit the npm policy and still publish, bypassing the documented contract for npm flows and validation such asaccess: public(AGENTS.md:60-64). Forwardnpm-required: truehere.
quality-policy-path: ${{ inputs.quality-policy-path }}
.github/workflows/npm-changesets.yml:81
- This new flow pins
required-quality.ymltod169eff..., while every existing entry point pins the releasedv1.4.0revision46ae18b...(for example,npm-main.yml:11andcloudflare-main.yml:30). Because internal workflow pins must stay aligned, consumers using this entry point would silently run a different quality revision under the same release label. Align this with the canonicalv1.4.0SHA, or advance all dependent pins together.
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
AGENTS.md:18
- This hunk qualifies the API write-back exception, but
README.md:170still says “Workflows in this repository never create commits or push branches.” Sincenpm-changesets.ymldeliberately creates the version branch/commits through the GitHub API, that unchanged statement now contradicts the documented contract. Update the global sentence to distinguish git-CLI writes from the authorized Changesets API write-back.
exception to write-back: `npm-changesets.yml` maintains its version pull
request and release tags through the GitHub API (`commitMode: github-api`)
— that write-back is Changesets' entire contract and the reason the flow
exists, and API commits are signed by GitHub, which branch protection
wants. Nothing may extend this exception to the git CLI.
The credential guard moves ahead of the global npm pin: npm reads the generated user config while downloading itself, so a present credential would be expanded and sent before the job failed. Nothing before the guard now invokes npm at all. changesets/action resolves the workspace, the changeset files, and the CLI at the repository root, so a quality policy with a nested workingDirectory would have the quality gates and the release job silently disagreeing about what they are releasing. That invocation is rejected up front rather than accommodated. Both caught in review.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
The Validate invocation step now leads the job, so the checkout is no longer steps[0]. Locate it by its uses reference.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Suppressed comments (5)
Previously missed (1) — in code that hasn't changed since the last review.
.github/workflows/npm-changesets.yml:83
- This is an npm flow, but the call to
required-qualityforwards only the quality policy and leavesnpm-requiredfalse. A consumer can therefore omit.github/npm-policy.jsonc, so the centralized npm constraints (including public access) are not validated, contrary toAGENTS.md:60-66; this also prevents app-scoped npm policy paths. Expose/forward annpm-policy-pathand require the npm policy here (and document the caller contract).
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
with:
quality-policy-path: ${{ inputs.quality-policy-path }}
.github/workflows/npm-changesets.yml:194
changesets/actionpublishes packages one at a time, so a failure can leave an earlier subset permanently published while later packages remain unavailable. This reusable workflow ends at the action and exposes neither a post-publish hook nor the action's published-package output, so callers cannot run the dependency-train verification used by the existing xion.js Changesets workflow. Add a post-publish validation hook/output (including a path that runs after action failure), or document that consumers must retain such a gate before switching to this flow.
# `prepublishOnly`, so a broken build fails the job with its own error
# instead of surfacing as a publish failure halfway through a
# multi-package publish.
- name: Build
run: ${{ fromJSON(needs.quality.outputs.quality-policy).commands.build }}
.github/workflows/npm-changesets.yml:191
changesets/actionresolves its optionalcwdinput to the process working directory when omitted, so this action step runs from the repository root. The shell steps above instead use the policy'sworkingDirectory; with a valid policy such asworkingDirectory: packages/foo, install/build run in the workspace but Changesets looks for.changeset, package manifests, and the version/publish scripts at the root, so the release flow cannot operate on that workspace. Pass the policy directory through the action'scwdinput (or explicitly reject non-root policies).
- name: Install
run: ${{ fromJSON(needs.quality.outputs.quality-policy).commands.install }}
# Build before publishing rather than leaning on per-package
# `prepublishOnly`, so a broken build fails the job with its own error
# instead of surfacing as a publish failure halfway through a
.github/workflows/npm-changesets.yml:147
- The guard only checks
ACTIONS_ID_TOKEN_REQUEST_URL, butchangesets/actiontreats OIDC as available only when bothACTIONS_ID_TOKEN_REQUEST_URLandACTIONS_ID_TOKEN_REQUEST_TOKENare set. If the URL is present while the request token is missing, this step passes and the action falls through to its no-OIDC path, so publishing fails later instead of reporting the missing caller permission here. Check both request variables (and cover the missing-token case).
exit 1
fi
if [ -n "${NODE_AUTH_TOKEN:-}" ] || [ -n "${NPM_TOKEN:-}" ]; then
echo "::error::An npm token is present. This flow publishes with trusted publishing and must not carry one."
README.md:177
- The new Changesets flow makes the unconditional README statement at line 170 that workflows never create commits or push branches false:
commitMode: github-apicreates the version-PR commit/branch and release tags. The AGENTS guidance now documents this exception, so update the README invariant to prohibit the git CLI while explicitly naming this API write-back exception.
Repositories publishing multiple interdependent packages use
`npm-changesets.yml`, which wraps Changesets — version pull request on merge,
publish when it lands — in the same trusted-publishing posture as the rest of
the family. The tag-derived flows below are for repositories publishing one
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
Previously missed (2) — in code that hasn't changed since the last review.
.github/workflows/npm-changesets.yml:81
- This new workflow pins
required-quality.ymltod169eff38ec93b6405f15a2b2dd86b4bcda21bcb, while every other internal caller in this repository (for examplenpm-pr.yml:11andnpm-main.yml:11) pins46ae18b0f5ff1735e07601e41a6044b358379853for the samev1.4.0comment. Under the lockstep-pin rule, this flow can silently run a different policy implementation despite the identical release metadata. Please use the repository-wide released SHA or make a coordinated pin/release update for all references.
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
README.md:178
- The new paragraph routes consumers to a workflow that intentionally writes version-PR commits and tags through the GitHub API, but the preceding README statement still says workflows in this repository never create commits or push branches. That leaves the public documentation contradicting both this flow and the clarified invariant in
AGENTS.md:12-18; qualify the statement as Git-CLI-only or document the Changesets API exception here.
Repositories publishing multiple interdependent packages use
`npm-changesets.yml`, which wraps Changesets — version pull request on merge,
publish when it lands — in the same trusted-publishing posture as the rest of
the family. The tag-derived flows below are for repositories publishing one
package.
.github/workflows/npm-changesets.yml:170
- The guard still misses npm's client-certificate authentication path:
certfileandkeyfilein.npmrcsupply a client certificate/key to registry requests, but neither key matches this grep. A repository or caller with those settings can therefore publish outside OIDC while this check passes. Include both keys in the scan (and cover the rejection with a fixture) before relying on the guard's claim that every npm credential path is rejected.
if grep -E '(_authToken|_auth|_password|tokenHelper)[[:space:]]*=' "$npmrc" |
grep -vE '_authToken[[:space:]]*=[[:space:]]*(\$\{NODE_AUTH_TOKEN\})?[[:space:]]*$' > /dev/null; then
.github/workflows/npm-changesets.yml:58
- These defaults are not part of a standard Changesets workspace: the action supplies
changeset versiononly whenversionis omitted, and a publish input must invoke Changesets itself. A consumer with the usual@changesets/clisetup but without bespokeversion:packages/publish:packagesscripts will fail when this flow first updates the version PR and again when it publishes. Default to local Changesets commands, or make these inputs required and document the script contract.
default: npm run version:packages
.github/workflows/npm-changesets.yml:196
changesets/action@v1.9.0defaultscreateGithubReleasesto true, so this step publishes a GitHub Release for every package tag. In a consumer that also uses the documentedcloudflare-release.ymltrigger, those events carry tags such as@scope/pkg@1.2.3, which fail that workflow's strictv...tag guard (or may trigger another release handler unexpectedly). Make release creation configurable/disabled for this flow, or document and filter this cross-flow behavior.
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
AGENTS.md:222
- Unlike the tag-derived
npm-main/npm-releaseflows, this workflow accepts onlyquality-policy-pathand callsrequired-qualitywithoutnpm-required, so it never needs or loadsnpm-policy.jsonc. The policy-files guidance above still says every npm flow requires that file, which gives Changesets consumers contradictory onboarding instructions. Carve this flow out of that requirement and document its actual caller inputs/permissions.
- **Multiple interdependent packages** — `npm-changesets.yml`. The tag flow
cannot attribute a commit to a package, so with two packages every change
stream would bump both; Changesets scopes each change to the packages it
names and cascades bumps through dependents. The caller is a single
push-to-main trigger with no routing: merging the version pull request is
the release act, and the same run publishes.
…ract The concurrency group serializes runs but does not order them: an older push whose quality job finished late can enter the group after a newer run and force-update the version pull request backwards. A freshness check before the Changesets step makes such a run stand down in favor of the newer commit's run. certfile and keyfile join the credential scan — npm authenticates a registry with client certificates as readily as with tokens — with a fixture. The header now states the partial-publish recovery contract: changeset publish skips versions that already exist, so the fix for a publish that failed partway is always a re-run, never registry surgery. All caught in review.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Suppressed comments (4)
.github/workflows/npm-changesets.yml:91
- Unlike the other npm entry points, this call only requests the quality policy.
required-qualityvalidates the npm policy only when the file exists, so a consumer can invoke this new npm flow without one, contradicting the repository contract inAGENTS.md:60-64. Either add an npm policy path and passnpm-required: true, or scope that contract/documentation to the tag-derived flows.
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
with:
quality-policy-path: ${{ inputs.quality-policy-path }}
.github/workflows/npm-changesets.yml:223
- The pinned Changesets action only creates tags/releases for packages whose publish output contains
New tag:. If a run is interrupted afterchangeset publishhas published a package but before the action's tag loop, a retry sees those versions as already published, emits noNew tag:lines, and cannot create the missing tags/releases. The retry contract documented above therefore cannot repair this failure state; add an always-run reconciliation/tagging step or another recovery mechanism that derives tags from the published package versions.
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
.github/workflows/npm-changesets.yml:103
cancel-in-progress: falsehere only serializes runs that reach this reusable job; a caller's workflow-level concurrency can still cancel the entire run. Thus the header's claim that adding caller concurrency is harmless is false—xion.js's existing Changesets caller usescancel-in-progress: true, so a later push can kill this job betweenpublishand Changesets' tag loop. Require callers to omit cancellation (and document it) or provide recovery that handles cancellation.
group: npm-changesets-${{ github.repository }}
cancel-in-progress: false
AGENTS.md:222
- The npm wiring section still provides only the tag-flow caller template, which grants
contentsandid-tokenbut notpull-requests: write. This newly documented Changesets shape needs that permission to maintain its version PR (and also has script/trusted-publisher inputs), so a consumer copying the only template will fail at release-PR creation. Add a dedicated Changesets caller example with the complete permission and setup contract.
- **Multiple interdependent packages** — `npm-changesets.yml`. The tag flow
cannot attribute a commit to a package, so with two packages every change
stream would bump both; Changesets scopes each change to the packages it
names and cascades bumps through dependents. The caller is a single
push-to-main trigger with no routing: merging the version pull request is
the release act, and the same run publishes.
A branch name is caller-controlled text; interpolating it into the freshness check's script would let shell syntax in the name execute. It now reaches the script through the environment, like every other untrusted value. The README's no-commits statement also gains the same Changesets exception AGENTS.md's invariant carries, so the two do not contradict. Caught in review.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (9)
Previously missed (1) — in code that hasn't changed since the last review.
.github/workflows/npm-changesets.yml:118
validateQualityPolicyaccepts any non-emptyworkingDirectory, so a valid root spelling such as./still runs at the repository root but fails this raw string comparison before checkout. Normalize the policy path (or otherwise accept root-equivalent paths) so this flow does not reject otherwise valid root policies.
if: fromJSON(needs.quality.outputs.quality-policy).workingDirectory != '.'
.github/workflows/npm-changesets.yml:220
- The freshness check and the Changesets API write are separate operations. If a new push advances
mainafter thisgh apicall but beforechangesets/actionruns, this run still seesstale=false; the action'scommitChangesFromRepouses the run SHA withforce: true, so it can overwritechangeset-release/mainfrom the older commit and drop the newer changesets. This needs an atomic compare-and-swap/ref update or another mechanism that prevents the write after the head changes.
head="$(gh api "repos/${{ github.repository }}/git/ref/heads/$REF_NAME" --jq .object.sha)"
.github/workflows/npm-changesets.yml:229
- A rerun cannot always repair the failure described by this publish flow. If npm accepts a package but the process stops before the action creates its GitHub tag, the next
changeset publishplan excludes that already-published version and emits noNew tag:line;changesets/action@v1.9.0only callspushTagfor packages parsed from those lines. The package can therefore remain published without its expected tag/release. Add an independent tag/release reconciliation path, or narrow the recovery contract instead of promising that a rerun is always sufficient.
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
.github/workflows/npm-changesets.yml:178
- The guard still omits
usernamefrom the npm credential keys it scans. npm's publish path treatscreds.usernameas sufficient auth state, so a registry-scoped.npmrcentry such as//registry.npmjs.org/:username=...can authenticate while this check passes, despite the comment claiming all credential keys are covered. Includeusernamein the pattern and add a fixture for it.
if grep -E '(_authToken|_auth|_password|tokenHelper|certfile|keyfile)[[:space:]]*=' "$npmrc" |
grep -vE '_authToken[[:space:]]*=[[:space:]]*(\$\{NODE_AUTH_TOKEN\})?[[:space:]]*$' > /dev/null; then
.github/workflows/npm-changesets.yml:89
- This is the only internal workflow reference pinned to
d169eff...; every otherrequired-quality.ymlcall here uses46ae18... # v1.4.0. The PR says this flow should use the released v1.4.0 revision, and AGENTS.md:638-650 requires internal pins to stay in lockstep, so this can run a different/stale quality workflow. Use the established released SHA, or update all references and release metadata together.
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
.github/workflows/npm-changesets.yml:177
- The first grep scans commented lines as if npm would parse them. A normal
.npmrccontaining# //registry.npmjs.org/:_authToken=${NPM_TOKEN}(or a;comment) will therefore fail this guard even though npm ignores it; the test also explicitly treats comments as allowed token-name occurrences. Ignore full-line#/;comments before checking credential keys.
if grep -E '(_authToken|_auth|_password|tokenHelper|certfile|keyfile)[[:space:]]*=' "$npmrc" |
.github/workflows/npm-changesets.yml:202
- This build runs in the same checkout immediately before the Changesets API commit. In
github-apimode the action adds the current working-tree changes (the equivalent ofgit add .) to the version-PR commit, so a valid consumer build that writes tracked or otherwise addable artifacts can put those outputs in the release PR and change the files later published. Run the build in an isolated checkout or clean/restore its side effects before invoking Changesets.
- name: Build
run: ${{ fromJSON(needs.quality.outputs.quality-policy).commands.build }}
.github/workflows/npm-changesets.yml:150
- The guard treats OIDC as available when only
ACTIONS_ID_TOKEN_REQUEST_URLis set, butchangesets/actionuses trusted publishing only when bothACTIONS_ID_TOKEN_REQUEST_TOKENandACTIONS_ID_TOKEN_REQUEST_URLare present; otherwise it falls through to assuming npm is already authenticated. Check both variables and add a fixture with the URL set but the request token unset so a partial OIDC setup fails before publish.
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
AGENTS.md:222
- This section classifies
npm-changesets.ymlas an npm flow, but the policy contract atAGENTS.md:60-64still requires.github/npm-policy.jsoncfor every npm flow; this workflow has no npm-policy input and does not askrequired-qualityto validate one. Either wire that policy through or explicitly document the Changesets exception in the new section and README so consumers do not get contradictory setup requirements.
- **Multiple interdependent packages** — `npm-changesets.yml`. The tag flow
cannot attribute a commit to a package, so with two packages every change
stream would bump both; Changesets scopes each change to the packages it
names and cascades bumps through dependents. The caller is a single
push-to-main trigger with no routing: merging the version pull request is
the release act, and the same run publishes.
Permissions are job-scoped, so consumer install, build, and prepublishOnly code runs in the job holding id-token: write — here and in npm-publish.yml alike. A tokenless build job would not close this: changeset publish still runs prepublishOnly under OIDC, and a dependency able to exfiltrate the token could as easily poison the artifact a split job would hand over. The lockfile and the gates in front of it are the boundary that holds, and the token mints only the repository's own publish identity. Raised in review; documented as accepted rather than restructured around.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (9)
Previously missed (2) — in code that hasn't changed since the last review.
.github/workflows/npm-changesets.yml:101
- This npm flow is listed as an npm entry point, but the quality call neither requires nor passes an npm policy.
required-qualitycan therefore succeed without.github/npm-policy.jsonc, and this flow never enforces theaccess: publicand other npm-policy contract that AGENTS.md:60-64 requires for npm flows. Wire in a Changesets-compatible policy or explicitly revise that invariant instead of silently bypassing it.
quality:
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
with:
quality-policy-path: ${{ inputs.quality-policy-path }}
.github/workflows/npm-changesets.yml:244
- Unlike
npm-publish.yml, this path does not enforce the repository'saccess: publiccontract:changeset publishtakes access from Changesets config (defaultrestricted) or each package'spublishConfig, and this workflow never validates either. A scoped package can therefore publish successfully as restricted instead of public. Make public access an explicit validated requirement for this flow rather than relying on each consumer's Changesets configuration.
version: ${{ inputs.version-command }}
publish: ${{ inputs.publish-command }}
.github/workflows/npm-changesets.yml:128
validateQualityPolicyonly requires a non-empty workingDirectory, so a value such as./is valid and resolves to the repository root in required-quality. Comparing the raw value to exactly.rejects that valid root policy before checkout even though the Changesets workspace is still at the root. Compare a canonicalized path (or canonicalize the policy output) rather than requiring one spelling.
if: fromJSON(needs.quality.outputs.quality-policy).workingDirectory != '.'
.github/workflows/npm-changesets.yml:212
- The build runs before
changesets/actioningithub-apimode. That mode commits every tracked and non-ignored worktree change relative to the run SHA, so any consumer build that writes a tracked dist/generated file will include that artifact in the version pull request. Run the action from a clean checkout or otherwise isolate/reset build outputs before it commits the version changes.
- name: Build
run: ${{ fromJSON(needs.quality.outputs.quality-policy).commands.build }}
.github/workflows/npm-changesets.yml:247
commitMode: github-apiis not fail-closed for release tags in the pinnedchangesets/actionv1.9.0: itsGit.pushTag()catches everycreateReferror, logs only a warning, and continues. With the action's defaultcreateGithubReleases: true, a successful npm publish can therefore finish without a tag (for example, when the package has no changelog and there is no release-creation fallback), while this job still succeeds. Add verification/failure handling for every expected tag or use an action path that propagates tag errors.
# Commit and tag over the API rather than the git CLI, which has no
# credentials now that checkout does not persist them.
commitMode: github-api
.github/workflows/npm-changesets.yml:230
- The head lookup and the Changesets API update are separate operations. If a new push lands after
gh apisucceeds but before the next step starts, the pinned action still force-commitschangeset-release/<branch>from this run'sgithub.context.sha, so this supposedly fresh run can overwrite a newer changeset branch. Make the API update conditional on the current head, or serialize/refuse the entire release operation rather than relying on this non-atomic check.
head="$(gh api "repos/${{ github.repository }}/git/ref/heads/$REF_NAME" --jq .object.sha)"
.github/workflows/npm-changesets.yml:99
- This pin is inconsistent with the rest of the workflow family: every other internal
required-quality.ymlcall here uses46ae18b0f5ff1735e07601e41a6044b358379853while this line usesd169eff38ec93b6405f15a2b2dd86b4bcda21bcb, yet both are labeledv1.4.0. Consumers adopting this flow would therefore run a different central quality revision under the same release metadata. Align it with the established v1.4.0 SHA, or update all related pins and release metadata together.
uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0
README.md:173
- The README still understates the API write-back exception: this flow also creates package release tags through the GitHub API, as documented by the workflow and AGENTS.md:15, not only the version pull request. Please mention the release tags here so the user-facing invariant matches the implementation.
write-back exception is the Changesets flow's version pull request,
maintained through the GitHub API — see the invariant in AGENTS.md.
tests/workflows.test.mjs:501
- The exception
-n "\$\{"is too broad: a prohibited workflow line such asrun: if [ -n "${NPM_TOKEN:-}" ]; then npm publish; fiwould satisfy this assertion, so the security test can pass while a token is used. Narrow the allowlist to the exact guard form (or parse and inspect only the credential-guard step) instead of accepting any line containing that substring.
/-n "\$\{|must not carry one|^\s*#|NODE_AUTH_TOKEN\\\}/,
Two layers with a hard boundary: this repository as the platform layer owning all logic, conditionals, and credential handling; consumer repositories as declaration layers holding only policy files and thin trigger callers. The routing rules that were settled while migrating provider-devtool — one package on the tag flow, two or more on Changesets, the load-bearing caller filename, sanctioned bespoke with a documented gap list, and the two-repository bar for schema growth — now live where the next repository onboards by reading instead of asking. The single multiplexed ci.yml is rejected in writing, with the reasons.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
2xburnt has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (7)
Previously missed (1) — in code that hasn't changed since the last review.
tests/workflows.test.mjs:501
- The
-n "${allowlist alternative is too broad: it matches any-n "${...}on a line, not just theNODE_AUTH_TOKEN/NPM_TOKENemptiness checks. A future workflow line could combine a benign variable check with a real$NPM_TOKENuse and this test would pass, weakening the tokenless invariant. Match the two exact guard forms instead.
/-n "\$\{|must not carry one|^\s*#|NODE_AUTH_TOKEN\\\}/,
.github/workflows/npm-changesets.yml:212
- The
Buildstep mutates the same checkout that the API-mode Changesets action uses to create the version commit.changesets/actionemulatesgit add .from itscwdwhen pushing that commit, so a consumer build that writes tracked or non-ignored generated files will include those artifacts in the version pull request. Run the verification build in an isolated checkout, or clean/reset generated changes before invoking Changesets.
- name: Build
run: ${{ fromJSON(needs.quality.outputs.quality-policy).commands.build }}
.github/workflows/npm-changesets.yml:239
changesets/actiondefaultscreateGithubReleasesto true; ingithub-apimode its publish path creates the package tag and a GitHub Release for each package it detects. That side effect is not in this flow's caller contract, which documents a push-to-main/version-PR lifecycle, and can emitreleaseevents that invoke a consumer's other release workflows (for example, the standard Cloudflare release caller) forpkg@versiontags. Decide this explicitly: provide a tag-only strategy or guard/document the generated Release events before adopting the flow.
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
.github/workflows/npm-changesets.yml:33
- A caller-level concurrency group with
cancel-in-progress: truecan still cancel the entire called workflow, despite this job-level group being non-cancelling. That cancellation can happen after some packages have been published but before Changesets writes the API commit/tags, so saying that adding caller concurrency is harmless is unsafe. Explicitly require caller groups to usecancel-in-progress: false(or forbid them for this flow).
# Serialization is enforced here — the release job carries a per-repository,
# non-cancelling concurrency group — so a caller needs no concurrency of its
# own, though adding one is harmless.
.github/workflows/npm-changesets.yml:81
- These inputs expose repository-specific release commands and PR metadata in the caller workflow, while the new declaration-layer contract says configuration belongs in validated policy files rather than inline in trigger workflows. This flow has no Changesets policy input or validation, so consumers must either put customization in the trigger or diverge from that contract. Add a policy-backed contract, or make the commands fixed and document the required package-level setup.
version-command:
description: Command Changesets runs to apply pending changesets
required: false
default: npm run version:packages
type: string
publish-command:
description: Command Changesets runs to publish the packages
required: false
default: npm run publish:packages
AGENTS.md:37
- The new routing rule sends multi-package consumers to this workflow, but the npm wiring section remains tag-flow-only and its example omits the required
pull-requests: writepermission for the Changesets version PR. A consumer following the new rule can copy the documented permissions and then fail when the action creates or updates the release PR. Add a Changesets caller template and its permission requirements, and clarify whether.github/npm-policy.jsoncapplies to this flow.
1. A repository publishing **one** npm package uses the tag-derived flow
(`npm-main.yml` / `npm-release.yml`). **Two or more** interdependent
packages use `npm-changesets.yml`. See "Two npm flow shapes" below for
README.md:183
npm-main.ymlandnpm-release.ymlare the tag-derived pair, but neither runs on pull requests or performs the dry run;npm-pr.ymlowns that behavior. This sentence attributes the PR dry run to the pair and can mislead consumers about which workflow must be configured.
The tag-derived pair performs a package dry run on pull requests, publishes
| quality-policy-path: | ||
| description: Repository-relative quality policy JSONC path | ||
| required: false | ||
| default: .github/quality-policy.jsonc | ||
| type: string |
There was a problem hiding this comment.
💡 Codex Review
When pending changesets exist, this invokes changesets/action to create the version pull request without any draft handling; the pinned action's declared input contract exposes no draft option, so the PR is opened ready for review. Add a subsequent API step that marks a newly created version PR as draft (while preserving that state on updates), otherwise every consumer of this flow violates the repository's required PR posture.
AGENTS.md reference: AGENTS.md:L81-L81
github-workflows/.github/workflows/npm-changesets.yml
Lines 187 to 188 in c52eeb4
When a consumer has a commented-out auth example such as ; //registry.npmjs.org/:_authToken=npm_example in either scanned .npmrc, the first grep still treats it as a live credential and the second grep does not exclude it, so this workflow fails before installation even though npm would ignore the line. npm's .npmrc documentation specifies that lines beginning with ; or # are comments; exclude those lines before applying the credential checks.
When this action creates or updates the Changesets version PR using the repository GITHUB_TOKEN, GitHub suppresses the resulting pull_request workflows; GitHub's workflow-trigger documentation states that events caused by GITHUB_TOKEN do not start new workflow runs except for workflow_dispatch and repository_dispatch. Consequently the organization-required quality workflow never reports on this PR, leaving it blocked by required checks or allowing generated version changes to merge without the intended gates; use a GitHub App token or another supported mechanism whose PR events trigger CI.
AGENTS.md reference: AGENTS.md:L583-L590
github-workflows/.github/workflows/npm-changesets.yml
Lines 73 to 77 in c52eeb4
When a consumer's scripts differ from these defaults, the documented integration requires placing executable version-command and publish-command strings in the thin caller's with: block. That makes repository-specific release logic inline, bypasses JSONC schema validation, and breaks the platform/declaration boundary; use fixed script names or add the shared configuration to a validated policy instead of exposing raw command inputs.
AGENTS.md reference: AGENTS.md:L17-L23
ℹ️ 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".
Adds
npm-changesets.yml, the second npm flow shape: for repositories publishing multiple interdependent packages from one workspace. First consumers: provider-devtool (a second published package lands there within the week, which crosses the line the tag flow cannot cross) and xion.js (already on changesets, via a softer bespoke workflow).Why a second shape
The tag-derived flow reads the bump from all commits since the last release, with no way to attribute a commit to a package — with two packages, every change stream bumps both. Changesets scopes each change to the packages it names and cascades bumps through dependents; that is the one thing tags cannot recreate, so it gets its own flow rather than a reimplementation.
What the wrapper adds over bespoke changesets workflows
Same posture as the rest of the family: SHA-pinned actions, the npm 12 pin,
persist-credentials: falsewithcommitMode: github-api, quality gates via required-quality, OIDC trusted publishing with a token guard, and provenance only when the source repository is public. The guard checks what npm will actually authenticate with —NODE_AUTH_TOKENand the generated.npmrc— never org-scoped secrets (the failure mode that broke provider-devtool's first release), allows setup-node's literal${NODE_AUTH_TOKEN}placeholder, and sees through ini whitespace around=(both bugs caught in review of the guard's first in-repo incarnation, provider-devtool#81).Callers are a single push-to-main trigger with no event routing — merging the version pull request is the release act — unlike the tag flow's one-file/two-trigger shape that npm's one-trusted-publisher-workflow-per-package rule forces.
Verification
pnpm test: 81 pass, including an executable test running the credential guard through bash against eight.npmrcfixtures (placeholder, empty, literal token, whitespace-around-=bypass, env token, missing OIDC)pnpm lint,actionlint: cleanAddendum: the org CI shape, written down
AGENTS.md now opens with The shape: the two-layer architecture (this repository as the platform layer owning all logic and credentials; consumer repositories as declaration layers holding only policy files and thin trigger callers with no
if:conditions) and the routing rules — one package → tag flow, two+ → Changesets, the load-bearing caller filename, sanctioned-bespoke-with-gap-list for repositories the standard cannot serve yet, and the two-repository bar for policy schema growth. The single multiplexed ci.yml is rejected in writing, with reasons (per-event permission unioning, loss ofon:filters, check identity, concurrency namespacing).