From 9853570e107db3f4c5aec5b3266ed2c9e9703902 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:17:26 +0100 Subject: [PATCH] feat(release): signed, SHA-pinned musl release with build provenance Rewrite release.yml so a v* tag produces a real, verifiable artefact: - build: checkout, dtolnay/rust-toolchain at 1.85.0 (the workspace MSRV) with the x86_64-unknown-linux-musl target, musl-tools, then `cargo build --locked --release -p squabble-cli --target x86_64-unknown-linux-musl`. Stage dist/squabble-x86_64-linux-musl and dist/SHA256SUMS (bare names), fail if either is missing or empty, and upload dist/ as one artifact. - release: needs build; job permissions are exactly contents: write, id-token: write, attestations: write. Download dist/, refuse an empty or incomplete dist/, attest build provenance for dist/* with no skip condition, then publish dist/* with generated release notes. - Drop the git-cliff changelog job and the language auto-detect stub. - Top-level permissions are contents: read only. Every uses: is pinned by full commit SHA with the tag in a comment. actions.lock is edited by hand for the release.yml entry only: its list now names the six SHA refs, three SHA-keyed dependency records are added (download-artifact, attest-build-provenance with its nested actions/attest, softprops/action-gh-release), and the two tag-keyed records that only release.yml used (attest-build-provenance@v4.2.2, softprops/action-gh-release@v3.0.3) are pruned. The lock stays transitively closed. Justfile release-tag now creates a signed tag (git tag -s) so the Immutable-Tags required_signatures rule does not depend on local tag.gpgsign config. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015bTuGfwCcvjrmNFejydTML --- .github/workflows/actions.lock | 23 ++-- .github/workflows/release.yml | 196 ++++++++++++--------------------- Justfile | 2 +- 3 files changed, 86 insertions(+), 135 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index afd1eb2..39629f2 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -40,10 +40,12 @@ workflows: - 'actions/checkout@v7.0.1' - 'editorconfig-checker/action-editorconfig-checker@v3.0.0' '.github/workflows/release.yml': - - 'actions/attest-build-provenance@v4.2.2' - - 'actions/checkout@v7.0.1' - - 'actions/upload-artifact@v7.0.1' - - 'softprops/action-gh-release@v3.0.3' + - 'actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + - 'softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64' '.github/workflows/rhodibot.yml': - 'actions/checkout@v7.0.1' '.github/workflows/runtime-policy.yml': @@ -73,8 +75,8 @@ dependencies: commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' owner_id: 580492 repo_id: 298565987 - 'actions/attest-build-provenance@v4.2.2': - ref: 'v4.2.2' + 'actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8': + ref: '4d101475d8b20a2381f78447822ac1eab6504dd8' commit: 'sha1-4d101475d8b20a2381f78447822ac1eab6504dd8' owner_id: 44036562 repo_id: 760702757 @@ -105,6 +107,11 @@ dependencies: commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 + 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c': + ref: '3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' + commit: 'sha1-3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' + owner_id: 44036562 + repo_id: 192626254 'actions/download-artifact@v8.0.1': ref: 'v8.0.1' commit: 'sha1-3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' @@ -238,8 +245,8 @@ dependencies: commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' owner_id: 108928776 repo_id: 512644635 - 'softprops/action-gh-release@v3.0.3': - ref: 'v3.0.3' + 'softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64': + ref: 'efb35369e0ad2afab669f228072c1b0d510eae64' commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' owner_id: 2242 repo_id: 204253808 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6600f3..a965799 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,9 +2,16 @@ # This workflow is managed by gh actions-lock. # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # -# Release workflow — triggered by version tags (v*). -# Builds artifacts, generates changelog via git-cliff, creates a GitHub Release, -# and produces GitHub native build-provenance attestations (OIDC + Sigstore). +# Release workflow, triggered by a version tag (v*). +# Builds a static `squabble` binary for x86_64-unknown-linux-musl with the +# workspace MSRV toolchain, stages it with a SHA256SUMS file, attests build +# provenance for both files (OIDC + Sigstore), and publishes them on the +# GitHub Release for the tag. Every step fails closed: a missing file stops +# the run before anything is attested or published. +# +# Verify a downloaded binary with: +# sha256sum -c SHA256SUMS +# gh attestation verify squabble-x86_64-linux-musl --repo hyperpolymath/cicd-squabbler name: Release on: push: @@ -14,147 +21,84 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false permissions: - actions: read contents: read jobs: build: - name: Build Artifacts - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - steps: - - uses: actions/checkout@v7.0.1 - - name: Detect project type and build - id: build - run: | - # Auto-detect build system from project files. - # Order matters: more specific markers checked first. - if [ -f "mix.exs" ]; then - echo "::notice::Detected Elixir/Gleam project (mix.exs)" - echo "build_type=mix" >> "$GITHUB_OUTPUT" - mix local.hex --force --if-missing - mix local.rebar --force --if-missing - mix deps.get --only prod - MIX_ENV=prod mix release - elif [ -f "Cargo.toml" ]; then - echo "::notice::Detected Rust project (Cargo.toml)" - echo "build_type=cargo" >> "$GITHUB_OUTPUT" - cargo build --release - elif [ -f "build.zig" ]; then - echo "::notice::Detected Zig project (build.zig)" - echo "build_type=zig" >> "$GITHUB_OUTPUT" - zig build -Doptimize=ReleaseSafe - elif [ -f "deno.json" ] || [ -f "deno.jsonc" ]; then - echo "::notice::Detected Deno project (deno.json)" - echo "build_type=deno" >> "$GITHUB_OUTPUT" - deno task build - elif [ -f "gossamer.conf.json" ]; then - echo "::notice::Detected Gossamer project (gossamer.conf.json)" - echo "build_type=gossamer" >> "$GITHUB_OUTPUT" - gossamer build - elif [ -f "gleam.toml" ]; then - echo "::notice::Detected Gleam project (gleam.toml)" - echo "build_type=gleam" >> "$GITHUB_OUTPUT" - gleam build - elif [ -f "rebar.config" ]; then - echo "::notice::Detected Erlang/Rebar project (rebar.config)" - echo "build_type=rebar" >> "$GITHUB_OUTPUT" - rebar3 as prod release - elif [ -f "Justfile" ] || [ -f "justfile" ]; then - echo "::notice::Detected Justfile — running 'just build'" - echo "build_type=just" >> "$GITHUB_OUTPUT" - just build - else - echo "::error::No recognised build system found." - echo "Expected one of: mix.exs, Cargo.toml, build.zig, deno.json, gossamer.conf.json, gleam.toml, rebar.config, Justfile" - exit 1 - fi - # TODO: Upload build artifacts if needed (pin actions/upload-artifact - # to a full commit SHA when enabling, per the SHA-pin policy): - # - uses: actions/upload-artifact@ # vX.Y.Z - # with: - # name: release-artifacts - # path: target/release/ - changelog: - name: Generate Changelog + name: Build squabble (x86_64-linux-musl) runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 permissions: contents: read - outputs: - changelog: ${{ steps.cliff.outputs.content }} - version: ${{ steps.version.outputs.version }} steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 - - name: Extract version from tag - id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - - name: Install git-cliff + persist-credentials: false + # Untagged dtolnay/rust-toolchain master commit (2026-08-04, merge of #182); + # an ancestor of the moving `v1` branch. Pinned by SHA, so no tag applies. + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-04 + with: + toolchain: 1.85.0 + targets: x86_64-unknown-linux-musl + - name: Install musl-tools run: | - curl -sSfL https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-$(uname -m)-unknown-linux-gnu.tar.gz \ - | tar -xz --strip-components=1 -C /usr/local/bin/ git-cliff-*/git-cliff - - name: Generate changelog for this release - id: cliff + sudo apt-get update + sudo apt-get install -y musl-tools + - name: Build release binary + run: cargo build --locked --release -p squabble-cli --target x86_64-unknown-linux-musl + - name: Stage dist/ run: | - # Generate changelog for the current tag only - CHANGELOG=$(git cliff --latest --strip header) - # Write to output using delimiter to handle multiline - { - echo "content<> "$GITHUB_OUTPUT" - - name: Update full CHANGELOG.md + mkdir -p dist + cp target/x86_64-unknown-linux-musl/release/squabble dist/squabble-x86_64-linux-musl + cd dist + sha256sum squabble-x86_64-linux-musl > SHA256SUMS + - name: Require both release files run: | - git cliff --output CHANGELOG.md - - name: Upload updated CHANGELOG.md - uses: actions/upload-artifact@v7.0.1 + for f in dist/squabble-x86_64-linux-musl dist/SHA256SUMS; do + test -s "$f" || { echo "::error::missing or empty release file: $f"; exit 1; } + done + cd dist + sha256sum -c SHA256SUMS + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: changelog - path: CHANGELOG.md + name: release-dist + path: dist/ + if-no-files-found: error retention-days: 5 release: - name: Create GitHub Release - needs: [build, changelog] + name: Publish GitHub Release + needs: build runs-on: ubuntu-latest timeout-minutes: 15 permissions: - contents: write - id-token: write # mint the OIDC token attestation provenance is signed with - attestations: write # write the build-provenance attestation (the "claim") + contents: write # create the release and upload its assets + id-token: write # mint the OIDC token the provenance attestation is signed with + attestations: write # store the build-provenance attestation steps: - - uses: actions/checkout@v7.0.1 - # TODO: Download build artifacts if uploading to the release (pin - # actions/download-artifact to a full commit SHA when enabling): - # - uses: actions/download-artifact@ # vX.Y.Z - # with: - # name: release-artifacts - # path: artifacts/ - - name: Create GitHub Release - uses: softprops/action-gh-release@v3.0.3 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - body: ${{ needs.changelog.outputs.changelog }} - draft: false - prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} - generate_release_notes: false - # TODO: Add artifact files to the release - # files: | - # artifacts/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # GitHub native artifact attestation (build provenance). Generates a - # signed, verifiable claim binding each released artifact to this build - # (commit, workflow, runner) via OIDC + Sigstore — verify with - # `gh attest verify --repo ${{ github.repository }}`. - # Replaces the older SLSA-generator job; native attestations need no - # separate isolated workflow. - # TODO: point subject-path at the artifacts this release actually ships - # (must match the `files:` uploaded above, e.g. artifacts/*). + name: release-dist + path: dist + - name: Refuse an empty or incomplete dist/ + run: | + shopt -s nullglob + files=(dist/*) + if [ "${#files[@]}" -eq 0 ]; then + echo "::error::dist/ is empty; nothing to attest or release" + exit 1 + fi + for f in dist/squabble-x86_64-linux-musl dist/SHA256SUMS; do + test -s "$f" || { echo "::error::missing or empty release file: $f"; exit 1; } + done + cd dist + sha256sum -c SHA256SUMS - name: Attest build provenance - if: ${{ hashFiles('artifacts/*') != '' }} # skip until real artifacts are wired - uses: actions/attest-build-provenance@v4.2.2 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: 'dist/*' + - name: Create GitHub Release + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: - subject-path: 'artifacts/*' + files: dist/* + fail_on_unmatched_files: true + generate_release_notes: true + prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} diff --git a/Justfile b/Justfile index e232b4f..f927e32 100644 --- a/Justfile +++ b/Justfile @@ -530,7 +530,7 @@ release-tag version: just changelog git add CHANGELOG.md git commit -m "chore(release): prepare $TAG" - git tag -a "$TAG" -m "Release $TAG" + git tag -s "$TAG" -m "Release $TAG" echo "Created tag $TAG — push with: git push origin main --tags" # ═══════════════════════════════════════════════════════════════════════════════