-
Notifications
You must be signed in to change notification settings - Fork 0
Add the Changesets flow for multi-package repositories #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
2xburnt
merged 11 commits into
main
from
work/2xburnt/npm-changesets-flow-20260821T175709Z
Aug 21, 2026
Merged
Changes from 3 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
ab5a932
Add the Changesets flow for multi-package repositories
2xburnt 6863360
Harden the Changesets flow per review
2xburnt 415376a
Scan the workspace root npmrc from any working directory
2xburnt f552c11
Exercise the workspace-root npmrc scan
2xburnt c0dbf82
Close two credential-scan gaps
2xburnt a7c52ec
Guard before npm runs, and reject nested working directories
2xburnt e375cce
Find the checkout step by action, not position
2xburnt 3b0c93d
Stand down stale runs, scan certificate auth, state the recovery cont…
2xburnt ed4f9fd
Keep the ref name out of the script source
2xburnt cded444
Name the OIDC exposure alongside the token one
2xburnt c52eeb4
Write the shape down
2xburnt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| # The Changesets-shaped npm flow, for repositories that publish MULTIPLE | ||
| # interdependent packages from one workspace. | ||
| # | ||
| # The tag-derived flow (npm-main.yml / npm-release.yml) owns the single-package | ||
| # case and cannot be stretched to this one: 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 — | ||
| # that is the one thing it does that cannot be recreated with tags, and the | ||
| # reason both flows exist. One published package: use the tag flow. Two or | ||
| # more interdependent ones: use this. | ||
| # | ||
| # Versioning is Changesets' own: merges accumulate changeset files, the flow | ||
| # maintains a version pull request, and merging that pull request is the | ||
| # release act — this same flow then publishes the new versions to npm. The | ||
| # publish authenticates with GitHub OIDC trusted publishing; there is no | ||
| # token anywhere, and a guard fails the run if one appears. Provenance is | ||
| # requested only when the source repository is public, because the registry | ||
| # refuses it from private repositories (E422) rather than degrading. | ||
| # | ||
| # What a caller provides: | ||
| # - permissions: contents: write, pull-requests: write (the version pull | ||
| # request), id-token: write (trusted publishing) | ||
| # - the trusted publisher on npmjs.com for EVERY published package pointing | ||
| # at the CALLER's workflow file; OIDC identifies the top-level workflow, | ||
| # and npm allows one workflow per package, so all publishes must run from | ||
| # that one file | ||
| # - version-command / publish-command when its scripts differ from the | ||
| # defaults; both run from the repository root | ||
| # | ||
| # 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. | ||
| # | ||
| # Known, accepted exposure: changesets/action hands its environment — | ||
| # GITHUB_TOKEN included — to the version and publish commands, which run | ||
| # consumer code. That is inherent to the Changesets model (the changelog | ||
| # writers need the token to read pull-request metadata) and identical to the | ||
| # bespoke changesets workflows this replaces; the quality gates, the | ||
| # lockfile, and npm 12's install-time script denial are the compensating | ||
| # controls. The tag-derived flow does not carry the token into consumer | ||
| # commands, which is one more reason single-package repositories belong | ||
| # there. | ||
|
|
||
| name: Burnt npm Changesets | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| quality-policy-path: | ||
| description: Repository-relative quality policy JSONC path | ||
| required: false | ||
| default: .github/quality-policy.jsonc | ||
| type: string | ||
|
Comment on lines
+68
to
+72
|
||
| 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 | ||
| type: string | ||
| pr-title: | ||
| description: Title of the version pull request | ||
| required: false | ||
| default: "chore(release): version packages 🦋" | ||
| type: string | ||
| pr-commit: | ||
| description: Commit message of the version pull request | ||
| required: false | ||
| default: "chore: update versions" | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| quality: | ||
| uses: burnt-labs/github-workflows/.github/workflows/required-quality.yml@d169eff38ec93b6405f15a2b2dd86b4bcda21bcb # v1.4.0 | ||
| with: | ||
| quality-policy-path: ${{ inputs.quality-policy-path }} | ||
|
|
||
| release: | ||
| name: Version or publish | ||
| needs: quality | ||
| runs-on: ubicloud-standard-4 | ||
| concurrency: | ||
| # Enforced here rather than trusted to the caller: two concurrent runs | ||
| # both force-update Changesets' changeset-release branch, and NOT | ||
| # cancel-in-progress because a run cancelled between publishing and | ||
| # tagging leaves versions on the registry with nothing behind them. | ||
| group: npm-changesets-${{ github.repository }} | ||
| cancel-in-progress: false | ||
|
2xburnt marked this conversation as resolved.
|
||
| permissions: | ||
| # The version pull request, and the tags and releases Changesets | ||
| # creates after publishing. | ||
| contents: write | ||
| pull-requests: write | ||
| # npm trusted publishing (OIDC). | ||
| id-token: write | ||
|
2xburnt marked this conversation as resolved.
|
||
| defaults: | ||
| run: | ||
| working-directory: ${{ fromJSON(needs.quality.outputs.quality-policy).workingDirectory }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| # Changesets reads history and tags to decide what is unpublished. | ||
| fetch-depth: 0 | ||
| # Do not leave the job's token in `.git/config`. The install and | ||
| # build run package code before anything publishes, so a persisted | ||
| # credential is reachable by the dependency tree. `commitMode: | ||
| # github-api` below authenticates through GITHUB_TOKEN instead, and | ||
| # API commits are signed by GitHub, which branch protection tends | ||
| # to want. | ||
| persist-credentials: false | ||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: lts/* | ||
| registry-url: https://registry.npmjs.org | ||
| - run: corepack enable | ||
| - name: Pin the npm CLI | ||
| # See "npm install-time security" in AGENTS.md. npm 12 is the floor | ||
| # for both halves of this flow: it enforces the install-time | ||
| # defaults, and it is well past the 11.5.1 that trusted publishing | ||
| # needs — Changesets shells out to `npm publish`, whichever package | ||
| # manager installed the workspace. | ||
| run: | | ||
| npm install --global npm@12 | ||
|
2xburnt marked this conversation as resolved.
Outdated
|
||
| if [ "$(npm --version | cut -d. -f1)" != "12" ]; then | ||
| echo "::error::Expected npm 12 on PATH, found $(npm --version). Something is shadowing the pinned CLI." | ||
| exit 1 | ||
| fi | ||
| # Before Install, matching npm-publish.yml: a credential this guard | ||
| # would reject must fail the job before any consumer lifecycle or build | ||
| # code has had it in scope. This checks what npm will actually | ||
| # authenticate with, NOT whether a token exists somewhere in secrets — | ||
| # reading org-level secrets into the environment to assert they are | ||
| # empty reports the organization's configuration, not this job's, and | ||
| # fails repositories that never handed npm anything. | ||
| - name: Verify trusted-publishing credentials | ||
| run: | | ||
| if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then | ||
| echo "::error::No OIDC token available. The calling workflow must grant id-token: write." | ||
| 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." | ||
| exit 1 | ||
| fi | ||
| # Both the generated user config and the repository's own .npmrc, | ||
| # and every credential key npm accepts — `_auth` and `_password` | ||
| # authenticate a registry just as `_authToken` does. setup-node | ||
| # writes an `_authToken` line holding the LITERAL text of the | ||
| # interpolation placeholder — the action does not expand it, npm | ||
| # does, at read time (actions/setup-node, src/authutil.ts) — so the | ||
| # placeholder is expected content and must not be read as a | ||
| # credential. npm parses ini-style and trims whitespace around `=`, | ||
| # so the patterns allow it too: `_authToken = <token>` | ||
| # authenticates just as well. | ||
| # Both the repository root — where the action runs the version and | ||
| # publish commands — and this step's own working directory, which a | ||
| # policy may point elsewhere. Scanning a file twice is harmless. | ||
| for npmrc in "${NPM_CONFIG_USERCONFIG:-$HOME/.npmrc}" "${GITHUB_WORKSPACE:-.}/.npmrc" .npmrc; do | ||
| [ -f "$npmrc" ] || continue | ||
| if grep -E '(_authToken|_auth|_password)[[:space:]]*=' "$npmrc" | | ||
|
2xburnt marked this conversation as resolved.
Outdated
|
||
| grep -qvE '_authToken[[:space:]]*=[[:space:]]*(\$\{NODE_AUTH_TOKEN\})?[[:space:]]*$'; then | ||
|
2xburnt marked this conversation as resolved.
Outdated
2xburnt marked this conversation as resolved.
Outdated
|
||
| echo "::error::$npmrc carries a registry credential. This flow publishes with trusted publishing and must not carry one." | ||
| exit 1 | ||
| fi | ||
| done | ||
| - 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 | ||
| # multi-package publish. | ||
| - name: Build | ||
| run: ${{ fromJSON(needs.quality.outputs.quality-policy).commands.build }} | ||
| - name: Create release pull request or publish to npm | ||
| uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 | ||
|
2xburnt marked this conversation as resolved.
|
||
| with: | ||
| title: ${{ inputs.pr-title }} | ||
| commit: ${{ inputs.pr-commit }} | ||
| version: ${{ inputs.version-command }} | ||
| publish: ${{ inputs.publish-command }} | ||
|
2xburnt marked this conversation as resolved.
|
||
| # 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 | ||
|
2xburnt marked this conversation as resolved.
2xburnt marked this conversation as resolved.
|
||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
2xburnt marked this conversation as resolved.
|
||
| # The registry refuses provenance from private source repositories | ||
| # (E422) rather than publishing without the attestation, so the | ||
| # flag follows visibility: a private repository publishes through | ||
| # the same OIDC path without attesting, and starts attesting the | ||
| # moment it goes public, with no workflow change. | ||
| NPM_CONFIG_PROVENANCE: ${{ github.event.repository.private == false }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.