- Nix 59.9%
- Nushell 40.1%
| modules | ||
| scripts | ||
| tests | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
rot.nix
Server update checker for NixOS: check the last-update timestamp of every host
in your deploy-rs config. Ships a NixOS module that creates a dedicated
low-privilege ssh user, a client module that installs the tooling and ssh key,
a nushell check script, and NixOS VM tests.
The "last update" is the mtime of /run/current-system (the symlink mtime
changes on every nixos-rebuild switch / deploy-rs activation).
Layout
modules/update-check-user.nix # server module: rotcheck ssh user + authorized key
modules/rotcheck-client.nix # client module: installs check-updates + ssh key
modules/waybar.nix # home-manager module: waybar custom/rot widget
scripts/check-updates.nu # nushell: collect (structured) + print
scripts/waybar-updates.nu # nushell: waybar JSON widget (oldest host + hover list)
tests/ # VM tests (single / private-key / multi)
Server module (nixosModules.default)
Import it into every host you want to check, and set exactly one of:
{ config, ... }:
{
imports = [ rot-nix.nixosModules.default ];
services.rotcheck = {
enable = true;
# either a public key...
publicKey = "ssh-ed25519 AAAA...";
# ...or a private key file (e.g. an agenix secret) whose public key is
# derived at runtime by a systemd oneshot before sshd starts:
# privateKeyFile = "/run/secrets/rotcheck";
};
}
This creates the rotcheck user (configurable via services.rotcheck.userName)
with a shell-less-but-bash login, no groups, and access only to
stat/readlink on world-readable paths.
Client setup
On the machine that runs the checks, generate a keypair once and install it on every server:
ssh-keygen -t ed25519 -f ~/.ssh/rotcheck -C rotcheck # local keypair
ssh-copy-id -i ~/.ssh/rotcheck.pub rotcheck@server # for each server
The client module (nixosModules.rotcheckClient) installs the check-updates
binary and drops the private key where the script expects it
(/var/lib/rotcheck-client/key by default):
{ config, ... }:
{
imports = [ rot-nix.nixosModules.rotcheckClient ];
services.rotcheckClient = {
enable = true;
package = rot-nix.packages.${pkgs.system}.check-updates;
privateKeyFile = /path/to/rotcheck-private-key;
# user = "root"; # who owns the key / runs the checks
# keyPath = "/var/lib/rotcheck-client/key";
};
}
Running checks
nix run .#check-updates # discover hosts from .#rot.nodes
nix run .#check-updates -- --json # machine-readable output
nix run .#check-updates -- --flake ~/.dotfiles # hosts from another flake
nix run .#check-updates -- --hosts hosts.json --user rotcheck
--flake <ref> is the flake whose rot.nodes lists the hosts (default: the
flake in the current directory, .; a leading ~ is expanded for you).
--hosts <file> takes a JSON list of { "name": ..., "hostname": ... } records
(used by the VM tests) and wins over --flake. Without either, hosts are
discovered via nix eval .#rot.nodes.
Waybar widget
The homeModules.waybar home-manager module adds a custom/rot waybar module
that shows the single most out-of-date host as hostname: 12d and a full
per-host list (oldest first) in the tooltip on hover. It hands the flake you
configure (default ~/.dotfiles) to check-updates on PATH, which does the
nix eval and the ssh checks — so the machines it runs on should also import
rotcheckClient:
{ config, ... }:
{
imports = [ rot-nix.homeModules.waybar ];
programs.waybar.enable = true;
programs.rotcheckWaybar = {
enable = true;
flake = "~/.dotfiles"; # source of rot.nodes (default)
key = "/home/you/.ssh/rotcheck"; # waybar runs as your desktop user
# user = "rotcheck";
# interval = 3600; # re-check every hour
# make = [ "flake.nix" "flake.lock" ]; # only re-evaluate when these change
};
}
Or run nix run .#waybar-updates in your own waybar config:
"custom/rot": {
"exec": "waybar-updates --flake ~/.dotfiles --key /home/you/.ssh/rotcheck --make flake.nix flake.lock",
"return-type": "json",
"interval": 3600
}
Make-style host discovery
nix eval .#rot.nodes costs about a second, which is nothing once and a lot
when a waybar widget re-runs it every hour. --make <file>... makes the host
list a make target: the cached node list is the target, the files you pass are
its prerequisites, so the eval is redone only once one of them changed:
nix run .#check-updates -- --make flake.nix flake.lock
nix run .#waybar-updates -- --flake ~/.dotfiles --make flake.nix flake.lock
The staleness question is literally gnumake's: the script writes a one-line
makefile (<cache>: flake.nix flake.lock) and asks make -q whether it would
rebuild. Consequences worth knowing:
- The cache lands in
$XDG_CACHE_HOME/rot.nix/<full flake path>/rot-nodes.json(default~/.cache/...), keyed by the flake's location, so two flakes never share a node list. - A file you list that doesn't exist forces a re-eval, since nothing can prove the cache still matches.
- If the
nix evalitself fails (e.g. you broke the flake mid-edit), a non-empty cache is used instead of giving up, with a note on stderr. - The packaged scripts put
gnumakeon PATH (viamakeWrapperArgs), somakeis there wherever they are installed. If you run the.nufiles straight from the repo,nix developgives you bothnuandmake. - Relative paths are relative to the flake's directory in both scripts (waybar's cwd is unrelated to the flake it is pointed at).
Tests
nix flake check
The test keypair in tests/ is committed intentionally; it is test-only.