# 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.
```yaml
- 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:
```sh
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:
```yaml
- 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`.
**Unrelated changes.** By default an attestation names a whole tree, so any
commit anywhere — a README fix on main, another package in a monorepo — moves
the tree and voids it. Declare what each gate actually reads and it survives
them:
```
# .github/attest-inputs — literal paths and directories only: git ls-tree
# does NOT glob, `tests/*.sh` would match nothing and fingerprint nothing.
pre-push-cargo-test .github/workflows/ci.yaml Cargo.toml Cargo.lock rust-toolchain.toml src
ci-fmt .github/workflows/ci.yaml Cargo.toml rust-toolchain.toml src
```
A gate is then covered on any tree whose declared paths are byte-identical to
the attested ones (producers from 1.3.0 write the fingerprint into the note;
this file is itself an input, so editing it re-runs everything once). Name
the workflow file too: the gate name is only worth the command behind it.
ASCII paths only, no spaces; name a parent directory instead. Over-broad is
safe, under-broad is a silent false skip — so this file is a trust boundary
exactly like `allowed_signers`, and belongs in the same CODEOWNERS line.
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](https://github.com/fredericrous/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`](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:
```sh
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](https://github.com/fredericrous/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`](SPEC.md) | the `amont-attest-v2` wire format, input fingerprints included |
| [`action.yml`](action.yml) | the composite action, for GitHub **and** Forgejo |
| [`sign/`](sign/) | the CI producer: `sign/action.yml` wraps `sign/sign.sh` |
| [`.github/attest-inputs`](.github/attest-inputs) | this repository's own input spec: the dogfood, and a template |
| [`verify.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`](tests/conformance.sh) | the fixtures both implementations must pass |
### Forgejo
Same file, full-URL `uses:`:
```yaml
- uses: https://github.com/fredericrous/attest@v1
```
### Other CI
```sh
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:
```sh
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.