mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-19 20:22:21 -04:00
1068 lines
45 KiB
YAML
1068 lines
45 KiB
YAML
name: Release
|
|
|
|
# The unified release workflow for every product in this repository: the
|
|
# TypeScript pnpm CLI (v11), the Rust pnpm CLI + NAPI addon (v12), and the
|
|
# pnpr registry server. Versions are committed (bumped by `pnpm bump` via the
|
|
# release PR that create-release-pr.yml opens); pushing a version tag runs the
|
|
# `plan` job, which asks the npm registry which committed versions are not
|
|
# published yet, and only those products' jobs run. Tagging e.g. a v12
|
|
# prerelease therefore skips the TypeScript jobs whose version is already on
|
|
# npm, and vice versa.
|
|
on:
|
|
push:
|
|
tags:
|
|
# pnpm (TypeScript, v11.x) and pacquet (v12.x) share the `v<version>`
|
|
# namespace that get.pnpm.io's install scripts fetch by. pnpr gets its own
|
|
# Changesets-style `pnpr@<version>` namespace: pnpm's tag history includes
|
|
# v0.x/v1.x that pnpr's low version numbers would otherwise collide with.
|
|
- "v*.*.*"
|
|
- "pnpr@*.*.*"
|
|
# Manual trigger for rerunning a tag release or verifying TypeScript release
|
|
# artifacts from a branch without publishing.
|
|
workflow_dispatch:
|
|
inputs:
|
|
verify_ts_release:
|
|
description: Verify TypeScript release artifacts without publishing
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
# A release PR that bumps several products auto-tags each one (tag-release.yml),
|
|
# so several version tags can land together and start a run apiece. Serialize
|
|
# them: the plan job gates every publish on what npm reports as unpublished, so
|
|
# once the first run publishes a product the rest skip it — but only if they
|
|
# don't run concurrently and all read "unpublished" at once. Never cancel a
|
|
# run mid-publish.
|
|
concurrency:
|
|
group: release
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
validate-release-ref:
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
permissions: {}
|
|
steps:
|
|
- name: Validate release ref
|
|
run: |
|
|
case "${GITHUB_REF}" in
|
|
refs/tags/v*.*.*) ;;
|
|
refs/tags/pnpr@*.*.*) ;;
|
|
*)
|
|
if [ "${{ inputs.verify_ts_release }}" = "true" ]; then
|
|
exit 0
|
|
fi
|
|
echo "::error::The release workflow must run from a version tag like v11.9.0 or pnpr@0.2.0. Current ref: ${GITHUB_REF}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
plan:
|
|
name: Plan (which products need publishing)
|
|
needs: validate-release-ref
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
ts: ${{ steps.decide.outputs.ts }}
|
|
ts_version: ${{ steps.decide.outputs.ts_version }}
|
|
rust: ${{ steps.decide.outputs.rust }}
|
|
rust_version: ${{ steps.decide.outputs.rust_version }}
|
|
rust_tag: ${{ steps.decide.outputs.rust_tag }}
|
|
pnpr: ${{ steps.decide.outputs.pnpr }}
|
|
pnpr_version: ${{ steps.decide.outputs.pnpr_version }}
|
|
pnpr_tag: ${{ steps.decide.outputs.pnpr_tag }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Read committed versions and query the registry
|
|
id: decide
|
|
# A product is released when its committed version is not on npm yet.
|
|
# `needs_release` treats only E404 as "not published"; any other npm
|
|
# failure aborts the plan so a registry outage can't be misread as a
|
|
# pending release. The gate packages are the ones each publish job
|
|
# publishes last (`pnpm` for both CLIs, `@pnpm/pnpr` for pnpr), so a
|
|
# rerun after a partial failure resumes instead of skipping.
|
|
run: |
|
|
set -eu
|
|
needs_release() {
|
|
local out
|
|
# Query from outside the checkout: npm enforces the repository's
|
|
# devEngines (which pin pnpm) for any command run inside it.
|
|
if out=$(cd "${RUNNER_TEMP:-/tmp}" && npm view "$1" version 2>&1); then
|
|
echo false
|
|
elif echo "$out" | grep -q 'E404'; then
|
|
echo true
|
|
else
|
|
echo "::error::npm view $1 failed: $out"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
TS_VERSION=$(jq -r .version pnpm11/pnpm/package.json)
|
|
RUST_VERSION=$(jq -r .version pnpm/npm/pnpm/package.json)
|
|
NAPI_VERSION=$(jq -r .version pnpm/npm/napi/package.json)
|
|
PNPR_VERSION=$(jq -r .version pnpr/npm/pnpr/package.json)
|
|
|
|
# pacquet and @pnpm/napi are a versioning.fixed group; a drift means
|
|
# the versions were edited by hand inconsistently.
|
|
if [ "$RUST_VERSION" != "$NAPI_VERSION" ]; then
|
|
echo "::error::pnpm/npm/pnpm ($RUST_VERSION) and pnpm/npm/napi ($NAPI_VERSION) versions differ"
|
|
exit 1
|
|
fi
|
|
|
|
# A v12 prerelease must never move the production dist-tags; derive
|
|
# next-<major> the way the TypeScript packages' publishConfig does.
|
|
RUST_TAG="next-${RUST_VERSION%%.*}"
|
|
|
|
# A pnpr prerelease must not move the mutable `latest` dist-tag,
|
|
# mirroring the docker-pnpr job's image-tag handling.
|
|
case "$PNPR_VERSION" in
|
|
*-*) PNPR_TAG="next" ;;
|
|
*) PNPR_TAG="latest" ;;
|
|
esac
|
|
|
|
# Assign first so `set -e` aborts the plan on a `needs_release`
|
|
# failure. Inside `echo "$(needs_release …)"` the non-zero exit is
|
|
# the subshell's, discarded by echo, so a registry outage would
|
|
# otherwise be written out as an empty (falsy) value and silently
|
|
# skip the release.
|
|
TS_NEEDS=$(needs_release "pnpm@$TS_VERSION")
|
|
RUST_NEEDS=$(needs_release "pnpm@$RUST_VERSION")
|
|
PNPR_NEEDS=$(needs_release "@pnpm/pnpr@$PNPR_VERSION")
|
|
|
|
{
|
|
echo "ts=$TS_NEEDS"
|
|
echo "ts_version=$TS_VERSION"
|
|
echo "rust=$RUST_NEEDS"
|
|
echo "rust_version=$RUST_VERSION"
|
|
echo "rust_tag=$RUST_TAG"
|
|
echo "pnpr=$PNPR_NEEDS"
|
|
echo "pnpr_version=$PNPR_VERSION"
|
|
echo "pnpr_tag=$PNPR_TAG"
|
|
} | tee -a "$GITHUB_OUTPUT"
|
|
|
|
verify-ts-release:
|
|
name: Verify TypeScript pnpm release artifacts
|
|
needs:
|
|
- validate-release-ref
|
|
- plan
|
|
if: needs.plan.outputs.ts == 'true' || inputs.verify_ts_release
|
|
permissions:
|
|
contents: read
|
|
# Runs on macOS so the darwin artifacts can be ad-hoc signed with native
|
|
# `codesign` (no need to build/install `ldid` on the runner) and so
|
|
# `verify-binary.mjs` can smoke-test the darwin-arm64 SEA in place — a
|
|
# macos-latest runner is Apple Silicon and can execute the arm64 binary.
|
|
# Note: this does NOT fix the darwin-x64 crash (nodejs/node#62893) — that's
|
|
# an upstream Node.js SEA bug independent of signing; see pack-app docs.
|
|
# npm trusted publishing still requires this publish job to run on GitHub-hosted macOS.
|
|
runs-on: macos-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
- uses: garnet-org/action@2b7fc9d79b54f551b43358c27424a36064b3e078 # v2.0.2
|
|
with:
|
|
api_token: ${{ secrets.GARNET_API_TOKEN }}
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@77cf06832101b3ac8c65caaf76a21643936d07a4
|
|
with:
|
|
runtime: node@26.5.0
|
|
- name: Build TypeScript executable artifacts
|
|
run: pn --filter=@pnpm/exe run build-artifacts
|
|
|
|
- name: Verify TypeScript npm packages
|
|
# Inspect the same tarballs that publish will upload before the first
|
|
# immutable npm publish, including the generated platform packages.
|
|
run: |
|
|
set -euo pipefail
|
|
release_tarballs=$(mktemp -d)
|
|
for package in pnpm11/pnpm/artifacts/{darwin-arm64,exe,linux-arm64,linux-arm64-musl,linux-x64,linux-x64-musl,win32-arm64,win32-x64} pnpm11/pnpm; do
|
|
package_tarballs="$release_tarballs/${package//\//_}"
|
|
mkdir "$package_tarballs"
|
|
(
|
|
cd "$package"
|
|
pn pack --pack-destination "$package_tarballs"
|
|
)
|
|
|
|
tarball=$(find "$package_tarballs" -type f -name '*.tgz' -print -quit)
|
|
test -n "$tarball"
|
|
node -e '
|
|
const fs = require("node:fs")
|
|
const path = require("node:path")
|
|
const manifestPath = path.resolve(process.argv[1])
|
|
const manifest = require(manifestPath)
|
|
const packageDir = path.dirname(manifestPath)
|
|
const excluded = (manifest.files ?? []).filter((file) => file.startsWith("!")).map((file) => globToRegExp(file.slice(1)))
|
|
const files = new Set()
|
|
for (const file of manifest.files ?? []) {
|
|
if (!file.startsWith("!") && !/[?*[]/.test(file)) add(file)
|
|
}
|
|
for (const file of manifest.publishConfig?.executableFiles ?? []) add(file)
|
|
for (const file of Object.values(typeof manifest.bin === "string" ? { default: manifest.bin } : manifest.bin ?? {})) add(file)
|
|
if (files.size === 0) throw new Error("No literal payload files declared by " + manifest.name)
|
|
console.log([...files].join("\n"))
|
|
|
|
function add(file) {
|
|
const relative = file.replace(/^\.\//, "")
|
|
if (excluded.some((pattern) => pattern.test(relative))) return
|
|
const source = path.join(packageDir, relative)
|
|
const stat = fs.statSync(source, { throwIfNoEntry: false })
|
|
if (stat?.isFile()) {
|
|
files.add(relative)
|
|
return
|
|
}
|
|
if (!stat?.isDirectory()) throw new Error("Missing payload file " + source)
|
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) add(path.join(relative, entry.name))
|
|
}
|
|
|
|
function globToRegExp(pattern) {
|
|
return new RegExp("^" + pattern.replace(/[.+^{}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\x00").replace(/\*\*/g, "\x01").replace(/\*/g, "[^/]*").replace(/\x00/g, "(?:.*/)?").replace(/\x01/g, ".*") + String.fromCharCode(36))
|
|
}
|
|
' "$package/package.json" | while IFS= read -r file; do
|
|
test -s "$package/$file"
|
|
test "$(tar -xOf "$tarball" "package/$file" | shasum -a 256 | cut -d ' ' -f 1)" = "$(shasum -a 256 "$package/$file" | cut -d ' ' -f 1)"
|
|
done
|
|
done
|
|
|
|
pnpm_tarball=$(find "$release_tarballs/pnpm11_pnpm" -type f -name '*.tgz' -print -quit)
|
|
test -n "$pnpm_tarball"
|
|
|
|
smoke_dir=$(mktemp -d)
|
|
tar -xzf "$pnpm_tarball" -C "$smoke_dir"
|
|
node "$smoke_dir/package/bin/pnpm.mjs" --version
|
|
|
|
exe_tarball=$(find "$release_tarballs/pnpm11_pnpm_artifacts_exe" -type f -name '*.tgz' -print -quit)
|
|
native_tarball=$(find "$release_tarballs/pnpm11_pnpm_artifacts_darwin-arm64" -type f -name '*.tgz' -print -quit)
|
|
test -n "$exe_tarball"
|
|
test -n "$native_tarball"
|
|
(
|
|
cd "$smoke_dir"
|
|
command -v node
|
|
node --version
|
|
command -v npm
|
|
npm --version
|
|
npm config get ignore-scripts
|
|
npm config get strict-allow-scripts
|
|
npm install --foreground-scripts --ignore-scripts=false --loglevel verbose "$exe_tarball" "$native_tarball"
|
|
)
|
|
ls -l "$smoke_dir/node_modules/@pnpm/exe/pnpm"
|
|
file "$smoke_dir/node_modules/@pnpm/exe/pnpm"
|
|
"$smoke_dir/node_modules/@pnpm/exe/pnpm" --version
|
|
|
|
release:
|
|
name: Release the TypeScript pnpm CLI
|
|
needs:
|
|
- validate-release-ref
|
|
- plan
|
|
- verify-ts-release
|
|
if: needs.plan.outputs.ts == 'true' && !inputs.verify_ts_release
|
|
permissions:
|
|
id-token: write # Required for OIDC
|
|
contents: write # for softprops/action-gh-release to create GitHub release
|
|
attestations: write # for actions/attest-build-provenance
|
|
runs-on: macos-latest
|
|
environment: release
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
- uses: garnet-org/action@2b7fc9d79b54f551b43358c27424a36064b3e078 # v2.0.2
|
|
with:
|
|
api_token: ${{ secrets.GARNET_API_TOKEN }}
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@77cf06832101b3ac8c65caaf76a21643936d07a4
|
|
with:
|
|
runtime: node@26.5.0
|
|
# The publish phase is split into three sequential steps to control which packages
|
|
# use trusted publishing (OIDC) vs. a static token. `pnpm publish` currently bails
|
|
# out of OIDC as soon as a static `_authToken` is configured, so the only way to
|
|
# force trusted publishing for a given package today is to run its publish in a
|
|
# step that doesn't have NPM_TOKEN set. See https://github.com/pnpm/pnpm/pull/11495
|
|
# for the longer-term fix that lets OIDC override a configured token.
|
|
- name: Build TypeScript executable artifacts
|
|
run: pn --filter=@pnpm/exe run build-artifacts
|
|
|
|
- name: Publish @pnpm/exe (trusted publishing)
|
|
# No NPM_TOKEN: pnpm has no static token to short-circuit on, so it will perform
|
|
# the OIDC token exchange against npm's trusted-publishing config for `@pnpm/exe`.
|
|
run: |
|
|
pn --filter=@pnpm/exe publish --tag=next-11 --access=public --provenance
|
|
- name: Publish internal workspace packages (static token)
|
|
# The other workspace packages don't have trusted publishing configured on npm,
|
|
# so we still need a static token here. The token is removed from pnpm's config
|
|
# at the end of the step so it can't leak into the trusted-publishing step that
|
|
# follows (where its presence would silently downgrade `pnpm` to token publishing).
|
|
# The Rust products' wrapper packages are private workspace packages, so the
|
|
# recursive publish skips them; they are published by their own jobs below.
|
|
env:
|
|
# Setting the "npm_config_//registry.npmjs.org/:_authToken" env variable directly
|
|
# doesn't work — pnpm doesn't appear to pass auth tokens to child processes.
|
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
run: |
|
|
trap 'pn config delete "//registry.npmjs.org/:_authToken" || true' EXIT
|
|
pn config set "//registry.npmjs.org/:_authToken" "${NPM_TOKEN}"
|
|
pn publish --filter=!pnpm --filter=!@pnpm/exe --access=public --provenance
|
|
- name: Publish pnpm CLI (trusted publishing)
|
|
# No NPM_TOKEN — same rationale as the @pnpm/exe step above. This must come after
|
|
# the previous step has cleared its NPM_TOKEN from pnpm's config.
|
|
run: pn publish --filter=pnpm --tag=next-11 --access=public --provenance
|
|
- name: Copy Artifacts
|
|
run: pn copy-artifacts
|
|
- name: Attest build provenance
|
|
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
|
with:
|
|
subject-path: 'dist/*'
|
|
- name: Generate release description
|
|
run: pn make-release-description
|
|
- name: Release
|
|
# `gh release create` could replace this, but the release pipeline is
|
|
# sensitive and softprops/action-gh-release is widely battle-tested.
|
|
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.0 # zizmor: ignore[superfluous-actions]
|
|
with:
|
|
draft: true
|
|
files: dist/*
|
|
body_path: RELEASE.md
|
|
|
|
build-rust:
|
|
needs: plan
|
|
if: needs.plan.outputs.rust == 'true' && !inputs.verify_ts_release
|
|
permissions:
|
|
contents: read
|
|
id-token: write # needed for actions/attest-build-provenance
|
|
attestations: write
|
|
strategy:
|
|
matrix:
|
|
include:
|
|
- os: blacksmith-8vcpu-windows-2025
|
|
target: x86_64-pc-windows-msvc
|
|
code-target: win32-x64
|
|
|
|
- os: blacksmith-8vcpu-windows-2025
|
|
target: aarch64-pc-windows-msvc
|
|
code-target: win32-arm64
|
|
|
|
- os: blacksmith-8vcpu-ubuntu-2404
|
|
target: x86_64-unknown-linux-gnu
|
|
code-target: linux-x64
|
|
|
|
- os: blacksmith-8vcpu-ubuntu-2404
|
|
target: aarch64-unknown-linux-gnu
|
|
code-target: linux-arm64
|
|
|
|
- os: blacksmith-8vcpu-ubuntu-2404
|
|
target: x86_64-unknown-linux-musl
|
|
code-target: linux-x64-musl
|
|
|
|
- os: blacksmith-8vcpu-ubuntu-2404
|
|
target: aarch64-unknown-linux-musl
|
|
code-target: linux-arm64-musl
|
|
|
|
- os: blacksmith-12vcpu-macos-latest
|
|
target: x86_64-apple-darwin
|
|
code-target: darwin-x64
|
|
|
|
- os: blacksmith-12vcpu-macos-latest
|
|
target: aarch64-apple-darwin
|
|
code-target: darwin-arm64
|
|
|
|
name: Package pnpm (Rust) ${{ matrix.code-target }}
|
|
runs-on: ${{ matrix.os }}
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install cross
|
|
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2.82.5
|
|
with:
|
|
tool: cross
|
|
|
|
# No build cache: GitHub Actions caches are writable by any workflow on
|
|
# the default branch, so restoring one here would let a poisoned cache
|
|
# entry inject code into the published, attested release binaries.
|
|
- name: Add Rust Target
|
|
run: rustup target add ${{ matrix.target }}
|
|
|
|
- name: Verify the committed version
|
|
# `pacquet --version` and the default User-Agent read the PNPM_VERSION
|
|
# constant, which `pnpm bump` keeps in sync with the npm wrapper's
|
|
# version. Verify instead of patching so the binary is reproducible
|
|
# from the tagged sources.
|
|
shell: bash
|
|
env:
|
|
VERSION: ${{ needs.plan.outputs.rust_version }}
|
|
run: |
|
|
grep -F "PNPM_VERSION: &str = \"$VERSION\"" pnpm/crates/config/src/defaults.rs
|
|
|
|
- name: Build with cross
|
|
run: cross build --locked -p pacquet-cli --bin pnpm --release --target=${{ matrix.target }}
|
|
|
|
- name: Build NAPI addon with cross
|
|
# musl targets enable `crt-static` by default, but a `cdylib` (the
|
|
# `.node` addon) can't be produced with a statically linked CRT. Disable
|
|
# it so the addon builds as a shared object that dynamically links musl
|
|
# libc at load time (this is what napi-rs does for musl prebuilds). The
|
|
# CLI binary keeps `crt-static` — it's built in the separate step above,
|
|
# so it stays fully static for portability across musl distros.
|
|
#
|
|
# Set RUSTFLAGS only for musl: an empty RUSTFLAGS is not a no-op — Cargo
|
|
# treats it as an override that discards any `rustflags` from
|
|
# `.cargo/config.toml`, so we leave it unset on every other target.
|
|
shell: bash
|
|
env:
|
|
TARGET: ${{ matrix.target }}
|
|
run: |
|
|
if [[ "$TARGET" == *musl* ]]; then
|
|
export RUSTFLAGS="-C target-feature=-crt-static"
|
|
fi
|
|
cross build --locked -p pacquet-napi --profile napi-release --target="$TARGET"
|
|
|
|
- name: Prepare NAPI addon
|
|
shell: bash
|
|
run: |
|
|
case "${{ runner.os }}" in
|
|
Windows) source="target/${{ matrix.target }}/napi-release/pacquet_napi.dll" ;;
|
|
macOS) source="target/${{ matrix.target }}/napi-release/libpacquet_napi.dylib" ;;
|
|
*) source="target/${{ matrix.target }}/napi-release/libpacquet_napi.so" ;;
|
|
esac
|
|
cp "$source" "pnpm-napi.${{ matrix.code-target }}.node"
|
|
|
|
# The binary is archived to fix permission loss
|
|
# (https://github.com/actions/upload-artifact#permission-loss) and ships
|
|
# under its final name: `pnpm-<target>` archives with a plain `pnpm`
|
|
# binary at the root — the layout the v11 releases use and get.pnpm.io's
|
|
# install scripts expect — so the github-release-rust job can upload them
|
|
# to the GitHub release byte-for-byte as attested here.
|
|
- name: Archive Binary
|
|
if: runner.os == 'Windows'
|
|
shell: bash
|
|
run: |
|
|
mv target/${{ matrix.target }}/release/pnpm.exe pnpm.exe
|
|
7z a pnpm-${{ matrix.code-target }}.zip pnpm.exe
|
|
|
|
- name: Archive Binary
|
|
if: runner.os != 'Windows'
|
|
# The binary is staged in a scratch directory because the repo root
|
|
# already has a `pnpm/` directory (the Rust sub-project) — renaming the
|
|
# binary to `pnpm` in place would move it *into* that directory and tar
|
|
# up the whole source tree instead.
|
|
run: |
|
|
mkdir stage
|
|
mv target/${{ matrix.target }}/release/pnpm stage/pnpm
|
|
tar czf pnpm-${{ matrix.code-target }}.tar.gz -C stage pnpm
|
|
|
|
- name: Attest build provenance
|
|
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
|
with:
|
|
subject-path: |
|
|
pnpm-${{ matrix.code-target }}.*
|
|
pnpm-napi.${{ matrix.code-target }}.node
|
|
|
|
- name: Upload Binary
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
if-no-files-found: error
|
|
name: binaries-rust-${{ matrix.code-target }}
|
|
path: |
|
|
*.zip
|
|
*.tar.gz
|
|
*.node
|
|
|
|
verify-rust-artifacts:
|
|
name: Verify pnpm (Rust) npm artifacts
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
needs:
|
|
- plan
|
|
- build-rust
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@77cf06832101b3ac8c65caaf76a21643936d07a4
|
|
with:
|
|
runtime: node@22
|
|
install: false
|
|
|
|
- name: Download Artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: binaries-rust-*
|
|
merge-multiple: true
|
|
|
|
- name: Extract archives
|
|
# Every archive holds a binary named `pnpm` / `pnpm.exe` at its root
|
|
# (see the Archive Binary steps), so extracting them all into the same
|
|
# directory would clobber one target with another. Extract each into a
|
|
# scratch dir and move the binary out under its target-qualified name,
|
|
# which is what generate-packages.mjs reads.
|
|
run: |
|
|
for archive in pnpm-*.zip; do
|
|
rm -rf extract && mkdir extract
|
|
unzip -q "$archive" -d extract
|
|
mv extract/pnpm.exe "${archive%.zip}.exe"
|
|
done
|
|
for archive in pnpm-*.tar.gz; do
|
|
rm -rf extract && mkdir extract
|
|
tar -xzf "$archive" -C extract
|
|
mv extract/pnpm "${archive%.tar.gz}"
|
|
done
|
|
|
|
- name: Generate npm packages
|
|
# Rewrites the committed private `pacquet` wrapper into the publishable
|
|
# `pnpm` manifest and generates the per-platform packages.
|
|
run: |
|
|
node pnpm/npm/pnpm/scripts/generate-packages.mjs
|
|
node pnpm/npm/napi/scripts/generate-packages.mjs
|
|
cat pnpm/npm/pnpm/package.json
|
|
cat pnpm/npm/napi/package.json
|
|
for package in pnpm/npm/pacquet-* pnpm/npm/napi.*; do cat "$package/package.json" ; echo ; done
|
|
|
|
- name: Verify generated npm packages
|
|
# `pnpm publish` packages its input immediately before upload. Verify
|
|
# those tarballs before the first immutable npm publish, including the
|
|
# wrapper's preinstall path on this Linux runner.
|
|
run: |
|
|
set -euo pipefail
|
|
release_tarballs=$(mktemp -d)
|
|
for package in pnpm/npm/pacquet-* pnpm/npm/napi.* pnpm/npm/napi pnpm/npm/pnpm-exe pnpm/npm/pnpm; do
|
|
package_tarballs="$release_tarballs/${package//\//_}"
|
|
mkdir "$package_tarballs"
|
|
(
|
|
cd "$package"
|
|
pnpm pack --pack-destination "$package_tarballs"
|
|
)
|
|
|
|
tarball=$(find "$package_tarballs" -type f -name '*.tgz' -print -quit)
|
|
test -n "$tarball"
|
|
node -e '
|
|
const fs = require("node:fs")
|
|
const path = require("node:path")
|
|
const manifestPath = path.resolve(process.argv[1])
|
|
const manifest = require(manifestPath)
|
|
const packageDir = path.dirname(manifestPath)
|
|
const excluded = (manifest.files ?? []).filter((file) => file.startsWith("!")).map((file) => globToRegExp(file.slice(1)))
|
|
const files = new Set()
|
|
for (const file of manifest.files ?? []) {
|
|
if (!file.startsWith("!") && !/[?*[]/.test(file)) add(file)
|
|
}
|
|
for (const file of manifest.publishConfig?.executableFiles ?? []) add(file)
|
|
for (const file of Object.values(typeof manifest.bin === "string" ? { default: manifest.bin } : manifest.bin ?? {})) add(file)
|
|
if (files.size === 0) throw new Error("No literal payload files declared by " + manifest.name)
|
|
console.log([...files].join("\n"))
|
|
|
|
function add(file) {
|
|
const relative = file.replace(/^\.\//, "")
|
|
if (excluded.some((pattern) => pattern.test(relative))) return
|
|
const source = path.join(packageDir, relative)
|
|
const stat = fs.statSync(source, { throwIfNoEntry: false })
|
|
if (stat?.isFile()) {
|
|
files.add(relative)
|
|
return
|
|
}
|
|
if (!stat?.isDirectory()) throw new Error("Missing payload file " + source)
|
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) add(path.join(relative, entry.name))
|
|
}
|
|
|
|
function globToRegExp(pattern) {
|
|
return new RegExp("^" + pattern.replace(/[.+^{}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\x00").replace(/\*\*/g, "\x01").replace(/\*/g, "[^/]*").replace(/\x00/g, "(?:.*/)?").replace(/\x01/g, ".*") + String.fromCharCode(36))
|
|
}
|
|
' "$package/package.json" | while IFS= read -r file; do
|
|
test -s "$package/$file"
|
|
test "$(tar -xOf "$tarball" "package/$file" | sha256sum | cut -d ' ' -f 1)" = "$(sha256sum "$package/$file" | cut -d ' ' -f 1)"
|
|
done
|
|
done
|
|
|
|
smoke_dir=$(mktemp -d)
|
|
native_tarball=$(find "$release_tarballs/pnpm_npm_pacquet-linux-x64" -type f -name '*.tgz' -print -quit)
|
|
wrapper_tarball=$(find "$release_tarballs/pnpm_npm_pnpm" -type f -name '*.tgz' -print -quit)
|
|
test -n "$native_tarball"
|
|
test -n "$wrapper_tarball"
|
|
mkdir -p "$smoke_dir/node_modules/@pnpm/exe.linux-x64" "$smoke_dir/node_modules/pnpm"
|
|
tar -xzf "$native_tarball" \
|
|
-C "$smoke_dir/node_modules/@pnpm/exe.linux-x64" --strip-components=1
|
|
tar -xzf "$wrapper_tarball" \
|
|
-C "$smoke_dir/node_modules/pnpm" --strip-components=1
|
|
node "$smoke_dir/node_modules/pnpm/install.js"
|
|
"$smoke_dir/node_modules/pnpm/pnpm" --version
|
|
|
|
publish-rust:
|
|
name: Publish pnpm (Rust) and @pnpm/napi to npm
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
permissions:
|
|
# Required for npm trusted publishing and provenance attestations via OIDC.
|
|
id-token: write
|
|
needs:
|
|
- plan
|
|
- build-rust
|
|
- verify-rust-artifacts
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@77cf06832101b3ac8c65caaf76a21643936d07a4
|
|
with:
|
|
runtime: node@22
|
|
install: false
|
|
|
|
- name: Download Artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: binaries-rust-*
|
|
merge-multiple: true
|
|
|
|
- name: Extract archives
|
|
run: |
|
|
for archive in pnpm-*.zip; do
|
|
rm -rf extract && mkdir extract
|
|
unzip -q "$archive" -d extract
|
|
mv extract/pnpm.exe "${archive%.zip}.exe"
|
|
done
|
|
for archive in pnpm-*.tar.gz; do
|
|
rm -rf extract && mkdir extract
|
|
tar -xzf "$archive" -C extract
|
|
mv extract/pnpm "${archive%.tar.gz}"
|
|
done
|
|
|
|
- name: Generate npm packages
|
|
run: |
|
|
node pnpm/npm/pnpm/scripts/generate-packages.mjs
|
|
node pnpm/npm/napi/scripts/generate-packages.mjs
|
|
|
|
# Everything publishes via trusted publishing: the `pnpm` / `@pnpm/exe`
|
|
# trusted publishers are bound to this workflow file (they are shared
|
|
# with the TypeScript release job above), and the natives' and
|
|
# `@pnpm/napi`'s publishers are bound here too. Publishing the natives
|
|
# first keeps the wrappers' exact-version optionalDependencies
|
|
# installable the moment each wrapper goes live, and the `pnpm` wrapper
|
|
# goes last because the plan job uses it as the "release completed"
|
|
# gate. The trailing slash publishes the directory; the build dirs
|
|
# `pacquet-*` are published as `@pnpm/exe.<target>`.
|
|
- name: Publish (trusted publishing)
|
|
env:
|
|
TAG: ${{ needs.plan.outputs.rust_tag }}
|
|
run: |
|
|
for package in pnpm/npm/pacquet-* pnpm/npm/napi.* pnpm/npm/napi pnpm/npm/pnpm-exe pnpm/npm/pnpm; do
|
|
pnpm publish "$package/" --tag "$TAG" --access public --provenance --no-git-checks
|
|
done
|
|
|
|
github-release-rust:
|
|
name: Draft GitHub release for pnpm (Rust)
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
permissions:
|
|
contents: write # create the draft release and its tag
|
|
needs:
|
|
- plan
|
|
- build-rust
|
|
- publish-rust
|
|
steps:
|
|
- name: Download Artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: binaries-rust-*
|
|
merge-multiple: true
|
|
|
|
- name: Release
|
|
# The build job's archives already carry the release layout (a `pnpm`
|
|
# binary at the root of `pnpm-<target>.tar.gz` / `.zip`), so they're
|
|
# uploaded byte-for-byte as attested there — get.pnpm.io's install
|
|
# scripts fetch them by these exact names. Draft so a maintainer
|
|
# publishes it deliberately. `make_latest: false` keeps a published v12
|
|
# prerelease from stealing the "Latest" badge from the stable v11
|
|
# line.
|
|
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.0 # zizmor: ignore[superfluous-actions]
|
|
with:
|
|
draft: true
|
|
prerelease: ${{ contains(needs.plan.outputs.rust_version, '-') }}
|
|
make_latest: false
|
|
tag_name: v${{ needs.plan.outputs.rust_version }}
|
|
target_commitish: ${{ github.sha }}
|
|
name: pnpm ${{ needs.plan.outputs.rust_version }}
|
|
files: |
|
|
pnpm-*.tar.gz
|
|
pnpm-*.zip
|
|
|
|
build-pnpr:
|
|
needs: plan
|
|
if: needs.plan.outputs.pnpr == 'true' && !inputs.verify_ts_release
|
|
permissions:
|
|
contents: read
|
|
id-token: write # needed for actions/attest-build-provenance
|
|
attestations: write
|
|
strategy:
|
|
matrix:
|
|
include:
|
|
- os: blacksmith-8vcpu-windows-2025
|
|
target: x86_64-pc-windows-msvc
|
|
code-target: win32-x64
|
|
|
|
- os: blacksmith-8vcpu-windows-2025
|
|
target: aarch64-pc-windows-msvc
|
|
code-target: win32-arm64
|
|
|
|
- os: blacksmith-8vcpu-ubuntu-2404
|
|
target: x86_64-unknown-linux-gnu
|
|
code-target: linux-x64
|
|
|
|
- os: blacksmith-8vcpu-ubuntu-2404
|
|
target: aarch64-unknown-linux-gnu
|
|
code-target: linux-arm64
|
|
|
|
- os: blacksmith-8vcpu-ubuntu-2404
|
|
target: x86_64-unknown-linux-musl
|
|
code-target: linux-x64-musl
|
|
|
|
- os: blacksmith-8vcpu-ubuntu-2404
|
|
target: aarch64-unknown-linux-musl
|
|
code-target: linux-arm64-musl
|
|
|
|
- os: blacksmith-12vcpu-macos-latest
|
|
target: x86_64-apple-darwin
|
|
code-target: darwin-x64
|
|
|
|
- os: blacksmith-12vcpu-macos-latest
|
|
target: aarch64-apple-darwin
|
|
code-target: darwin-arm64
|
|
|
|
name: Package pnpr ${{ matrix.code-target }}
|
|
runs-on: ${{ matrix.os }}
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install cross
|
|
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2.82.5
|
|
with:
|
|
tool: cross
|
|
|
|
# No build cache: GitHub Actions caches are writable by any workflow on
|
|
# the default branch, so restoring one here would let a poisoned cache
|
|
# entry inject code into the published, attested release binaries.
|
|
- name: Add Rust Target
|
|
run: rustup target add ${{ matrix.target }}
|
|
|
|
- name: Verify the committed version
|
|
# `pnpr --version` reads CARGO_PKG_VERSION via clap's derive `version`
|
|
# attribute; `pnpm bump` keeps the crate version in sync with the npm
|
|
# wrapper's version. Verify instead of patching so the binary is
|
|
# reproducible from the tagged sources.
|
|
shell: bash
|
|
env:
|
|
VERSION: ${{ needs.plan.outputs.pnpr_version }}
|
|
run: grep -E "^version\s*=\s*\"$VERSION\"" pnpr/crates/pnpr/Cargo.toml
|
|
|
|
- name: Build with cross
|
|
run: cross build --locked -p pnpr --bin pnpr --release --target=${{ matrix.target }}
|
|
|
|
# The binary is zipped to fix permission loss https://github.com/actions/upload-artifact#permission-loss
|
|
# Rename the binary to include the target triple so the archive
|
|
# that generate-packages.mjs picks up is already named
|
|
# `pnpr-<target>`.
|
|
- name: Archive Binary
|
|
if: runner.os == 'Windows'
|
|
shell: bash
|
|
run: |
|
|
BIN_NAME=pnpr-${{ matrix.code-target }}
|
|
mv target/${{ matrix.target }}/release/pnpr.exe $BIN_NAME.exe
|
|
7z a $BIN_NAME.zip $BIN_NAME.exe
|
|
|
|
# The binary is zipped to fix permission loss https://github.com/actions/upload-artifact#permission-loss
|
|
- name: Archive Binary
|
|
if: runner.os != 'Windows'
|
|
run: |
|
|
BIN_NAME=pnpr-${{ matrix.code-target }}
|
|
mv target/${{ matrix.target }}/release/pnpr $BIN_NAME
|
|
tar czf $BIN_NAME.tar.gz $BIN_NAME
|
|
# Pin the binary's checksum at build time so the Docker job can
|
|
# verify the artifact it stages into the image context.
|
|
shasum -a 256 $BIN_NAME > $BIN_NAME.sha256
|
|
|
|
- name: Attest build provenance
|
|
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
|
with:
|
|
subject-path: |
|
|
pnpr-${{ matrix.code-target }}*
|
|
|
|
- name: Upload Binary
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
if-no-files-found: error
|
|
name: binaries-pnpr-${{ matrix.code-target }}
|
|
path: |
|
|
*.zip
|
|
*.tar.gz
|
|
*.sha256
|
|
|
|
verify-pnpr-artifacts:
|
|
name: Verify pnpr npm artifacts
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
needs:
|
|
- plan
|
|
- build-pnpr
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@77cf06832101b3ac8c65caaf76a21643936d07a4
|
|
with:
|
|
runtime: node@22
|
|
install: false
|
|
|
|
- name: Download Artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: binaries-pnpr-*
|
|
merge-multiple: true
|
|
|
|
- name: Unzip
|
|
uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 # v1.0.0
|
|
with:
|
|
args: unzip -qq *.zip -d .
|
|
|
|
- name: Extract tarballs
|
|
run: |
|
|
for archive in ./*.gz; do
|
|
tar xf "$archive"
|
|
done
|
|
|
|
- name: Generate npm packages
|
|
run: |
|
|
node pnpr/npm/pnpr/scripts/generate-packages.mjs
|
|
cat pnpr/npm/pnpr/package.json
|
|
for package in pnpr/npm/pnpr*; do cat "$package/package.json" ; echo ; done
|
|
|
|
- name: Verify generated npm packages
|
|
# `pnpm publish` packages its input immediately before upload. Verify
|
|
# those tarballs, then exercise the wrapper's native-binary install
|
|
# path before the first immutable npm publish.
|
|
run: |
|
|
set -euo pipefail
|
|
release_tarballs=$(mktemp -d)
|
|
for package in pnpr/npm/pnpr-* pnpr/npm/pnpr; do
|
|
package_tarballs="$release_tarballs/${package//\//_}"
|
|
mkdir "$package_tarballs"
|
|
(
|
|
cd "$package"
|
|
pnpm pack --pack-destination "$package_tarballs"
|
|
)
|
|
|
|
tarball=$(find "$package_tarballs" -type f -name '*.tgz' -print -quit)
|
|
test -n "$tarball"
|
|
node -e '
|
|
const fs = require("node:fs")
|
|
const path = require("node:path")
|
|
const manifestPath = path.resolve(process.argv[1])
|
|
const manifest = require(manifestPath)
|
|
const packageDir = path.dirname(manifestPath)
|
|
const excluded = (manifest.files ?? []).filter((file) => file.startsWith("!")).map((file) => globToRegExp(file.slice(1)))
|
|
const files = new Set()
|
|
for (const file of manifest.files ?? []) {
|
|
if (!file.startsWith("!") && !/[?*[]/.test(file)) add(file)
|
|
}
|
|
for (const file of manifest.publishConfig?.executableFiles ?? []) add(file)
|
|
for (const file of Object.values(typeof manifest.bin === "string" ? { default: manifest.bin } : manifest.bin ?? {})) add(file)
|
|
if (files.size === 0) throw new Error("No literal payload files declared by " + manifest.name)
|
|
console.log([...files].join("\n"))
|
|
|
|
function add(file) {
|
|
const relative = file.replace(/^\.\//, "")
|
|
if (excluded.some((pattern) => pattern.test(relative))) return
|
|
const source = path.join(packageDir, relative)
|
|
const stat = fs.statSync(source, { throwIfNoEntry: false })
|
|
if (stat?.isFile()) {
|
|
files.add(relative)
|
|
return
|
|
}
|
|
if (!stat?.isDirectory()) throw new Error("Missing payload file " + source)
|
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) add(path.join(relative, entry.name))
|
|
}
|
|
|
|
function globToRegExp(pattern) {
|
|
return new RegExp("^" + pattern.replace(/[.+^{}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\x00").replace(/\*\*/g, "\x01").replace(/\*/g, "[^/]*").replace(/\x00/g, "(?:.*/)?").replace(/\x01/g, ".*") + String.fromCharCode(36))
|
|
}
|
|
' "$package/package.json" | while IFS= read -r file; do
|
|
test -s "$package/$file"
|
|
test "$(tar -xOf "$tarball" "package/$file" | sha256sum | cut -d ' ' -f 1)" = "$(sha256sum "$package/$file" | cut -d ' ' -f 1)"
|
|
done
|
|
done
|
|
|
|
smoke_dir=$(mktemp -d)
|
|
native_tarball=$(find "$release_tarballs/pnpr_npm_pnpr-linux-x64" -type f -name '*.tgz' -print -quit)
|
|
wrapper_tarball=$(find "$release_tarballs/pnpr_npm_pnpr" -type f -name '*.tgz' -print -quit)
|
|
test -n "$native_tarball"
|
|
test -n "$wrapper_tarball"
|
|
mkdir -p "$smoke_dir/node_modules/@pnpm/pnpr.linux-x64" "$smoke_dir/node_modules/@pnpm/pnpr"
|
|
tar -xzf "$native_tarball" -C "$smoke_dir/node_modules/@pnpm/pnpr.linux-x64" --strip-components=1
|
|
tar -xzf "$wrapper_tarball" -C "$smoke_dir/node_modules/@pnpm/pnpr" --strip-components=1
|
|
node "$smoke_dir/node_modules/@pnpm/pnpr/install.js"
|
|
"$smoke_dir/node_modules/@pnpm/pnpr/bin/pnpr" --version
|
|
|
|
publish-pnpr:
|
|
name: Publish @pnpm/pnpr to npm
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
permissions:
|
|
# Required for npm trusted publishing and provenance attestations via OIDC.
|
|
id-token: write
|
|
needs:
|
|
- plan
|
|
- build-pnpr
|
|
- verify-pnpr-artifacts
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@77cf06832101b3ac8c65caaf76a21643936d07a4
|
|
with:
|
|
runtime: node@22
|
|
install: false
|
|
|
|
- name: Download Artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: binaries-pnpr-*
|
|
merge-multiple: true
|
|
|
|
- name: Unzip
|
|
uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 # v1.0.0
|
|
with:
|
|
args: unzip -qq *.zip -d .
|
|
|
|
- name: Extract tarballs
|
|
run: |
|
|
for archive in ./*.gz; do
|
|
tar xf "$archive"
|
|
done
|
|
|
|
- name: Generate npm packages
|
|
run: node pnpr/npm/pnpr/scripts/generate-packages.mjs
|
|
|
|
- name: Publish npm packages
|
|
# Auth is via npm's trusted publishing: `id-token: write` above grants
|
|
# this job an OIDC token that pnpm/npm exchange with the registry,
|
|
# so no NPM_TOKEN is needed. `--provenance` attaches the same OIDC
|
|
# token to a provenance attestation on each tarball.
|
|
# The natives go first so the wrapper's exact-version
|
|
# optionalDependencies are installable the moment it goes live; the
|
|
# wrapper goes last because the plan job uses it as the "release
|
|
# completed" gate. The trailing slash publishes the directory.
|
|
env:
|
|
TAG: ${{ needs.plan.outputs.pnpr_tag }}
|
|
run: |
|
|
for package in pnpr/npm/pnpr-* pnpr/npm/pnpr; do
|
|
pnpm publish "$package/" --tag "$TAG" --access public --provenance --no-git-checks
|
|
done
|
|
|
|
docker-pnpr:
|
|
name: Publish the pnpr Docker image
|
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
environment: release
|
|
needs:
|
|
- plan
|
|
- build-pnpr
|
|
- publish-pnpr
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
env:
|
|
IMAGE: ghcr.io/${{ github.repository_owner }}/pnpr
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Compute image tags
|
|
id: tags
|
|
# A version containing a hyphen (e.g. 0.2.3-rc.1) is a prerelease and
|
|
# must not move the mutable `latest` tag.
|
|
env:
|
|
VERSION: ${{ needs.plan.outputs.pnpr_version }}
|
|
run: |
|
|
set -eu
|
|
tags="${IMAGE}:${VERSION}"
|
|
case "$VERSION" in
|
|
*-*) ;;
|
|
*) tags="${tags},${IMAGE}:latest" ;;
|
|
esac
|
|
echo "tags=$tags" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Download Linux musl binaries
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: binaries-pnpr-linux-*-musl
|
|
merge-multiple: true
|
|
|
|
- name: Stage binaries for the build context
|
|
id: stage
|
|
# The static musl binaries match what was just published to npm. The
|
|
# Dockerfile selects one via the build's TARGETARCH (amd64 / arm64) and
|
|
# re-verifies it against the build-time checksum surfaced here.
|
|
#
|
|
# Extract into a throwaway directory and move only the expected,
|
|
# checksum-verified regular files into the build context, so a malformed
|
|
# archive cannot escape it (path traversal, symlinks) and overwrite the
|
|
# Dockerfile or the staged binaries before they are pushed to GHCR.
|
|
run: |
|
|
set -eu
|
|
rm -rf extracted
|
|
mkdir extracted
|
|
for f in pnpr-linux-*-musl.tar.gz; do
|
|
tar -xzf "$f" -C extracted --no-same-owner
|
|
done
|
|
( cd extracted && sha256sum -c ../pnpr-linux-x64-musl.sha256 ../pnpr-linux-arm64-musl.sha256 )
|
|
for pair in x64:amd64 arm64:arm64; do
|
|
bin="extracted/pnpr-linux-${pair%:*}-musl"
|
|
test -f "$bin" && test ! -L "$bin"
|
|
mv "$bin" "pnpr/docker/pnpr-${pair#*:}"
|
|
done
|
|
{
|
|
echo "sha_amd64=$(awk '{print $1}' pnpr-linux-x64-musl.sha256)"
|
|
echo "sha_arm64=$(awk '{print $1}' pnpr-linux-arm64-musl.sha256)"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
|
|
|
- name: Login to GHCR
|
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
|
with:
|
|
context: ./pnpr/docker
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
tags: ${{ steps.tags.outputs.tags }}
|
|
build-args: |
|
|
PNPR_VERSION=${{ needs.plan.outputs.pnpr_version }}
|
|
PNPR_SHA256_AMD64=${{ steps.stage.outputs.sha_amd64 }}
|
|
PNPR_SHA256_ARM64=${{ steps.stage.outputs.sha_arm64 }}
|
|
provenance: mode=max
|
|
sbom: true
|