git-attest 1.2.0

Verify a signed attestation that a test suite already passed on this exact tree, so CI can skip it
git-attest-1.2.0 is not a library.
Visit the last successful build: git-attest-1.7.0

attest

Your laptop already ran the tests. Stop paying CI to run them again.

When a pre-push hook runs your test suite, that result is thrown away the moment the push succeeds — and CI runs the same suite again, on the same content, for minutes you pay for. attest lets the hook sign what it proved, and lets CI verify that signature and skip the work.

- uses: actions/checkout@v4
- id: attest
  uses: fredericrous/attest@v1
  with:
    anywhere: ci-fmt        # gates whose result cannot depend on where they ran

- id: fmt
  run: cargo fmt --all -- --check
  if: ${{ !contains(fromJSON(steps.attest.outputs.gates), 'ci-fmt') }}
- id: test
  run: cargo test --workspace
  if: ${{ !contains(fromJSON(steps.attest.outputs.gates), 'pre-push-cargo-test') }}

# Last, and only after every gate above: sign what THIS job ran, so the next
# run of the same tree (a re-run, the push to main after the merge) skips it.
- uses: fredericrous/attest/sign@v1
  with:
    key: ${{ secrets.ATTEST_SIGNING_KEY }}
    gates: >-
      ${{ steps.fmt.outcome  == 'success' && 'ci-fmt'              || '' }}
      ${{ steps.test.outcome == 'success' && 'pre-push-cargo-test' || '' }}

That is the whole integration. Nothing is skipped unless a signature by a key you committed to the repository covers the exact tree CI checked out, on the same platform the job is running — or, for the gates you named in anywhere, on any platform.

Safety, stated plainly

Nothing here can make an untested tree skip CI. It can only cost a redundant run. Every failure path — no note, no key, no ssh-keygen, a rebase, a tampered payload, a signature from a stranger — reports "nothing covered", and nothing covered means your tests run. The verifier always exits 0; it can never fail your build.

The price of that is silence, so the action explains itself in the job log:

attest: attested on aarch64-macos, this leg is x86_64-linux
attest: no attestation found for tree 9f2a1c…
attest: covered by you@example.com: pre-push-cargo-test pre-push-clippy

Setup

Three steps, once.

1. Make a signing key and tell your hook runner to use it:

ssh-keygen -t ed25519 -N '' -C "$(git config user.email)" -f ~/.ssh/amont-attest
git config --global amont.attest true

2. Commit the public half as .github/allowed_signers (or .forgejo/allowed_signers), pinned to the namespace:

you@example.com namespaces="amont-attest" ssh-ed25519 AAAAC3Nza…

Use your real address, the one in the note's principal column. The verifier reads the signer's identity from the signature itself, so a file naming a whole team just works and there is nothing to keep in sync.

This file decides who may skip CI, and it is read from the tree being verified: a pull request that adds a key to it can attest itself. That is the same trust as editing a workflow file, so give it the same protection — if .github/workflows/ is under CODEOWNERS or required review, put allowed_signers there too.

3. Add the step, as shown at the top. The action is pinned by a moving v1 tag above; for a step that decides whether tests run, prefer pinning the commit — each release's notes name it:

- uses: fredericrous/attest@<sha>  # v1.x.y

What gets skipped, and when

The gate name is the whole contract between your hook and your CI step. A hook gate called pre-push-cargo-test that runs cargo test --lib does not cover a CI step running cargo test --workspace, whatever the name says — make them the same command.

Platform. By default an attestation counts only on the platform it was minted on, as <arch>-<os>; platform: linux accepts any architecture, and platform: any accepts anywhere. Most laptops are macOS and most CI is Linux, so with only a laptop producing, the default skips nothing — which is what anywhere and the sign action are for. anywhere names the gates whose result cannot depend on where they ran: formatting, shell lint, secret scanning, dependency audit. It is a loaded gun. Never list a check that compiles or executes the product, and that includes clippy: cfg and target-specific dependencies change what it sees.

