Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
# Keep the GitHub Actions used by the workflows pinned and current.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "automated"
14 changes: 9 additions & 5 deletions .github/workflows/fmt.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Nix Format Check
name: Nix Format & Lint Check

on:
push:
Expand All @@ -13,7 +13,7 @@ on:

jobs:
fmt:
name: Check Nix formatting
name: Check Nix formatting & lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
Expand All @@ -26,7 +26,11 @@ jobs:
extra_nix_config: |
experimental-features = nix-command flakes

- name: Check formatting
- name: Cache Nix store
uses: DeterminateSystems/magic-nix-cache-action@v8

# Builds the treefmt check (alejandra + statix + deadnix). Fails if any
# file is unformatted or trips a lint. `nix fmt` locally fixes them.
- name: Check formatting & lint
working-directory: nixos
run: |
nix fmt -- --check .
run: nix build .#checks.x86_64-linux.treefmt -L
30 changes: 30 additions & 0 deletions .github/workflows/update-flake-lock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Update flake.lock

on:
schedule:
# Mondays at 06:00 UTC
- cron: "0 6 * * 1"
workflow_dispatch: {} # allow manual runs

jobs:
update-lock:
name: Update nixos/flake.lock
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main

- name: Update flake.lock and open PR
uses: DeterminateSystems/update-flake-lock@main
with:
path-to-flake-dir: nixos
pr-title: "chore(flake): update flake.lock"
pr-labels: |
dependencies
automated
6 changes: 6 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,9 @@ jobs:
run: |
echo "Building moria..."
nix build .#darwinConfigurations.moria.config.system.build.toplevel --dry-run

- name: Validate citadel
working-directory: nixos
run: |
echo "Building citadel..."
nix build .#darwinConfigurations.citadel.config.system.build.toplevel --dry-run
61 changes: 46 additions & 15 deletions nixos/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,55 @@ See [.devcontainer/README.md](.devcontainer/README.md) for details.

If needed, see [README.md](README.md) for detailed documentation on repository structure, VM setup, and development workflows.

## Future: Automatic Nix Garbage Collection for Darwin Hosts
## Automatic Nix Garbage Collection for Darwin Hosts

