Add a top-level README mapping the boxes and a full docs/ tree: topic pages (architecture, networking, deployment), per-site box pages for colony and home with containers nested under their hosts, remote and mobile boxes, the installer, and the home switch fabric reference (folded in from home-switches.md, with AGENTS.md and code comments retargeted to its new home). Box pages carry marked assignment tables that CI regenerates from nixos.allAssignments. AGENTS.md points at the new docs and keeps its terse agent version of the mechanics, referring to the topic pages for depth.
11 KiB
Deployment and workflows
How boxes get deployed, the devshell commands that drive everyday work, secrets, dev VMs, and CI.
deploy-rs
Rendering
The top-level deploy-rs.nix renders the flake's deploy output:
- It collects
nixos.systemsandhome-manager.homes(mangling@in home names to-at-, since deploy-rs node names can't contain@). An assertion rejects name collisions between systems and homes. - A system/home becomes a deploy node only when
configuration.config.my.deploy.enableis true. The node config is the definitions of the box'smy.deploy.nodeoption, re-imported as a module so deploy-rs's submodule merging still applies. autoRollbackandmagicRollbackare off globally; globalsshOptsare[ "-i" ".keys/deploy.key" ](set inflake.nix).- The result is passed through
lib.my.deploy-rs.filterOpts, which strips nulls so unset options don't override deploy-rs defaults, and exposed asdeploy = deploy-rs.rendered.
Per-box nodes
The shared nixos/modules/deploy-rs.nix module provides
my.deploy:
my.deploy.enabledefaults to true, but is automatically forced off for dev VMs and NixOS containers (my.build.isDevVM/boot.isContainer— containers are deployed through their host instead, see below). The installer also disables it explicitly.- Node defaults:
hostname= the box's FQDN,sshUser = "deploy",user = "root",sudo = "doas -u"(orsudo -u),sshOpts= the box's first OpenSSH port. The module creates thedeploysystem user (bash shell, wheel, keys frommy.deploy.authorizedKeys, which defaults to.keys/deploy.pub). - Generated profiles:
system— activatesconfig.system.build.toplevelwithswitch-to-configuration switch, with workarounds for the/tmpcwd issue (NixOS/nixpkgs#73404) and the systemd-bootloader.confdefault-entry issue (deploy-rs#31), then prunes old generations (nix-env --delete-generations +10, tunable viakeepGenerations, 0 disables).container-<name>— one permy.containers.instancesentry, activating the container'smy.buildAs.containertoplevel into/nix/var/nix/profiles/per-container/<name>/system. WithhotReload(default) the profile reloadssystemd-nspawn@<name>(restarting only if the container is down or still running the placeholder "dummy" init); otherwise it restarts it. Generation cleanup applies here too.- Profiles deploy in order
system, then the containers.
Usage
deploy .#<host>— the devshell'sdeployis a wrapper that adds--skip-checks. Node names are the system names (deploy .#git).deploy-multi <hosts...>— loopsdeployover several nodes (extra args via$O).deploy --boot .#<host>— stages the config as the boot default without live-switching (deploy-rs's--bootmaps to the custom activation'sbootphase, which also does the generation cleanup). Use it when a liveswitchwould cut the box off mid-change (e.g. a router WAN rework), then reboot to cut over.ssh-machine <name> [cmd]— resolvesuser@hostand the merged global+nodesshOptsfrom.#deploy(samemy.deploy.enablegate asdeploy, same@→-at-mangling), then execsssh. Boxes default to thefishlogin shell; pipe multi-statement remote scripts throughbash(ssh-machine <name> bash -s < script.sh). If outbound SSH hangs at the publickey step (flaky agent), useSSH_AUTH_SOCK= ssh-machine ….nix flake checkincludes deploy-rs's owndeployChecksfor the wholedeployattrset.
Devshell commands
The repo ships a numtide/devshell (devshell/, entered via direnv). Run a
command with no arguments for its help. From
devshell/commands.nix:
| Command | What it does |
|---|---|
check-system <host> [nix args] |
Evaluates .#nixosConfigurations."<host>".config.system.build.toplevel.drvPath — catches module/option errors without building. Prefer this to validate a config change. |
build-system <host> [nix args] |
Builds the system's toplevel (extra args pass through to nix build). |
build-n-switch <args> |
doas nixos-rebuild --flake . (adds the repo as a git safe.directory for root first). |
build-home <name> [nix args] |
Builds .#homeConfigurations."<name>".activationPackage. |
home-switch [args] |
home-manager switch --flake .. |
deploy [args] |
deploy-rs --skip-checks (wrapper package in devshell/default.nix). |
deploy-multi <nodes...> |
Deploys several nodes in sequence. |
ssh-machine <name> [cmd] |
SSH to a system or home by name, resolving target/options from its deploy-rs node (see above). |
run-vm <host> |
Boots a system as a dev VM: installs .keys/dev.key into a temp xchg/, then nix runs config.my.buildAs.devVM. |
build-iso <host> |
Builds config.my.buildAs.iso. |
build-kexec <host> |
Builds config.my.buildAs.kexecTree. |
build-netboot <host> |
Builds config.my.buildAs.netbootTree. |
ragenix [args] |
ragenix --identity .keys/dev.key (see Secrets). |
repl |
nix repl .#. |
fmt [args] |
nixpkgs-fmt (the canonical formatter). |
update-nixpkgs |
nix flake update nixpkgs-{unstable,stable,mine,mine-stable}. |
update-home-manager |
nix flake update home-manager-{unstable,stable}. |
update-installer |
Force-pushes the installer tag to trigger the installer release workflow. |
home-link / home-unlink |
Symlink (or remove) this flake.nix at ~/.config/home-manager/flake.nix for standalone home-manager use. |
qemu-genmac |
Prints a random QEMU-suitable MAC (52:54:00:xx:xx:xx). |
ssh-get-ed25519 <host> |
Prints a host's ed25519 pubkey via ssh-keyscan. |
json2nix |
Converts JSON on stdin to formatted Nix. |
From devshell/install.nix (driven by $INSTALLER, the address of
a running custom installer, SSHing as root with .keys/deploy.key):
| Command | What it does |
|---|---|
installer-shell [cmd] |
Runs a command (default: a shell) inside the installer. |
do-install [--no-bootloader] [--no-substitute] <system> |
Builds the system's toplevel, nix copys it into the installer's target store, sets the system profile, and activates it with switch-to-configuration boot (with NIXOS_INSTALL_BOOTLOADER=1 unless --no-bootloader). |
From devshell/vm-tasks.nix (remote VM consoles; they forward the
VM's unix sockets from /run/vms/<vm>/ on <host> over SSH):
| Command | What it does |
|---|---|
vm-tty <host> <vm> |
Serial TTY of a VM in minicom. |
vm-monitor <host> <vm> |
QEMU monitor socket in minicom. |
vm-viewer <host> <vm> |
SPICE display in virt-viewer (not on Darwin). |
Secrets
Secrets are age-encrypted files in secrets/, managed with ragenix (a fork
with a rekey flag, from the flake inputs).
- Each box declares
my.secrets.key(the host public key its secrets encrypt to) andmy.secrets.files.<name>(files to decrypt; attribute values merge into the agenix secret, e.g.owner/mode). At runtime thesecretsmodule decrypts viaage.secrets."<name>".path, with identity paths derived from the box's OpenSSH host keys — read from the tmproot persistence dir when there is one, since agenix runs before persisted keys would otherwise be available. secrets.nix(the ragenix rules file at the repo root) is generated: it evaluates the flake, collects every system'smy.secrets.files+key, and emits each secret with its recipient list — always including.keys/dev.pubso the dev key can open everything. Re-runningragenix -rre-keys after adding a box or secret.- The
ragenixdevshell command wrapsragenix --identity .keys/dev.key; use it to edit/rekey secrets. - The
.keys/directory holds the dev key (dev.key/dev.pub), the deploy key (deploy.key/deploy.pub, authorized on every box'sdeployuser), and assorted other keys referenced bylib.my.c.sshKeyFiles. It is required for editing secrets, deploying, and running dev VMs.
Dev VMs
Any system can be built as a throwaway QEMU VM via config.my.buildAs.devVM (the build
module extends the config with qemu-vm.nix and sets my.build.isDevVM). run-vm <host>
creates a temp dir, installs .keys/dev.key as xchg/dev.key, and runs the VM; inside, the
secrets module switches age.identityPaths to that dev key (my.secrets.vmKeyPath,
default /tmp/xchg/dev.key), so dev VMs can decrypt the boxes' secrets without the real host
keys. Dev VMs also get DHCP on eth0, an SSH port forward (host 2222 → guest 22), and are
automatically excluded from deploy targets.
CI
GitHub/Gitea Actions workflows live in .gitea/workflows/:
ci.yaml(push tomaster):nix flake check --no-build, then for every attribute of.#ci.x86_64-linux(each system assystem-<name>, each home ashome-<name>with@→-at-, each package aspackage-<name>, plus the devshell) it builds and pushes the result to the Harmonia binary cache withci/push-to-cache.sh. Finally it builds.#ciDrv.x86_64-linux(alinkFarmof all CI attrs) and pushes it withUPDATE_PROFILE=1, which updates thenixfilesprofile on the cache box and garbage collects paths older than 60 days. The cache isssh-ng://harmonia@object-ctr.ams1.int.nul.ie(remote store/var/lib/harmonia), keyed by theHARMONIA_SSH_KEYsecret with a pinnedci/known_hosts; clients consume it ashttps://nix-cache.nul.ie(seelib.my.c.nix.cache).installer.yaml(push of theinstallertag;update-installerrefreshes it): builds the installer'smy.buildAs.isoandmy.buildAs.netbootArchiveand attaches both to a release.update-docs.yaml(push to the docs branch, skipping its own commits): runsnix run .#update-docs-assignmentsand commits any changes asdocs: update assignment tables.
The docs assignment-table updater
update-docs-assignments is registered in pkgs/default.nix (a
writeShellScriptBin wrapping ci/update-docs-assignments.py).
It evaluates .#nixfiles.config.nixos.allAssignments to JSON, walks docs/**/*.md, and for
each file looks for a marker line <!-- assignments: <box> --> where <box> is the file's
own stem (e.g. river.md → river) and names a real box. The generated table (columns
Name | Assignment | IPv4 | IPv6 | Domain | Notes) is (re)written between the marker and a
closing <!-- assignments-end --> line, with an <!-- assignments-start --> line inserted
after the marker on the first run. Hand-written text in the Notes column is preserved
across runs (keyed by the Assignment cell), so notes survive regeneration. Pages that want a
table only need the marker + end marker; pages without markers (including the three top-level
docs) are left alone. The script exits non-zero when it changed something, which is how the
workflow knows whether to commit.