The signature covers the tree, so an attestation survives a reword, an amend, a rebase and a squash-merge. On a pull_request trigger, though, CI checks out a merge commit the forge just made, and its tree equals your pushed tip's tree only when the branch is up to date with its base. Otherwise the job log reads attested tree … is not the checked-out tree … and the tests run — correctly, because that merged result was never tested. Rebase to get the skip back, or verify on push.

The attestation is only as honest as the machine that made it. The signing key is a file on that machine, and anything running there as you can sign with it; so can anything that runs your test suite as you. Nothing here protects against that, any more than a green CI run protects against a developer with push access.

Producing attestations

amont is the reference producer on a laptop: with amont.attest enabled it signs a note at pre-push naming the gates that actually passed. Any tool can produce one — SPEC.md is the format, and it is short.

From CI

CI tests the merge tree during the pull request, then the push to main tests the identical tree again. fredericrous/attest/sign records the first result so the second run skips it — and so do re-runs and duplicated matrix legs. Setup, once:

ssh-keygen -t ed25519 -N '' -C ci@example.invalid -f attest-ci
gh secret set ATTEST_SIGNING_KEY < attest-ci
printf '%s namespaces="amont-attest" %s\n' ci@example.invalid "$(cat attest-ci.pub)" >> .github/allowed_signers
rm attest-ci

Then the trailing step shown at the top, with three rules that keep it honest:

  • Put it last, never under if: always(). The default success() is what guarantees a failed gate is never signed.
  • Name only what ran here, with the steps.<id>.outcome == 'success' pattern. A step that was skipped because it was already attested has outcome == 'skipped', and re-signing it would claim someone else's platform as yours.
  • The job needs permissions: contents: write to push the notes ref. That permission is not ref-scoped — GitHub has no notes-only token — so put a ruleset on main; rulesets apply to GITHUB_TOKEN too. A signing key in a secret is exactly as trusted as a workflow file: whoever can change .github/workflows/ can make CI sign anything.

Fork pull requests get no secret and a read-only token; both paths just report and succeed. A tree your laptop already attested gets CI's block appended beside yours, each on its own platform. ed25519 keys only. sign@v1 resolves only from 1.2.0 on.

An attestation is only as honest as the push that produced it, and the way a push gets dishonest in practice is a coding agent masking its failure — git push … 2>&1 | tail -5 reports tail's exit status, so a rejected push reads as success. amont-agent guards that end: a PreToolUse hook that refuses the command shape before it runs. Independent of this verifier — related by author, and by the shared conviction: trust what was verified, never what was reported.

What is in here

SPEC.md the amont-attest-v2 wire format
action.yml the composite action, for GitHub and Forgejo
sign/ the CI producer: sign/action.yml wraps sign/sign.sh
verify.sh the verifier the action runs. git and ssh-keygen, nothing else
src/ git-attest, the same contract as a binary
tests/conformance.sh the fixtures both implementations must pass

Forgejo

Same file, full-URL uses::

- uses: https://github.com/fredericrous/attest@v1

Other CI

cargo install git-attest
git-attest covered                     # names, or nothing
git-attest explain                     # ...and why

git-attest explain is the answer to "why didn't it skip?", which is otherwise unanswerable by design.

Zero dependencies

The verifier decides whether tests may be skipped, so it is a security boundary, and everything it links is something you have to trust. It links nothing. The hard parts — ed25519, the OpenSSH signature envelope — are handled by ssh-keygen, which is already on every runner and audited by people who do that for a living.

Contributing

tests/conformance.sh runs the fixtures against any implementation:

make check                                  # both implementations
./tests/conformance.sh ./verify.sh
./tests/conformance.sh "target/release/git-attest covered"

tests/legacy.sh is a negative control — the verifier this project replaces, kept so the suite can prove it is not vacuous. Cases marked (defect N) must fail against it. Please don't fix it. tests/compat/ holds the 1.1.0 verifier, frozen, for the same reason: make compat proves a one-block reader degrades safely on a multi-block note. tests/sign.sh is the producer's suite.

MIT.