The first pnpm 12 beta release used pnpm 12.0.0-alpha.21 to publish a wrapper whose workspace name is `pacquet`. That version predates `publishConfig.name`, so npm trusted publishing was attempted for `pacquet` instead of `pnpm` and the root package failed after all native packages had already been published. Use pnpm 11.18.0, the released TypeScript CLI that supports `publishConfig.name`, for release tooling. Update every `pnpm/setup` consumer to the revision that can install v11 from GitHub release archives. Make the Rust publishing loop query each effective published name and skip versions already on npm, while preserving hard failures for registry errors other than 404. This allows a moved beta tag to resume the partial release and reach the dependent GitHub release job.
236 lines
10 KiB
YAML
236 lines
10 KiB
YAML
name: Create Release PR
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
target:
|
|
description: Branch to release (the PR base; e.g. main or release/11.1).
|
|
default: main
|
|
required: true
|
|
# Each product is a separate checkbox, so a release can cover any
|
|
# combination. Only the checked products' pending changesets are consumed;
|
|
# the rest stay in the ledger for a later release. This lets a rapid v12
|
|
# (Rust) cadence release without dragging the TypeScript CLI (v11) along —
|
|
# and with it the near-daily "Update available!" notification. Leaving all
|
|
# three checked reproduces the historical "release everything pending" run.
|
|
pnpm11:
|
|
description: Release pnpm v11 (the TypeScript CLI) and the rest of the default lane.
|
|
type: boolean
|
|
default: true
|
|
pnpm:
|
|
description: Release pnpm (the Rust CLI, v12 alpha prerelease) and its @pnpm/napi addon.
|
|
type: boolean
|
|
default: true
|
|
pnpr:
|
|
description: Release pnpr (the registry server).
|
|
type: boolean
|
|
default: true
|
|
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
|
|
# Serialize per target so two dispatches for the same branch can't race on the
|
|
# force-pushed release-pr/<target> branch.
|
|
concurrency:
|
|
group: create-release-pr-${{ github.event.inputs.target }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
create-release-pr:
|
|
if: github.repository == 'pnpm/pnpm' # Only run on the main repository, not forks
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
env:
|
|
# The husky hooks are a developer safety net; CI has its own gates. Without
|
|
# this, `pnpm install` wires the hooks and the pre-push hook runs the full
|
|
# TS compile/lint plus the Rust clippy/doc sweep during "Commit and push"
|
|
# (the shallow clone and URL remote make its change detection always
|
|
# conclude that Rust sources changed).
|
|
HUSKY: 0
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
# Don't persist the write-scoped token in .git/config; the install below
|
|
# runs third-party lifecycle scripts. Pushing is done with an explicit,
|
|
# single-use remote URL in the "Commit and push" step.
|
|
persist-credentials: false
|
|
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@6523ce966bcf7a1061ce6401e5c6e0b60466bd31
|
|
with:
|
|
runtime: node@26.3.0
|
|
|
|
# Base the release on the tip of the target branch, fetched explicitly so the
|
|
# run is correct even when dispatched from another ref. A force-push to
|
|
# release-pr/<target> reuses an already-open release PR for the same target
|
|
# rather than opening a second.
|
|
- name: Prepare release branch
|
|
env:
|
|
TARGET: ${{ github.event.inputs.target }}
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
git fetch origin "$TARGET"
|
|
git checkout -B "release-pr/$TARGET" FETCH_HEAD
|
|
|
|
# Refresh the npm registry signing keys embedded in
|
|
# @pnpm/deps.security.signatures from what npm advertises. pnpm verifies
|
|
# package-manager binaries (pacquet, the version-switch pnpm) against these
|
|
# keys, so a stale set could break verification after a key rotation. Any
|
|
# drift is committed below along with the version bumps, so the new trust
|
|
# roots are reviewed as part of the release PR diff.
|
|
# The embedded trust roots ship in the TypeScript CLI (v11) release, so they
|
|
# are only refreshed when pnpm11 is one of the products being released. A run
|
|
# that releases only the Rust products leaves v11 alone; any key drift is
|
|
# picked up by the next release that includes pnpm11.
|
|
- name: Update embedded npm signing keys
|
|
if: github.event.inputs.pnpm11 == 'true'
|
|
run: node pnpm11/deps/security/signatures/scripts/update-npm-signing-keys.mjs --update
|
|
|
|
# Refresh the embedded Node.js release keys (used to verify the signature of
|
|
# a downloaded runtime's SHASUMS256.txt) from the canonical
|
|
# nodejs/release-keys list, so a new release signer cannot break Node.js
|
|
# runtime verification. Reviewed in the release PR diff like the npm keys.
|
|
- name: Update embedded Node.js release keys
|
|
if: github.event.inputs.pnpm11 == 'true'
|
|
run: node pnpm11/crypto/shasums-file/scripts/update-node-release-keys.mjs --update
|
|
|
|
# A refreshed trust root must show up in the changelogs, so synthesize a
|
|
# changeset for whichever key sets drifted before `pnpm bump` consumes the
|
|
# pending changesets. The `drifted` output drives an explicit review signal
|
|
# on the PR, so a trust-root change cannot hide in a large version-bump diff.
|
|
- name: Add changesets for refreshed keys
|
|
id: keys
|
|
if: github.event.inputs.pnpm11 == 'true'
|
|
run: |
|
|
drifted=""
|
|
if ! git diff --quiet -- pnpm11/deps/security/signatures/src/npmSigningKeys.ts; then
|
|
drifted="npm registry signing keys"
|
|
cat > .changeset/release-refresh-npm-signing-keys.md <<'EOF'
|
|
---
|
|
"@pnpm/deps.security.signatures": patch
|
|
"pnpm": patch
|
|
---
|
|
|
|
Updated the embedded npm registry signing keys to the set currently advertised by npm.
|
|
EOF
|
|
fi
|
|
if ! git diff --quiet -- pnpm11/crypto/shasums-file/src/nodeReleaseKeys.ts; then
|
|
drifted="${drifted:+$drifted and }Node.js release keys"
|
|
cat > .changeset/release-refresh-node-release-keys.md <<'EOF'
|
|
---
|
|
"@pnpm/crypto.shasums-file": patch
|
|
"pnpm": patch
|
|
---
|
|
|
|
Updated the embedded Node.js release keys to the current canonical `nodejs/release-keys` list.
|
|
EOF
|
|
fi
|
|
echo "drifted=$drifted" >> "$GITHUB_OUTPUT"
|
|
|
|
# Consumes the pending changesets: bumps versions, writes changelogs, updates
|
|
# the ledger, and syncs manifests. Only the checked products' intents are
|
|
# consumed; the rest stay in the ledger for a later release. A no-op (nothing
|
|
# pending in scope) leaves the tree clean and the steps below skip.
|
|
- name: Bump versions
|
|
env:
|
|
RELEASE_PNPM11: ${{ github.event.inputs.pnpm11 }}
|
|
RELEASE_PNPM: ${{ github.event.inputs.pnpm }}
|
|
RELEASE_PNPR: ${{ github.event.inputs.pnpr }}
|
|
run: |
|
|
args=()
|
|
[ "$RELEASE_PNPM11" = "true" ] && args+=(--release pnpm11)
|
|
[ "$RELEASE_PNPM" = "true" ] && args+=(--release pnpm)
|
|
[ "$RELEASE_PNPR" = "true" ] && args+=(--release pnpr)
|
|
if [ ${#args[@]} -eq 0 ]; then
|
|
echo "Select at least one product to release." >&2
|
|
exit 1
|
|
fi
|
|
pnpm run bump -- "${args[@]}"
|
|
|
|
- name: Check for changes
|
|
id: changes
|
|
run: |
|
|
if [ -z "$(git status --porcelain)" ]; then
|
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# One release PR covers every product; the commit subject lists the ones
|
|
# whose version actually changed against the target branch, e.g.
|
|
# "chore(release): 11.13.0, pacquet 12.0.0-alpha.9, pnpr 0.2.0". When only
|
|
# the TypeScript CLI bumped, the subject stays the historical bare version.
|
|
# `@pnpm/napi` is not listed separately: it is a versioning.fixed group with
|
|
# pacquet, so it always carries pacquet's version.
|
|
- name: Compose release summary
|
|
id: version
|
|
if: steps.changes.outputs.changed == 'true'
|
|
run: |
|
|
summary=""
|
|
add() {
|
|
local label="$1" manifest="$2" new old
|
|
new=$(jq -r .version "$manifest")
|
|
old=$(git show "FETCH_HEAD:$manifest" 2>/dev/null | jq -r .version || echo "")
|
|
if [ "$new" != "$old" ]; then
|
|
summary="${summary:+$summary, }${label:+$label }$new"
|
|
fi
|
|
}
|
|
add "" pnpm11/pnpm/package.json
|
|
add "pacquet" pnpm/npm/pnpm/package.json
|
|
add "pnpr" pnpr/npm/pnpr/package.json
|
|
# Changes without a version bump (e.g. refreshed trust-root changesets
|
|
# consumed into changelogs only) still need a commit subject.
|
|
if [ -z "$summary" ]; then
|
|
summary=$(jq -r .version pnpm11/pnpm/package.json)
|
|
fi
|
|
echo "version=$summary" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Commit and push
|
|
if: steps.changes.outputs.changed == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.UPDATE_LOCKFILE_TOKEN }}
|
|
TARGET: ${{ github.event.inputs.target }}
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
git add -A
|
|
git commit -m "chore(release): ${VERSION}"
|
|
git push -f "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "release-pr/$TARGET"
|
|
|
|
- name: Create PR if needed
|
|
if: steps.changes.outputs.changed == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.UPDATE_LOCKFILE_TOKEN }}
|
|
TARGET: ${{ github.event.inputs.target }}
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
BRANCH="release-pr/$TARGET"
|
|
|
|
# An already-open PR now points at the freshly force-pushed branch, so
|
|
# there is nothing more to do.
|
|
if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
|
|
echo "PR already exists; the new versions were force-pushed to it"
|
|
else
|
|
gh pr create \
|
|
--title "chore(release): ${VERSION}" \
|
|
--body "Automated release PR created by the create-release-pr workflow.
|
|
|
|
Releasing \`${TARGET}\`: ${VERSION}. Merging this PR consumes the pending changesets and records them in the committed \`.changeset/ledger.yaml\`, then auto-tags each released product (tag-release.yml) to run the release workflow." \
|
|
--base "$TARGET" \
|
|
--head "$BRANCH"
|
|
fi
|
|
|
|
# Embedded trust roots changed in this release, so leave a hard-to-miss
|
|
# comment: the refreshed keys need deliberate review, not a scroll-past
|
|
# among the version bumps and changelogs.
|
|
- name: Flag refreshed trust roots on the PR
|
|
if: steps.changes.outputs.changed == 'true' && steps.keys.outputs.drifted != ''
|
|
env:
|
|
GH_TOKEN: ${{ secrets.UPDATE_LOCKFILE_TOKEN }}
|
|
TARGET: ${{ github.event.inputs.target }}
|
|
DRIFTED: ${{ steps.keys.outputs.drifted }}
|
|
run: |
|
|
PR=$(gh pr list --head "release-pr/$TARGET" --state open --json number --jq '.[0].number')
|
|
gh pr comment "$PR" --body "⚠️ This release refreshes embedded trust roots: **${DRIFTED}**. Review those key diffs deliberately before merging — they gate signature verification of Node.js runtimes and package-manager binaries."
|