auto-update-daily-20260202
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.nix-flake-updater;
12
13 instanceOpts =
14 { config, ... }:
15 {
16 options = {
17 enable = mkEnableOption "this flake updater instance";
18
19 repoPath = mkOption {
20 type = types.str;
21 example = "/home/user/nixos-config";
22 description = "Path to the git repository containing the flake";
23 };
24
25 flakePath = mkOption {
26 type = types.str;
27 default = config.repoPath;
28 example = "/home/user/nixos-config";
29 description = "Path to the flake (usually same as repoPath)";
30 };
31
32 gitRemote = mkOption {
33 type = types.str;
34 default = "origin";
35 description = "Git remote name to push to";
36 };
37
38 mainBranch = mkOption {
39 type = types.str;
40 default = "main";
41 description = "Main branch name (for auto-merge)";
42 };
43
44 branchPrefix = mkOption {
45 type = types.str;
46 default = "flake-update-";
47 description = "Prefix for update branches";
48 };
49
50 flakeInputs = mkOption {
51 type = types.listOf types.str;
52 default = [ ];
53 example = [
54 "chick-group"
55 "chapeau-rouge"
56 ];
57 description = "List of specific flake inputs to update (empty = all)";
58 };
59
60 autoMerge = mkOption {
61 type = types.bool;
62 default = false;
63 description = "If true, automatically merge to main branch on successful build";
64 };
65
66 inboxOrg = mkOption {
67 type = types.str;
68 default = "/home/${config.user}/desktop/org/inbox.org";
69 example = "/home/user/org/inbox.org";
70 description = "Path to org-mode inbox file for TODO entries on failure";
71 };
72
73 buildSystems = mkOption {
74 type = types.listOf types.str;
75 default = [ ];
76 example = [
77 "aomi"
78 "sakhalin"
79 ];
80 description = "List of NixOS systems to build for verification";
81 };
82
83 schedule = mkOption {
84 type = types.str;
85 default = "weekly";
86 example = "Mon *-*-* 02:00:00";
87 description = "Systemd timer schedule (OnCalendar format or 'weekly'/'daily')";
88 };
89
90 ntfyTopic = mkOption {
91 type = types.str;
92 default = "nix-updates";
93 description = "ntfy topic for notifications";
94 };
95
96 ntfyServer = mkOption {
97 type = types.str;
98 default = "https://ntfy.sh";
99 example = "http://ntfy.sbr.pm";
100 description = "ntfy server URL";
101 };
102
103 ntfyTokenFile = mkOption {
104 type = types.nullOr types.path;
105 default = null;
106 description = "Path to file containing ntfy authentication token (optional)";
107 };
108
109 dryRun = mkOption {
110 type = types.bool;
111 default = false;
112 description = "If true, don't push to remote (testing mode)";
113 };
114
115 user = mkOption {
116 type = types.str;
117 default = "root";
118 description = "User to run the update as";
119 };
120
121 randomizedDelaySec = mkOption {
122 type = types.int;
123 default = 3600;
124 description = "Random delay in seconds before starting (0-value)";
125 };
126 };
127 };
128
129 mkUpdateScript =
130 name: instanceCfg:
131 pkgs.writeShellScript "nix-flake-update-${name}" ''
132 export REPO_PATH="${instanceCfg.repoPath}"
133 export FLAKE_PATH="${instanceCfg.flakePath}"
134 export GIT_REMOTE="${instanceCfg.gitRemote}"
135 export MAIN_BRANCH="${instanceCfg.mainBranch}"
136 export BRANCH_PREFIX="${instanceCfg.branchPrefix}"
137 export NTFY_TOPIC="${instanceCfg.ntfyTopic}"
138 export NTFY_SERVER="${instanceCfg.ntfyServer}"
139 export BUILD_SYSTEMS="${toString instanceCfg.buildSystems}"
140 export DRY_RUN="${toString instanceCfg.dryRun}"
141 export FLAKE_INPUTS="${toString instanceCfg.flakeInputs}"
142 export AUTO_MERGE="${toString instanceCfg.autoMerge}"
143 export INBOX_ORG="${instanceCfg.inboxOrg}"
144 ${optionalString (
145 instanceCfg.ntfyTokenFile != null
146 ) ''export NTFY_TOKEN_FILE="${instanceCfg.ntfyTokenFile}"''}
147
148 # Execute the packaged update script (already has tools in PATH)
149 exec ${pkgs.nix-flake-update}/bin/nix-flake-update
150 '';
151
152 mkService =
153 name: instanceCfg:
154 nameValuePair "nix-flake-updater-${name}" {
155 description = "Automated Nix flake.lock updater (${name})";
156
157 serviceConfig = {
158 Type = "oneshot";
159 User = instanceCfg.user;
160 ExecStart = "${mkUpdateScript name instanceCfg}";
161 Environment = ''"GIT_SSH_COMMAND=ssh -o ControlMaster=no"'';
162
163 # Don't fail if update fails (e.g., no changes, build failures)
164 SuccessExitStatus = "0 1";
165
166 # Security hardening
167 PrivateTmp = true;
168 ProtectSystem = "strict";
169 ProtectHome = "read-only";
170 ReadWritePaths = [
171 instanceCfg.repoPath
172 "/var/log/nix-flake-updater"
173 # Worktree location (script creates worktrees in ~/tmp)
174 "/home/${instanceCfg.user}/tmp"
175 # Nix cache for flake fetcher
176 "/home/${instanceCfg.user}/.cache/nix"
177 # Org inbox for TODOs
178 (dirOf instanceCfg.inboxOrg)
179 ];
180 NoNewPrivileges = true;
181
182 # Logging
183 StandardOutput = "journal";
184 StandardError = "journal";
185 SyslogIdentifier = "nix-flake-updater-${name}";
186 };
187 };
188
189 mkTimer =
190 name: instanceCfg:
191 nameValuePair "nix-flake-updater-${name}" {
192 description = "Timer for automated Nix flake.lock updates (${name})";
193 wantedBy = [ "timers.target" ];
194
195 timerConfig = {
196 OnCalendar = instanceCfg.schedule;
197 RandomizedDelaySec = instanceCfg.randomizedDelaySec;
198 Persistent = true;
199 };
200 };
201
202in
203{
204 options.services.nix-flake-updater = mkOption {
205 type = types.attrsOf (types.submodule instanceOpts);
206 default = { };
207 description = "Automated Nix flake.lock updater instances";
208 };
209
210 config = mkIf (cfg != { }) (
211 let
212 # Collect all unique users from enabled instances
213 users = unique (
214 mapAttrsToList (_: instanceCfg: instanceCfg.user) (filterAttrs (_: v: v.enable) cfg)
215 );
216 in
217 {
218 systemd.services = listToAttrs (
219 mapAttrsToList (name: instanceCfg: mkService name instanceCfg) (filterAttrs (_: v: v.enable) cfg)
220 );
221
222 systemd.timers = listToAttrs (
223 mapAttrsToList (name: instanceCfg: mkTimer name instanceCfg) (filterAttrs (_: v: v.enable) cfg)
224 );
225
226 # Ensure log directory exists (shared by all instances)
227 # Create with permissions for all users that need access
228 systemd.tmpfiles.rules = [
229 "d /var/log/nix-flake-updater 0775 root users -"
230 ]
231 ++ map (user: "Z /var/log/nix-flake-updater - ${user} - -") users;
232 }
233 );
234}