19 KiB
Continuous integration
Status: written together with the workflows, and none of them has run on GitHub: they were checked with
actionlint and shellcheck, and every nix attribute they name was evaluated (see "How to verify before merging").
What a hosted runner does (KVM, memory, disk, the macOS image, the Windows ARM image, Cachix) is not known until
the first run.
The workflows are in .github/workflows/; the paths that say which platform a change concerns are in
.github/ci-paths.yaml; the action that installs nix is .github/actions/nix.
The ladder (ci.yaml, named ci)
Each stage is cheaper than the next and starts only when the one before it passed, so a typo costs two minutes
and not an hour. A stage that a change does not concern is skipped, and a skipped job does not stop the stages after
it (their conditions start with !cancelled() && !failure()). ci / required needs every job, and passes when
each is success or skipped: it is the only check a ruleset needs to require.
| stage | jobs | runner | what it covers | when it runs |
|---|---|---|---|---|
| (gate) | changes |
ubuntu-24.04 | dorny/paths-filter over .github/ci-paths.yaml: linux, macos, windows |
always; a manual run concerns all platforms |
| 0 | hygiene |
ubuntu-24.04, nix develop .#hygiene |
no CRLF; the macros just lint bans (eyre!, dbg!, println! in the Linux and macOS daemons); actionlint; shellcheck of scripts/linux/wsl2-probe.sh and tools/*; tools/kde-closure-check; tools/test-partition check; dependencies.json is what just depgen writes |
always |
| 0 | fmt |
ubuntu-24.04, nix develop .#hygiene |
the nightly rustfmt of the flake over every file (tools/format-all --check), all platforms |
always |
| 0 | deny |
ubuntu-24.04, nix develop .#hygiene |
cargo deny check over the workspace |
always |
| 1 | nix-eval |
ubuntu-24.04, nix, swap | nix flake check --no-build --all-systems (every output of every system evaluates: the Windows, Linux and macOS packages and checks); builds checkSets.fmt, checkSets.linux-options, checkSets.macos-options (the macOS options and the nix-darwin module, evaluated on Linux), and the Windows e2e Python |
always |
| 2 | lint (4 legs) |
ubuntu-24.04, nix | checkSets.{windows-lint,linux-lint,recorder-lint,nixgen-lint}: clippy and rustdoc, Windows by cross build |
always |
| 2 | mac-typecheck |
ubuntu-24.04, nix | tools/mac-check.sh: the macOS packages type-checked for aarch64-apple-darwin, no link, no run |
macOS paths on a pull request; always on a push |
| 3 | test (4 legs) |
ubuntu-24.04, nix | checkSets.{linux-test,windows-test,recorder-test,nixgen-test}: Linux tests, the Windows test executables cross-built (not run), the recorder's and nixgen's tests |
always |
| 3 | windows-native-test |
windows-latest | cargo nextest of the windows and os_sensitive classes of tools/test-partition |
Windows or shared paths |
| 3 | windows-native-test-arm |
windows-11-arm | the same on ARM | label ci:win-arm; a push to master with Windows paths; by hand |
| 4 | discover, vm-fast |
ubuntu-24.04, KVM | the fast tier of the KDE VM suites, one machine each (max-parallel: 8, 45 minutes), a machine with no KVM drawn again (_vm.yaml, see KVM) |
Linux paths; label ci:vm; every push to master |
| 5 | macos-hosted |
macos-14 (_macos.yaml) |
the macOS checks, the recorder's macOS tests, the binaries, the schemas | label ci:macos on a pull request; macOS paths on a push |
Beside the ladder:
| workflow | jobs | runner | what it covers | when it runs |
|---|---|---|---|---|
macos-selfhosted.yaml |
macos (_macos.yaml) |
the Mac Mini | the same as macos-hosted, on the machine of the owner |
variable MACOS_SELF_HOSTED=true, and: by hand, a push to master, or label ci:macos-self on a pull request from this repository |
sweeps.yaml |
vm-slow |
ubuntu-24.04, KVM | the slow tier of the VM suites, 60 minutes each, logs kept 7 days, a machine with no KVM drawn again (see KVM) | label ci:vm-slow; every push to master; by hand |
nixos-eval, linux-demo |
ubuntu-24.04, swap | every NixOS system of the module and of the Plasma guests (about 11.6 GB to evaluate), and the demo guest | the same | |
canary (3 legs) |
ubuntu-24.04, KVM | every check against the tip of nixpkgs-unstable, one leg for the checks, one for each VM tier; it opens an issue when one fails | Saturday 05:20 UTC; by hand | |
update-flake-lock.yaml |
update |
ubuntu-24.04 | a pull request that moves flake.lock |
Monday 03:00 UTC; by hand |
windows-guest.yaml |
guest |
ubuntu-24.04, KVM | EXPERIMENTAL: builds the Windows guest from the evaluation ISO and runs nix run .#windows-nextest in it |
by hand only |
ci runs on a pull request (opened, pushed to, reopened, labeled), in the merge queue, on a push to master, and by hand. A
pull request cancels its own earlier run; nothing else cancels. The token of every workflow is read-only (contents: read), and
no workflow uses pull_request_target: a pull request from a fork gets no secret and no self-hosted runner.
The tools of stage 0 are the latest, and the flake is how CI gets them
Every step of hygiene, fmt and deny runs in nix develop .#hygiene (nix/common.nix), not with the
copy of a tool that the runner image or an action ships, so that a local nix develop .#hygiene and CI give the
same answer. The policy for those tools is to run the latest release and to change the code to satisfy it, not to
pin an older one or to silence a finding: when a mismatch shows up, move the flake forward first. The shell holds
the toolchain of rust-toolchain.toml (stable), the nightly rustfmt, cargo-deny, shellcheck, actionlint, jq
and ripgrep, and hygiene prints their versions at the start of every run, so a stale pin is visible in the log.
When this was written every one of them was the latest upstream release (actionlint 1.7.12, shellcheck 0.11.0,
cargo-deny 0.20.2, ripgrep 15.2.0, jq 1.8.2, cargo 1.99.1) and the nightly rustfmt was the one of 2026-11-04.
They stay current in two ways: the weekly update-flake-lock workflow (Mondays) moves every input, and a tool that
is behind the release in between is brought forward by hand with nix flake update rust-overlay (the toolchains
and the nightly rustfmt) or nix flake update nixpkgs (the rest; this one also moves Plasma, which the VM suites
test, so check them after it). Two tools are not in the shell and keep their own versions: taiki-e/install-action
installs nextest on the Windows runners, which have no nix, and windows.yaml (the release workflow) runs its own
cargo-deny action.
What is and is not covered
| Windows | Linux (KDE) | macOS | |
|---|---|---|---|
| format | fmt, checkSets.fmt |
the same | the same |
| clippy | lint (cross build) |
lint |
macos-hosted only (macos-lint); mac-typecheck type-checks on Linux |
| unit tests | windows-native-test on Windows, and windows.yaml on every event |
test (linux-test) |
macos-hosted only |
| integration | none (the guest is experimental: windows-guest.yaml) |
vm-fast, vm-slow |
none |
| options and schemas | linux-options, macos-options on Linux; the Windows schemas are checked by the Windows e2e Python |
linux-options |
macos-options on Linux; the schemas by macos-hosted |
The gaps:
- No Windows e2e: the guest needs the evaluation ISO and about 20 minutes of Windows Setup, and nothing says a hosted runner can do it.
- macOS is run only when asked for (the label, the paths on a push) or on the Mac Mini: hosted macOS minutes cost ten times those of Linux and the
macOS image may not have the nix store warm. Until then a macOS break is caught by
mac-typecheck(types) andnix-eval(evaluation), not by a test. - The nightly rustfmt of
fmtis the nightly of the day;checkSets.fmtin stage 1 uses the one the flake pins. When onlyfmtfails, the nightly moved. mac-typechecktakes the std ofaarch64-apple-darwinfrom the toolchain of the runner (rustup target add), because the dev shell of the flake only has the Windows target. clippy could run the same way (cargo clippy --target aarch64-apple-darwin);tools/mac-check.shdoes not.- A change that touches no platform path (documentation,
README.md) still runs stages 0 to 3, because they are cheap with the cache.
How this lives next to windows.yaml
windows.yaml is the release workflow of the Windows build and is not touched: it keeps running cargo test for every crate of the workspace on
every pull request and push to master (about 40 to 50 Windows minutes per event) and builds and publishes the nightly, the MSI and winget. The new
workflows do not publish anything, use no write-all, and use their own concurrency groups and artifact names (ci-*).
needs:does not cross workflows, sowindows-native-testwaits forlintofci, and not for the build ofwindows.yaml. The two run at the same time.windows.yamlhas nomerge_grouptrigger, so a ruleset cannot require it in a merge queue: it would never start. Requireci / requiredonly.- The partition makes
windows-native-testrun fewer tests thanwindows.yaml(just test-windows-fullandwindows-nextest-fullrun all of them by hand). When it has been green for a while, the test step ofwindows.yamlcan be removed by the owner of that file. - The partition (
tools/test-partition.json) putskomorebi-windowsin the classwindowsandkomorebi-config,-protocol,-transportandkomorebic-coreinos_sensitive: those are whatwindows-native-testruns.komorebi-client,komorebi-barandkomorebi-recorderaremixed, so a platform job names them with-p. The nix setwindows-testbuilds the same subset; the whole suite is the setwindows-test-full(not in the default sets, so not inci) andjust test-windows-full. - Both run
cargo-deny;ci / denyis the cheaper copy and stops the ladder first. actionlintinhygieneleaveswindows.yamlout: it reads the PowerShell of that file as bash.just lintruns it on every workflow and fails on that file.
Repository settings that the owner has to change
None of these can be set from a workflow.
- A ruleset on
masterthat requires the status checkci / required(and nothing else), with the merge queue on if wanted. - Settings, Actions, General: the default token permission is read-only; approval to run workflows from outside collaborators (first-time contributors at least).
- Already created, nothing to do: the public Cachix cache
komorebiand the variableCACHIX_CACHE=komorebi, the secretCACHIX_AUTH_TOKEN(read and write), the secretCACHIX_AUTH_TOKEN_ROand the variableCACHIX_PUBLIC_KEY. The workflows use onlyCACHIX_CACHEandCACHIX_AUTH_TOKEN: the cache is public, andcachix/cachix-actionconfigures the substituter and its key with no token at all, so a pull request, a fork and the merge queue read the cache without one andCACHIX_AUTH_TOKEN_ROandCACHIX_PUBLIC_KEYare not needed (they would only be for a hand-writtenextra-substituters). Every job that runs nix goes through.github/actions/nix(theupdate-flake-lockjob too), whose callers passvars.CACHIX_CACHEandsecrets.CACHIX_AUTH_TOKEN(the reusable_macos.yamlgets the secrets withsecrets: inherit). The write token is handed tocachix-action(as itsauthTokeninput) on apush(master, and the temporary komomono trigger, which no expression names), onworkflow_dispatchand onschedule; on apull_requestor in the merge queue it is an empty string and the action logsPushing is disabled. The only job that reads and never writes is the nixpkgs canary (push: "false"): it builds against another nixpkgs, which nothing else needs. A job that has noCACHIX_CACHEbuilds with no cache. Still the owner's: the variableMACOS_SELF_HOSTED(trueonce the Mac Mini has a runner with the labelsself-hosted,macOS,ARM64). - The labels:
ci:macos,ci:macos-self,ci:vm,ci:vm-slow,ci:win-arm. - Settings, Actions: allow the actions the workflows use (
cachix/*,dorny/paths-filter,taiki-e/install-action,Swatinem/rust-cache,DeterminateSystems/update-flake-lock,EmbarkStudios/cargo-deny-action) if the repository restricts them.
KVM on hosted runners
What GitHub documents: hardware acceleration (KVM) on the Linux runners, with the udev rule that .github/actions/nix writes
(changelog of 2024-04-02, and of 2023-02-23 for the first release on larger runners). What it does not document: that every
host of the ubuntu-24.04 pool has it. The pages "GitHub-hosted runners" and "Larger runners" say nothing about nested
virtualization, and no service level covers it. Nothing in the repository can choose the host: a job is given whichever one the
pool has. In run 42182554717 (all 16 hosts the same image, ubuntu24/20261101.360, spread over six Azure regions) 3 of 16 failed
the check of /dev/kvm, 13 had it; what is known of those 3 is only that the check failed 42 to 46 ms after the udev trigger, which
cannot tell a missing device from a rule that was not applied yet. The step now logs which of the two it is.
What the repository does:
.github/actions/nix, step "Enable KVM" (the only place that enables it:enable_kvmofcachix/install-nix-actionwrites the same rule, ignores the outcome, and is turned off): writes the rule, loadskvm_intelorkvm_amd, reloads and triggers udev, waits forudevadm settle, polls/dev/kvmfor 30 seconds for read and write access, and as the last resort sets the mode by hand. It printsls -l /dev/kvm,id,systemd-detect-virt, the count ofvmx/svmflags and the loadedkvmmodules, before and after._vm.yamlcalls_vm-attempt.yamlup to three times. In the first two a host with no KVM does not fail its leg: the leg uploads the artifactkvm-missing-<run attempt>-<attempt>-<suite>and ends green without having run the suite, and the next attempt (a jobplanthat lists the artifacts) runs those suites again, each on a new machine. In the third, a host with no KVM fails the suite. So a suite is run, or the call fails: three draws without KVM are the only way a suite is not run and the check is red for it.vm-fast(ci.yaml) andvm-slow(sweeps.yaml) are calls of_vm.yaml;requiredreads the result of the call. It works on any branch and any event, because it is inside the one run: it needs no workflow on the default branch and no rerun. (A rerun of the failed jobs of a run cannot be started by the run itself while it is in progress, and aworkflow_runworkflow runs only from the file on the default branch; that is why the earlierrerun-no-kvm.yamlis gone.)- Not covered: the three legs of
canaryandwindows-guest.yaml(one machine each, hours long): a host with no KVM fails the step "Enable KVM", with the messagethis runner has no usable /dev/kvm; run them again by hand.
When GitHub gives no KVM at all for a long stretch (every leg of three draws fails, the log of the step says the device is absent
and systemd-detect-virt or the vmx count says why): the check cannot pass on hosted runners. The ways out are a larger
runner (runs-on with a label of a larger-runner group: billed per minute also on a public repository, and nothing in the
documentation promises nested virtualization there either, only that the Android emulator is supported), or a self-hosted x86-64
Linux machine with KVM, with runs-on changed in _vm-attempt.yaml.
Not verified: none of this has run on GitHub. In particular the artifact listing of plan, the nesting of two reusable workflows
and the result of a call whose legs are skipped.
The komomono removal checklist
ci.yaml and macos-selfhosted.yaml run on a push to the temporary branch komomono, so that the stack can be tried before it is merged to master. Each
has a line - komomono under the comment # TEMPORARY: remove when komomono is merged to master. When it is merged:
- delete the
- komomonoline and its comment in.github/workflows/ci.yaml - delete the same two lines in
.github/workflows/macos-selfhosted.yaml sweeps.yamlnever runs on that branch (its push trigger ismasteronly): nothing to delete there
grep -rn komomono .github finds nothing afterwards. No expression in a workflow names the branch: the conditions say "a push" and "a push to master".
How to verify before merging
Nothing can be run on GitHub from a checkout, so the first run is the test.
nix shell nixpkgs#actionlint nixpkgs#shellcheck -c actionlintover the workflows other thanwindows.yaml.nix evalof every attribute the workflows build, for examplenix eval .#legacyPackages.x86_64-linux.checkSets.linux-lint.nameandnix eval --json .#legacyPackages.x86_64-linux.vmTiers;nix flake check --no-build --all-systems.- Push the stack to a scratch branch, add that branch to the
pushlist ofci.yamlin the scratch branch, and runciwithworkflow_dispatch(all platforms). Then: a pull request with no label (stages 0 to 3), one withci:vm, one withci:macos, and one withci:win-arm. - Break a check on purpose and see that
ci / requiredfails; skip a stage with a path and see that it passes. - Run
sweepsandwindows-guestby hand once.
Verify the cache after the first push to komomono
Pushes happen in the post step of cachix-action (the daemon registers a post-build hook, so only paths the job builds are pushed, not those substituted from cache.nixos.org;
the post step runs when the job fails too).
- the log of the
Post Run ./.github/actions/nixstep ofhygieneshowsCachix: pushandPushinglines (and notPushing is disabled) - the Cachix dashboard of
komorebilists new store paths and a size - a second run of the same commit is faster, and its log shows
copying path ... from 'https://komorebi.cachix.org' - a pull request run of the same jobs shows
Pushing is disabledand still copies fromkomorebi.cachix.org - the size of the cache against the plan of the account: the xwin Windows SDK (over 1 GB) and the NixOS guest closures (about 10 GB each for Plasma) are pushed unfiltered; if the free
tier is too small, a
pushFilteron thecachix-actionstep is the lever, at the price of rebuilding what it excludes
Open items
- Cachix: whether the free tier of the cache
komorebiis large enough for the store of this flake (several GB per system) is not verified. - KVM on hosted runners: documented for public repositories; see KVM below: GitHub does not promise it of every host, so the workflows draw again, up to three times in all.
- The macOS image:
macos-14is Apple silicon with its own nix install each run; whether the cache makes the checks fit the 2 hours of the job is not known. - The first run of stage 0 sets up the
hygieneshell, which is a download of about 1 GB until the Cachix cache has it (a push and a manual run push it). - The memory of
nix flake check --no-buildover the nixosConfigurations (about 7 GB) against a runner that has 7 GB (private repositories) or 16 GB (public): the swap of the action is the mitigation. windows-native-test-armassumes thewindows-11-armrunner exists for this repository and installs Rust itself when the image has none.