Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ jobs:
if: needs.changes.outputs.linux == 'true'
runs-on: ubuntu-26.04
timeout-minutes: 60
permissions:
attestations: read
contents: read
steps:
- name: Check out image sources
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -105,10 +108,69 @@ jobs:
context: .
file: linux/Dockerfile
target: validation
outputs: type=cacheonly
load: true
tags: atrinik-linux-validation:ci
cache-from: type=gha,scope=linux-build-image
cache-to: type=gha,mode=max,scope=linux-build-image,ignore-error=true

- name: Verify non-root gh-stack contract
run: |
docker run --rm \
--user ubuntu \
--env HOME=/home/ubuntu \
--env GH_TOKEN=unused \
atrinik-linux-validation:ci \
sh -eu -c '
test "$(gh version | sed -n "1s/^gh version \([^ ]*\) .*/\1/p")" = 2.97.0
test "$(git version)" = "git version 2.53.0"
test "$(gh extension list)" = "gh stack$(printf "\t")github/gh-stack$(printf "\t")v0.1.0"
grep -Fx "owner: github" /home/ubuntu/.local/share/gh/extensions/gh-stack/manifest.yml
grep -Fx "name: gh-stack" /home/ubuntu/.local/share/gh/extensions/gh-stack/manifest.yml
grep -Fx "tag: v0.1.0" /home/ubuntu/.local/share/gh/extensions/gh-stack/manifest.yml
grep -Fx "ispinned: true" /home/ubuntu/.local/share/gh/extensions/gh-stack/manifest.yml
test "$(gh stack --version)" = "gh stack version 0.1.0"
gh stack --help >/dev/null
echo "358552dd7dce0a46ce153fe196270cec482b84f080947890aad4061a8d44bc0b /home/ubuntu/.local/share/gh/extensions/gh-stack/gh-stack" | sha256sum -c -
'

- name: Verify gh-stack release attestation
env:
GH_TOKEN: ${{ github.token }}
run: |
install -d "${RUNNER_TEMP}/gh-stack-attestation"
gh release download v2.97.0 \
--repo cli/cli \
--pattern gh_2.97.0_linux_amd64.tar.gz \
--dir "${RUNNER_TEMP}/gh-stack-attestation"
gh release download v0.1.0 \
--repo github/gh-stack \
--pattern linux-amd64 \
--dir "${RUNNER_TEMP}/gh-stack-attestation"
cat <<EOF | sha256sum -c -
a2c9b8497e1f85b1ad0dfcb78b5a622e098801b8e461e459e88e1ee12f018112 ${RUNNER_TEMP}/gh-stack-attestation/gh_2.97.0_linux_amd64.tar.gz
358552dd7dce0a46ce153fe196270cec482b84f080947890aad4061a8d44bc0b ${RUNNER_TEMP}/gh-stack-attestation/linux-amd64
EOF
tar -xzf \
"${RUNNER_TEMP}/gh-stack-attestation/gh_2.97.0_linux_amd64.tar.gz" \
-C "${RUNNER_TEMP}/gh-stack-attestation"
"${RUNNER_TEMP}/gh-stack-attestation/gh_2.97.0_linux_amd64/bin/gh" \
attestation verify \
"${RUNNER_TEMP}/gh-stack-attestation/linux-amd64" \
--repo github/gh-stack \
--signer-workflow github/gh-stack/.github/workflows/release.yml \
--source-ref refs/tags/v0.1.0 \
--source-digest a1b4a3d4d0bcde9ec3a78ab99b2d63af121857a9 \
--signer-digest a1b4a3d4d0bcde9ec3a78ab99b2d63af121857a9 \
--format json \
| jq -e '
any(
.[].verificationResult.statement.subject[];
.name == "linux-amd64"
and .digest.sha256 ==
"358552dd7dce0a46ce153fe196270cec482b84f080947890aad4061a8d44bc0b"
)
'

windows:
name: Windows cross-build images
needs: changes
Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ packages and all four variants remain matched.

## Local validation

The isolated `gh extension list` smoke needs a non-empty `GH_TOKEN` environment
value to suppress GitHub CLI's login prerequisite, but it does not contact the
API to list the installed extension. A non-secret placeholder is sufficient and
no credential is supplied to the image build or runtime container.

