diff --git a/.devcontainer/sandbox-pnpm/devcontainer.json b/.devcontainer/sandbox-pnpm/devcontainer.json new file mode 100644 index 0000000..59a2fbd --- /dev/null +++ b/.devcontainer/sandbox-pnpm/devcontainer.json @@ -0,0 +1,19 @@ +{ + // Build args are read from the environment without defaults, so build-checks.yml + // supplies the values it just built the image with and the Dev Container CLI + // reuses those layers. A missing value fails the build instead of silently + // selecting a different version; AGENTS.md documents how to export them by hand. + "name": "sandbox-pnpm", + "build": { + "dockerfile": "../../pnpm/Dockerfile", + "args": { + "DEBIAN_TAG": "${localEnv:DEBIAN_TAG}", + "PNPM_VERSION": "${localEnv:PNPM_VERSION}" + } + }, + "runArgs": [ + "--cap-drop=ALL", + "--security-opt=no-new-privileges" + ], + "remoteUser": "dev" +} diff --git a/.github/workflows/build-checks.yml b/.github/workflows/build-checks.yml index 2a09eeb..7f47ed4 100644 --- a/.github/workflows/build-checks.yml +++ b/.github/workflows/build-checks.yml @@ -12,6 +12,8 @@ on: - '*/tofu-ls-checksums.txt' - '*/rustup-init-*.sha256' - '*/uv-*.sha256' + - '*/sigstore-trusted-root.json' + - '*/githubcli-archive-keyring.gpg' - .devcontainer/feature-** - .devcontainer/sandbox-** - scripts/build-config.sh @@ -29,6 +31,8 @@ on: - '*/tofu-ls-checksums.txt' - '*/rustup-init-*.sha256' - '*/uv-*.sha256' + - '*/sigstore-trusted-root.json' + - '*/githubcli-archive-keyring.gpg' - .devcontainer/feature-** - .devcontainer/sandbox-** - scripts/build-config.sh @@ -283,6 +287,11 @@ jobs: load: true tags: ${{ matrix.image }}:${{ matrix.variant }} build-args: ${{ steps.read.outputs.image_args }} + # Read by images whose build queries the GitHub API; an anonymous + # request is capped at 60 per hour per IP. Mounted at build time only, + # so it never lands in a layer. + secrets: | + github_token=${{ secrets.GITHUB_TOKEN }} # Cache scopes are arch-suffixed to match publish-image.yml so PR # builds and release dry-runs share the same cache entries. cache-from: type=gha,scope=${{ matrix.image }}-${{ matrix.variant }}-${{ matrix.arch }} diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 85473df..d7c98f9 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -177,6 +177,11 @@ jobs: push: false outputs: type=oci,dest=/tmp/image,tar=false build-args: ${{ needs.prepare.outputs.build_args }} + # Read by images whose build queries the GitHub API; an anonymous + # request is capped at 60 per hour per IP. Mounted at build time only, + # so it never lands in a layer, the SBOM, or the provenance. + secrets: | + github_token=${{ secrets.GITHUB_TOKEN }} labels: ${{ needs.prepare.outputs.labels }} annotations: ${{ needs.prepare.outputs.annotations }} sbom: true diff --git a/README.md b/README.md index 19c0fab..f0ba5a6 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ [![Attestation Checks](https://github.com/bare-devcontainer/images/actions/workflows/attest-check.yml/badge.svg?branch=main)](https://github.com/bare-devcontainer/images/actions/workflows/attest-check.yml) Minimal, multi-arch Dev Container images: a small Debian base, plus one image per stack for -Go, Node.js, Deno, Bun, Python, Rust, Zig, Terraform, and OpenTofu, and one with mise for -polyglot projects. Each carries only what its target stack needs, is built from a small set -of verified upstreams, and ships with SLSA provenance, a GitHub artifact attestation, and an -SBOM. +Go, Node.js, Deno, Bun, Python, Rust, Zig, Terraform, and OpenTofu, one with pnpm for Node.js +projects that pin their own runtime, and one with mise for polyglot projects. Each carries +only what its target stack needs, is built from a small set of verified upstreams, and ships +with SLSA provenance, a GitHub artifact attestation, and an SBOM. ## Quick start @@ -60,9 +60,9 @@ README says otherwise: extensions compile, but a library a project links against brings its own `-dev` package. - **No editors, shells, or CLI tooling beyond the basics.** `bash` and `vim-tiny` are present; editors, alternative shells, cloud CLIs, and linters are not. -- **No language toolchain in the version-manager images.** `mise`, `rustup`, and `uv` install - the version the project declares rather than one baked into the image. The `Not installed` - section of each image's README states exactly what is left out. +- **No language toolchain in the version-manager images.** `mise`, `pnpm`, `rustup`, and `uv` + install the version the project declares rather than one baked into the image. The + `Not installed` section of each image's README states exactly what is left out. There are two ways to add what a project needs on top: @@ -97,6 +97,7 @@ There are two ways to add what a project needs on top: | [mise](mise/README.md) | `ghcr.io/bare-devcontainer/mise` | Polyglot projects that pin their own runtimes | | [node](node/README.md) | `ghcr.io/bare-devcontainer/node` | Node.js, with Corepack instead of npm | | [opentofu](opentofu/README.md) | `ghcr.io/bare-devcontainer/opentofu` | Infrastructure as code with OpenTofu | +| [pnpm](pnpm/README.md) | `ghcr.io/bare-devcontainer/pnpm` | Node.js, with the runtime version managed by pnpm | | [rustup](rustup/README.md) | `ghcr.io/bare-devcontainer/rustup` | Rust, with the toolchain chosen by the project | | [terraform](terraform/README.md) | `ghcr.io/bare-devcontainer/terraform` | Infrastructure as code with Terraform | | [uv](uv/README.md) | `ghcr.io/bare-devcontainer/uv` | Python, with the interpreter managed by uv | diff --git a/pnpm/Dockerfile b/pnpm/Dockerfile new file mode 100644 index 0000000..b40c2c2 --- /dev/null +++ b/pnpm/Dockerfile @@ -0,0 +1,88 @@ +# syntax=docker/dockerfile:1@sha256:ecfaec9ed6d810b56388c508f4121597bfbba70d41a6dfeee4d8cad5f295fc32 +# check=skip=InvalidDefaultArgInFrom;error=true +# ARGs are intentionally required to prevent accidental builds with the wrong base image. + +ARG DEBIAN_TAG + +FROM ghcr.io/bare-devcontainer/debian:${DEBIAN_TAG} AS builder + +ARG PNPM_VERSION +ARG TARGETARCH + +# hadolint ignore=DL3002 # builder stage is a throwaway; never shipped or run +USER root + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + --mount=type=bind,source=githubcli-archive-keyring.gpg,target=/tmp/githubcli-archive-keyring.gpg,ro \ + install -D -m 644 /tmp/githubcli-archive-keyring.gpg /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=${TARGETARCH} signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get --no-install-recommends install -y gh + +RUN --mount=type=secret,id=github_token,required=false \ + --mount=type=bind,source=sigstore-trusted-root.json,target=/tmp/sigstore-trusted-root.json,ro \ + case "${TARGETARCH}" in \ + amd64) PNPM_ARCH="x64" ;; \ + arm64) PNPM_ARCH="arm64" ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && PNPM_TMPDIR="$(mktemp -d)" \ + # The published trusted root is pretty-printed; gh requires JSON Lines. + && jq -c . /tmp/sigstore-trusted-root.json > "${PNPM_TMPDIR}/trusted-root.jsonl" \ + && PNPM_TARBALL="pnpm-linux-${PNPM_ARCH}.tar.gz" \ + && wget -q -T 30 -t 3 -P "${PNPM_TMPDIR}" "https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/${PNPM_TARBALL}" \ + && PNPM_DIGEST="$(sha256sum "${PNPM_TMPDIR}/${PNPM_TARBALL}" | cut -d' ' -f1)" \ + # Anonymous GitHub API requests are capped at 60 per hour per IP, so the + # workflows pass a token. + && GH_API_ARGS=(--header="Accept: application/vnd.github+json" --header="X-GitHub-Api-Version: 2022-11-28") \ + && if [ -s /run/secrets/github_token ]; then \ + GH_API_ARGS+=(--header="Authorization: Bearer $(cat /run/secrets/github_token)"); \ + fi \ + && wget -q -T 30 -t 3 -O "${PNPM_TMPDIR}/attestations.json" "${GH_API_ARGS[@]}" \ + "https://api.github.com/repos/pnpm/pnpm/attestations/sha256:${PNPM_DIGEST}?predicate_type=https%3A%2F%2Fslsa.dev%2Fprovenance%2Fv1" \ + && jq -c '.attestations[].bundle' "${PNPM_TMPDIR}/attestations.json" > "${PNPM_TMPDIR}/bundles.jsonl" \ + && test -s "${PNPM_TMPDIR}/bundles.jsonl" \ + # pnpm publishes no signature and no checksum on a host separate from the + # release, so the archive is verified against its build provenance instead. + # --deny-self-hosted-runners is deliberately absent: pnpm builds its Linux + # release binaries on Blacksmith runners, so the signing certificate reports + # a self-hosted runner environment and the flag would reject every release. + && gh attestation verify "${PNPM_TMPDIR}/${PNPM_TARBALL}" \ + --bundle "${PNPM_TMPDIR}/bundles.jsonl" \ + --custom-trusted-root "${PNPM_TMPDIR}/trusted-root.jsonl" \ + --repo pnpm/pnpm \ + --signer-workflow "pnpm/pnpm/.github/workflows/release.yml" \ + --source-ref "refs/tags/v${PNPM_VERSION}" \ + # The whole archive is extracted rather than just the executable: pnpm + # resolves node-gyp from the dist/ directory beside its binary. + && mkdir -p /opt/pnpm \ + && tar xzf "${PNPM_TMPDIR}/${PNPM_TARBALL}" -C /opt/pnpm \ + && chmod 755 /opt/pnpm/pnpm \ + && rm -rf "${PNPM_TMPDIR}" \ + && test "$(/opt/pnpm/pnpm --version)" = "${PNPM_VERSION}" \ + && /opt/pnpm/pnpm completion bash > /usr/share/bash-completion/completions/pnpm + +FROM ghcr.io/bare-devcontainer/debian:${DEBIAN_TAG} + +COPY --from=builder /opt/pnpm /opt/pnpm +COPY --from=builder /usr/share/bash-completion/completions/pnpm /usr/share/bash-completion/completions/pnpm + +USER root + +RUN ln -s /opt/pnpm/pnpm /usr/local/bin/pnpm + +# pnpm refuses a global install when PNPM_HOME is not writable or its bin/ is +# not on PATH, so it belongs to the user; this is pnpm's default path on Linux. +ENV PNPM_HOME=/home/dev/.local/share/pnpm +ENV PATH=/home/dev/.local/share/pnpm/bin:${PATH} + +USER dev + +RUN mkdir -p \ + /home/dev/.cache/pnpm \ + /home/dev/.local/share/pnpm/bin diff --git a/pnpm/README.md b/pnpm/README.md new file mode 100644 index 0000000..dd65b14 --- /dev/null +++ b/pnpm/README.md @@ -0,0 +1,105 @@ +# pnpm + +Dev container image with [pnpm](https://pnpm.io/) installed, built on the [debian](../debian) +base image. pnpm installs the Node.js version the project asks for, so the runtime is chosen by +the project rather than baked into the image. + +## Image + +``` +ghcr.io/bare-devcontainer/pnpm: +``` + +Reference it from `.devcontainer/devcontainer.json`, pinning the digest as well as the tag: + +```json +{ + "image": "ghcr.io/bare-devcontainer/pnpm:12@sha256:" +} +``` + +## Dev Container Template + +A ready-to-use Dev Container template for this image is available at +[bare-devcontainer/templates](https://github.com/bare-devcontainer/templates/tree/main/src/pnpm). +It provides the recommended configuration for this image, including security hardening and +volume mounts that persist cache directories for faster rebuilds. + +## Tags + + +| Tags | Debian variant | +|------|----------------| +| `12.3.1-trixie`, `12-trixie`, `12.3.1`, `12`, `trixie` | trixie | +| `12.3.1-bookworm`, `12-bookworm`, `bookworm` | bookworm | + +Tags are also published with a date suffix on each build (e.g., `12.3.1-trixie-`). + + +The version in these tags is the version of pnpm itself, not of any Node.js runtime. + +## Installed software + +Everything from the [debian](../debian) base image, plus: + +- [pnpm](https://pnpm.io/) (`pnpm`), with bash completions installed + +pnpm is installed under `/opt/pnpm` with a `/usr/local/bin/pnpm` symlink, the layout of pnpm's +own [`ghcr.io/pnpm/pnpm`](https://pnpm.io/docker#official-pnpm-base-image) image. `PNPM_HOME` is +set to `~/.local/share/pnpm` and its `bin` directory is on `PATH`, so runtimes and globally +installed packages resolve without further setup. + +## Not installed + +- **No Node.js.** pnpm downloads and manages the runtime the project declares in + [`devEngines.runtime`](https://pnpm.io/package_json#devenginesruntime), so the version in use is + the one the project asks for. See [Working with runtimes](#working-with-runtimes) below. +- **No `npm`, `npx`, or Corepack.** pnpm covers those workflows, and installing a Node.js runtime + with pnpm deliberately leaves the bundled `npm` unextracted. Run `pnpm add -g npm` if a project + needs it. For a project whose package manager is not pnpm, use the [node](../node) image instead. +- **No global JavaScript tooling.** Linters, formatters, and test runners are left to the + project's own dependencies. + +## Working with runtimes + +Declare the runtime in the project's `package.json` and pnpm installs it on first use: + +```json +{ + "devEngines": { + "runtime": { "name": "node", "version": "^24.0.0", "onFail": "download" } + } +} +``` + +Inside that project a bare `node` runs the pinned version, because pnpm's global `node` is a shim +that dispatches to what the project asks for. Outside any project it runs the globally installed +version, which is set with [`pnpm runtime`](https://pnpm.io/cli/runtime): + +```sh +pnpm runtime set node lts -g +``` + +To install the project's runtime and dependencies when the container is created rather than on +first use, run `pnpm install` from a `postCreateCommand`. + +Two directories are worth persisting across container rebuilds as volumes: + +- `~/.local/share/pnpm` — the store, the managed runtimes, and the bins of globally installed + packages. All of it is re-downloaded on every rebuild unless this directory survives. +- `~/.cache/pnpm` — the metadata cache. + +## Supply chain + +pnpm is downloaded from [GitHub Releases](https://github.com/pnpm/pnpm/releases), the same archive +pnpm's own container image installs. pnpm publishes no detached signature and no checksum on a host +separate from the release, so the archive is verified against its +[build provenance attestation](https://github.com/pnpm/pnpm/blob/main/.github/workflows/release.yml) +instead: `gh attestation verify` checks that the archive's digest was signed by pnpm's release +workflow at the tag being installed. The Sigstore trusted root +(`pnpm/sigstore-trusted-root.json`) is committed to this repository and kept in sync by +`.github/workflows/update-material.yml`, so the attestation is checked against keys reviewed here +rather than keys fetched at build time. + +Note that this covers the pnpm binary only. Runtimes and packages that pnpm installs at runtime are +fetched from their own upstreams under pnpm's own verification, outside this image's build pipeline. diff --git a/pnpm/build.yaml b/pnpm/build.yaml new file mode 100644 index 0000000..117afa7 --- /dev/null +++ b/pnpm/build.yaml @@ -0,0 +1,43 @@ +description: "Dev Container image for Node.js development with pnpm, built on Debian" +materials: + - path: sigstore-trusted-root.json + url: https://raw.githubusercontent.com/sigstore/root-signing/main/targets/trusted_root.json + description: >- + Sigstore public good instance trusted root from sigstore/root-signing, + holding the Fulcio, Rekor, certificate transparency, and timestamp + authority keys. This committed file is the trust anchor the Dockerfile + verifies pnpm's build provenance attestation against, so the attestation + is checked against keys reviewed here rather than keys fetched at build + time. + - path: githubcli-archive-keyring.gpg + url: https://cli.github.com/packages/githubcli-archive-keyring.gpg + description: >- + GitHub CLI apt repository signing key, matching the fingerprints + published in cli/cli's docs/install_linux.md. Used only in the builder + stage, to install the `gh` that verifies the attestation; Debian's own + `gh` package predates the `gh attestation` command. +variants: + - variant: "12-trixie" + tags: + # renovate: datasource=github-releases depName=pnpm/pnpm versioning=semver extractVersion=^v(?.+) + - "12.3.1-trixie" + - "12-trixie" + # renovate: datasource=github-releases depName=pnpm/pnpm versioning=semver extractVersion=^v(?.+) + - "12.3.1" + - "12" + - "trixie" + debian_variant: "trixie" + build_args: + # renovate: datasource=github-releases depName=pnpm/pnpm versioning=semver extractVersion=^v(?.+) + PNPM_VERSION: "12.3.1" + + - variant: "12-bookworm" + tags: + # renovate: datasource=github-releases depName=pnpm/pnpm versioning=semver extractVersion=^v(?.+) + - "12.3.1-bookworm" + - "12-bookworm" + - "bookworm" + debian_variant: "bookworm" + build_args: + # renovate: datasource=github-releases depName=pnpm/pnpm versioning=semver extractVersion=^v(?.+) + PNPM_VERSION: "12.3.1" diff --git a/pnpm/githubcli-archive-keyring.gpg b/pnpm/githubcli-archive-keyring.gpg new file mode 100644 index 0000000..db21174 Binary files /dev/null and b/pnpm/githubcli-archive-keyring.gpg differ diff --git a/pnpm/sigstore-trusted-root.json b/pnpm/sigstore-trusted-root.json new file mode 100644 index 0000000..effb0a1 --- /dev/null +++ b/pnpm/sigstore-trusted-root.json @@ -0,0 +1,126 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27Z" + } + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + } + }, + { + "baseUrl": "https://log2025-1.rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=", + "keyDetails": "PKIX_ED25519", + "validFor": { + "start": "2025-09-23T00:00:00Z" + } + }, + "logId": { + "keyId": "zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" + } + ] + }, + "validFor": { + "start": "2021-03-07T03:20:29Z", + "end": "2022-12-31T23:59:59.999Z" + } + }, + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + } + ] + }, + "validFor": { + "start": "2022-04-13T20:06:15Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstore.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-03-14T00:00:00Z", + "end": "2022-10-31T23:59:59.999Z" + } + }, + "logId": { + "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" + } + }, + { + "baseUrl": "https://ctfe.sigstore.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-10-20T00:00:00Z" + } + }, + "logId": { + "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" + } + } + ], + "timestampAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore-tsa-selfsigned" + }, + "uri": "https://timestamp.sigstore.dev/api/v1/timestamp", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw==" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E" + } + ] + }, + "validFor": { + "start": "2025-07-04T00:00:00Z" + } + } + ] +} diff --git a/pnpm/smoke-test.sh b/pnpm/smoke-test.sh new file mode 100755 index 0000000..deff6fb --- /dev/null +++ b/pnpm/smoke-test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Verifying tool installations ===" +echo "pnpm: $(pnpm --version)" + +echo "=== Verifying no runtime or package manager is baked in ===" +# The image ships pnpm alone; the Node.js version comes from the project. +for cmd in node npm npx corepack; do + if command -v "$cmd" >/dev/null 2>&1; then + echo "$cmd should not be installed" >&2 + exit 1 + fi +done + +echo "=== Verifying the build-time verifier is not shipped ===" +if command -v gh >/dev/null 2>&1; then + echo "gh should stay in the builder stage" >&2 + exit 1 +fi + +echo "=== Verifying shell completions ===" +test -s /usr/share/bash-completion/completions/pnpm + +echo "=== Verifying pnpm home ===" +echo "PNPM_HOME: ${PNPM_HOME:-unset}" +test -w "${PNPM_HOME:?PNPM_HOME must be set}" +# Global installs go to $PNPM_HOME/bin, which pnpm requires to be on PATH. +case ":${PATH}:" in + *":${PNPM_HOME}/bin:"*) ;; + *) echo "${PNPM_HOME}/bin is not on PATH" >&2; exit 1 ;; +esac +[[ "$(pnpm store path)" == "${PNPM_HOME}/"* ]] + +echo "=== Verifying global runtime installation ===" +pnpm runtime set node 22 -g +node --version +[[ "$(node --version)" == v22.* ]] + +echo "=== Verifying project-pinned runtime ===" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +# Exercise the project workflow against a package authored on the fly (no npm +# registry packages, to avoid supply chain risk in this smoke test). +mkdir -p "$TMPDIR/local-cli" "$TMPDIR/project" + +cat > "$TMPDIR/local-cli/package.json" <<'EOF' +{ + "name": "local-cli", + "version": "1.0.0", + "bin": { "local-cli": "./cli.js" } +} +EOF +cat > "$TMPDIR/local-cli/cli.js" <<'EOF' +#!/usr/bin/env node +console.log("Hello from pnpm exec"); +EOF +chmod +x "$TMPDIR/local-cli/cli.js" + +# A project asking for a different major than the global one; pnpm downloads it +# on first use and its shims dispatch to it inside the project. +cat > "$TMPDIR/project/package.json" <<'EOF' +{ + "name": "smoketest", + "version": "1.0.0", + "private": true, + "devEngines": { + "runtime": { "name": "node", "version": "^24.0.0", "onFail": "download" } + } +} +EOF + +cd "$TMPDIR/project" +pnpm add ../local-cli +[ "$(pnpm exec local-cli)" = "Hello from pnpm exec" ] +[[ "$(pnpm exec node --version)" == v24.* ]] +[[ "$(node --version)" == v24.* ]] + +echo "=== Verifying the global runtime outside the project ===" +cd "$TMPDIR" +[[ "$(node --version)" == v22.* ]]