Commit 61065af898ba

Vincent Demeester <vincent@sbr.pm>
2026-01-13 16:52:41
feat(harmonia): add nightly cache pre-population builder
Add systemd timer service to build systems nightly for cache pre-population: - Builder configuration with customizable system list - Nightly schedule (02:00 for aomi, 02:30 for aion) - ntfy notifications for build status - Auto git pull before building aomi builds: aomi, kyushu, sakhalin (x86_64-linux) aion builds: aion, athena, demeter, aix, rhea (aarch64-linux) This ensures the cache is always populated with fresh builds of all systems, reducing build times when deploying configuration changes. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent b597134
Changed files (3)
modules
harmonia
systems
modules/harmonia/default.nix
@@ -1,6 +1,7 @@
 {
   config,
   lib,
+  pkgs,
   ...
 }:
 
@@ -53,29 +54,194 @@ in
           Lower values = higher priority.
         '';
       };
-    };
-  };
 
-  config = mkIf cfg.enable {
-    services.harmonia = {
-      enable = true;
-      signKeyPaths = [ cfg.signKeyPath ];
-      settings = {
-        bind = "[::]:${toString cfg.port}";
-        workers = cfg.workers;
-        max_connection_rate = cfg.maxConnectionRate;
-        priority = cfg.priority;
+      builder = {
+        enable = mkEnableOption "Nightly cache pre-population builds";
+
+        systems = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          example = [
+            "aomi"
+            "kyushu"
+          ];
+          description = ''
+            List of NixOS system configurations to build nightly.
+            These should be system names from the flake's nixosConfigurations.
+          '';
+        };
+
+        flakePath = mkOption {
+          type = types.str;
+          default = "/home/vincent/src/home";
+          description = ''
+            Path to the flake repository containing system configurations.
+          '';
+        };
+
+        schedule = mkOption {
+          type = types.str;
+          default = "daily";
+          example = "02:00";
+          description = ''
+            When to run the cache builder. Can be a systemd calendar expression
+            or one of: hourly, daily, weekly, monthly.
+          '';
+        };
+
+        notification = {
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Enable ntfy notifications for build status";
+          };
+
+          ntfyUrl = mkOption {
+            type = types.str;
+            default = "https://ntfy.sbr.pm";
+            description = "ntfy server URL";
+          };
+
+          topic = mkOption {
+            type = types.str;
+            default = "builds";
+            description = "ntfy topic to publish to";
+          };
+
+          tokenFile = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = "Path to file containing ntfy auth token";
+          };
+        };
       };
     };
-
-    networking.firewall.allowedTCPPorts = [ cfg.port ];
-
-    # Ensure the signing key file exists and has correct permissions
-    assertions = [
-      {
-        assertion = cfg.signKeyPath != "";
-        message = "services.harmonia-cache.signKeyPath must be set";
-      }
-    ];
   };
