Files
komorebi/.github/workflows/ci.yaml
T
LGUG2ZandClaude Sonnet 5.5 9fce48d9d7 ci(starlight): the docs site is built when its files change, informationally
The `starlight` filter of ci-paths.yaml, and a job that runs `npm ci` and `npm run build` in starlight/. It is not
in the needs of `required` yet: the site has not been built by CI before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-10 14:05:07 -08:00

408 lines
16 KiB
YAML

# 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 }}
starlight: ${{ steps.final.outputs.starlight }}
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 }}
STARLIGHT: ${{ steps.filter.outputs.starlight }}
run: |
if [ "$ALL" = true ]; then LINUX=true MACOS=true WINDOWS=true STARLIGHT=true; fi
{
echo "linux=$LINUX"
echo "macos=$MACOS"
echo "windows=$WINDOWS"
echo "starlight=$STARLIGHT"
} | 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, docgen-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
# the docs site (starlight/), built as it is deployed. Informational: not in the needs of `required`, as the site
# has not been built by this workflow before. The reference pages in it are what `just docgen` writes.
starlight:
needs: [changes]
if: ${{ !cancelled() && (github.event_name != 'pull_request' || needs.changes.outputs.starlight == 'true') }}
runs-on: ubuntu-24.04
timeout-minutes: 15
defaults:
run:
working-directory: starlight
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: starlight/package-lock.json
- run: npm ci
- run: npm run build
# ---- 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, docgen-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