Both darwin hosts use Determinate Nix (`nix.enable = false` in `modules/darwin/common.nix`),
which means nix-darwin's built-in `nix.gc` module **cannot** be used (it asserts `nix.gc.automatic
requires nix.enable`). If store bloat becomes a problem, create a custom launchd daemon in
`modules/darwin/nix-gc.nix`:
The darwin hosts run Determinate Nix (`nix.enable = false` in `modules/darwin/common.nix`),
so nix-darwin's built-in `nix.gc` module can't be used (it asserts `nix.gc.automatic` requires
`nix.enable`). **A hand-rolled `launchd.daemons.nix-gc` is no longer the right answer** —
Determinate Nix ships **Determinate Nixd**, a daemon that performs garbage collection natively.

To enable scheduled GC, adopt the Determinate nix-darwin module instead of writing custom
launchd jobs:

1. Add the flake input: `determinate.url = "github:DeterminateSystems/determinate"`.
2. Import `inputs.determinate.darwinModules.default` in `modules/darwin/common.nix`.
3. Configure the collector, e.g.:

```nix
{ ... }: {
launchd.daemons.nix-gc = {
command = "/nix/var/nix/profiles/default/bin/nix-collect-garbage --delete-older-than 30d";
serviceConfig = {
RunAtLoad = false;
StartCalendarInterval = [{ Weekday = 7; Hour = 3; Minute = 0; }];
};
};
{
determinate-nix.customSettings = { };
# GC strategy lives under the Determinate Nixd config:
# /etc/determinate/config.json (written by the module)
# e.g. garbageCollector.strategy = "automatic";
}
```

Import it from each host's `default.nix`. Runs as root (launchd daemons default), so it
cleans both user and system generations — without `sudo`, only user generations are collected.
See <https://docs.determinate.systems/guides/nix-darwin/> for the current option names
(`determinateNixd.garbageCollector.strategy`). This replaces — and is simpler than — the
old custom-daemon approach, and is correctly disk-pressure aware.

## Secret Management — Decision Record (1Password vs. agenix / sops-nix)

**Current approach (keep):** Secrets live in 1Password (vault **Infra**). Committed `.tpl`
files hold `{{ op://Infra/Item/field }}` references; `just secrets` runs `op inject` to
write the real (gitignored) files. See the toolbox root `CLAUDE.md` → "Secret Management"
for the exact commands and prerequisites.

**Why it's worth a note:** `op inject` writes **plaintext** generated files to disk and
needs an interactive 1Password GUI unlock. On headless **dungeon** that means connecting
via VNC + Touch ID before every `just secrets` — a manual, non-reproducible step that
doesn't fit the otherwise declarative activation flow.

**Alternatives considered (not adopted this round):**

- **agenix** — secrets are `age`-encrypted *into the repo* (safe to commit) and decrypted
at activation to a tmpfs (RAM, never written to disk) using each host's existing SSH host
key. Simplest fit for our small set of standalone tokens; no GUI, no manual step, works
headless. Tradeoff: re-keying when host keys change, and editing requires the `agenix` CLI.
- **sops-nix** — same activation-time, key-based decryption but with `sops`/YAML/`age` and
better ergonomics for *bundled* multi-key secret files. More machinery than we need today.

**Decision: defer.** 1Password stays the source of truth. The manual headless `just secrets`
step is tolerable while dungeon is the only headless Darwin host. **Revisit (lean agenix)**
if a second headless host appears, or if the VNC-unlock dance becomes a recurring pain —
both decrypt at activation to tmpfs and eliminate the plaintext-on-disk + GUI-unlock steps.
20 changes: 10 additions & 10 deletions nixos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,25 @@ See [.devcontainer/README.md](.devcontainer/README.md) for more details.

### Git Hooks

Git hooks are available to automate validation:
Git hooks are managed declaratively via [git-hooks.nix](https://github.com/cachix/git-hooks.nix)
(see [flake-modules/dev.nix](flake-modules/dev.nix)) and installed automatically when you
enter the dev shell (`nix develop` from `nixos/`):

- **pre-commit**: Runs `nix fmt` on staged `.nix` files
- **pre-push**: Validates all NixOS configs using the devcontainer
- **pre-commit**: Runs `treefmt` (alejandra + statix + deadnix) on staged `.nix` files
- **pre-push**: Runs `nix flake check`

**Installation:**
```bash
./nixos/scripts/install-hooks.sh
```

After installation, the hooks run automatically. To skip (not recommended):
To skip (not recommended):
```bash
git commit --no-verify
git push --no-verify
```

## Pre Configured Shell

We have a pre configured shell available, with all the required tooling. Simply run `$ nix-shell` and you have everything set up. This is powered by our `shell.nix` file, and is useful on a freshly installed NixOS machine.
A reproducible dev shell with all required tooling (treefmt, just, nh, ...) is defined as the
flake's default dev shell in [flake-modules/dev.nix](flake-modules/dev.nix). From `nixos/`, run
`$ nix develop` (or let direnv do it) to get everything set up — useful on a freshly installed
machine. Entering the shell also installs the git hooks above.

## Claude Code Best Practices

Expand Down
41 changes: 41 additions & 0 deletions nixos/flake-modules/dev.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Dev shell + git hooks.
#
# Replaces the standalone shell.nix and the hand-rolled scripts/hooks/*:
# * `nix develop` gives a reproducible env (treefmt + the tools the justfile uses)
# and installs the git hooks on entry.
# * pre-commit runs treefmt (alejandra + statix + deadnix) on staged files.
# * pre-push runs `nix flake check` so broken configs never reach the remote.
{
perSystem = {
config,
pkgs,
...
}: {
pre-commit.settings.hooks = {
treefmt = {
enable = true;
package = config.treefmt.build.wrapper;
};

flake-check = {
enable = true;
name = "nix flake check";
entry = "nix flake check";
pass_filenames = false;
stages = ["pre-push"];
};
};

devShells.default = pkgs.mkShell {
inputsFrom = [config.treefmt.build.devShell];
shellHook = config.pre-commit.installationScript;
packages = with pkgs; [
git
just
nh
nix-output-monitor
nvd
];
};
};
}
110 changes: 110 additions & 0 deletions nixos/flake-modules/hosts.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# NixOS + nix-darwin host definitions.
#
# Each host used to repeat an identical specialArgs + module list; the mkNixos/
# mkDarwin helpers below collapse that boilerplate so a host is one entry
# (system + module path + any host-specific extra modules / vars overrides).
{
inputs,
self,
...
}: let
inherit (inputs) nixpkgs nixos-wsl nixos-hardware home-manager stylix darwin;

vars = import ../config/vars.nix {inherit (nixpkgs) lib;};

# Minimal home-manager wiring shared by every host.
mkHomeManagerModule = _: {
home-manager = {
useUserPackages = true;
backupFileExtension = "backup";
users.${vars.user.name} = {};
};
};

mkNixos = {
system,
modulePath,
extraModules ? [],
hostVars ? vars,
}:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit inputs;
vars = hostVars;
outputs = self;
};
modules =
[
modulePath
stylix.nixosModules.stylix
home-manager.nixosModules.home-manager
mkHomeManagerModule
]
++ extraModules;
};

mkDarwin = {
modulePath,
system ? "aarch64-darwin",
extraModules ? [],
hostVars ? vars,
}:
darwin.lib.darwinSystem {
inherit system;
specialArgs = {
inherit inputs;
vars = hostVars;
outputs = self;
};
modules =
[
modulePath
home-manager.darwinModules.home-manager
]
++ extraModules;
};
in {
flake.nixosConfigurations = {
foundation = mkNixos {
system = "x86_64-linux";
modulePath = ../hosts/vms/foundation;
extraModules = [nixos-wsl.nixosModules.default];
};

isengard = mkNixos {
system = "x86_64-linux";
modulePath = ../hosts/pcs/isengard;
extraModules = [nixos-hardware.nixosModules.lenovo-thinkpad-t420];
};

home-lab = mkNixos {
system = "x86_64-linux";
modulePath = ../hosts/vms/home-lab;
};

# Writerdeck — console-only, distraction-free writing machine.
# See: https://veronicaexplains.net/my-first-writerdeck/
rohan = mkNixos {
system = "x86_64-linux";
modulePath = ../hosts/pcs/rohan;
};

mines = mkNixos {
system = "aarch64-linux";
modulePath = ../hosts/vms/mines;
};
};

flake.darwinConfigurations = {
dungeon = mkDarwin {modulePath = ../hosts/macs/dungeon;};

moria = mkDarwin {modulePath = ../hosts/macs/moria;};

# Mozilla work machine: override the user name (drives git identity, home dir).
citadel = mkDarwin {
modulePath = ../hosts/macs/citadel;
hostVars = vars // {user = vars.user // {name = "greghilston";};};
};
};
}
22 changes: 22 additions & 0 deletions nixos/flake-modules/treefmt.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Formatting + linting, exposed as `nix fmt` and a `nix flake check` check.
#
# treefmt-nix wires these together: alejandra formats, statix flags anti-patterns,
# deadnix removes dead code. `nix fmt` applies fixes; `nix flake check` fails on diff.
{
perSystem = _: {
treefmt = {
projectRootFile = "flake.nix";

programs = {
alejandra.enable = true;
statix.enable = true;
deadnix = {
enable = true;
# Leave unused function args (e.g. `{ pkgs, ... }`) alone — they are
# part of NixOS/home-manager module signatures, not dead code.
no-lambda-pattern-names = true;
};
};
};
};
}
Loading
Loading