diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abd3ce5..31c4784 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,57 @@ jobs: # follow at lint time; not a real finding. shellcheck --shell=bash --exclude=SC1091 "${scripts[@]}" + commit-signature-verification: + runs-on: ubuntu-latest + steps: + - name: Signed postil-cli commit verifies + env: + CLI_REF: e448377c918c1d6fb91f9347d8be5153a4518480 + run: | + set -euo pipefail + export GNUPGHOME="$RUNNER_TEMP/postil-gnupg" + install -m 700 -d "$GNUPGHOME" + curl -fsSL --retry 3 --retry-connrefused --retry-delay 2 https://github.com/web-flow.gpg | gpg --batch --import + + verify_repo="$RUNNER_TEMP/postil-cli-signature" + rm -rf "$verify_repo" + git init -q "$verify_repo" + git -C "$verify_repo" fetch --quiet --depth=1 https://github.com/postil-dev/postil-cli "$CLI_REF" + git -C "$verify_repo" verify-commit "$CLI_REF" + + - name: Unsigned commit fails with clear error + run: | + set -euo pipefail + unsigned_repo="$RUNNER_TEMP/unsigned-source" + git init -q "$unsigned_repo" + git -C "$unsigned_repo" config user.name "Unsigned Commit" + git -C "$unsigned_repo" config user.email "unsigned@example.invalid" + git -C "$unsigned_repo" -c commit.gpgsign=false commit --quiet --allow-empty -m "unsigned fixture" + CLI_REF="$(git -C "$unsigned_repo" rev-parse HEAD)" + + verify_repo="$RUNNER_TEMP/unsigned-signature" + rm -rf "$verify_repo" + git init -q "$verify_repo" + git -C "$verify_repo" fetch --quiet --depth=1 "$unsigned_repo" "$CLI_REF" + + log="$RUNNER_TEMP/unsigned-verify.log" + set +e + ( + if ! git -C "$verify_repo" verify-commit "$CLI_REF"; then + echo "::error::postil-cli commit $CLI_REF is not signed by a trusted key, or git cannot access the signer's public key. Import the signer's public key before using source builds." + exit 42 + fi + ) >"$log" 2>&1 + status=$? + set -e + + if [ "$status" -ne 42 ]; then + cat "$log" + echo "::error::unsigned commit verification exited with status $status, expected 42" + exit 1 + fi + grep -F "is not signed by a trusted key, or git cannot access the signer's public key" "$log" + # The action has three composite steps: "Validate inputs", "Install # postil" (fetch + cosign-verify the pinned CLI, or build from source), # and "Review" (the actual LLM call). They are not separable via `uses:` @@ -140,6 +191,12 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + strategy: + fail-fast: false + matrix: + target: + - x86_64-unknown-linux-gnu + - x86_64-unknown-linux-musl steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 @@ -147,17 +204,17 @@ jobs: env: CLI_REF: 87f4bf08b63712d3600030a7c458f0b790cfc0d5 # postil-cli v0.1.1 CLI_RELEASE: v0.1.1 + TARGET: ${{ matrix.target }} run: | set -euo pipefail DEST="$RUNNER_TEMP/postil-bin" mkdir -p "$DEST" - target="x86_64-unknown-linux-gnu" base="https://github.com/postil-dev/postil-cli/releases/download/$CLI_RELEASE" - curl -fsSL -o "$DEST/postil.tar.gz" "$base/postil-$target.tar.gz" - curl -fsSL -o "$DEST/postil.tar.gz.sha256" "$base/postil-$target.tar.gz.sha256" - curl -fsSL -o "$DEST/postil.tar.gz.sig" "$base/postil-$target.tar.gz.sig" - curl -fsSL -o "$DEST/postil.tar.gz.pem" "$base/postil-$target.tar.gz.pem" + curl -fsSL --retry 3 --retry-connrefused --retry-delay 2 -o "$DEST/postil.tar.gz" "$base/postil-$TARGET.tar.gz" + curl -fsSL --retry 3 --retry-connrefused --retry-delay 2 -o "$DEST/postil.tar.gz.sha256" "$base/postil-$TARGET.tar.gz.sha256" + curl -fsSL --retry 3 --retry-connrefused --retry-delay 2 -o "$DEST/postil.tar.gz.sig" "$base/postil-$TARGET.tar.gz.sig" + curl -fsSL --retry 3 --retry-connrefused --retry-delay 2 -o "$DEST/postil.tar.gz.pem" "$base/postil-$TARGET.tar.gz.pem" expected=$(awk '{print $1}' "$DEST/postil.tar.gz.sha256") actual=$(sha256sum "$DEST/postil.tar.gz" | awk '{print $1}') diff --git a/README.md b/README.md index 4cc7525..cc98ad7 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,22 @@ gate without failing this job, set `soft-fail: true` and mark the `postil/gate` required in branch protection instead — that is the recommended setup: advisory comments never block, the gate check does. +## Platform support + +Linux runners use prebuilt CLI release artifacts when `cli-release` is set and +the release tag points at the pinned `cli-ref`. The action selects the target +from the runner CPU and libc: + +| Runner | Selected target | +|---|---| +| x86_64 glibc Linux | `x86_64-unknown-linux-gnu` | +| aarch64 glibc Linux | `aarch64-unknown-linux-gnu` | +| x86_64 Alpine/musl Linux | `x86_64-unknown-linux-musl` | +| aarch64 Alpine/musl Linux | `aarch64-unknown-linux-musl` | + +If a matching prebuilt artifact is not present on the pinned CLI release, the +action falls back to building `postil` from `cli-ref`. + ## Inputs | Input | Required | Description | diff --git a/action.yml b/action.yml index b42d70c..a8cb694 100644 --- a/action.yml +++ b/action.yml @@ -107,12 +107,46 @@ runs: if: inputs.cli-release != '' uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 + - name: Select postil target + id: target + shell: bash + run: | + set -euo pipefail + + target="" + if [ "$RUNNER_OS" = "Linux" ]; then + libc="gnu" + if command -v ldd >/dev/null 2>&1; then + ldd_output="$(ldd --version 2>&1 || true)" + if printf '%s\n' "$ldd_output" | grep -qi musl; then + libc="musl" + fi + elif [ "$libc" != "musl" ] \ + && { compgen -G "/lib/ld-musl-*.so.1" >/dev/null \ + || compgen -G "/usr/lib/ld-musl-*.so.1" >/dev/null; }; then + libc="musl" + fi + + case "$(uname -m)" in + x86_64) target="x86_64-unknown-linux-$libc" ;; + aarch64|arm64) target="aarch64-unknown-linux-$libc" ;; + esac + fi + + if [ -n "$target" ]; then + echo "Selected postil target $target" + else + echo "::notice::no prebuilt target for $RUNNER_OS/$(uname -m); falling back to source build" + fi + echo "target=$target" >> "$GITHUB_OUTPUT" + - name: Install postil shell: bash env: CLI_REF: ${{ inputs.cli-ref }} CLI_RELEASE: ${{ inputs.cli-release }} GH_TOKEN: ${{ inputs.github-token }} + POSTIL_TARGET: ${{ steps.target.outputs.target }} run: | set -euo pipefail DEST="$RUNNER_TEMP/postil-bin" @@ -133,13 +167,7 @@ runs: if [ "$tag_sha" = "$CLI_REF" ]; then # Prebuilt artifacts are Linux-only here; other runners fall back # to the source build rather than fetching a binary that cannot run. - target="" - if [ "$RUNNER_OS" = "Linux" ]; then - case "$(uname -m)" in - x86_64) target="x86_64-unknown-linux-gnu" ;; - aarch64|arm64) target="aarch64-unknown-linux-gnu" ;; - esac - fi + target="$POSTIL_TARGET" if [ -n "$target" ]; then base="https://github.com/postil-dev/postil-cli/releases/download/$CLI_RELEASE" if curl -fsSL --retry 3 --retry-connrefused --retry-delay 2 -o "$DEST/postil.tar.gz" "$base/postil-$target.tar.gz" \ @@ -168,8 +196,6 @@ runs: else echo "::warning::no prebuilt artifact (or signature) for $target on $CLI_RELEASE; falling back to source build" fi - else - echo "::notice::no prebuilt target for $RUNNER_OS/$(uname -m); falling back to source build" fi else echo "::warning::release $CLI_RELEASE points at ${tag_sha:-unknown}, not cli-ref $CLI_REF; falling back to source build" @@ -178,6 +204,20 @@ runs: if [ "$prebuilt_ok" = false ]; then echo "Building postil from postil-dev/postil-cli@$CLI_REF" + verify_repo="$RUNNER_TEMP/postil-cli-signature" + rm -rf "$verify_repo" + git init -q "$verify_repo" + if ! git -C "$verify_repo" fetch --quiet --depth=1 https://github.com/postil-dev/postil-cli "$CLI_REF"; then + echo "::error::failed to fetch postil-cli commit $CLI_REF for signature verification" + exit 1 + fi + # Source builds use Git's built-in GPG verification. The runner's + # git/GPG config must be able to resolve the signer's public key, + # for example from a keyserver or repository .git/config. + if ! git -C "$verify_repo" verify-commit "$CLI_REF"; then + echo "::error::postil-cli commit $CLI_REF is not signed by a trusted key, or git cannot access the signer's public key. Import the signer's public key before using source builds." + exit 1 + fi if ! command -v cargo >/dev/null 2>&1; then curl -fsSL --retry 3 --retry-connrefused --retry-delay 2 https://sh.rustup.rs | sh -s -- -y --profile minimal . "$HOME/.cargo/env"