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"