diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c9fb997..b6d1f4a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 @@ -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 < "/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}" \ @@ -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 \ @@ -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,