name: CI on: push: branches: [main] pull_request: branches: [main] # Spelled out only to keep `ready_for_review` in the list — naming any type # replaces the whole default set, so the other three have to be restated. # It is inert on this instance (draft state here is the `WIP:` title # prefix, so un-drafting is a title edit and raises no # `ready_for_review` action) and costs nothing. # # The consequence, which is the part that bites: the `if:` guard below is # evaluated when a run is CREATED, and un-drafting creates no run. A PR # opened as a draft keeps its skip decision until something else produces # one. Push an empty commit after un-WIP'ing. types: [opened, synchronize, reopened, ready_for_review] # gitdan-ci runs four repos' CI on two capacity slots, and the compiler-backed # suites below are multi-minute. A superseded run costs a slot in front of # somebody's build, so drop it. # # `push` groups on `github.sha` rather than `github.ref`: a constant per-branch # group is what let this Gitea (1.26.0) cancel two of daniel/gitdan's merge # runs outright while `cancel-in-progress` was gated away from `push` entirely # — see the long note in that repo's ci.yaml for the evidence. Giving every # commit its own group leaves that behaviour nothing to act on. concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }} cancel-in-progress: true jobs: selftest: name: shellcheck + selftests # Two clauses, both load-bearing. The second skips draft PRs: Gitea sets # draft:true when the title starts with `WIP:`, so work-in-progress pushes # cost the shared runner nothing until the PR is un-WIP'd. The first is # what keeps that from also skipping pushes to `main` — a `push` event has # no `pull_request` context, so `github.event.pull_request.draft` is empty # there and the negation alone would be unreliable. Never drop it. if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-latest timeout-minutes: 20 # Deliberately no `container.volumes:` entry, unlike every repo that # CONSUMES this action. The suites below build throwaway workspaces under # `mktemp -d` and want a cold target dir every time — a persistent cache # would make "did this run rebuild?" unanswerable, which is the question # restore-mtimes-selftest.sh exists to ask. So this job takes no share of # the shared CI cache disk budget. steps: - name: Checkout sources uses: actions/checkout@v4 - name: Install shellcheck uses: taiki-e/install-action@v2 with: tool: shellcheck # `hardlink-clone-selftest.sh` and `restore-mtimes-selftest.sh` drive a # real Cargo against a real scratch workspace — they are the only things # here that verify the hardlink-aliasing and mtime-freshness behaviour # against the compiler rather than against a fixture, and selftest.sh's # own header says `--fast` is for iterating, not for signing off a # change. So CI installs a toolchain and runs the full set. # # The scratch workspaces use path dependencies only, so nothing here # reaches crates.io. # Nightly first, stable second, so stable ends up the default and # nightly is only reachable through an explicit `+nightly`. # # `hardlink-clone-selftest.sh`'s last scenario needs a Cargo that # resolves freshness by CONTENT — the mode where the dep-info file # carries per-source checksums, which is the mutation that turns a # hardlink clone into silent stale-artifact reuse rather than a slow # build. This step is what supplies it, and as of 2026-08-26 it does: # the scenario ran and passed against 1.100.0-nightly. # # It briefly did not. Cargo PR #17382 (2026-08-22) demoted # `-Z checksum-freshness` to a gate and gave `build.fingerprint` the # choice, defaulting to `mtime`, so the suite — which set only the gate — # measured a genuine INACTIVE and skipped its strongest scenario. That # read as "upstream withdrew content freshness" and was written up here # as this step buying nothing. It was a moved switch, not a withdrawal; # the suite now exports both and the coverage is back. See # daniel/gitdan#62 for the investigation. - name: Install Rust nightly uses: dtolnay/rust-toolchain@nightly - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable # Every script, including the suites themselves. `-x` follows the # `. cache-lib.sh` each one sources, which is where most of the logic # being checked actually lives; without it shellcheck reports SC1091 and # analyses each file with a hole in it. - name: shellcheck run: shellcheck -x --source-path=scripts scripts/*.sh # One command, not six: selftest.sh is the entry point a developer runs, # so a suite added there is gated here without a matching edit in this # file. - name: Selftests run: bash scripts/selftest.sh