git-attest 1.6.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.
#
# Composite, and deliberately WITHOUT a download step. The contract here is
# fail-open — every failure means "run the tests" — so a step that fetched a
# binary would turn a flaky network into a silently disabled gate. Everything
# below needs only `git` and `ssh-keygen`, which every runner already has.
#
# 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"

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 }}
      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
        # 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.
        out=$(bash "$GITHUB_ACTION_PATH/verify.sh" --github-output "$@")
        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