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
# Verify a signed attestation, so a CI job can skip work that already ran.
#
# Runs git-attest, attest's one implementation (decisions ADR-0037), at the
# version RELEASE pins and only with the archive digest RELEASE names: the
# commit you pin names the bytes it runs. fetch.sh takes the archive from the
# runner's tool cache or the release, checks it, and proves it runs.
#
# Fail-open, as ever: every failure means "run the tests". So whatever goes
# wrong on the way to an answer — no download, a digest mismatch, no asset
# for this runner, a binary that will not execute, a run that exits non-zero
# or writes no `covered=` line — prints one `::warning`, and verify.sh, which
# needs only `git` and `ssh-keygen`, answers instead. A flaky network can
# cost a skip, never disable a gate, never fail the job.
#
# Forgejo uses the same file: `uses: https://github.com/fredericrous/attest@v1`.
# Forgejo Actions sets the same GITHUB_* variables this relies on.
name: attest
description: Skip a CI job when a signed note proves its gate already passed on this exact tree
branding:
  icon: check-circle
  color: green

inputs:
  signers:
    description: >-
      Path to an allowed_signers file, relative to the repository root.
      Default: .forgejo/allowed_signers, then .github/allowed_signers.
    required: false
    default: ""
  principal:
    description: >-
      Accept only an attestation signed by this identity. Default: whoever the
      signature says, provided that key is in the signers file — so a file
      naming a whole team just works, and there is nothing to keep in sync.
    required: false
    default: ""
  platform:
    description: >-
      Only accept an attestation minted on this platform, as <arch>-<os>
      (e.g. x86_64-linux). Default: the runner this step is on, so a matrix leg
      skips only work that really ran on its own platform. An OS alone
      (`linux`) accepts any architecture. Pass `any` to state that the whole
      suite's result does not depend on where it ran.
    required: false
    default: ""
  anywhere:
    description: >-
      Gate names, space-separated, whose result cannot depend on where they
      ran: formatting, shell lint, secret scanning, dependency audit. A
      verified attestation from ANY platform covers these, so a laptop's note
      skips them on every leg of a matrix. This is your committed statement
      about those checks, made once, here. Never list a check that compiles or
      executes the product — tests, clippy, a typecheck under cfg, a build —
      whatever its name says.
    required: false
    default: ""
  include-local:
    description: >-
      `true` also reads refs/notes/attest-local/*: blocks a `sign` step wrote
      with `push: false`, which never reached origin and so cannot be revoked
      there. Off by default, so a persistent runner's leftovers never cover a
      later job. Only `true`, `false` or empty; anything else fails the step.
    required: false
    default: "false"
  binary:
    description: >-
      TEST-ONLY: a git-attest binary to run instead of the pinned release,
      relative to the workspace. attest's own CI sets it after `cargo build`
      so a branch tests its own code; anywhere else it only takes away the
      digest check. Setting it prints a notice.
    required: false
    default: ""

outputs:
  gates:
    description: >-
      The covered gate names as a JSON array — `[]` when nothing is covered.
      USE THIS ONE, in an `if:` expression:
      "not contains(fromJSON(steps.attest.outputs.gates || '[]'), 'your-gate')";
      the `|| '[]'` keeps the expression valid when this step was skipped.
      `contains` over an array matches ELEMENTS; over the `covered` string
      below it matches substrings, so an unrelated gate named
      `pre-push-cargo-test-slow` would satisfy a check for
      `pre-push-cargo-test` and skip the real suite. See the README for the
      expression written out — it cannot be spelled here, because GitHub
      EVALUATES a dollar-brace expression inside this description rather than
      displaying it, and the action then fails to load.
    value: ${{ steps.verify.outputs.gates }}
  covered:
    description: >-
      The same names, space-separated. Kept only so workflows written against
      the older inline templates keep working; prefer `gates`.
    value: ${{ steps.verify.outputs.covered }}
  notes:
    description: >-
      What became of refs/notes/amont-attest, origin's mirror: fetched, absent
      (origin has none; the local mirror was deleted), unreachable (origin did
      not answer; nothing was judged), unwritable (origin's copy could not
      replace a stale local one, a leftover lock say; nothing was judged),
      undeletable, or no-origin.
    value: ${{ steps.verify.outputs.notes }}
  inputs_notes:
    description: The same, for refs/notes/amont-attest-inputs.
    value: ${{ steps.verify.outputs.inputs_notes }}

runs:
  using: composite
  steps:
    - id: verify
      shell: bash
      env:
        ATTEST_SIGNERS: ${{ inputs.signers }}
        ATTEST_PRINCIPAL: ${{ inputs.principal }}
        ATTEST_PLATFORM: ${{ inputs.platform }}
        ATTEST_ANYWHERE: ${{ inputs.anywhere }}
        ATTEST_INCLUDE_LOCAL: ${{ inputs.include-local }}
        ATTEST_BINARY: ${{ inputs.binary }}
      run: |
        set -u
        # Positional parameters rather than a bash array: macOS runners can
        # still put bash 3.2 first on PATH, where an empty array under `set -u`
        # is an error. `"$@"` is exempt from nounset in every POSIX shell.
        set --
        if [ -n "$ATTEST_SIGNERS" ];   then set -- "$@" --signers   "$ATTEST_SIGNERS";   fi
        if [ -n "$ATTEST_PRINCIPAL" ]; then set -- "$@" --principal "$ATTEST_PRINCIPAL"; fi
        if [ -n "$ATTEST_PLATFORM" ];  then set -- "$@" --platform  "$ATTEST_PLATFORM";  fi
        if [ -n "$ATTEST_ANYWHERE" ];  then set -- "$@" --anywhere  "$ATTEST_ANYWHERE";  fi
        # Strict, like an unknown flag: a typo such as `ture` that silently
        # meant `false` would just never cover anything.
        case "$ATTEST_INCLUDE_LOCAL" in
          true) set -- "$@" --include-local ;;
          false | "") ;;
          *) echo "attest: include-local must be true or false, not '$ATTEST_INCLUDE_LOCAL'" >&2; exit 2 ;;
        esac
        # The binary: the pinned release, or (tests only) the one given.
        bin= why=
        if [ -n "$ATTEST_BINARY" ]; then
          echo "::notice title=attest::binary: is test-only; running $ATTEST_BINARY, not the pinned release"
          bin=$ATTEST_BINARY
        else
          err=$(mktemp)
          bin=$(bash "$GITHUB_ACTION_PATH/fetch.sh" 2> "$err") || bin=
          cat "$err" >&2
          if [ -z "$bin" ]; then
            why=$(tail -n 1 "$err" | sed 's/^attest: //')
            [ -n "$why" ] || why="fetch.sh named no binary"
          fi
          rm -f "$err"
        fi
        # Reasons go to stderr and land in the job log. When an expected skip
        # does not happen, that log says which of tree, platform, signature or
        # signers was the reason — the alternative is a silent non-answer.
        # GitHub runs this under `-e`: every status below is tested, not left
        # to end the step.
        out=
        if [ -n "$bin" ]; then
          if out=$("$bin" covered --github-output "$@"); then
            case $'\n'"$out" in
              *$'\n'covered=*) ;;
              *) why="git-attest wrote no covered= line" ;;
            esac
          else
            why="git-attest exited $?"
          fi
        fi
        if [ -n "$why" ]; then
          echo "::warning title=attest::$why; verify.sh answers instead"
          out=$(ATTEST_NO_DEPRECATION=1 bash "$GITHUB_ACTION_PATH/verify.sh" --github-output "$@")
        fi
        printf '%s\n' "$out" >> "$GITHUB_OUTPUT"
        # A mirror nobody could fetch covers nothing, silently in the gates —
        # so say it where a fleet-wide slowdown would otherwise hide it.
        # Two checks, not one case: both can be true, and each deserves its line.
        case "$out" in
          *"notes=unreachable"*)
            echo "::warning title=attest::origin's notes could not be fetched; nothing was skipped (see the log above)" ;;
        esac
        case "$out" in
          *"notes=unwritable"*)
            echo "::warning title=attest::a stale lock keeps origin's notes out of the mirror; nothing was skipped (the log above says what to rm)" ;;
        esac