ci(pnpr): publish a Docker image on release (#12619)

* ci(pnpr): publish a Docker image on release

Add a `docker` job to the pnpr release workflow that builds and pushes a
multi-arch (linux/amd64, linux/arm64) image to ghcr.io/pnpm/pnpr after the
npm packages are published.

The image is built from the static musl binaries the release job already
produces, so no Rust toolchain runs in the Docker build and the image
matches exactly what ships to npm. It is based on debian:stable-slim with
ca-certificates (pnpr reaches upstream registries over HTTPS via rustls'
platform cert verifier), runs as a non-root user, declares storage and
cache volumes, exposes 7677, and defaults to binding 0.0.0.0 so the server
is reachable from outside the container. The build self-checks that
`pnpr --version` matches the PNPR_VERSION build-arg.

Stable releases also move the mutable `latest` tag; prereleases (versions
containing a hyphen) only get the exact-version tag.

* ci(pnpr): drop unused id-token permission from docker job

The docker job uses only docker/build-push-action's built-in provenance
and SBOM attestations, which are pushed to GHCR via GITHUB_TOKEN. It does
not call actions/attest-build-provenance, so the OIDC id-token: write
permission was unused. Drop it to follow least privilege.

* ci(pnpr): verify staged binary checksum in the docker image build

The build job now pins each binary's SHA256 at build time and uploads it
with the artifact. The docker job verifies the staged binaries against
those checksums and passes them as build-args, and the Dockerfile
re-verifies the binary it copies before trusting it. A 'pnpr --version'
check only confirms reported metadata, so it cannot stand in for an
integrity check on the binary itself. Mirrors the existing pnpm image.

* ci(pnpr): extract release artifacts into an isolated directory

Extract the downloaded musl tarballs into a throwaway directory and move
only the expected, checksum-verified regular files into the Docker build
context. A malformed archive (path traversal or symlink entries) can no
longer escape into the workspace and overwrite the Dockerfile or staged
binaries before the image is pushed to GHCR.

* docs(pnpr): add a language to the docker README code fence

The image-name fenced block lacked a language identifier, tripping
markdownlint MD040. Tag it as text.
This commit is contained in:
Zoltan Kochan
2026-06-24 16:40:29 +02:00
committed by GitHub
parent e3f2be02f0
commit e2dab007f8
3 changed files with 238 additions and 2 deletions

View File

@@ -2,8 +2,10 @@ name: Release @pnpm/pnpr
# Manual-trigger only. Type the version to publish — the workflow patches
# pnpr/npm/pnpr/package.json with it before generating per-platform
# packages and publishing. No git tag is created and no GitHub release
# asset is uploaded; npm is the authoritative artifact store.
# packages and publishing. It then builds and pushes a multi-arch Docker
# image (ghcr.io/pnpm/pnpr) from the same static musl binaries. No git tag
# is created and no GitHub release asset is uploaded; npm and GHCR are the
# authoritative artifact stores.
on:
workflow_dispatch:
inputs:
@@ -125,6 +127,9 @@ jobs:
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@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
@@ -140,6 +145,7 @@ jobs:
path: |
*.zip
*.tar.gz
*.sha256
publish:
name: Publish
@@ -212,3 +218,99 @@ jobs:
for package in pnpr/npm/pnpr*; do
pnpm publish "$package/" --tag latest --access public --provenance --no-git-checks
done
docker:
name: Docker
runs-on: ubuntu-latest
needs:
- build
- publish
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/pnpr
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
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: ${{ inputs.version }}
run: |
set -eu
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Invalid version: '$VERSION' (expected semver like 0.2.3 or 0.2.3-rc.1)"
exit 1
fi
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-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=${{ inputs.version }}
PNPR_SHA256_AMD64=${{ steps.stage.outputs.sha_amd64 }}
PNPR_SHA256_ARM64=${{ steps.stage.outputs.sha_arm64 }}
provenance: mode=max
sbom: true