name: ci
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 6 * * 1" workflow_dispatch:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - id: attest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
uses: ./
with:
anywhere: ci-fmt ci-shellcheck
- id: fmt
if: ${{ !contains(fromJSON(steps.attest.outputs.gates || '[]'), 'ci-fmt') }}
run: cargo fmt --all -- --check
- id: clippy
if: ${{ !contains(fromJSON(steps.attest.outputs.gates || '[]'), 'ci-clippy') }}
run: cargo clippy --all-targets -- -D warnings
- id: shellcheck
if: ${{ !contains(fromJSON(steps.attest.outputs.gates || '[]'), 'ci-shellcheck') }}
run: shellcheck verify.sh sign/sign.sh tests/*.sh tests/fault/git tests/fake/* scripts/*.sh
- name: every flag is in its --help
run: ./scripts/help-lists-every-flag.sh
- name: the three platform tables are identical
run: ./scripts/platform-tables-match.sh
- uses: ./sign
with:
key: ${{ secrets.ATTEST_SIGNING_KEY }}
gates: >-
${{ steps.fmt.outcome == 'success' && 'ci-fmt' || '' }}
${{ steps.clippy.outcome == 'success' && 'ci-clippy' || '' }}
${{ steps.shellcheck.outcome == 'success' && 'ci-shellcheck' || '' }}
conformance:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 40
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - id: attest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
uses: ./
- id: test
if: ${{ !contains(fromJSON(steps.attest.outputs.gates || '[]'), 'pre-push-cargo-test') }}
run: cargo test
shell: bash
- id: conformance
if: ${{ !contains(fromJSON(steps.attest.outputs.gates || '[]'), 'ci-conformance') }}
run: make conformance sign-test
shell: bash
- uses: ./sign
with:
key: ${{ secrets.ATTEST_SIGNING_KEY }}
gates: >-
${{ steps.test.outcome == 'success' && 'pre-push-cargo-test' || '' }}
${{ steps.conformance.outcome == 'success' && 'ci-conformance' || '' }}
compat:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - run: make compat
action:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - name: isolate from the real attestations
run: |
set -eu
git init -q --bare "$RUNNER_TEMP/origin.git"
git remote set-url origin "$RUNNER_TEMP/origin.git"
git update-ref -d refs/notes/amont-attest || true
# `git notes add` writes an object and needs a committer identity;
# a bare runner has none.
git config user.email ci@example.org
git config user.name CI
- id: bare
uses: ./
- name: nothing covered, and the step succeeded
run: |
set -eu
[ "${{ steps.bare.outputs.gates }}" = "[]" ] || {
echo "expected [], got: ${{ steps.bare.outputs.gates }}"; exit 1; }
[ -z "${{ steps.bare.outputs.covered }}" ] || {
echo "expected empty covered"; exit 1; }
[ '${{ steps.bare.outputs.notes }}' = absent ] || {
echo 'expected notes=absent, got ${{ steps.bare.outputs.notes }}'; exit 1; }
- name: sign this exact tree by hand
run: |
set -eu
ssh-keygen -q -t ed25519 -N '' -C ci@example.org -f "$RUNNER_TEMP/k"
printf '%s namespaces="amont-attest" %s\n' ci@example.org "$(cat "$RUNNER_TEMP/k.pub")" \
> "$RUNNER_TEMP/ci-signers"
tree=$(git rev-parse 'HEAD^{tree}')
printf 'amont-attest-v2\ntree %s\ngates pre-push-cargo-test pre-push-clippy\nplatform x86_64-linux\namont ci\n' \
"$tree" > "$RUNNER_TEMP/p"
ssh-keygen -Y sign -n amont-attest -f "$RUNNER_TEMP/k" "$RUNNER_TEMP/p"
git notes --ref amont-attest add -f \
-m "$(printf '%s\n\n%s' "$(cat "$RUNNER_TEMP/p")" "$(cat "$RUNNER_TEMP/p.sig")")" "$tree"
git push -q -f origin refs/notes/amont-attest:refs/notes/amont-attest
- id: signed
uses: ./
with:
signers: ${{ runner.temp }}/ci-signers
- name: the attestation is reported, exactly
run: |
set -eu
got='${{ steps.signed.outputs.gates }}'
want='["pre-push-cargo-test","pre-push-clippy"]'
[ "$got" = "$want" ] || { echo "want $want, got $got"; exit 1; }
- name: a covered gate is skipped
if: ${{ contains(fromJSON(steps.signed.outputs.gates), 'pre-push-cargo-test') }}
run: echo "correctly identified as covered"
- name: an uncovered gate still runs
if: ${{ !contains(fromJSON(steps.signed.outputs.gates), 'pre-push-audit-rust') }}
run: echo "correctly identified as NOT covered"
- id: nokey
uses: ./sign
with:
key: ""
gates: ci-lint-only
- name: an empty key signs nothing and succeeds
run: |
set -eu
[ '${{ steps.nokey.outputs.status }}' = no-key ] || exit 1
[ '${{ steps.nokey.outputs.signed }}' = false ] || exit 1
- id: readkey
run: |
set -eu
{
echo 'key<<ATTEST_EOF'
cat "$RUNNER_TEMP/k"
echo 'ATTEST_EOF'
} >> "$GITHUB_OUTPUT"
- id: appended
uses: ./sign
with:
key: ${{ steps.readkey.outputs.key }}
gates: ci-lint-only
platform: aarch64-macos
push: false
- name: the block was appended locally, beside origin's mirror
run: |
set -eu
[ '${{ steps.appended.outputs.status }}' = local ] || { echo 'status: ${{ steps.appended.outputs.status }}'; exit 1; }
tree=$(git rev-parse 'HEAD^{tree}')
n=$(git notes --ref attest-local/amont-attest show "$tree" | grep -c '^-----BEGIN SSH SIGNATURE-----$')
[ "$n" = 1 ] || { echo "expected 1 unpushed block, got $n"; exit 1; }
n=$(git notes --ref amont-attest show "$tree" | grep -c '^-----BEGIN SSH SIGNATURE-----$')
[ "$n" = 1 ] || { echo "expected 1 block in the mirror, got $n"; exit 1; }
- id: strict
uses: ./
with:
signers: ${{ runner.temp }}/ci-signers
include-local: true
- id: loose
uses: ./
with:
signers: ${{ runner.temp }}/ci-signers
anywhere: ci-lint-only
include-local: true
- id: osonly
uses: ./
with:
signers: ${{ runner.temp }}/ci-signers
platform: macos
include-local: true
- id: nolocal
uses: ./
with:
signers: ${{ runner.temp }}/ci-signers
anywhere: ci-lint-only
- name: strict, anywhere and os-only read the two blocks as specified
run: |
set -eu
[ '${{ steps.strict.outputs.gates }}' = '["pre-push-cargo-test","pre-push-clippy"]' ] || {
echo 'strict: ${{ steps.strict.outputs.gates }}'; exit 1; }
[ '${{ steps.loose.outputs.gates }}' = '["pre-push-cargo-test","pre-push-clippy","ci-lint-only"]' ] || {
echo 'anywhere: ${{ steps.loose.outputs.gates }}'; exit 1; }
[ '${{ steps.osonly.outputs.gates }}' = '["ci-lint-only"]' ] || {
echo 'os-only: ${{ steps.osonly.outputs.gates }}'; exit 1; }
# Without include-local the unpushed block is not read at all.
[ '${{ steps.nolocal.outputs.gates }}' = '["pre-push-cargo-test","pre-push-clippy"]' ] || {
echo 'no include-local: ${{ steps.nolocal.outputs.gates }}'; exit 1; }
[ '${{ steps.nolocal.outputs.notes }}' = fetched ] || {
echo 'notes: ${{ steps.nolocal.outputs.notes }}'; exit 1; }
- id: readkey2
run: |
set -eu
git notes --ref attest-local/amont-attest remove "$(git rev-parse 'HEAD^{tree}')" 2>/dev/null || true
{
echo 'key<<ATTEST_EOF'
cat "$RUNNER_TEMP/k"
echo 'ATTEST_EOF'
} >> "$GITHUB_OUTPUT"
- id: fpsign
uses: ./sign
with:
key: ${{ steps.readkey2.outputs.key }}
gates: ci-fmt
push: false
- name: the producer wrote a fingerprint and filed the block under it
run: |
set -eu
[ '${{ steps.fpsign.outputs.inputs }}' = 1/1 ] || { echo 'inputs: ${{ steps.fpsign.outputs.inputs }}'; exit 1; }
git notes --ref attest-local/amont-attest show "$(git rev-parse 'HEAD^{tree}')" | grep -q '^input ci-fmt [0-9a-f]\{40,64\}$'
echo unrelated > UNRELATED.md && git add UNRELATED.md && git commit -qm unrelated
- id: fpmoved
uses: ./
with:
signers: ${{ runner.temp }}/ci-signers
include-local: true
- name: an unrelated commit keeps the gate covered, by fingerprint
run: |
set -eu
[ '${{ steps.fpmoved.outputs.gates }}' = '["ci-fmt"]' ] || { echo 'got: ${{ steps.fpmoved.outputs.gates }}'; exit 1; }
echo '// touched' >> src/main.rs && git add src/main.rs && git commit -qm touch
- id: fpinput
uses: ./
with:
signers: ${{ runner.temp }}/ci-signers
include-local: true
- name: a commit to a declared input does not
run: |
set -eu
[ '${{ steps.fpinput.outputs.gates }}' = '[]' ] || { echo 'got: ${{ steps.fpinput.outputs.gates }}'; exit 1; }
# Back to the original tree, and without the fingerprint-keyed
# block: the steps below reason about the tree key alone.
git reset -q --hard HEAD~2
git update-ref -d refs/notes/attest-local/amont-attest-inputs
- name: re-sign with only a prefix-colliding gate
run: |
set -eu
tree=$(git rev-parse 'HEAD^{tree}')
printf 'amont-attest-v2\ntree %s\ngates pre-push-cargo-test-slow\nplatform x86_64-linux\namont ci\n' \
"$tree" > "$RUNNER_TEMP/p2"
ssh-keygen -Y sign -n amont-attest -f "$RUNNER_TEMP/k" "$RUNNER_TEMP/p2"
git notes --ref amont-attest add -f \
-m "$(printf '%s\n\n%s' "$(cat "$RUNNER_TEMP/p2")" "$(cat "$RUNNER_TEMP/p2.sig")")" "$tree"
git push -q -f origin refs/notes/amont-attest:refs/notes/amont-attest
- id: collide
uses: ./
with:
signers: ${{ runner.temp }}/ci-signers
- name: a merely-prefix-matching gate does NOT count as covered
run: |
set -eu
[ '${{ steps.collide.outputs.gates }}' = '["pre-push-cargo-test-slow"]' ] || exit 1
# the legacy string form WOULD have matched — that is the defect:
case '${{ steps.collide.outputs.covered }}' in
*pre-push-cargo-test*) echo "substring match, as expected of the old form" ;;
esac
- name: and the array form correctly says it is not covered
if: ${{ !contains(fromJSON(steps.collide.outputs.gates), 'pre-push-cargo-test') }}
run: echo "the real suite would still run — correct"
- name: install a spy ssh-keygen
run: |
set -eu
real=$(command -v ssh-keygen)
mkdir -p "$RUNNER_TEMP/spy"
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "${ATTEST_KEY+leaked}" >> "%s/spy.log"\nexec "%s" "$@"\n' \
"$RUNNER_TEMP" "$real" > "$RUNNER_TEMP/spy/ssh-keygen"
chmod +x "$RUNNER_TEMP/spy/ssh-keygen"
: > "$RUNNER_TEMP/spy.log"
echo "$RUNNER_TEMP/spy" >> "$GITHUB_PATH"
- id: readkey3
run: |
set -eu
{
echo 'key<<ATTEST_EOF'
cat "$RUNNER_TEMP/k"
echo 'ATTEST_EOF'
} >> "$GITHUB_OUTPUT"
- id: spied
uses: ./sign
with:
key: ${{ steps.readkey3.outputs.key }}
gates: ci-spy
push: false
- name: the key was not in ssh-keygen's environment
run: |
set -eu
[ '${{ steps.spied.outputs.status }}' = local ] || { echo 'status: ${{ steps.spied.outputs.status }}'; exit 1; }
[ -s "$RUNNER_TEMP/spy.log" ] || { echo "the spy was never run"; exit 1; }
if grep -qx leaked "$RUNNER_TEMP/spy.log"; then echo "ATTEST_KEY reached ssh-keygen"; exit 1; fi
echo "ssh-keygen ran $(wc -l < "$RUNNER_TEMP/spy.log") time(s) without the key in its environment"
msrv:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - run: |
v=$(awk -F'"' '/^rust-version/ {print $2}' Cargo.toml)
rustup toolchain install "$v" --profile minimal
cargo "+$v" check --locked