```sh
docker build --check --file linux/Dockerfile .
docker build --check --file windows/Dockerfile .
Expand All @@ -79,7 +84,17 @@ docker build --file windows/Dockerfile \
--tag atrinik-windows-check .

docker run --rm atrinik-linux-build clang --version
docker run --rm atrinik-linux-build gh --version
docker run --rm --user ubuntu --env HOME=/home/ubuntu \
atrinik-linux-build gh version
docker run --rm --user ubuntu --env HOME=/home/ubuntu \
--env GH_TOKEN=unused atrinik-linux-build gh extension list
docker run --rm --user ubuntu --env HOME=/home/ubuntu \
atrinik-linux-build gh stack --version
docker run --rm --user ubuntu --env HOME=/home/ubuntu \
atrinik-linux-build gh stack --help
docker run --rm --user ubuntu --env HOME=/home/ubuntu \
atrinik-linux-build sha256sum \
/home/ubuntu/.local/share/gh/extensions/gh-stack/gh-stack
docker run --rm atrinik-linux-build actionlint --version
docker run --rm atrinik-linux-build devcontainer --version
docker run --rm atrinik-linux-build go version
Expand Down Expand Up @@ -194,6 +209,26 @@ snapshot update must pass before any remaining package is installed. Snapshot
sources disable metadata expiry so the fixed timestamp remains rebuildable;
APT still verifies its signed metadata and package hashes.

The broad Linux image installs the official GitHub CLI 2.97.0 archive, pinned
by its published SHA-256, as the sole `gh` executable. It also installs the
official `github/gh-stack` extension v0.1.0 for the non-root `ubuntu` user with
`gh extension install github/gh-stack --pin v0.1.0`. The image validates the
extension's pinned manifest, ordinary `gh stack` dispatch, help output, and
the attested `linux-amd64` binary SHA-256
`358552dd7dce0a46ce153fe196270cec482b84f080947890aad4061a8d44bc0b`.
Both projects are MIT licensed; their pinned license texts are installed under
`/usr/local/share/licenses/`.

The upstream release attestation ties `github/gh-stack`'s
`.github/workflows/release.yml`, `refs/tags/v0.1.0`, and source commit
`a1b4a3d4d0bcde9ec3a78ab99b2d63af121857a9` to that asset digest. Pull-request
validation verifies those coordinates with the trusted runner's
`gh attestation verify` after the image build. The repository token is never
passed into the review-built container. No GitHub credential enters the
Dockerfile, build arguments, image layers, or published image. Upgrades require
reviewed version, checksum, source, license, and attestation changes; do not run
`gh extension upgrade stack --force` as a runtime substitute.

The Linux image includes the pinned replacement toolchains recorded in
[`toolchains.json`](toolchains.json): Go, Rust/rustup, Protobuf/protoc, Buf,
Node.js, pnpm, Syft, and Trivy. It also includes GCC, Clang with compiler-rt,
Expand Down
87 changes: 84 additions & 3 deletions linux/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
flex \
fzf \
gdb \
gh \
git \
iproute2 \
jq \
Expand Down Expand Up @@ -79,6 +78,13 @@ ARG ACTIONLINT_SHA256=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b3
ARG BUF_VERSION=1.72.0
ARG BUF_SHA256=8720830e26a733da55bb89bcd3cb44849c0965fc0c44fb5d691cccdc64dca5af
ARG DEVCONTAINER_CLI_VERSION=0.88.0
ARG GH_VERSION=2.97.0
ARG GH_SHA256=a2c9b8497e1f85b1ad0dfcb78b5a622e098801b8e461e459e88e1ee12f018112
ARG GH_STACK_VERSION=0.1.0
ARG GH_STACK_SHA256_AMD64=358552dd7dce0a46ce153fe196270cec482b84f080947890aad4061a8d44bc0b
ARG GH_STACK_SOURCE_COMMIT=a1b4a3d4d0bcde9ec3a78ab99b2d63af121857a9
ARG GH_STACK_LICENSE_SHA256=2510b446bc1f0cf9702453075d20cd88631e20e5642658edb7325d9c1eb534f7
ARG GIT_VERSION=2.53.0
ARG GO_VERSION=1.26.5
ARG GO_SHA256=5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053
ARG NODE_VERSION=24.18.1
Expand Down Expand Up @@ -155,6 +161,25 @@ RUN curl --fail --location --silent --show-error \
&& tar -xzf /tmp/trivy.tar.gz -C /usr/local/bin trivy \
&& rm -f /tmp/syft.tar.gz /tmp/trivy.tar.gz

# Install one checksum-pinned GitHub CLI from its immutable v2.97.0 release.
# The archive carries cli/cli's MIT license; Ubuntu's distro package is omitted
# above so SBOM and executable ownership remain unambiguous.
RUN curl --fail --location --silent --show-error \
"https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" \
--output /tmp/gh.tar.gz \
&& echo "${GH_SHA256} /tmp/gh.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/gh.tar.gz -C /tmp \
&& install -m 0755 \
"/tmp/gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh \
&& install -D -m 0644 \
"/tmp/gh_${GH_VERSION}_linux_amd64/LICENSE" \
/usr/local/share/licenses/gh/LICENSE \
&& rm -rf /tmp/gh.tar.gz "/tmp/gh_${GH_VERSION}_linux_amd64" \
&& test "$(command -v gh)" = /usr/local/bin/gh \
&& test ! -e /usr/bin/gh \
&& test "$(gh version | sed -n '1s/^gh version \([^ ]*\) .*/\1/p')" \
= "${GH_VERSION}"

# Ubuntu does not currently package SDL3_mixer. Build the checksum-pinned
# release with static libogg, libopus, and libopusfile inputs so OPUS, WAV,
# and built-in Vorbis remain available without host codec libraries.
Expand Down Expand Up @@ -212,6 +237,30 @@ RUN install -d -o "${USERNAME}" -g "${USERNAME}" "/home/${USERNAME}/.codex" \
> "/etc/sudoers.d/${USERNAME}" \
&& chmod 0440 "/etc/sudoers.d/${USERNAME}"

# gh-stack v0.1.0 is a non-immutable release tag, so retain the supported pin
# while independently enforcing its attested linux-amd64 asset digest. The
# source commit is the verified release-workflow signer/source digest for
# github/gh-stack/.github/workflows/release.yml at refs/tags/v0.1.0.
RUN HOME="/home/${USERNAME}" runuser -u "${USERNAME}" -- \
gh extension install github/gh-stack --pin "v${GH_STACK_VERSION}" \
&& echo "${GH_STACK_SHA256_AMD64} /home/${USERNAME}/.local/share/gh/extensions/gh-stack/gh-stack" \
| sha256sum -c - \
&& runuser -u "${USERNAME}" -- grep -Fx 'owner: github' \
"/home/${USERNAME}/.local/share/gh/extensions/gh-stack/manifest.yml" \
&& runuser -u "${USERNAME}" -- grep -Fx 'name: gh-stack' \
"/home/${USERNAME}/.local/share/gh/extensions/gh-stack/manifest.yml" \
&& runuser -u "${USERNAME}" -- grep -Fx "tag: v${GH_STACK_VERSION}" \
"/home/${USERNAME}/.local/share/gh/extensions/gh-stack/manifest.yml" \
&& runuser -u "${USERNAME}" -- grep -Fx 'ispinned: true' \
"/home/${USERNAME}/.local/share/gh/extensions/gh-stack/manifest.yml" \
&& curl --fail --location --silent --show-error \
"https://raw.githubusercontent.com/github/gh-stack/${GH_STACK_SOURCE_COMMIT}/LICENSE" \
--output /usr/local/share/licenses/gh-stack.LICENSE \
&& echo "${GH_STACK_LICENSE_SHA256} /usr/local/share/licenses/gh-stack.LICENSE" \
| sha256sum -c - \
&& rm -rf "/home/${USERNAME}/.cache/gh" \
"/home/${USERNAME}/.local/state/gh"

RUN clang --version \
&& test "$(go env GOVERSION)" = "go${GO_VERSION}" \
&& test "$(rustc --version | cut -d' ' -f2)" = "${RUST_VERSION}" \
Expand All @@ -222,7 +271,17 @@ RUN clang --version \
&& test "$(protoc-gen-go --version)" \
= "protoc-gen-go v${PROTOC_GEN_GO_VERSION}" \
&& test "$(protoc-gen-prost --version)" = "${PROTOC_GEN_PROST_VERSION}" \
&& gh --version \
&& test "$(runuser -u "${USERNAME}" -- env HOME="/home/${USERNAME}" \
gh version | sed -n '1s/^gh version \([^ ]*\) .*/\1/p')" \
= "${GH_VERSION}" \
&& test "$(runuser -u "${USERNAME}" -- env HOME="/home/${USERNAME}" \
git version)" = "git version ${GIT_VERSION}" \
&& test "$(runuser -u "${USERNAME}" -- env HOME="/home/${USERNAME}" \
gh stack --version)" = "gh stack version ${GH_STACK_VERSION}" \
&& runuser -u "${USERNAME}" -- env HOME="/home/${USERNAME}" \
gh stack --help >/dev/null \
&& echo "${GH_STACK_SHA256_AMD64} /home/${USERNAME}/.local/share/gh/extensions/gh-stack/gh-stack" \
| runuser -u "${USERNAME}" -- sha256sum -c - \
&& llvm-config --version \
&& syft version \
&& trivy --version \
Expand Down Expand Up @@ -261,7 +320,29 @@ RUN actionlint \
| jq -e --slurpfile inventory /workspace/audio-toolchain.json \
'([.artifacts[] | [.name, .version]] | sort) == \
(($inventory[0].dependencies + [$inventory[0].sdl_mixer]) \
| map([.name, .version]) | sort)'
| map([.name, .version]) | sort)' \
&& syft dir:/ --select-catalogers +sbom-cataloger -o spdx-json \
| jq -e --slurpfile inventory /workspace/audio-toolchain.json ' \
.spdxVersion == "SPDX-2.3" \
and any( \
.packages[]; \
.name == "github.com/cli/cli/v2" \
and .versionInfo == "v2.97.0" \
) \
and any( \
.packages[]; \
.name == "github.com/github/gh-stack" \
and (.versionInfo | startswith("v0.1.0")) \
) \
and ( \
[.packages[] | [.name, .versionInfo]] \
| contains(( \
$inventory[0].dependencies \
+ [$inventory[0].sdl_mixer] \
| map([.name, .version]) \
)) \
) \
'

# Classic Check uses a separate task-focused target. It intentionally starts
# from the pinned Ubuntu base instead of inheriting the replacement toolchain,
Expand Down