Flake checks

Updated

July 30, 2026

Flake checks

Goal: Add flake checks so nix flake check exercises eval/format/build assumptions locally—the same shape CI will use later.

Why this chapter exists

A flake that only switches on your laptop is a private ritual. Checks turn “I hope this still evaluates” into a command:

nix flake check

This chapter is not full nixosTest mastery (ops/testing chapters). It is lightweight gates: format, build a shell, evaluate a host, maybe build a package.


Theory 1 — What checks are

Flake schema: checks.<system>.<name> → derivations. nix flake check builds them (with some special cases).

Check type Purpose
Format / lint Style consistency
nixosSystem eval/build Config still evaluates
mkShell build DevShell inputs resolve
Small package build Packaging sanity
nixosTest VM integration (later parts)

Failed check → non-zero exit → CI red.


Theory 2 — Hand-rolled checks skeleton

# inside outputs, with forAllSystems / pkgsFor from the flake-parts chapter
checks = forAllSystems (system:
  let
    pkgs = pkgsFor system;
  in {
    # 1) formatter check
    format = pkgs.runCommand "check-format" {
      nativeBuildInputs = [ pkgs.nixpkgs-fmt ];
      src = pkgs.lib.cleanSource ./.;
    } ''
      set -euo pipefail
      cp -r $src tree
      chmod -R u+w tree
      cd tree
      find . -name '*.nix' -print0 | xargs -0 nixpkgs-fmt --check
      mkdir -p $out
      touch $out/ok
    '';

    # 2) devShell builds
    devshell = self.devShells.${system}.default;
  });

Building the full host toplevel is an excellent check but can be slow and architecture-bound. For a laptop-only x86_64-linux lab, pinning checks to that system is fine.

Host toplevel (strong, heavy)

// nixpkgs.lib.optionalAttrs (system == "x86_64-linux") {
  lab-toplevel = self.nixosConfigurations.lab.config.system.build.toplevel;
}

Gate by system so aarch64 eval of an x86-only host is not nonsense.

Lighter host check — eval-oriented

Some teams use CI scripts (nixos-rebuild build) without putting toplevel in checks. Either works if documented.


Theory 3 — flake-parts checks

Under perSystem:

perSystem = { self', pkgs, ... }: {
  checks = {
    devshell = self'.devShells.default;
    # treefmt / git-hooks modules exist in the ecosystem—optional
  };
};

Community modules (treefmt-nix, git-hooks.nix) are optional accelerators—do not require them if time is short.


Theory 4 — Format check that actually fails

A useful format check must see your sources and use the formatter’s check mode.

Notes:

  • --check must exist for your formatter CLI (nixpkgs-fmt supports it; other formatters differ)
  • Copying entire flake including .git can be heavy—trim with lib.cleanSource
  • IFD and huge contexts slow checks—keep them lean
src = pkgs.lib.cleanSource ./.;

Alternative: treefmt (awareness)

treefmt + treefmt-nix can unify formatters. Optional complexity—only adopt if you want multi-language format in one gate.


Theory 5 — What belongs in check vs push-time CI only

Local nix flake check CI later
Format Same + matrix systems
Shell / small builds Cache push
Optional host toplevel Full VM tests
Fast enough to run pre-commit Longer integration

Keep local checks fast enough that you run them. A 40-minute check nobody runs is theater.

Budget (guideline) Contents
< 1–2 min warm format + shell
< 10–15 min + host toplevel on one system
CI nightly tests, multi-system, slow builds

Theory 6 — nix flake check UX

nix flake check
nix flake check -L          # logs
nix build .#checks.x86_64-linux.format -L

Individual check attrs help debug without running the full set.

Common failure modes

Failure Meaning
format Style drift
devshell Missing package / bad mkShell
lab-toplevel Host module error
Eval error before build checks attrset itself broken

Theory 7 — Checks and secrets

Checks often copy sources into the builder. Do not place plaintext secrets in the flake tree. Encrypted sops files are ciphertext (OK); private keys and .env must stay out.

.env
*.pem
keys.txt

Theory 8 — Wiring consistency with pkgs/overlays

If overlays affect hello-lab or formatters, pkgsFor in checks must match host/devShell construction—or document intentional differences.

pkgsFor = system: import nixpkgs {
  inherit system;
  overlays = import ./overlays;
};

Worked example — Practical minimal set

checks = forAllSystems (system:
  let pkgs = pkgsFor system; in {
    shell = self.devShells.${system}.default;
  } // nixpkgs.lib.optionalAttrs (system == "x86_64-linux") {
    # lab-toplevel = self.nixosConfigurations.lab.config.system.build.toplevel;
  });

Uncomment toplevel when you accept the build cost.

Worked example — Script wrapper

# tools/check.sh
#!/usr/bin/env bash
set -euo pipefail
nix flake check -L
chmod +x tools/check.sh
./tools/check.sh

Exercises

Exercise 1 — Baseline without checks

cd /path/to/your-flake
nix flake show
nix flake check || true

Note current behavior (may pass with zero checks or warn).

Exercise 2 — Add devShell as a check

Wire checks.<system>.shell = self.devShells.${system}.default (or flake-parts equivalent).

nix build .#checks.x86_64-linux.shell -L

Exercise 3 — Add a format check that can fail

  1. Implement format check on *.nix
  2. Run nix flake check
  3. Break formatting in a temp edit
  4. Confirm check fails
  5. Restore format

Exercise 4 — Optional host toplevel check

If disk/time allow:

nix build .#checks.x86_64-linux.lab-toplevel -L

Record wall-clock time. If over your patience budget, document “CI-only toplevel” and keep shell+format local.

Exercise 5 — Developer loop script (optional)

Add tools/check.sh or a Makefile target. Not required if you prefer raw commands.

Exercise 6 — Journal: CI shape

Write the future CI job in five lines:

checkout
install nix
nix flake check
# later: deploy job separate

Exercise 7 — Single-attr debug

nix build .#checks.x86_64-linux.format -L

Practice isolating one check.

Exercise 8 — cleanSource impact

If format check is slow or includes junk, switch to lib.cleanSource and remeasure.

Exercise 9 — System gate

If you list multiple systems, ensure host toplevel is only on the host architecture.

Exercise 10 — Pre-commit habit

Run nix flake check before a real commit. Note anything surprising (downloads, rebuilds).

Exercise 11 — Document check inventory

In docs/flake-style.md or docs/checks.md, list each check and what failure means.

Exercise 12 — Secret scan of check context

Confirm no private key paths are required for checks to pass.

Exercise 13 — Fail-closed demo

Leave a format break on a throwaway branch; show nix flake check non-zero; fix on mainline branch only.

Exercise 14 — Overlay consistency

If you have a wrapper package from overlays, add a tiny check that builds it—or document why not.


Common gotchas

Symptom / mistake What to do
Checks empty / not discovered Must be under checks output correctly
IFD surprises slow checks Avoid unnecessary import-from-derivation
Building host on wrong system optionalAttrs gate
Format check always succeeds You didn’t pass real sources
Check context includes secrets Don’t; keep secrets out of flake source
nix flake check downloads the world First run cold; then cached—note substituters
Top-level too slow → abandoned Split local vs CI checks
flake-parts checks in wrong section Use perSystem.checks
Formatter mismatch vs nix fmt Same package in formatter + format check
Broken self reference Ensure self.devShells exists before check

Checkpoint

  • At least two meaningful checks (e.g. shell + format)
  • nix flake check fails when format intentionally broken
  • Know how to build one check attribute
  • Host toplevel check added or explicitly deferred with reason
  • Future CI sketch written
  • Checks are fast enough to run before commit
  • Documented inventory of checks
  • No secrets required to evaluate checks

Journal (optional)

Write three personal gotchas before continuing.