No description
  • Nix 59.9%
  • Nushell 40.1%
Find a file
2026-10-10 22:52:59 +02:00
modules feat: add make like flake eval cache 2026-09-30 15:16:06 +02:00
scripts fix: get actual build timestamp and simplify extraction 2026-10-10 22:52:59 +02:00
tests test: remove unnecessary direct ssh invocations 2026-09-21 23:47:20 +02:00
flake.lock init 2026-09-21 23:47:18 +02:00
flake.nix feat: add make like flake eval cache 2026-09-30 15:16:06 +02:00
README.md feat: add make like flake eval cache 2026-09-30 15:16:06 +02:00

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 eval itself 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 gnumake on PATH (via makeWrapperArgs), so make is there wherever they are installed. If you run the .nu files straight from the repo, nix develop gives you both nu and make.
  • 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.