auto-update-daily-20260202
1#!/usr/bin/env bash
2set -euo pipefail
3
4# Automated NixOS flake.lock updater
5# This script updates flake.lock, builds verification systems, and pushes to remote
6
7# Configuration from environment or defaults
8REPO_PATH="${REPO_PATH:-/home/vincent/src/home}"
9FLAKE_PATH="${FLAKE_PATH:-$REPO_PATH}"
10GIT_REMOTE="${GIT_REMOTE:-origin}"
11MAIN_BRANCH="${MAIN_BRANCH:-main}"
12BRANCH_PREFIX="${BRANCH_PREFIX:-flake-update-}"
13NTFY_TOPIC="${NTFY_TOPIC:-nix-updates}"
14NTFY_SERVER="${NTFY_SERVER:-https://ntfy.sh}"
15NTFY_TOKEN_FILE="${NTFY_TOKEN_FILE:-}"
16BUILD_SYSTEMS="${BUILD_SYSTEMS:-}"
17DRY_RUN="${DRY_RUN:-false}"
18FLAKE_INPUTS="${FLAKE_INPUTS:-}" # Space-separated list of inputs to update (empty = all)
19AUTO_MERGE="${AUTO_MERGE:-false}" # If true, merge to main on success
20INBOX_ORG="${INBOX_ORG:-$HOME/desktop/org/inbox.org}" # Path to org-mode inbox
21
22LOG_FILE="/var/log/nix-flake-updater/$(date +%Y%m%d-%H%M%S).log"
23mkdir -p "$(dirname "$LOG_FILE")"
24
25# Worktree directory for isolated work (use ~/tmp to avoid tmpfs/RAM)
26WORKTREE_DIR="$HOME/tmp/nix-flake-updater-$(date +%Y%m%d-%H%M%S)"
27mkdir -p "$HOME/tmp"
28
29log() {
30 echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
31}
32
33notify() {
34 local priority="$1"
35 local title="$2"
36 local message="$3"
37 local tags="$4"
38
39 if [ -n "$NTFY_TOKEN_FILE" ] && [ -f "$NTFY_TOKEN_FILE" ]; then
40 # Use authentication token
41 curl -s \
42 -H "Authorization: Bearer $(tr -d '\n' < "$NTFY_TOKEN_FILE")" \
43 -H "Title: $title" \
44 -H "Priority: $priority" \
45 -H "Tags: $tags" \
46 -d "$message" \
47 "$NTFY_SERVER/$NTFY_TOPIC" || true
48 else
49 # No authentication
50 curl -s \
51 -H "Title: $title" \
52 -H "Priority: $priority" \
53 -H "Tags: $tags" \
54 -d "$message" \
55 "$NTFY_SERVER/$NTFY_TOPIC" || true
56 fi
57}
58
59add_todo_to_inbox() {
60 local title="$1"
61 local details="$2"
62
63 if [ -f "$INBOX_ORG" ]; then
64 log "Adding TODO to $INBOX_ORG"
65 cat >> "$INBOX_ORG" <<EOF
66* TODO $title
67 SCHEDULED: <$(date '+%Y-%m-%d %a')>
68 :PROPERTIES:
69 :CREATED: [$(date '+%Y-%m-%d %a %H:%M')]
70 :END:
71
72$details
73
74Log file: $LOG_FILE
75EOF
76 else
77 log "WARNING: Inbox file not found: $INBOX_ORG"
78 fi
79}
80
81cleanup() {
82 local exit_code=$?
83
84 # Clean up worktree if it exists
85 if [ -d "$WORKTREE_DIR" ]; then
86 log "Cleaning up worktree: $WORKTREE_DIR"
87 cd "$REPO_PATH"
88 git worktree remove --force "$WORKTREE_DIR" 2>&1 | tee -a "$LOG_FILE" || true
89 rm -rf "$WORKTREE_DIR" || true
90 fi
91
92 if [ $exit_code -ne 0 ]; then
93 log "ERROR: Update process failed with exit code $exit_code"
94
95 # Add TODO to inbox on failure
96 local input_desc="all inputs"
97 if [ -n "$FLAKE_INPUTS" ]; then
98 input_desc="inputs: $FLAKE_INPUTS"
99 fi
100
101 add_todo_to_inbox "Fix flake update failure" \
102 "Flake update failed for $input_desc.
103Build systems: $BUILD_SYSTEMS
104Auto-merge: $AUTO_MERGE"
105
106 notify "high" "โ Flake Update Failed" \
107 "Build failed for $input_desc. TODO added to inbox. See logs: $LOG_FILE" \
108 "warning,flake"
109 fi
110}
111
112trap cleanup EXIT
113
114log "Starting flake update process"
115cd "$REPO_PATH"
116
117# Fetch latest changes
118log "Fetching latest changes from $GIT_REMOTE"
119git fetch "$GIT_REMOTE"
120
121# Create update branch name
122BRANCH_NAME="$BRANCH_PREFIX$(date +%Y%m%d)"
123if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
124 log "Branch $BRANCH_NAME already exists, using unique name"
125 BRANCH_NAME="$BRANCH_PREFIX$(date +%Y%m%d-%H%M%S)"
126fi
127
128# Create worktree from main branch (skip LFS to avoid hook failures)
129log "Creating worktree at $WORKTREE_DIR from $GIT_REMOTE/main"
130GIT_LFS_SKIP_SMUDGE=1 git worktree add "$WORKTREE_DIR" "$GIT_REMOTE/main"
131
132# Switch to worktree
133cd "$WORKTREE_DIR"
134log "Working in isolated worktree: $WORKTREE_DIR"
135
136# Create update branch in the worktree
137log "Creating update branch: $BRANCH_NAME"
138git checkout -b "$BRANCH_NAME"
139
140# Update flake.lock (work in worktree, flake is at root)
141log "Updating flake.lock"
142if [ -n "$FLAKE_INPUTS" ]; then
143 log "Updating specific inputs: $FLAKE_INPUTS"
144 for input in $FLAKE_INPUTS; do
145 log "Updating input: $input"
146 nix flake lock --update-input "$input" 2>&1 | tee -a "$LOG_FILE"
147 done
148else
149 log "Updating all inputs"
150 nix flake update 2>&1 | tee -a "$LOG_FILE"
151fi
152
153# Check if there are changes
154if ! git diff --quiet flake.lock; then
155 log "Changes detected in flake.lock"
156
157 # Show what changed
158 log "Flake input changes:"
159 git diff flake.lock | grep -E '^\+.*"(narHash|rev)"' | head -20 | tee -a "$LOG_FILE"
160
161 # Build test systems (build from worktree)
162 BUILD_SUCCESS=true
163 for system in $BUILD_SYSTEMS; do
164 log "Building system: $system"
165 if nix build ".#nixosConfigurations.$system.config.system.build.toplevel" \
166 --no-link \
167 --print-build-logs 2>&1 | tee -a "$LOG_FILE"; then
168 log "โ $system built successfully"
169 else
170 log "โ $system build failed"
171 BUILD_SUCCESS=false
172 break
173 fi
174 done
175
176 if [ "$BUILD_SUCCESS" = true ]; then
177 # Commit changes (we're already in WORKTREE_DIR)
178 git add flake.lock
179
180 # Generate commit message with changed inputs
181 input_desc="all inputs"
182 if [ -n "$FLAKE_INPUTS" ]; then
183 input_desc="$FLAKE_INPUTS"
184 fi
185
186 COMMIT_MSG="chore(flake): update $input_desc
187
188$(nix flake metadata . --json 2>/dev/null | \
189 jq -r '.locks.nodes | to_entries[] | select(.key != "root") | "- \(.key): \(.value.locked.rev // .value.locked.narHash // "updated")"' 2>/dev/null || echo "Updated flake inputs")
190
191๐ค Automated update
192Built systems: $BUILD_SYSTEMS
193"
194
195 git commit -m "$COMMIT_MSG"
196
197 if [ "$DRY_RUN" = "false" ]; then
198 if [ "$AUTO_MERGE" = "true" ]; then
199 # Auto-merge: rebase onto main and push directly
200 log "Auto-merge enabled: rebasing onto $GIT_REMOTE/$MAIN_BRANCH"
201
202 # Fetch latest main
203 git fetch "$GIT_REMOTE" "$MAIN_BRANCH"
204
205 # Rebase our commit onto main
206 if git rebase "$GIT_REMOTE/$MAIN_BRANCH"; then
207 log "Rebase successful, pushing to $GIT_REMOTE/$MAIN_BRANCH"
208
209 # Push directly to main
210 git push "$GIT_REMOTE" "HEAD:$MAIN_BRANCH"
211
212 # Notify success
213 notify "default" "โ
Flake Auto-Updated & Merged" \
214 "Updates for $input_desc merged to $MAIN_BRANCH. All builds passed: $BUILD_SYSTEMS" \
215 "white_check_mark,flake,merged"
216
217 log "SUCCESS: Flake updated and merged to $MAIN_BRANCH"
218 else
219 log "ERROR: Rebase failed, main branch may have moved"
220 git rebase --abort || true
221
222 add_todo_to_inbox "Flake update rebase conflict" \
223 "Auto-merge failed due to rebase conflict.
224Inputs: $input_desc
225Branch: $BRANCH_NAME (in worktree, needs manual rebase)"
226
227 notify "high" "โ ๏ธ Flake Update Rebase Failed" \
228 "Could not rebase $input_desc onto $MAIN_BRANCH. TODO added to inbox." \
229 "warning,flake,conflict"
230 exit 1
231 fi
232 else
233 # Branch mode: push to feature branch
234 log "Pushing to $GIT_REMOTE/$BRANCH_NAME"
235 git push "$GIT_REMOTE" "$BRANCH_NAME"
236
237 # Notify success
238 notify "default" "โ
Flake Updated Successfully" \
239 "Branch $BRANCH_NAME created and pushed. All builds passed: $BUILD_SYSTEMS" \
240 "white_check_mark,flake"
241
242 log "SUCCESS: Flake updated and pushed to $BRANCH_NAME"
243 fi
244 else
245 log "DRY RUN: Would push to $GIT_REMOTE/$BRANCH_NAME"
246 notify "low" "๐งช Flake Update (Dry Run)" \
247 "Branch $BRANCH_NAME created locally. All builds passed: $BUILD_SYSTEMS" \
248 "test_tube,flake"
249 fi
250
251 else
252 log "Build failed, not committing changes"
253
254 input_desc="all inputs"
255 if [ -n "$FLAKE_INPUTS" ]; then
256 input_desc="$FLAKE_INPUTS"
257 fi
258
259 add_todo_to_inbox "Flake update build failure" \
260 "Build failed after updating $input_desc.
261Build systems tested: $BUILD_SYSTEMS
262Auto-merge: $AUTO_MERGE"
263
264 notify "high" "โ Flake Update Build Failed" \
265 "Builds failed for updated $input_desc. TODO added to inbox. Check logs: $LOG_FILE" \
266 "x,flake,warning"
267
268 # Clean up failed branch in main repo
269 cd "$REPO_PATH"
270 git branch -D "$BRANCH_NAME" 2>&1 | tee -a "$LOG_FILE" || true
271 exit 1
272 fi
273
274else
275 log "No changes in flake.lock, nothing to do"
276 notify "low" "โน๏ธ No Flake Updates" \
277 "flake.lock is already up to date" \
278 "information_source,flake"
279
280 # Clean up unused branch in main repo
281 cd "$REPO_PATH"
282 git branch -D "$BRANCH_NAME" 2>&1 | tee -a "$LOG_FILE" || true
283fi
284
285log "Flake update process complete"