fedora-csb-system-manager
  1{
  2  config,
  3  lib,
  4  pkgs,
  5  ...
  6}:
  7
  8with lib;
  9
 10let
 11  cfg = config.services.nix-flake-updater;
 12
 13  # Use the nix-flake-update package
 14  updateScript = pkgs.writeShellScript "nix-flake-update-wrapper" ''
 15    export REPO_PATH="${cfg.repoPath}"
 16    export FLAKE_PATH="${cfg.flakePath}"
 17    export GIT_REMOTE="${cfg.gitRemote}"
 18    export BRANCH_PREFIX="${cfg.branchPrefix}"
 19    export NTFY_TOPIC="${cfg.ntfyTopic}"
 20    export NTFY_SERVER="${cfg.ntfyServer}"
 21    export BUILD_SYSTEMS="${toString cfg.buildSystems}"
 22    export DRY_RUN="${toString cfg.dryRun}"
 23    ${optionalString (cfg.ntfyTokenFile != null) ''export NTFY_TOKEN_FILE="${cfg.ntfyTokenFile}"''}
 24
 25    # Execute the packaged update script (already has tools in PATH)
 26    exec ${pkgs.nix-flake-update}/bin/nix-flake-update
 27  '';
 28
 29in
 30{
 31  options.services.nix-flake-updater = {
 32    enable = mkEnableOption "automated Nix flake.lock updates";
 33
 34    repoPath = mkOption {
 35      type = types.str;
 36      example = "/home/user/nixos-config";
 37      description = "Path to the git repository containing the flake";
 38    };
 39
 40    flakePath = mkOption {
 41      type = types.str;
 42      default = cfg.repoPath;
 43      example = "/home/user/nixos-config";
 44      description = "Path to the flake (usually same as repoPath)";
 45    };
 46
 47    gitRemote = mkOption {
 48      type = types.str;
 49      default = "origin";
 50      description = "Git remote name to push to";
 51    };
 52
 53    branchPrefix = mkOption {
 54      type = types.str;
 55      default = "flake-update-";
 56      description = "Prefix for update branches";
 57    };
 58
 59    buildSystems = mkOption {
 60      type = types.listOf types.str;
 61      default = [ ];
 62      example = [
 63        "aomi"
 64        "sakhalin"
 65      ];
 66      description = "List of NixOS systems to build for verification";
 67    };
 68
 69    schedule = mkOption {
 70      type = types.str;
 71      default = "weekly";
 72      example = "Mon *-*-* 02:00:00";
 73      description = "Systemd timer schedule (OnCalendar format or 'weekly'/'daily')";
 74    };
 75
 76    ntfyTopic = mkOption {
 77      type = types.str;
 78      default = "nix-updates";
 79      description = "ntfy topic for notifications";
 80    };
 81
 82    ntfyServer = mkOption {
 83      type = types.str;
 84      default = "https://ntfy.sh";
 85      example = "http://ntfy.sbr.pm";
 86      description = "ntfy server URL";
 87    };
 88
 89    ntfyTokenFile = mkOption {
 90      type = types.nullOr types.path;
 91      default = null;
 92      description = "Path to file containing ntfy authentication token (optional)";
 93    };
 94
 95    dryRun = mkOption {
 96      type = types.bool;
 97      default = false;
 98      description = "If true, don't push to remote (testing mode)";
 99    };
100
101    user = mkOption {
102      type = types.str;
103      default = "root";
104      description = "User to run the update as";
105    };
106
107    randomizedDelaySec = mkOption {
108      type = types.int;
109      default = 3600;
110      description = "Random delay in seconds before starting (0-value)";
111    };
112  };
113
114  config = mkIf cfg.enable {
115    systemd.services.nix-flake-updater = {
116      description = "Automated Nix flake.lock updater";
117
118      serviceConfig = {
119        Type = "oneshot";
120        User = cfg.user;
121        ExecStart = "${updateScript}";
122        Environment = ''"GIT_SSH_COMMAND=ssh -o ControlMaster=no"'';
123
124        # Security hardening
125        PrivateTmp = true;
126        ProtectSystem = "strict";
127        ProtectHome = "read-only";
128        ReadWritePaths = [
129          cfg.repoPath
130          "/var/log/nix-flake-updater"
131          # Worktree location (script creates worktrees in ~/tmp)
132          "/home/${cfg.user}/tmp"
133          # Nix cache for flake fetcher
134          "/home/${cfg.user}/.cache/nix"
135        ];
136        NoNewPrivileges = true;
137
138        # Logging
139        StandardOutput = "journal";
140        StandardError = "journal";
141        SyslogIdentifier = "nix-flake-updater";
142      };
143
144      # Don't fail if update fails (e.g., no changes, build failures)
145      unitConfig = {
146        SuccessExitStatus = "0 1";
147      };
148    };
149
150    systemd.timers.nix-flake-updater = {
151      description = "Timer for automated Nix flake.lock updates";
152      wantedBy = [ "timers.target" ];
153
154      timerConfig = {
155        OnCalendar = cfg.schedule;
156        RandomizedDelaySec = cfg.randomizedDelaySec;
157        Persistent = true;
158      };
159    };
160
161    # Ensure log directory exists
162    systemd.tmpfiles.rules = [
163      "d /var/log/nix-flake-updater 0750 ${cfg.user} ${config.users.users.${cfg.user}.group} -"
164    ];
165  };
166}