diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fb4a90e..b42bbebf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,24 @@ -# Runs the checks in docs/VERIFICATION.md on every push to main and every -# pull request. The full verification is split into parts (xtask/src/parts.rs) -# so that no job comes near GitHub's six-hour limit: +# Runs the checks in docs/VERIFICATION.md, in full, on every push to main, +# every manual run (Actions, "Run workflow", which needs write access to the +# repository) and every pull request. On a pull request the proofs (the +# `crate` jobs, which take hours) wait for a person to approve them: they +# need the `approval` job, which uses the `full-verification` environment, +# whose required reviewers are set in the repository's settings +# (Environments). Until one of them approves ("Review deployments" on the +# run), the proofs do not start and `Full verification` stays pending, so +# opening pull requests, however many, starts only the cheap jobs. Every push +# to a pull request starts a new run, which needs a new approval. The full +# verification is split into parts (xtask/src/parts.rs) so that no job comes +# near GitHub's six-hour limit: # # crates `cargo xtask check`, the fast static checks (unsafe, lint # overrides, escape hatches, tier declarations, pins, allowlist), # which fail in a minute or two so obvious mistakes show up early; # then the crate groups (`cargo xtask crates`) for the matrix # below, so a new crate cannot be left out. +# approval pull requests only, after `crates` passed: waits for a +# reviewer of the `full-verification` environment to approve +# the proof jobs, and does nothing else. It checks out no code. # base `verify --part base`: static checks, rustfmt, clippy and tests # over the whole workspace. # miri `verify --part miri`: the workspace's tests under Miri. @@ -19,10 +31,13 @@ # mutations (several mutants at once, xtask/src/mutate.rs) and # conformance. One job per crate with proof work; the crates # with none share one job. Each uploads what it measured, one -# file per crate, tagged with the commit. -# assemble `verify --part assemble`: needs every job above to pass, then -# checks that every crate's measurements are present and from -# this commit, and reports whether README.md's table matches them. +# file per crate, tagged with the commit. On a pull request, +# only once `approval` passed. +# assemble `verify --part assemble`, the job named `Full verification`: +# fails unless every job above passed (`approval` is skipped +# outside pull requests), then checks that every crate's +# measurements are present and from this commit, and reports +# whether README.md's table matches them. # table on pushes to main only, after `assemble` passed: writes # README.md's table from the same measurements # (`verify --part assemble --write`) and, if it changed, commits @@ -159,10 +174,30 @@ jobs: - name: Verify (Miri) run: cargo xtask verify --part miri + approval: + # The proofs take hours: on a pull request a reviewer of this + # environment approves them first (see the header). One job holds the + # approval, so one review starts every proof job of the run. + name: Approve the proofs + needs: crates + if: github.event_name == 'pull_request' + environment: full-verification + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Approved + run: echo "A reviewer approved the proof jobs for this run." + crate: # A group of crates with no proofs is one job (`cargo xtask crates`). name: Verify ${{ contains(matrix.crate, ',') && 'crates without proofs' || matrix.crate }} - needs: crates + needs: [crates, approval] + # After an approval on a pull request; `approval` is skipped, and not + # needed, everywhere else. + if: >- + ${{ !cancelled() && needs.crates.result == 'success' && + (needs.approval.result == 'success' || + (github.event_name != 'pull_request' && needs.approval.result == 'skipped')) }} runs-on: ubuntu-24.04 timeout-minutes: 360 strategy: @@ -200,10 +235,30 @@ jobs: assemble: name: Full verification - needs: [crates, base, base-macos, miri, crate] + needs: [crates, approval, base, base-macos, miri, crate] + # Runs even when a job it needs failed or was skipped, and fails then: + # a skipped job is not a pass, and a skipped required check would count + # as one. Only a cancelled run is not judged. + if: ${{ !cancelled() }} runs-on: ubuntu-24.04 timeout-minutes: 30 steps: + - name: Every job passed + env: + NEEDS: ${{ toJSON(needs) }} + EVENT: ${{ github.event_name }} + run: | + # `approval` is skipped outside pull requests, and only there. + bad=$(echo "$NEEDS" | jq -r --arg event "$EVENT" ' + to_entries[] + | select(.value.result != "success") + | select(.key != "approval" or $event == "pull_request" or .value.result != "skipped") + | "\(.key): \(.value.result)"') + if [ -n "$bad" ]; then + echo "Not every job passed:" + echo "$bad" + exit 1 + fi - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/CLAUDE.md b/CLAUDE.md index 5aecfc27..e95bb0c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,10 @@ to trust the agents. Read `docs/VERIFICATION.md` and `docs/ROADMAP.md` first. in `CHANGED CRATES PASSED`, which is not `ALL CHECKS PASSED` and must not be reported as such. Work is **done** only when CI's full verification (every job of `.github/workflows/ci.yml`, ending in `Full verification`) - is green on its pull request; say so if it has not run yet. To repeat CI's run locally, use `scripts/verify.sh` or the + is green on its pull request; say so if it has not run yet. On a pull + request the proof jobs wait until a reviewer of the `full-verification` + environment approves the run, and every push needs a new approval: ask + for one, and never report a run whose proofs are waiting as passed. To repeat CI's run locally, use `scripts/verify.sh` or the Docker command in README.md. `cargo xtask check` runs only the fast static checks (unsafe, the `clazel-sys` audit, lint overrides, escape hatches, tier declarations, pins); use it while iterating, never as the final diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index 83c12415..02f373d4 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -83,8 +83,17 @@ installs nothing if a checksum differs. The full `cargo xtask verify` takes hours, so it runs where it is the gate: in CI (`.github/workflows/ci.yml`), on Linux x86-64 with the pinned -toolchain, on every push to `main` and every pull request. Nothing is done, -and no tier is raised, until that run is green. +toolchain, on every push to `main`, every manual run (Actions, "Run +workflow") and every pull request. Nothing is done, and no tier is raised, +until that run is green. + +On a pull request the proofs (the `crate` parts below) wait for a person: +they need CI's `approval` job, which uses the `full-verification` +environment, and a reviewer set in that environment's settings approves it +("Review deployments" on the run). Until then only the cheap jobs run and +`Full verification` stays pending, so opening pull requests, however many, +never starts hours of proof jobs. Every push to a pull request starts a new +run, which needs a new approval. CI splits the run into parts, one job each, so no job comes near GitHub's six-hour limit (`xtask/src/parts.rs`): @@ -103,8 +112,10 @@ six-hour limit (`xtask/src/parts.rs`): writes what it measured to `/.json` for each crate, tagged with the commit (with `+dirty` if tracked files have uncommitted changes). -- `verify --part assemble --metrics `, which runs only after every - other job passed: static checks, then it requires a file for every +- `verify --part assemble --metrics `, the `Full verification` job, + which fails unless every other job passed (a skipped job included, but + `approval`, which runs on pull requests only): static checks, then it + requires a file for every workspace crate from this commit, and reports whether README.md's table matches them. - On pushes to `main` only, the `table` job runs the same part with