272 lines
12 KiB
YAML
272 lines
12 KiB
YAML
name: Tag
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: Version
|
|
required: true
|
|
tag:
|
|
description: Tag (use latest-<major> when tagging an older release line)
|
|
default: latest
|
|
required: true
|
|
skip_upgrade_check:
|
|
description: Skip the upgrade check (use only when the release line's current version is itself broken)
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions: {}
|
|
|
|
env:
|
|
# Passed to each command as `--registry`, which is the only form pnpm reads;
|
|
# it ignores `npm_config_registry`.
|
|
REGISTRY: https://registry.npmjs.org/
|
|
# Keeps a runner-level .npmrc from redirecting any of this.
|
|
npm_config_userconfig: /dev/null
|
|
# The pnpm that operates this workflow, never the version being released:
|
|
# validate and verify-upgrade decide what the release is measured against, and
|
|
# tag-in-registry holds the publish token. Bump deliberately, to a version that
|
|
# has already proven itself as `latest`.
|
|
RELEASE_TOOL_PNPM_VERSION: 11.13.1
|
|
|
|
jobs:
|
|
validate:
|
|
name: Validate ${{ github.event.inputs.version }} and ${{ github.event.inputs.tag }}
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
permissions: {}
|
|
steps:
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@84cb39b217b10273981911c288cd62326dc7c6d2 # v2.0.2
|
|
with:
|
|
version: ${{ env.RELEASE_TOOL_PNPM_VERSION }}
|
|
runtime: node@26.5.0
|
|
install: false
|
|
token: ''
|
|
# The latest-<major> tag is derived from the version instead of being
|
|
# hardcoded per release branch, so dispatching this workflow from the wrong
|
|
# branch cannot point a newer line's tag at an older release
|
|
# (https://github.com/pnpm/pnpm/issues/12906).
|
|
- name: Validate version and tag
|
|
env:
|
|
VERSION: ${{ github.event.inputs.version }}
|
|
TAG: ${{ github.event.inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Stable only: every dispatch moves latest-<major> whatever `tag` says,
|
|
# so a prerelease cannot be tagged here without reaching everyone.
|
|
if ! printf '%s\n' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
echo "::error::Version must be an exact, stable semver version like 11.2.0 (prereleases cannot be tagged). Got: ${VERSION}"
|
|
exit 1
|
|
fi
|
|
MAJOR="${VERSION%%.*}"
|
|
case "$TAG" in
|
|
latest-*)
|
|
if [ "$TAG" != "latest-${MAJOR}" ]; then
|
|
echo "::error::Tag ${TAG} does not match the major version of ${VERSION}. Expected latest-${MAJOR}."
|
|
exit 1
|
|
fi
|
|
;;
|
|
latest)
|
|
CURRENT_LATEST_MAJOR="$(pn view pnpm dist-tags.latest --registry="$REGISTRY" | cut -d . -f 1)"
|
|
# A dist-tag that could not be read must not pass the guard: an
|
|
# empty value makes the comparison below error out with a non-zero
|
|
# status, which `if` swallows, silently skipping the check.
|
|
if [ -z "$CURRENT_LATEST_MAJOR" ]; then
|
|
echo "::error::Could not read the current latest version of pnpm from the registry."
|
|
exit 1
|
|
fi
|
|
if [ "$MAJOR" -lt "$CURRENT_LATEST_MAJOR" ]; then
|
|
echo "::error::Refusing to move the latest tag back from v${CURRENT_LATEST_MAJOR} to ${VERSION}. Use latest-${MAJOR} to tag an older release line."
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
# Upgrading onto a version reads its published manifest and platform packages,
|
|
# so a release that is only broken as an upgrade target fails here and nowhere
|
|
# earlier — and the dist-tag move below is the last point at which it can still
|
|
# be held back.
|
|
#
|
|
# This job executes the release, so it holds no secrets: a lifecycle script
|
|
# could otherwise reach a later step through $GITHUB_PATH/$GITHUB_ENV and take
|
|
# the publish token with it.
|
|
verify-upgrade:
|
|
name: Verify upgrading onto ${{ github.event.inputs.version }}
|
|
needs: validate
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
permissions: {}
|
|
steps:
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@84cb39b217b10273981911c288cd62326dc7c6d2 # v2.0.2
|
|
with:
|
|
version: ${{ env.RELEASE_TOOL_PNPM_VERSION }}
|
|
runtime: node@26.5.0
|
|
install: false
|
|
token: ''
|
|
- name: Verify upgrading onto the new version
|
|
if: ${{ !inputs.skip_upgrade_check }}
|
|
env:
|
|
VERSION: ${{ github.event.inputs.version }}
|
|
run: |
|
|
set -eu
|
|
# pnpm enforces the devEngines of whatever manifest it finds by walking
|
|
# up from the working directory, so every command here runs from a
|
|
# scratch dir rather than the caller's cwd.
|
|
cd "$(mktemp -d)"
|
|
|
|
MAJOR="${VERSION%%.*}"
|
|
# Upgrade from what users on this release line are running today. A new
|
|
# major has no latest-<major> yet, so fall back to the current latest.
|
|
if ! FROM=$(pn view "pnpm@latest-${MAJOR}" version --registry="$REGISTRY" 2>/dev/null) || [ -z "$FROM" ]; then
|
|
FROM=$(pn view pnpm dist-tags.latest --registry="$REGISTRY")
|
|
fi
|
|
echo "Upgrading from v${FROM} to v${VERSION}"
|
|
|
|
# Both wrappers are exercised: self-update reinstalls the package it is
|
|
# running from, so `pnpm` and `@pnpm/exe` resolve different published
|
|
# manifests and a broken one surfaces on only that wrapper.
|
|
for package in pnpm @pnpm/exe; do
|
|
work=$(mktemp -d)
|
|
# Isolate the whole pnpm environment so the upgrade resolves and
|
|
# installs for real instead of reusing a warm store or global dir.
|
|
export PNPM_HOME="$work/home"
|
|
export XDG_DATA_HOME="$work/data"
|
|
export XDG_CACHE_HOME="$work/cache"
|
|
|
|
cd "$work"
|
|
mkdir -p "$work/from"
|
|
printf '{"name":"pnpm-upgrade-check","version":"0.0.0","private":true}\n' >"$work/from/package.json"
|
|
# `--allow-build` is what makes this install the starting point rather
|
|
# than a fake one: pnpm blocks dependency build scripts by default, and
|
|
# `@pnpm/exe`'s setup.js is what swaps its placeholder bin for the real
|
|
# native. Without it every run reproduces the exact breakage this gate
|
|
# exists to catch (the placeholder surviving) and blames the release.
|
|
pn add "${package}@${FROM}" \
|
|
--dir "$work/from" \
|
|
--allow-build=@pnpm/exe \
|
|
--registry="$REGISTRY"
|
|
# Drive through the bin shim rather than a path into the package: the
|
|
# entry point differs by major (pnpm.cjs in v10, pnpm.mjs in v11) and
|
|
# between the two wrappers, and the shim is what a user gets.
|
|
from_bin="$work/from/node_modules/.bin/pnpm"
|
|
|
|
# Check the starting point separately so a broken current release
|
|
# fails naming its own version rather than looking like a fault in
|
|
# $VERSION. When the current release is the broken one, it also blocks
|
|
# its own fix — dispatch with skip_upgrade_check to promote that.
|
|
test "$("$from_bin" --version)" = "$FROM"
|
|
|
|
"$from_bin" self-update "$VERSION"
|
|
|
|
# self-update installs the target using the layout of the version
|
|
# doing the installing: v11 links global bins into $PNPM_HOME/bin,
|
|
# v10 links them directly into $PNPM_HOME.
|
|
if [ -x "$PNPM_HOME/bin/pnpm" ]; then
|
|
upgraded="$PNPM_HOME/bin/pnpm"
|
|
else
|
|
upgraded="$PNPM_HOME/pnpm"
|
|
fi
|
|
# Run the upgraded binary rather than trust self-update's exit code:
|
|
# `@pnpm/exe`'s preinstall leaves its placeholder bin in place when the
|
|
# platform package ships no native, so that failure is invisible until
|
|
# the binary is actually invoked.
|
|
test "$("$upgraded" --version)" = "$VERSION"
|
|
|
|
# Hand the rest of the gate to the release itself, so what is gated is
|
|
# what ships rather than a reproduction of it maintained here. The
|
|
# command postdates the older lines this workflow can still tag, so
|
|
# skip it where it does not exist rather than fail a v10 promotion on
|
|
# a v11 command.
|
|
if "$upgraded" doctor --help >/dev/null 2>&1; then
|
|
"$upgraded" doctor --offline
|
|
else
|
|
echo "SKIP: ${package}@${VERSION} predates \`pnpm doctor\`; ran --version only"
|
|
fi
|
|
echo "OK: ${package}@${FROM} self-updated to v${VERSION}"
|
|
done
|
|
|
|
# Tagging with the promoted version reads as free verification and must not be
|
|
# done: this job is deciding whether that artifact is fit to ship, and pnpm is
|
|
# published by OIDC rather than by this token, so handing it the token turns a
|
|
# compromised dependency into org-wide publish access. verify-upgrade runs the
|
|
# release with no secrets so this job never has to.
|
|
tag-in-registry:
|
|
name: Tagging ${{ github.event.inputs.version }} as ${{ github.event.inputs.tag }}
|
|
needs: verify-upgrade
|
|
environment: release
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
steps:
|
|
- uses: garnet-org/action@3d47f4a9004f7356c980a0e8d420ef5984750e3c # v2.2.0
|
|
with:
|
|
api_token: ${{ secrets.GARNET_API_TOKEN }}
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@84cb39b217b10273981911c288cd62326dc7c6d2 # v2.0.2
|
|
with:
|
|
version: ${{ env.RELEASE_TOOL_PNPM_VERSION }}
|
|
runtime: node@26.5.0
|
|
install: false
|
|
token: ''
|
|
- name: Update tag
|
|
env:
|
|
# Written to pnpm's config below, not passed as
|
|
# `npm_config_//registry.npmjs.org/:_authToken`, which pnpm ignores.
|
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
VERSION: ${{ github.event.inputs.version }}
|
|
TAG: ${{ github.event.inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
trap 'pn config delete "//registry.npmjs.org/:_authToken" || true' EXIT
|
|
pn config set "//registry.npmjs.org/:_authToken" "${NPM_TOKEN}"
|
|
|
|
MAJOR="${VERSION%%.*}"
|
|
pn dist-tag add "pnpm@${VERSION}" "latest-${MAJOR}" --registry="$REGISTRY"
|
|
pn dist-tag add "@pnpm/exe@${VERSION}" "latest-${MAJOR}" --registry="$REGISTRY"
|
|
if [ "$TAG" != "latest-${MAJOR}" ]; then
|
|
pn dist-tag add "pnpm@${VERSION}" "${TAG}" --registry="$REGISTRY"
|
|
pn dist-tag add "@pnpm/exe@${VERSION}" "${TAG}" --registry="$REGISTRY"
|
|
fi
|
|
|
|
publish-to-winget:
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
environment: release
|
|
needs: tag-in-registry
|
|
steps:
|
|
- uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 # 2026-03-15 pin, no longer on a named ref # zizmor: ignore[ref-version-mismatch]
|
|
with:
|
|
identifier: pnpm.pnpm
|
|
version: ${{ github.event.inputs.version }}
|
|
release-tag: v${{ github.event.inputs.version }}
|
|
token: ${{ secrets.WINGET_TOKEN }}
|
|
|
|
post-to-reddit:
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
environment: release
|
|
needs: tag-in-registry
|
|
steps:
|
|
- uses: bluwy/release-for-reddit-action@b4ee0e0d64da893e0428912aac5cda675082bd85 # v2
|
|
with:
|
|
username: ${{ secrets.REDDIT_USERNAME }}
|
|
password: ${{ secrets.REDDIT_PASSWORD }}
|
|
app-id: ${{ secrets.REDDIT_APP_ID }}
|
|
app-secret: ${{ secrets.REDDIT_APP_SECRET }}
|
|
subreddit: pnpm
|
|
title: pnpm@${{ github.event.inputs.version }} is out!
|
|
url: https://github.com/pnpm/pnpm/releases/tag/v${{ github.event.inputs.version }}
|
|
|
|
post-to-mastodon:
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
environment: release
|
|
needs: tag-in-registry
|
|
steps:
|
|
- name: Send toot to Mastodon
|
|
id: mastodon
|
|
uses: cbrgm/mastodon-github-action@7fcc3650150fc062d707eeffb50e8f7a2704fd0f # v2.2.3
|
|
with:
|
|
message: |
|
|
pnpm@${{ github.event.inputs.version }} is out!
|
|
https://github.com/pnpm/pnpm/releases/tag/v${{ github.event.inputs.version }}
|
|
visibility: "public"
|
|
env:
|
|
MASTODON_URL: "https://fosstodon.org/"
|
|
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} # access token
|