git-attest 1.7.0

Sign and verify attestations that a test suite already passed on a tree, so CI can skip it: the library and its command line
Documentation
# Step 2 of a release: the tag publishes the draft prepare-release built.
#
# The trigger is `v*.*.*` — three parts, deliberately. The moving major alias
# (`v1`) that `uses:` pins to must NOT start a release run, or every alias move
# would re-run a publish against an immutable registry.
#
# Nothing is rebuilt here (decisions ADR-0037). The tagged commit's RELEASE
# names one sha256 per archive; the draft's archives must have exactly those,
# or nothing is published — so the tag, the release body's sha and, later,
# `v1` all name a commit whose actions carry the digests of the bytes they
# run. `v1` moves in move-major.yaml, after amont's CI has passed on the tag.
name: release

on:
  push:
    tags: ["v*.*.*"]

# Read by default; the job that publishes the release says so.
permissions:
  contents: read

defaults:
  run:
    shell: bash

jobs:
  # Nothing is published before the tag and the manifest agree. A mismatch here
  # is the failure that cannot be undone afterwards: crates.io versions are
  # immutable, so a wrong number can only be superseded.
  guard:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.v.outputs.version }}
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
      - id: v
        run: |
          set -eu
          tag=${GITHUB_REF#refs/tags/v}
          manifest=$(awk -F'"' '/^version/ {print $2; exit}' Cargo.toml)
          if [ "$tag" != "$manifest" ]; then
            echo "tag v$tag but Cargo.toml says $manifest" >&2
            exit 1
          fi
          pinned=$(awk '$1 == "version" {print $2; exit}' RELEASE)
          if [ "$tag" != "$pinned" ]; then
            echo "tag v$tag but RELEASE pins $pinned: run prepare-release and commit its RELEASE" >&2
            exit 1
          fi
          echo "version=$tag" >> "$GITHUB_OUTPUT"
      - run: make check

  # The draft's archives against the tagged RELEASE: the same targets, the
  # same digests. Both publishing jobs wait for it.
  draft:
    needs: guard
    runs-on: ubuntu-latest
    permissions:
      # Drafts are visible only to a token that can write releases.
      contents: write
    outputs:
      id: ${{ steps.check.outputs.id }}
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
      - id: check
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ needs.guard.outputs.version }}
        run: |
          set -eu
          ids=$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100" \
            --jq ".[] | select(.draft and .tag_name == \"v$VERSION\") | .id")
          [ "$(printf '%s\n' "$ids" | grep -c .)" = 1 ] || {
            echo "::error::want one draft release for v$VERSION (run prepare-release), found: ${ids:-none}"; exit 1; }
          mkdir art
          gh api "repos/$GITHUB_REPOSITORY/releases/$ids/assets?per_page=100" \
            --jq '.[] | "\(.id) \(.name)"' > assets
          while read -r aid name; do
            gh api -H 'Accept: application/octet-stream' \
              "repos/$GITHUB_REPOSITORY/releases/assets/$aid" > "art/$name"
          done < assets
          # Every pinned archive is there with its pinned digest, and no
          # archive is there that RELEASE does not pin.
          rc=0
          awk '$1 != "version" && $1 !~ /^#/ && NF { print $1, $2 }' RELEASE > pinned
          while read -r target want; do
            f=art/git-attest-$VERSION-$target.tar.gz
            if [ ! -f "$f" ]; then echo "::error::the draft has no $f"; rc=1; continue; fi
            got=$(sha256sum < "$f" | awk '{print $1}')
            if [ "$got" = "$want" ]; then echo "ok  $target $got"
            else echo "::error::$target: the draft's archive is $got, RELEASE pins $want"; rc=1; fi
          done < pinned
          for f in art/*.tar.gz; do
            t=${f#art/git-attest-"$VERSION"-}; t=${t%.tar.gz}
            awk -v t="$t" '$1 == t { found = 1 } END { exit !found }' pinned \
              || { echo "::error::the draft carries $f, which RELEASE does not pin"; rc=1; }
          done
          [ "$rc" = 0 ]
          echo "id=$ids" >> "$GITHUB_OUTPUT"

  publish:
    needs: [guard, draft, publish-crates]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: publish the checked draft
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ needs.guard.outputs.version }}
          ID: ${{ needs.draft.outputs.id }}
        run: |
          set -eu
          notes=$(gh api -X POST "repos/$GITHUB_REPOSITORY/releases/generate-notes" \
            -f tag_name="v$VERSION" --jq .body)
          # The commit, so a consumer can pin the step that decides whether
          # tests run to something that cannot move.
          body=$(printf 'Pin by commit: `uses: fredericrous/attest@%s # v%s`\n\n%s\n' "$GITHUB_SHA" "$VERSION" "$notes")
          gh api -X PATCH "repos/$GITHUB_REPOSITORY/releases/$ID" \
            -f tag_name="v$VERSION" -F draft=false -f make_latest=true -f body="$body" > /dev/null
          got=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/v$VERSION" --jq '.draft')
          [ "$got" = false ] || { echo "::error::v$VERSION is still a draft"; exit 1; }

  # crates.io is REQUIRED since 1.6.0 (decisions ADR-0037): amont's hook links
  # the git-attest library, so a release that is not on crates.io is a
  # release amont cannot use. A missing CARGO_REGISTRY_TOKEN fails the job
  # before the GitHub release is published. It runs after the draft check, so
  # a draft that does not match publishes nothing anywhere.
  #
  # To set it: `gh secret set CARGO_REGISTRY_TOKEN --repo fredericrous/attest`.
  publish-crates:
    needs: [guard, draft]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
      - name: CARGO_REGISTRY_TOKEN is set
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -eu
          if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
            echo "::error::no CARGO_REGISTRY_TOKEN; the library must reach crates.io"
            exit 1
          fi
      - run: cargo publish --locked
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      # Published is not resolvable: the index can lag. A consumer's
      # `cargo build` is what has to work, so ask the registry the way it will.
      - name: the version resolves on crates.io
        env:
          VERSION: ${{ needs.guard.outputs.version }}
        run: |
          set -eu
          for i in 1 2 3 4 5 6 7 8 9 10; do
            if cargo info "git-attest@$VERSION" > /dev/null 2>&1; then
              echo "git-attest@$VERSION resolves"
              exit 0
            fi
            sleep $((i * 6))
          done
          echo "::error::git-attest@$VERSION does not resolve on crates.io"
          exit 1