diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2d0bb5..1f8a4e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,13 @@ name: CI # Pull requests from forks (outside contributors) run the full gate, once the maintainer approves # the run, so the review sees it; so do Dependabot's (.github/dependabot.yml), which nobody tests # locally. The heavy job stays off pull requests. +# +# What runs when (docs/releasing.md, "What CI runs when"): the `plan` job picks a base, the last +# `develop` commit whose CI run passed (a pull request: its merge base; nightly and manual runs: +# none), and `cargo xtask affected` maps the change since it to crates. `cargo xtask ci --affected` +# then lints and tests those crates and their dependents only; the heavy and oracle jobs run when +# the change reaches the solvers, the oracle or everything (Cargo.toml/Cargo.lock/xtask/workflows). +# Nightly and manual runs, and every release (release.yml), run all of it. on: pull_request: paths: @@ -26,7 +33,7 @@ on: - 'docs/features.md' - 'docs/oracle.md' schedule: - # Nightly, on the default branch (develop): the heavy tests. + # Nightly, on the default branch (develop): the whole gate, the heavy tests and the oracle. - cron: '23 3 * * *' # Weekly: the oracle on macOS too, so results that differ by platform show up in CI (the # fixtures are often blessed on a Mac; #115). @@ -37,7 +44,8 @@ permissions: contents: read # A newer merge cancels the run still going for the one before it, so a burst of merges is tested -# once, at the tip. Nightly and manual runs are grouped apart and never cancelled by a merge. +# once, at the tip; nothing is missed, since the tip's run tests everything since the last green +# run. Nightly and manual runs are grouped apart and never cancelled by a merge. concurrency: group: ci-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} @@ -46,13 +54,75 @@ env: CARGO_TERM_COLOR: always jobs: + plan: + name: Plan (affected crates) + if: github.event.schedule != '41 4 * * 0' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + # To find the last green run on develop. + actions: read + outputs: + base: ${{ steps.base.outputs.base }} + everything: ${{ steps.affected.outputs.everything }} + solver: ${{ steps.affected.outputs.solver }} + wasm: ${{ steps.affected.outputs.wasm }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + key: plan + save-if: ${{ github.ref == 'refs/heads/develop' }} + - name: Pick the base + id: base + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT: ${{ github.event_name }} + PR_BASE: ${{ github.base_ref }} + run: | + base="" + case "$EVENT" in + push) + # The newest develop commit whose push run passed and that this commit contains. + # Runs that were cancelled or failed are skipped, so their changes are tested here. + for sha in $(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?branch=develop&event=push&status=success&per_page=100" --jq '.workflow_runs[].head_sha'); do + if [ "$sha" != "$GITHUB_SHA" ] && git merge-base --is-ancestor "$sha" HEAD; then + base="$sha" + break + fi + done + ;; + pull_request) + base="$(git merge-base "origin/$PR_BASE" HEAD || true)" + ;; + esac + # Nightly and manual runs, or no green run to start from: everything. + echo "base=$base" >>"$GITHUB_OUTPUT" + echo "### Base: \`${base:-none (everything)}\`" >>"$GITHUB_STEP_SUMMARY" + - name: cargo xtask affected + id: affected + env: + BASE: ${{ steps.base.outputs.base }} + run: | + if [ -n "$BASE" ]; then + cargo xtask affected --base "$BASE" --github-output | tee -a "$GITHUB_STEP_SUMMARY" + else + printf 'everything=true\nsolver=true\nwasm=true\n' >>"$GITHUB_OUTPUT" + fi + ci: name: cargo xtask ci - if: github.event.schedule != '41 4 * * 0' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') + needs: plan runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy @@ -60,12 +130,18 @@ jobs: - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/develop' }} - - run: cargo xtask ci + - name: cargo xtask ci (the affected crates, or everything) + env: + BASE: ${{ needs.plan.outputs.base }} + run: | + if [ -n "$BASE" ]; then cargo xtask ci --affected "$BASE"; else cargo xtask ci; fi heavy: - # The heavy verification and corpus tests (#[ignore]d) and the doc tests. + # The heavy verification and corpus tests (#[ignore]d) and the doc tests, when the change + # reaches them. name: cargo xtask test --heavy - if: github.event.schedule != '41 4 * * 0' && github.event_name != 'pull_request' + needs: plan + if: github.event_name != 'pull_request' && (needs.plan.outputs.solver == 'true' || needs.plan.outputs.everything == 'true') runs-on: ubuntu-24.04 timeout-minutes: 60 steps: @@ -79,9 +155,11 @@ jobs: oracle: # Verification and validation (docs/verification.md): every case against its reference and # its fixture, and docs/oracle.md must be the table the cases produce. Cases marked `slow` - # run here too while the whole set stays within a few minutes; `--quick` skips them. + # run here too while the whole set stays within a few minutes; `--quick` skips them. It runs + # when the change reaches the solvers or the oracle's package. name: cargo xtask oracle - if: github.event.schedule != '41 4 * * 0' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') + needs: plan + if: needs.plan.outputs.solver == 'true' || needs.plan.outputs.everything == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 steps: