Day 66 — CI pipeline
Day 66 — CI pipeline
Stage VI · ~4h
Goal: Run your flake checks/builds on GitHub Actions (or GitLab) with a modern Nix installer, optional Cachix/Attic push, and a PR-shaped workflow that fails on regressions.
If tests only run on your laptop, they are hobbies. 2026 default shape: forge CI + flakes + binary cache. Baseline nixpkgs: 26.05.
Why this day exists
Stage V packages and Stage VI modules become team infrastructure only when a clean runner reproduces them. CI also forces purity: “works on my machine” impurities die in public logs. Capstone Day 85 reuses this muscle.
Theory 1 — Minimal trustworthy pipeline
checkout → install Nix → (auth cache) → nix flake check / build → (push cache)
| Job / step | Purpose |
|---|---|
checkout |
Source + lockfile |
install Nix |
Known installer; flakes enabled |
cache auth |
Pull/push substitutes |
check |
nix flake check or selected checks |
build-pkg |
Stage V package |
build-hosts |
Optional toplevel builds |
Trust boundaries
| Surface | Rule |
|---|---|
| PR from fork | Often no secret cache write tokens |
main |
May push cache |
| Logs | Never print age keys, Cachix tokens, SSH keys |
trusted-users |
Do not weaken developer laptops for CI |
Theory 2 — GitHub Actions workflow (2026-shaped)
# .github/workflows/nix.yml
name: nix
on:
push:
branches: [ main ]
pull_request:
workflow_dispatch:
permissions:
contents: read
# id-token: write # if using OIDC for caches later
concurrency:
group: nix-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
# pin to a reviewed tag in real repos
- name: Magic Nix Cache (optional)
uses: DeterminateSystems/magic-nix-cache-action@main
# or Cachix below
# - uses: cachix/cachix-action@v15
# with:
# name: myorg
# authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Flake metadata
run: nix flake metadata
- name: Selected checks
run: |
nix build -L \
.#checks.x86_64-linux.cooltool-runs \
.#checks.x86_64-linux.ssh-hardening
- name: Build package
run: nix build .#cooltool -L
- name: Build edge toplevel (optional, heavy)
run: nix build .#nixosConfigurations.edge.config.system.build.toplevel -LPin action versions to tags or SHAs you have reviewed. Versions above are illustrative—confirm current upstream tags when you wire the repo. Freeze pins harder by Day 87 for Capstone.
Theory 3 — Cachix (and friends) in CI
- uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-26.05
- uses: cachix/cachix-action@v15
with:
name: myorg
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
# signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' # if used
- run: nix build .#cooltool -LHouse rules
| Rule | Why |
|---|---|
| Push from CI (common) | Shared binaries; less laptop upload chaos |
| Separate PR vs main push rights | Abuse / token theft mitigation |
| Public keys on consumers | Lab + admin substituters + trusted-public-keys |
| Never commit private signing keys | Rotate immediately if leaked |
Alternatives
| Tool | Notes |
|---|---|
| Cachix | Hosted; simple |
| Attic | Self-hosted multi-tenant cache |
| Harmonia | Serve local store as binary cache |
| Magic Nix Cache | UX-oriented GitHub cache integration |
Any one done well beats none.
Theory 4 — What not to run on free runners
| Heavy | Strategy |
|---|---|
| Full nixosTest matrix | Linux job; fewer nodes; path filters |
| Multiple toplevels | Build one primary host per PR |
| Nested KVM | May work on GH-hosted; else self-hosted runner |
| Secret full decrypt | Dummy configs for CI (Day 65) |
Unbounded flake check |
Select attrs until budget known |
on:
pull_request:
paths:
- 'flake.nix'
- 'flake.lock'
- 'modules/**'
- 'hosts/**'
- 'pkgs/**'
- 'tests/**'
- '.github/workflows/nix.yml'Timeouts and disks
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
# …Out-of-disk: slim checks, GC mid-job only if needed, or larger runners.
Theory 5 — GitLab CI sketch
# .gitlab-ci.yml (sketch)
image: debian:bookworm
variables:
NIX_CONFIG: "experimental-features = nix-command flakes"
before_script:
- apt-get update && apt-get install -y curl xz-utils
# install Nix via official/Determinate method — prefer current docs
- nix flake metadata
check:
script:
- nix flake check -L
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHPrefer maintained install methods over ancient nixos/nix container tags when flakes misbehave.
Theory 6 — CI vs deploy
| Pattern | Day 66 stance |
|---|---|
| CI verifies build/check | Default |
| CI pushes cache | Preferred |
| CI deploys to lab | Optional later; protect with environments |
| CI holds prod SSH keys | High risk; avoid early |
Human or gated CD deploy after green checks is enough for Stage VI. Capstone Day 85 revisits.
Worked example — selected checks job
- name: Fast checks
run: |
set -euo pipefail
nix build -L \
.#checks.x86_64-linux.cooltool-runs \
.#checks.x86_64-linux.ssh-hardening
- name: Optional full check
if: github.ref == 'refs/heads/main'
run: nix flake check -LSplit fast PR signal from heavier main-branch verification when needed.
Worked example — intentional red PR
# on a branch: break a check assertion or package
git checkout -b ci/red-test
# … introduce fail …
git commit -am "ci: temporary red"
git push -u origin ci/red-test
# open PR → confirm CI red
# fix → confirm greenIf you cannot get a red PR, the workflow is not protecting you.
Lab — multi-step
Suggested notes: ~/lab/90daysofx/02-nixos/day66
Step 1 — Add workflow file
Commit .github/workflows/nix.yml (or GitLab equivalent) to your lab flake repo. Use path filters if monorepo.
Step 2 — First green CI
Push branch; fix until green on a clean runner. Paste run URL into notes.
mkdir -p ~/lab/90daysofx/02-nixos/day66
echo "CI_URL=…" > ~/lab/90daysofx/02-nixos/day66/ci-url.txtStep 3 — Wire cache
Cachix or Magic Nix Cache or Attic. Confirm second run is faster or shows substitutes.
Step 4 — PR simulation
Open PR that breaks a check; confirm CI red; fix; green.
Step 5 — Secrets hygiene
Ensure tokens only in forge secrets; scan workflow for accidental echo of secrets.
Step 6 — Consumer config
Add substituter public keys to lab/admin Nix config; rebuild or set user nix.conf.
Step 7 — Document runner needs
KVM? memory? timeout? Write docs/CI.md with:
| Topic | Content |
|---|---|
| Triggers | PR/main |
| Commands | exact nix build lines |
| Cache | name + public key |
| Limits | known failures without KVM |
| Non-goals | deploy-from-CI policy |
Step 8 — Status badge (optional)
README badge for the workflow—only if green is honest.
Step 9 — Deploy from CI? (decide)
Write the choice: verify-only vs gated deploy. Default for this day: verify-only.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Flakes disabled | Installer action enables; set NIX_CONFIG |
| Disk full on runner | Slim checks; larger runner; GC carefully |
| Unpinned actions | Pin majors/SHAs per org policy |
| Cache not used | Public key / auth; path existence; wrong cache name |
| VM tests fail without KVM | Self-hosted or reduce tests |
| Monorepo wrong cwd | defaults.run.working-directory / filters |
| Fork PRs leaking write tokens | Restrict secret access |
| IFD / eval forever | Fix expressions; select attrs |
Checkpoint
- Workflow exists and runs on PR/push
- At least one Nix build/check green in CI
- Intentional red PR observed
- Cache strategy configured or explicitly deferred with reason
docs/CI.mdnotes runner limits and commands
- Tokens not in git
- Three personal gotchas
Commit
git add .
git commit -m "day66: GitHub Actions Nix CI pipeline"Write three personal gotchas before continuing.
Tomorrow
Day 67 — GC & store hygiene: roots, retention, optimise.