+
+  config = mkIf cfg.enable (mkMerge [
+    # Harmonia cache server
+    {
+      services.harmonia = {
+        enable = true;
+        signKeyPaths = [ cfg.signKeyPath ];
+        settings = {
+          bind = "[::]:${toString cfg.port}";
+          workers = cfg.workers;
+          max_connection_rate = cfg.maxConnectionRate;
+          priority = cfg.priority;
+        };
+      };
+
+      networking.firewall.allowedTCPPorts = [ cfg.port ];
+
+      # Ensure the signing key file exists and has correct permissions
+      assertions = [
+        {
+          assertion = cfg.signKeyPath != "";
+          message = "services.harmonia-cache.signKeyPath must be set";
+        }
+      ];
+    }
+
+    # Cache pre-population builder
+    (mkIf cfg.builder.enable {
+      assertions = [
+        {
+          assertion = cfg.builder.systems != [ ];
+          message = "services.harmonia-cache.builder.systems must not be empty when builder is enabled";
+        }
+      ];
+
+      systemd.services.harmonia-cache-builder = {
+        description = "Build NixOS systems for cache pre-population";
+        wants = [ "network-online.target" ];
+        after = [
+          "network-online.target"
+          "harmonia.service"
+        ];
+
+        serviceConfig = {
+          Type = "oneshot";
+          User = "root";
+          # Set a reasonable timeout (2 hours)
+          TimeoutStartSec = "2h";
+        };
+
+        script =
+          let
+            notifyStart = optionalString cfg.builder.notification.enable ''
+              ${pkgs.curl}/bin/curl -X POST \
+                ${
+                  optionalString (cfg.builder.notification.tokenFile != null)
+                    ''-H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.builder.notification.tokenFile})" \''
+                } \
+                -H "Title: Cache Builder Starting (${config.networking.hostName})" \
+                -d "Building ${toString (length cfg.builder.systems)} system(s) for cache pre-population" \
+                ${cfg.builder.notification.ntfyUrl}/${cfg.builder.notification.topic} || true
+            '';
+
+            notifySuccess = optionalString cfg.builder.notification.enable ''
+              ${pkgs.curl}/bin/curl -X POST \
+                ${
+                  optionalString (cfg.builder.notification.tokenFile != null)
+                    ''-H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.builder.notification.tokenFile})" \''
+                } \
+                -H "Title: Cache Builder Complete (${config.networking.hostName})" \
+                -H "Tags: white_check_mark" \
+                -d "Successfully built ${toString (length cfg.builder.systems)} system(s)" \
+                ${cfg.builder.notification.ntfyUrl}/${cfg.builder.notification.topic} || true
+            '';
+
+            notifyFailure = optionalString cfg.builder.notification.enable ''
+              ${pkgs.curl}/bin/curl -X POST \
+                ${
+                  optionalString (cfg.builder.notification.tokenFile != null)
+                    ''-H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.builder.notification.tokenFile})" \''
+                } \
+                -H "Title: Cache Builder Failed (${config.networking.hostName})" \
+                -H "Tags: x,warning" \
+                -H "Priority: high" \
+                -d "Failed to build systems. Check logs: journalctl -u harmonia-cache-builder.service" \
+                ${cfg.builder.notification.ntfyUrl}/${cfg.builder.notification.topic} || true
+            '';
+
+            buildSystems = concatMapStringsSep "\n" (system: ''
+              echo "Building system: ${system}"
+              ${pkgs.nixos-rebuild}/bin/nixos-rebuild build --flake ${cfg.builder.flakePath}#${system} || {
+                echo "Failed to build ${system}"
+                ${notifyFailure}
+                exit 1
+              }
+            '') cfg.builder.systems;
+          in
+          ''
+            set -euo pipefail
+
+            ${notifyStart}
+
+            cd ${cfg.builder.flakePath}
+
+            # Update flake inputs
+            echo "Updating flake inputs..."
+            ${pkgs.git}/bin/git pull || echo "Warning: Failed to pull latest changes"
+
+            ${buildSystems}
+
+            echo "All systems built successfully!"
+            ${notifySuccess}
+          '';
+      };
+
+      systemd.timers.harmonia-cache-builder = {
+        description = "Timer for harmonia cache pre-population";
+        wantedBy = [ "timers.target" ];
+
+        timerConfig = {
+          OnCalendar = cfg.builder.schedule;
+          Persistent = true;
+          RandomizedDelaySec = "30m";
+        };
+      };
+    })
+  ]);
 }
systems/aion/extra.nix
@@ -94,6 +94,23 @@ in
       port = 5000;
       workers = 4;
       priority = 30;
+
+      # Nightly cache pre-population
+      builder = {
+        enable = true;
+        systems = [
+          "aion" # Self
+          "athena" # RPi4
+          "demeter" # RPi4
+          "aix" # RPi4
+          "rhea" # Media server
+        ];
+        schedule = "02:30"; # 2:30 AM daily (offset from aomi)
+        notification = {
+          enable = true;
+          tokenFile = config.age.secrets."ntfy-token".path;
+        };
+      };
     };
 
     wireguard = {
systems/aomi/extra.nix
@@ -73,6 +73,21 @@
     port = 5000;
     workers = 4;
     priority = 30;
+
+    # Nightly cache pre-population
+    builder = {
+      enable = true;
+      systems = [
+        "aomi" # Self
+        "kyushu" # Work laptop
+        "sakhalin" # Server
+      ];
+      schedule = "02:00"; # 2 AM daily
+      notification = {
+        enable = true;
+        tokenFile = config.age.secrets."ntfy-token".path;
+      };
+    };
   };
 
   # Remote build system