Files
pnpm/.github/workflows/docker.yml
Zoltan Kochan 9d3b6c58ae feat(docker): publish an Alpine variant of the pnpm image (#17069)
Adds `<version>-alpine`, `<major>-alpine`, and `alpine` tags built from
docker/Dockerfile.alpine with the musl release tarball. Alpine users can
use the image as a base or copy /opt/pnpm into `node:*-alpine` without
fetching the installer from get.pnpm.io, which was returning 403 to
bursts of concurrent CI jobs.

The download, checksum, and version check move into
docker/install-pnpm.sh, shared by both Dockerfiles. The Alpine image
also installs libstdc++ and libgcc, which the musl Node.js builds
installed by `pnpm runtime` need. The Alpine build runs only for
releases that ship musl tarballs for both x64 and arm64, so v11 and
v10 releases skip it.

Every release used to publish the `latest` and `<major>` tags, so a v11
maintenance release moved `latest` back to v11. The builds now push only
the exact version tags. A final step lists the stable releases and, with
`docker buildx imagetools create`, points `<major>` at the highest
version in that major and `latest` at the highest version overall. The
Alpine tags follow the same rule. Deciding after the builds sees every
release of a burst, since lines are published seconds apart. A
concurrency group was rejected because GitHub cancels a pending run when
a third one queues. The selection lives in
.github/scripts/docker-image-tags.sh with a test run by CI, and fails on
an empty release list.

The v12 CI docs now run the Bitbucket Pipelines and GitLab CI examples
in `ghcr.io/pnpm/pnpm:12` instead of downloading the installer, and
recommend the image for CI that runs jobs in containers. CircleCI keeps
the installer because its checkout step needs git and ssh.

The CI examples that still use the standalone script fall back to the
latest pnpm/get.pnpm.io installer release when get.pnpm.io fails. They
run the script only after a download succeeds.

Related to pnpm/pnpm#16973.
2026-10-18 01:22:12 +02:00

152 lines
5.8 KiB
YAML

name: Docker
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "pnpm version to build (without leading v)"
required: true
prerelease:
description: "Treat as prerelease (skips mutable tags)"
type: boolean
default: false
jobs:
build:
name: Build
if: github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, 'v')
runs-on: blacksmith-8vcpu-ubuntu-2404
environment: release
permissions:
contents: read
packages: write
id-token: write
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/pnpm
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Resolve release metadata
id: meta
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_PRERELEASE: ${{ inputs.prerelease }}
run: |
set -eu
if [ "$EVENT_NAME" = "release" ]; then
version="${RELEASE_TAG#v}"
prerelease="$RELEASE_PRERELEASE"
else
version="${INPUT_VERSION#v}"
prerelease="$INPUT_PRERELEASE"
fi
case "$version" in
*[!0-9A-Za-z.+-]*) echo "invalid version: $version" >&2; exit 1 ;;
[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "invalid version: $version" >&2; exit 1 ;;
esac
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
- name: Compute pnpm tarball checksums
id: checksums
env:
VERSION: ${{ steps.meta.outputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
base="https://github.com/pnpm/pnpm/releases/download/v${VERSION}"
for pair in amd64:x64 arm64:arm64; do
key="${pair%:*}"
name="${pair#*:}"
sha="$(curl -fsSL --retry 3 --retry-delay 2 "${base}/pnpm-linux-${name}.tar.gz" \
| sha256sum | awk '{print $1}')"
echo "sha_${key}=${sha}" >> "$GITHUB_OUTPUT"
done
# The Alpine image is built only for release lines that ship musl
# tarballs for both architectures. Older lines lack the arm64 one.
assets="$(gh release view "v${VERSION}" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name')"
alpine=true
for name in x64 arm64; do
printf '%s\n' "$assets" | grep -qxF "pnpm-linux-${name}-musl.tar.gz" || alpine=false
done
echo "alpine=${alpine}" >> "$GITHUB_OUTPUT"
if [ "$alpine" = true ]; then
for pair in amd64:x64 arm64:arm64; do
key="${pair%:*}"
name="${pair#*:}"
sha="$(curl -fsSL --retry 3 --retry-delay 2 "${base}/pnpm-linux-${name}-musl.tar.gz" \
| sha256sum | awk '{print $1}')"
echo "sha_musl_${key}=${sha}" >> "$GITHUB_OUTPUT"
done
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: ./docker
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
build-args: |
PNPM_VERSION=${{ steps.meta.outputs.version }}
PNPM_SHA256_AMD64=${{ steps.checksums.outputs.sha_amd64 }}
PNPM_SHA256_ARM64=${{ steps.checksums.outputs.sha_arm64 }}
provenance: mode=max
sbom: true
- name: Build and push Alpine
if: steps.checksums.outputs.alpine == 'true'
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: ./docker
file: ./docker/Dockerfile.alpine
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}-alpine
build-args: |
PNPM_VERSION=${{ steps.meta.outputs.version }}
PNPM_SHA256_AMD64=${{ steps.checksums.outputs.sha_musl_amd64 }}
PNPM_SHA256_ARM64=${{ steps.checksums.outputs.sha_musl_arm64 }}
provenance: mode=max
sbom: true
# Runs after the builds so that every release published in the same
# burst is listed by the time this run decides which tags it may move.
- name: Move mutable tags
if: steps.meta.outputs.prerelease != 'true'
env:
VERSION: ${{ steps.meta.outputs.version }}
ALPINE: ${{ steps.checksums.outputs.alpine }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
releases="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
--jq '.[] | select(.draft == false and .prerelease == false) | .tag_name')"
moves="$(.github/scripts/docker-image-tags.sh "$IMAGE" "$VERSION" "$ALPINE" <<< "$releases")"
while read -r target source; do
[ -n "$target" ] || continue
docker buildx imagetools create --tag "$target" "$source"
done <<< "$moves"