Day 66 — CI pipeline

Updated

July 30, 2026

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.

Note

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 -L
Warning

Pin 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 -L

House 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_BRANCH

Prefer 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 -L

Split 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 green

If 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.txt

Step 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.md notes 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.