Files
pnpm/.github/workflows/release.yml
dependabot[bot] d67385eac1 chore(deps): bump the github-actions group across 1 directory with 12 updates
Bumps the github-actions group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [dorny/paths-filter](https://github.com/dorny/paths-filter) | `4.0.1` | `4.0.2` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `4.1.0` | `4.2.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.1.0` | `4.2.0` |
| [docker/login-action](https://github.com/docker/login-action) | `4.2.0` | `4.4.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.2.0` | `7.3.0` |
| [taiki-e/install-action](https://github.com/taiki-e/install-action) | `2.82.5` | `2.82.9` |
| [crate-ci/typos](https://github.com/crate-ci/typos) | `1.47.2` | `1.48.0` |
| [garnet-org/action](https://github.com/garnet-org/action) | `2.0.2` | `2.2.0` |
| [warpdotdev/oz-agent-action](https://github.com/warpdotdev/oz-agent-action) | `1.0.21` | `1.0.23` |



Updates `dorny/paths-filter` from 4.0.1 to 4.0.2
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](fbd0ab8f3e...7b450fff21)

Updates `github/codeql-action/init` from 4.36.2 to 4.36.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](8aad20d150...54f647b7e1)

Updates `github/codeql-action/autobuild` from 4.36.2 to 4.36.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](8aad20d150...54f647b7e1)

Updates `github/codeql-action/analyze` from 4.36.2 to 4.36.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](8aad20d150...54f647b7e1)

Updates `docker/setup-qemu-action` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](06116385d9...96fe6ef7f3)

Updates `docker/setup-buildx-action` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](d7f5e7f509...bb05f3f551)

Updates `docker/login-action` from 4.2.0 to 4.4.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](650006c6eb...af1e73f918)

Updates `docker/build-push-action` from 7.2.0 to 7.3.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](f9f3042f7e...53b7df96c9)

Updates `taiki-e/install-action` from 2.82.5 to 2.82.9
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](bffeee26d4...4684b84056)

Updates `crate-ci/typos` from 1.47.2 to 1.48.0
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](37bb98842b...bee27e3a4f)

Updates `garnet-org/action` from 2.0.2 to 2.2.0
- [Release notes](https://github.com/garnet-org/action/releases)
- [Commits](2b7fc9d79b...3d47f4a900)

Updates `warpdotdev/oz-agent-action` from 1.0.21 to 1.0.23
- [Release notes](https://github.com/warpdotdev/oz-agent-action/releases)
- [Commits](1922f22c00...8ba4e14206)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/build-push-action
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/login-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: garnet-org/action
  dependency-version: 2.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: taiki-e/install-action
  dependency-version: 2.82.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: warpdotdev/oz-agent-action
  dependency-version: 1.0.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-13 09:30:55 +00:00

717 lines
29 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:
- "v*.*.*"
# Manual trigger for rerunning a tag release. Dispatching this workflow from a
# branch fails before publishing because release artifacts are only built on
# version tags.
workflow_dispatch:
jobs:
validate-release-ref:
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Validate release ref
run: |
case "${GITHUB_REF}" in
refs/tags/v*.*.*) ;;
*)
echo "::error::The release workflow must run from a version tag like v11.9.0. Current ref: ${GITHUB_REF}"
exit 1
;;
esac
plan:
name: Plan (which products need publishing)
needs: validate-release-ref
runs-on: ubuntu-latest
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"
release:
name: Release the TypeScript pnpm CLI
needs:
- validate-release-ref
- plan
if: needs.plan.outputs.ts == 'true'
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 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.
runs-on: macos-latest
environment: release
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: garnet-org/action@3d47f4a9004f7356c980a0e8d420ef5984750e3c # v2.2.0
with:
api_token: ${{ secrets.GARNET_API_TOKEN }}
- name: Install pnpm and Node
uses: pnpm/setup@77cf06832101b3ac8c65caaf76a21643936d07a4
with:
runtime: node@26.4.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: 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`.
# The exe artifacts must be built before the publish, so they're built here too.
run: |
pn --filter=@pnpm/exe run build-artifacts
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'
permissions:
contents: read
id-token: write # needed for actions/attest-build-provenance
attestations: write
strategy:
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
code-target: win32-x64
- os: windows-latest
target: aarch64-pc-windows-msvc
code-target: win32-arm64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
code-target: linux-x64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
code-target: linux-arm64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
code-target: linux-x64-musl
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
code-target: linux-arm64-musl
- os: macos-latest
target: x86_64-apple-darwin
code-target: darwin-x64
- os: 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@4684b8405694ae9dd42c9f39ba901a70ae83f4a3 # v2.82.9
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
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
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
# 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: ubuntu-latest
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'
permissions:
contents: read
id-token: write # needed for actions/attest-build-provenance
attestations: write
strategy:
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
code-target: win32-x64
- os: windows-latest
target: aarch64-pc-windows-msvc
code-target: win32-arm64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
code-target: linux-x64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
code-target: linux-arm64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
code-target: linux-x64-musl
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
code-target: linux-arm64-musl
- os: macos-latest
target: x86_64-apple-darwin
code-target: darwin-x64
- os: 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@4684b8405694ae9dd42c9f39ba901a70ae83f4a3 # v2.82.9
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
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
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: ls *.gz | xargs -i tar xf {}
- 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: 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: ubuntu-latest
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@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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