# The ladder: each stage is cheaper than the next and only starts when the one before it has passed, so # a typo costs two minutes and not an hour. Every job is a `needs:` of `required`, the one check that a # ruleset has to require: a job that is skipped (a stage that the paths of the change do not concern) # counts as passed, a job that failed or was cancelled does not. The model is in docs/dev/ci.md. # # Nothing here publishes anything: the release, nightly, MSI and winget jobs are windows.yaml's and # are not touched by this file. The paths that say which platform a change concerns are in # .github/ci-paths.yaml. name: ci on: pull_request: # `labeled` so that adding ci:vm or ci:win-arm starts the stage the label asks for types: [opened, synchronize, reopened, labeled] merge_group: push: branches: - master # TEMPORARY: remove when komomono is merged to master - komomono workflow_dispatch: # a pull request cancels its own earlier run; the runs of a push, of the merge queue and by hand never concurrency: group: ci-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read jobs: # which platforms the change concerns, from .github/ci-paths.yaml. A manual run concerns all of them. changes: runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: contents: read pull-requests: read outputs: linux: ${{ steps.final.outputs.linux }} macos: ${{ steps.final.outputs.macos }} windows: ${{ steps.final.outputs.windows }} steps: - uses: actions/checkout@v6 with: # the merge queue and a push are compared with a commit that a shallow clone does not have fetch-depth: 0 - uses: dorny/paths-filter@v3 id: filter with: filters: .github/ci-paths.yaml # the merge queue has no base of its own for the action: it is the commit the group is on base: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || '' }} - id: final env: ALL: ${{ github.event_name == 'workflow_dispatch' }} LINUX: ${{ steps.filter.outputs.linux }} MACOS: ${{ steps.filter.outputs.macos }} WINDOWS: ${{ steps.filter.outputs.windows }} run: | if [ "$ALL" = true ]; then LINUX=true MACOS=true WINDOWS=true; fi { echo "linux=$LINUX" echo "macos=$MACOS" echo "windows=$WINDOWS" } | tee -a "$GITHUB_OUTPUT" # ---- stage 0: the pinned tools of the flake, no builds, about two minutes ---- # # Every step of these jobs runs in `nix develop .#hygiene` (nix/common.nix): the versions of rustfmt, # cargo-deny, shellcheck, actionlint, jq and ripgrep are the ones the flake pins, not the ones the runner # image or an action ships, so a local run of the same step gives the same answer. hygiene: runs-on: ubuntu-24.04 timeout-minutes: 15 defaults: run: shell: nix develop .#hygiene --command bash -euo pipefail {0} steps: - uses: actions/checkout@v6 - uses: ./.github/actions/nix with: cachix-cache: ${{ vars.CACHIX_CACHE }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} # the versions of the shell, in every log: the latest releases are the policy, so a pin that has fallen # behind shows here - name: The tools of the flake run: | rustc --version rustfmt --version cargo deny --version shellcheck --version | sed -n 2p echo "actionlint $(actionlint -version | head -1)" jq --version rg --version | head -1 - name: No file is stored with CRLF run: | if git ls-files --eol | grep '^i/crlf'; then echo "::error::files are stored with CRLF" exit 1 fi # what `just lint` bans - name: The banned macros run: | status=0 if rg -n 'eyre!' --type rust .; then echo "::error::eyre! macro not allowed"; status=1; fi if rg -n 'dbg!' --type rust .; then echo "::error::dbg! macro not allowed"; status=1; fi if rg -n 'println!' --type rust crates/komorebi-linux/src crates/komorebi-macos/src; then echo "::error::println! macro not allowed"; status=1 fi exit "$status" - name: actionlint # windows.yaml is the release workflow of the Windows build and is not linted (its PowerShell is # read as bash by actionlint); the workflows of this ladder are run: | files=$(git ls-files '.github/workflows/*.yaml' ':!:.github/workflows/windows.yaml') # shellcheck disable=SC2086 actionlint -color $files - name: shellcheck run: | files=(scripts/linux/wsl2-probe.sh) for f in tools/*; do if head -n 1 "$f" | grep -q '^#!.*\(ba\)\?sh'; then files+=("$f"); fi done shellcheck "${files[@]}" - name: Cargo run: cargo fetch --locked - name: The Linux source filter matches the cargo closure run: tools/kde-closure-check - name: Every crate is in one class of the test partition run: tools/test-partition check - name: dependencies.json is current run: | cargo deny list --format json | jq 'del(.unlicensed) | walk(if type == "string" then sub(" path\\+file://.*$"; "") else . end)' \ >"$RUNNER_TEMP/dependencies.json" diff -u dependencies.json "$RUNNER_TEMP/dependencies.json" || { echo "::error::dependencies.json is stale: regenerate it with just depgen"; exit 1; } # the nightly rustfmt of the flake over every .rs file of the repository: the same one `checkSets.fmt` # runs in stage 1, so that a difference is the code and not the nightly of the day fmt: runs-on: ubuntu-24.04 timeout-minutes: 10 defaults: run: shell: nix develop .#hygiene --command bash -euo pipefail {0} steps: - uses: actions/checkout@v6 - uses: ./.github/actions/nix with: cachix-cache: ${{ vars.CACHIX_CACHE }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} - run: RUSTFMT=rustfmt tools/format-all --check --root . deny: runs-on: ubuntu-24.04 timeout-minutes: 10 defaults: run: shell: nix develop .#hygiene --command bash -euo pipefail {0} steps: - uses: actions/checkout@v6 - uses: ./.github/actions/nix with: cachix-cache: ${{ vars.CACHIX_CACHE }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} - run: cargo deny check # ---- stage 1: nix evaluation, and the checks that are options and formatting ---- nix-eval: needs: [hygiene, fmt, deny] runs-on: ubuntu-24.04 timeout-minutes: 45 steps: - uses: actions/checkout@v6 - uses: ./.github/actions/nix with: swap: "true" cachix-cache: ${{ vars.CACHIX_CACHE }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} # every output of every system evaluates, and no check is built. `nix flake check` also evaluates the # nixosConfigurations, which peak at several GB: the swap of the action is for that - run: nix flake check --no-build --all-systems # what is built here is what no later stage builds: the formatting, the options and the # nix-darwin module (plain Nix: they evaluate against x86_64-linux nixpkgs, so the macOS ones # are checked on Linux too) and the Windows e2e Python - run: | nix build -L --no-link \ .#checkSets.fmt \ .#checkSets.linux-options \ .#checkSets.macos-options \ .#checkMembers.windows-test.windows-e2e-tests # ---- stage 2: lint ---- lint: needs: [nix-eval] runs-on: ubuntu-24.04 timeout-minutes: 60 strategy: fail-fast: false matrix: set: [windows-lint, linux-lint, recorder-lint, nixgen-lint] steps: - uses: actions/checkout@v6 - uses: ./.github/actions/nix with: disk-needed-gb: "20" cachix-cache: ${{ vars.CACHIX_CACHE }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} - run: nix build -L --no-link .#checkSets.${{ matrix.set }} # the macOS packages type-checked on Linux (target aarch64-apple-darwin, through the fake C compiler of # tools/cc-fake.sh): it cannot link or run anything. The dev shell of the flake has the Windows # target and not this one, so the std of the target comes from the toolchain of the runner. clippy # could be run the same way (`cargo clippy --target aarch64-apple-darwin`); tools/mac-check.sh only checks. mac-typecheck: needs: [changes, nix-eval] if: ${{ !cancelled() && !failure() && (github.event_name != 'pull_request' || needs.changes.outputs.macos == 'true') }} runs-on: ubuntu-24.04 timeout-minutes: 45 steps: - uses: actions/checkout@v6 - uses: ./.github/actions/nix with: cachix-cache: ${{ vars.CACHIX_CACHE }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} - run: rustup target add aarch64-apple-darwin - run: tools/mac-check.sh # ---- stage 3: tests ---- test: needs: [lint] runs-on: ubuntu-24.04 timeout-minutes: 60 strategy: fail-fast: false matrix: set: [linux-test, windows-test, recorder-test, nixgen-test] steps: - uses: actions/checkout@v6 - uses: ./.github/actions/nix with: disk-needed-gb: "20" cachix-cache: ${{ vars.CACHIX_CACHE }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} - run: nix build -L --no-link .#checkSets.${{ matrix.set }} # The Windows tests on Windows: the partition of tools/test-partition (the Windows crates and the ones # whose tests depend on the OS). windows.yaml still runs every test of the workspace. windows-native-test: needs: [changes, lint] if: ${{ !cancelled() && !failure() && needs.changes.outputs.windows == 'true' }} runs-on: windows-latest timeout-minutes: 45 steps: # the scripts of tools/ must keep their LF - run: git config --global core.autocrlf false - uses: actions/checkout@v6 - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" save-if: ${{ github.ref == 'refs/heads/master' }} - shell: bash run: | # the words of the two lists are the arguments # shellcheck disable=SC2046 cargo nextest run --locked $(tools/test-partition packages windows) $(tools/test-partition packages os_sensitive) # the same on Windows ARM: with the label ci:win-arm, on master, and by hand windows-native-test-arm: needs: [changes, lint] if: >- ${{ !cancelled() && !failure() && ( github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'ci:win-arm') || (github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.changes.outputs.windows == 'true')) }} runs-on: windows-11-arm timeout-minutes: 60 steps: - run: git config --global core.autocrlf false - uses: actions/checkout@v6 # the image may not have Rust - name: Rust shell: pwsh run: | if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { Invoke-WebRequest https://win.rustup.rs/aarch64 -OutFile rustup-init.exe ./rustup-init.exe -y --default-toolchain stable --profile minimal "$env:USERPROFILE\.cargo\bin" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH } - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" save-if: ${{ github.ref == 'refs/heads/master' }} - shell: bash run: | # the words of the two lists are the arguments # shellcheck disable=SC2046 cargo nextest run --locked $(tools/test-partition packages windows) $(tools/test-partition packages os_sensitive) # A hosted runner of Apple silicon with the latest macOS, the only one that is supported; free on a public repository. macos-hosted: needs: [changes, lint] if: ${{ !cancelled() && !failure() && needs.changes.outputs.macos == 'true' }} uses: ./.github/workflows/_macos.yaml with: runs-on: '"macos-latest"' secrets: inherit # ---- stage 4: the KDE guests ---- # The suites are found in the flake, so that a new directory under nix/linux/tests/vm/suites is run # without an edit here. discover: needs: [changes, test] if: >- ${{ !cancelled() && !failure() && ( needs.changes.outputs.linux == 'true' || contains(github.event.pull_request.labels.*.name, 'ci:vm') || (github.event_name == 'push' && github.ref == 'refs/heads/master')) }} runs-on: ubuntu-24.04 timeout-minutes: 15 outputs: fast: ${{ steps.tiers.outputs.fast }} steps: - uses: actions/checkout@v6 - uses: ./.github/actions/nix with: cachix-cache: ${{ vars.CACHIX_CACHE }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} - id: tiers run: | tiers=$(nix eval --json .#legacyPackages.x86_64-linux.vmTiers) echo "fast=$(jq -c '.fast // []' <<<"$tiers")" >> "$GITHUB_OUTPUT" # Each suite in its own machine, in a sandboxed `nix build` like any other check. A machine with no # KVM is drawn again, three draws at most (_vm.yaml); `actions: read` is for the planning of the later draws. vm-fast: needs: [discover] if: ${{ !cancelled() && !failure() && needs.discover.result == 'success' && needs.discover.outputs.fast != '[]' }} permissions: contents: read actions: read uses: ./.github/workflows/_vm.yaml with: suites: ${{ needs.discover.outputs.fast }} secrets: inherit # the only check that a ruleset requires: every job above passed or was skipped required: if: always() needs: - changes - hygiene - fmt - deny - nix-eval - lint - mac-typecheck - test - windows-native-test - windows-native-test-arm - discover - vm-fast - macos-hosted runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - env: NEEDS: ${{ toJSON(needs) }} run: | jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<<"$NEEDS" if jq -e 'any(.[]; .result == "failure" or .result == "cancelled")' <<<"$NEEDS" >/dev/null; then echo "::error::a job failed or was cancelled" exit 1 fi