name: CI # CI runs once per merge on `develop`, the trunk (AGENTS.md, "Workflow for every change"): the core # team's pull requests are checked locally on the crates they touch and merged; a red trunk is # fixed forward. `main` holds released code only (docs/releasing.md). # 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: - '**' - '!**/*.md' - '!docs/**' - 'docs/features.md' - 'docs/oracle.md' push: branches: [develop] # A merge that only touches prose starts no run. The generated pages are checked by the jobs # (features.md by `cargo xtask ci`, oracle.md by the oracle job), so they still count. # (Later patterns override earlier ones.) paths: - '**' - '!**/*.md' - '!docs/**' - 'docs/features.md' - 'docs/oracle.md' schedule: # 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). - cron: '41 4 * * 0' workflow_dispatch: 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; 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' }} 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: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || '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 bots: name: Claim and triage bot tests 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: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: sparse-checkout: scripts # scripts/claim and scripts/triage run in .github/workflows/claim.yml and triage.yml through # actions/github-script; their tests use only node:test against a fake client. - run: node --test scripts/claim/*.test.js scripts/triage/*.test.js ci: name: cargo xtask ci needs: plan runs-on: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || '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 targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: # Shared keys: release.yml's verify jobs restore these caches (they save only on main). shared-key: ci save-if: ${{ github.ref == 'refs/heads/develop' }} - name: Software GPU (lavapipe), so headless snapshots run the GPU pipeline run: sudo apt-get update -q && sudo apt-get install -y -q --no-install-recommends mesa-vulkan-drivers libvulkan1 - name: cargo xtask ci (the affected crates, or everything) env: BASE: ${{ needs.plan.outputs.base }} # The drawing tests run on lavapipe and fail, instead of skipping, without it. FIELDCRAFT_GPU: software FIELDCRAFT_REQUIRE_GPU: "1" 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, when the change # reaches them. name: cargo xtask test --heavy needs: plan if: github.event_name != 'pull_request' && (needs.plan.outputs.solver == 'true' || needs.plan.outputs.everything == 'true') runs-on: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || 'ubuntu-24.04' }} timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: heavy save-if: ${{ github.ref == 'refs/heads/develop' }} - run: cargo xtask test --heavy 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. It runs # when the change reaches the solvers or the oracle's package. name: cargo xtask oracle needs: plan if: needs.plan.outputs.solver == 'true' || needs.plan.outputs.everything == 'true' runs-on: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || 'ubuntu-24.04' }} timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: oracle save-if: ${{ github.ref == 'refs/heads/develop' }} - run: cargo xtask oracle - name: docs/oracle.md is current run: git diff --exit-code docs/oracle.md oracle-macos: # The same oracle on macOS, weekly and on demand: the solvers must agree across platforms # to the fixture bands, and docs/oracle.md must read the same. name: cargo xtask oracle (macOS) if: github.event.schedule == '41 4 * * 0' || github.event_name == 'workflow_dispatch' runs-on: macos-14 timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: oracle-macos save-if: ${{ github.ref == 'refs/heads/develop' }} - run: cargo xtask oracle - name: docs/oracle.md is current run: git diff --exit-code docs/oracle.md windows: # The desktop app's tests on Windows, nightly and on demand: the Windows-only code # (apps/fieldcraft/src/native_crash.rs, the exception filter) builds and a child process that # faults on purpose leaves its native crash report (#299). Releases build Windows too. name: Windows app tests if: github.event.schedule == '23 3 * * *' || github.event_name == 'workflow_dispatch' runs-on: windows-latest timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: windows save-if: ${{ github.ref == 'refs/heads/develop' }} - run: cargo test --locked --profile ci -p fieldcraft advisories: name: Dependency licences, advisories and sources 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: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || 'ubuntu-24.04' }} timeout-minutes: 20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - name: Install cargo-deny run: cargo install cargo-deny --version 0.20.2 --locked - run: cargo deny --locked check --warn unmaintained advisories bans licenses sources