mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-27 17:35:30 -04:00
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:
106
.github/workflows/pnpr-release-to-npm.yml
vendored
106
.github/workflows/pnpr-release-to-npm.yml
vendored
@@ -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
|
||||
|
||||
56
pnpr/docker/Dockerfile
Normal file
56
pnpr/docker/Dockerfile
Normal file
@@ -0,0 +1,56 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# Refresh periodically: resolve with
|
||||
# docker buildx imagetools inspect debian:stable-slim --format '{{.Manifest.Digest}}'
|
||||
FROM debian:stable-slim@sha256:e51bfcd2226c480a5416730e0fa2c40df28b0da5ff562fc465202feeef2f1116
|
||||
|
||||
ARG PNPR_VERSION
|
||||
ARG TARGETARCH
|
||||
ARG PNPR_SHA256_AMD64
|
||||
ARG PNPR_SHA256_ARM64
|
||||
|
||||
# pnpr reaches upstream registries over HTTPS via rustls' platform
|
||||
# verifier, which reads the system CA bundle, so ca-certificates is required.
|
||||
RUN set -eu; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends ca-certificates; \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
useradd --system --create-home --home-dir /home/pnpr --shell /usr/sbin/nologin pnpr
|
||||
|
||||
# The build context stages the prebuilt static musl binary for each
|
||||
# architecture as pnpr-<TARGETARCH> (pnpr-amd64, pnpr-arm64).
|
||||
COPY pnpr-${TARGETARCH} /usr/local/bin/pnpr
|
||||
|
||||
# Verify the staged binary against the checksum pinned by the release job
|
||||
# before trusting it. `pnpr --version` only confirms reported metadata, so
|
||||
# it cannot stand in for an integrity check on the binary itself.
|
||||
RUN set -eu; \
|
||||
test -n "$PNPR_VERSION"; \
|
||||
case "$TARGETARCH" in \
|
||||
amd64) expected_sha="$PNPR_SHA256_AMD64" ;; \
|
||||
arm64) expected_sha="$PNPR_SHA256_ARM64" ;; \
|
||||
*) echo "unsupported architecture: $TARGETARCH" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
test -n "$expected_sha" || { echo "missing PNPR_SHA256_* build-arg for $TARGETARCH" >&2; exit 1; }; \
|
||||
echo "$expected_sha /usr/local/bin/pnpr" | sha256sum -c -; \
|
||||
chmod 0755 /usr/local/bin/pnpr; \
|
||||
installed="$(pnpr --version | awk '{print $NF}')"; \
|
||||
test "$installed" = "$PNPR_VERSION" || { \
|
||||
echo "pnpr version mismatch: expected $PNPR_VERSION, got $installed" >&2; \
|
||||
exit 1; \
|
||||
}; \
|
||||
mkdir -p /pnpr/storage /pnpr/cache; \
|
||||
chown -R pnpr:pnpr /pnpr
|
||||
|
||||
USER pnpr
|
||||
WORKDIR /pnpr
|
||||
|
||||
# Persist published packages and the disposable upstream mirror.
|
||||
VOLUME ["/pnpr/storage", "/pnpr/cache"]
|
||||
|
||||
# pnpr's default port. Bind to 0.0.0.0 so the server is reachable from
|
||||
# outside the container (the binary defaults to 127.0.0.1).
|
||||
EXPOSE 7677
|
||||
|
||||
ENTRYPOINT ["pnpr"]
|
||||
CMD ["--listen", "0.0.0.0:7677", "--storage", "/pnpr/storage", "--cache", "/pnpr/cache"]
|
||||
78
pnpr/docker/README.md
Normal file
78
pnpr/docker/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# pnpr Docker image
|
||||
|
||||
Official image for [`pnpr`](../), the pnpm-compatible npm registry server,
|
||||
published to GitHub Container Registry.
|
||||
|
||||
```text
|
||||
ghcr.io/pnpm/pnpr
|
||||
```
|
||||
|
||||
Based on `debian:stable-slim` with the standalone `pnpr` binary (static musl
|
||||
build, the same artifact published to npm). The container runs as a
|
||||
non-root `pnpr` user and listens on port `7677`.
|
||||
|
||||
## Tags
|
||||
|
||||
| Tag | Meaning |
|
||||
| ----------- | ------------------------------------------------------------ |
|
||||
| `<version>` | Exact, immutable (e.g. `0.2.3`). Includes prereleases. |
|
||||
| `latest` | Most recent stable release. Not updated for prereleases. |
|
||||
|
||||
## Supported platforms
|
||||
|
||||
`linux/amd64`, `linux/arm64`.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
docker run --rm -p 7677:7677 \
|
||||
-v pnpr-storage:/pnpr/storage \
|
||||
ghcr.io/pnpm/pnpr:latest
|
||||
```
|
||||
|
||||
The default command binds to `0.0.0.0:7677` and stores published packages in
|
||||
`/pnpr/storage` and the disposable upstream mirror in `/pnpr/cache` (both
|
||||
declared as volumes). To use a custom config, mount it and point `pnpr` at it:
|
||||
|
||||
```sh
|
||||
docker run --rm -p 7677:7677 \
|
||||
-v "$PWD/config.yaml:/pnpr/config.yaml:ro" \
|
||||
-v pnpr-storage:/pnpr/storage \
|
||||
ghcr.io/pnpm/pnpr:latest --listen 0.0.0.0:7677 --config /pnpr/config.yaml
|
||||
```
|
||||
|
||||
Then point pnpm at it:
|
||||
|
||||
```sh
|
||||
pnpm config set registry http://localhost:7677
|
||||
```
|
||||
|
||||
## Build locally
|
||||
|
||||
The build context expects the binary for each target architecture, staged as
|
||||
`pnpr-amd64` / `pnpr-arm64`. Build one with `cross` (or `cargo` for the host
|
||||
arch) and drop it in:
|
||||
|
||||
The build verifies the binary against a SHA256 checksum before trusting it,
|
||||
so pass the checksum for the architecture you're building:
|
||||
|
||||
```sh
|
||||
VERSION=0.2.3
|
||||
cross build -p pnpr --bin pnpr --release --target x86_64-unknown-linux-musl
|
||||
cp target/x86_64-unknown-linux-musl/release/pnpr pnpr/docker/pnpr-amd64
|
||||
docker buildx build \
|
||||
--build-arg PNPR_VERSION=${VERSION} \
|
||||
--build-arg PNPR_SHA256_AMD64=$(shasum -a 256 pnpr/docker/pnpr-amd64 | awk '{print $1}') \
|
||||
--platform linux/amd64 \
|
||||
--load \
|
||||
-t pnpr-test ./pnpr/docker
|
||||
docker run --rm pnpr-test --version
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
Images are built and pushed by the `docker` job in
|
||||
[`.github/workflows/pnpr-release-to-npm.yml`](../../.github/workflows/pnpr-release-to-npm.yml),
|
||||
which runs after the npm packages are published. The build verifies each
|
||||
staged binary against the SHA256 checksum pinned by the release job and fails
|
||||
if `pnpr --version` in the image doesn't match the `PNPR_VERSION` build-arg.
|
||||
Reference in New Issue
Block a user