Commit the pnpm-managed Cargo source block. The block is a function of Cargo.lock, so an install regenerates it byte for byte and the tracked `.cargo/config.toml` stops reporting as modified after every install. A new Rust CI step fails if the committed block and the lockfile drift apart. Cargo now resolves every crate through `.pnpm/crates` and falls back to its own registry nowhere, so each job that runs cargo needs an install behind it. Eight did not have one: both cargo-unused jobs, the micro-benchmark, the integrated benchmark's build and executor jobs, and the three release builds. The new `install-crates` action gives them one and narrows the JavaScript half with `--filter pacquet`, which a filter does not do to the Cargo half. The release builds go through `cross`, which mounts the checkout at `/project`. pnpm links each crate into `.pnpm/crates` with a relative symlink, so a store outside the checkout stops resolving under that mount. Those jobs install into a store inside the checkout instead, on Linux, the one host where cross containerizes the build at all. The two workflows that commit no longer discard the block. It is tracked content now, and a run that changes it should carry the change. Claude-Session: https://claude.ai/code/session_01Gg6uVUzLw1MniCLC81TQjP Claude-Session: https://claude.ai/code/session_01HwJS1pAz9HHQpJWEJAiaUu Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
241 lines
11 KiB
YAML
241 lines
11 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: Initialize Rust before dependency installation
|
|
uses: $/.github/actions/rustup
|
|
with:
|
|
restore-cache: false
|
|
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0
|
|
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\`. Afterwards a maintainer pushes a signed tag per released product to run the release workflow — see RELEASING.md." \
|
|
--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."
|