Files
pnpm/.github/workflows/create-release-pr.yml
T
Zoltan Kochan 02f8807f56 fix(release): resume pacquet beta publish (#13483)
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.
2026-07-29 10:54:15 +02:00

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."