auto-update-daily-20260202
  1#!/usr/bin/env bash
  2set -euo pipefail
  3
  4# Nixpkgs Branch Consolidation Tool
  5# Consolidates multiple WIP/PR branches into a single consolidated branch via cherry-picking
  6# Uses git worktrees to avoid interfering with main repository
  7
  8# Configuration from environment or defaults
  9NIXPKGS_REPO_PATH="${NIXPKGS_REPO_PATH:-/home/vincent/src/nixpkgs}"
 10NIXPKGS_WORKTREE_PATH="${NIXPKGS_WORKTREE_PATH:-/home/vincent/src/nixpkgs-consolidate-work}"
 11NIXPKGS_UPSTREAM_REMOTE="${NIXPKGS_UPSTREAM_REMOTE:-upstream}"
 12NIXPKGS_FORK_REMOTE="${NIXPKGS_FORK_REMOTE:-origin}"
 13NIXPKGS_FORK_URL="${NIXPKGS_FORK_URL:-git@github.com:vdemeester/nixpkgs.git}"
 14NIXPKGS_UPSTREAM_URL="${NIXPKGS_UPSTREAM_URL:-https://github.com/NixOS/nixpkgs.git}"
 15NIXPKGS_CONSOLIDATED_BRANCH="${NIXPKGS_CONSOLIDATED_BRANCH:-wip-consolidated}"
 16NIXPKGS_CONFIG_FILE="${NIXPKGS_CONFIG_FILE:-$HOME/.config/nixpkgs-automation/branches.conf}"
 17HOME_FLAKE_PATH="${HOME_FLAKE_PATH:-/home/vincent/src/home}"
 18NTFY_SERVER="${NTFY_SERVER:-https://ntfy.sbr.pm}"
 19NTFY_TOPIC="${NTFY_TOPIC:-git-builds}"
 20NTFY_TOKEN_FILE="${NTFY_TOKEN_FILE:-/run/agenix/ntfy-token}"
 21LOG_DIR="${LOG_DIR:-/var/log/nixpkgs-consolidate}"
 22DRY_RUN="${DRY_RUN:-false}"
 23
 24# Global state variables
 25BASE_BRANCH=""
 26WIP_BRANCHES=()
 27TOTAL_COMMITS=0
 28COMPLETED_BRANCHES=()
 29FAILED_BRANCH=""
 30
 31# Setup logging
 32LOG_FILE="$LOG_DIR/$(date +%Y%m%d-%H%M%S).log"
 33mkdir -p "$LOG_DIR"
 34
 35log() {
 36  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
 37}
 38
 39notify() {
 40  local priority="$1"
 41  local title="$2"
 42  local message="$3"
 43  local tags="$4"
 44
 45  if [ -f "$NTFY_TOKEN_FILE" ]; then
 46    curl -s \
 47      -H "Authorization: Bearer $(tr -d '\n' < "$NTFY_TOKEN_FILE")" \
 48      -H "Title: $title" \
 49      -H "Priority: $priority" \
 50      -H "Tags: $tags" \
 51      -d "$message" \
 52      "$NTFY_SERVER/$NTFY_TOPIC" || true
 53  else
 54    log "WARNING: ntfy token file not found at $NTFY_TOKEN_FILE, skipping notification"
 55  fi
 56}
 57
 58cleanup() {
 59  local exit_code=$?
 60
 61  # Note: We intentionally do NOT remove the worktree here
 62  # It will be reused on the next run, avoiding "Device or resource busy" issues
 63  # The worktree will be cleaned up automatically by setup_worktree() on next run
 64
 65  if [ $exit_code -ne 0 ]; then
 66    log "ERROR: Consolidation failed with exit code $exit_code"
 67
 68    # Build completed branches string
 69    local completed_str=""
 70    if [ ${#COMPLETED_BRANCHES[@]} -gt 0 ]; then
 71      completed_str=$(IFS=", "; echo "${COMPLETED_BRANCHES[*]}")
 72    else
 73      completed_str="(none)"
 74    fi
 75
 76    # Send failure notification
 77    local failure_msg="Rebase conflict or error during consolidation
 78
 79Base: $BASE_BRANCH
 80Completed: $completed_str
 81Failed at: ${FAILED_BRANCH:-unknown}
 82
 83Logs: $LOG_FILE"
 84
 85    notify "high" \
 86      "❌ Nixpkgs Consolidation Failed" \
 87      "$failure_msg" \
 88      "x,warning,git,nixpkgs"
 89  fi
 90}
 91
 92trap cleanup EXIT
 93
 94detect_home_nixpkgs_rev() {
 95  if [ ! -f "$HOME_FLAKE_PATH/flake.lock" ]; then
 96    return 1
 97  fi
 98
 99  # Try to extract the main "nixpkgs" input revision from flake.lock using jq
100  # This specifically targets the "nixpkgs" node which should track nixos-unstable
101  if command -v jq >/dev/null 2>&1; then
102    local rev
103    # Get the revision for the nixpkgs node (not nixpkgs-master, nixpkgs-wip-consolidated, etc.)
104    rev=$(jq -r '.nodes.nixpkgs.locked.rev // empty' "$HOME_FLAKE_PATH/flake.lock" 2>/dev/null)
105
106    if [ -n "$rev" ]; then
107      echo "$rev"
108      return 0
109    fi
110  fi
111
112  # Fallback: try nix flake metadata (slower but more reliable)
113  if command -v nix >/dev/null 2>&1; then
114    local rev
115    rev=$(nix flake metadata "$HOME_FLAKE_PATH" --json 2>/dev/null | \
116          jq -r '.locks.nodes.nixpkgs.locked.rev // empty' 2>/dev/null)
117
118    if [ -n "$rev" ]; then
119      echo "$rev"
120      return 0
121    fi
122  fi
123
124  return 1
125}
126
127parse_config() {
128  log "Reading configuration from $NIXPKGS_CONFIG_FILE"
129
130  if [ ! -f "$NIXPKGS_CONFIG_FILE" ]; then
131    log "ERROR: Configuration file not found: $NIXPKGS_CONFIG_FILE"
132    echo "Please create a configuration file at $NIXPKGS_CONFIG_FILE"
133    echo "See tools/nixpkgs-consolidate/branches.conf.example for an example"
134    exit 1
135  fi
136
137  # Parse configuration file
138  while IFS= read -r line || [ -n "$line" ]; do
139    # Skip comments and empty lines
140    [[ "$line" =~ ^[[:space:]]*# ]] && continue
141    [[ -z "${line// }" ]] && continue
142
143    # Extract base branch (line starting with "base:")
144    if [[ "$line" =~ ^base:[[:space:]]*(.*) ]]; then
145      BASE_BRANCH="${BASH_REMATCH[1]}"
146      log "Base branch from config: $BASE_BRANCH"
147      continue
148    fi
149
150    # Otherwise it's a WIP branch
151    branch=$(echo "$line" | tr -d ' ')
152    if [ -n "$branch" ]; then
153      WIP_BRANCHES+=("$branch")
154    fi
155  done < "$NIXPKGS_CONFIG_FILE"
156
157  # Validate configuration
158  if [ -z "$BASE_BRANCH" ]; then
159    log "ERROR: No base branch defined in config (should start with 'base:')"
160    exit 1
161  fi
162
163  # Try to detect and use home flake nixpkgs revision
164  log "Attempting to detect nixpkgs revision from home flake at $HOME_FLAKE_PATH"
165  local detected_rev
166  if detected_rev=$(detect_home_nixpkgs_rev); then
167    log "✓ Detected nixpkgs revision from home flake: $detected_rev"
168    log "Using detected revision as base (overriding config: $BASE_BRANCH)"
169    BASE_BRANCH="$detected_rev"
170  else
171    log "Could not detect nixpkgs revision from home flake"
172    log "Using configured base branch: $BASE_BRANCH"
173  fi
174
175  if [ ${#WIP_BRANCHES[@]} -eq 0 ]; then
176    log "WARNING: No WIP branches defined in config"
177    log "Nothing to consolidate, exiting"
178    exit 0
179  fi
180
181  log "WIP branches (${#WIP_BRANCHES[@]}): ${WIP_BRANCHES[*]}"
182}
183
184ensure_repo() {
185  if [ ! -d "$NIXPKGS_REPO_PATH/.git" ]; then
186    log "Repository not found at $NIXPKGS_REPO_PATH"
187    log "Cloning from fork: $NIXPKGS_FORK_URL"
188
189    mkdir -p "$(dirname "$NIXPKGS_REPO_PATH")"
190    git clone "$NIXPKGS_FORK_URL" "$NIXPKGS_REPO_PATH"
191
192    cd "$NIXPKGS_REPO_PATH"
193
194    # Setup upstream remote
195    if ! git remote | grep -q "^${NIXPKGS_UPSTREAM_REMOTE}$"; then
196      log "Adding upstream remote: $NIXPKGS_UPSTREAM_URL"
197      git remote add "$NIXPKGS_UPSTREAM_REMOTE" "$NIXPKGS_UPSTREAM_URL"
198    fi
199
200    log "Repository cloned successfully"
201  else
202    log "Repository exists at $NIXPKGS_REPO_PATH"
203  fi
204
205  cd "$NIXPKGS_REPO_PATH"
206
207  # Verify remotes exist
208  if ! git remote | grep -q "^${NIXPKGS_UPSTREAM_REMOTE}$"; then
209    log "ERROR: Remote '$NIXPKGS_UPSTREAM_REMOTE' not found"
210    log "Available remotes: $(git remote | tr '\n' ' ')"
211    exit 1
212  fi
213
214  if ! git remote | grep -q "^${NIXPKGS_FORK_REMOTE}$"; then
215    log "ERROR: Remote '$NIXPKGS_FORK_REMOTE' not found"
216    log "Available remotes: $(git remote | tr '\n' ' ')"
217    exit 1
218  fi
219}
220
221setup_worktree() {
222  cd "$NIXPKGS_REPO_PATH"
223
224  # Check if worktree is registered in git
225  if git worktree list | grep -q "$NIXPKGS_WORKTREE_PATH"; then
226    log "Reusing existing worktree at $NIXPKGS_WORKTREE_PATH"
227    cd "$NIXPKGS_WORKTREE_PATH"
228
229    # Reset to upstream base branch
230    log "Resetting worktree to $NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH"
231    git checkout -B "$NIXPKGS_CONSOLIDATED_BRANCH" "$NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH" 2>&1 | tee -a "$LOG_FILE"
232    log "Worktree reset successfully"
233    return
234  fi
235
236  # Worktree not registered - clean up stale references and create fresh
237  log "Creating new worktree at $NIXPKGS_WORKTREE_PATH"
238  log "Branching from $NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH"
239
240  # Clean up any stale worktree references and remove directory if exists
241  git worktree prune 2>&1 | tee -a "$LOG_FILE" || true
242  if [ -d "$NIXPKGS_WORKTREE_PATH" ]; then
243    log "Removing stale worktree directory"
244    rm -rf "$NIXPKGS_WORKTREE_PATH"
245  fi
246
247  # Delete local consolidated branch if it exists and is not checked out
248  if git show-ref --verify --quiet "refs/heads/$NIXPKGS_CONSOLIDATED_BRANCH"; then
249    log "Deleting existing local branch $NIXPKGS_CONSOLIDATED_BRANCH"
250    git branch -D "$NIXPKGS_CONSOLIDATED_BRANCH" 2>&1 | tee -a "$LOG_FILE" || true
251  fi
252
253  if git worktree add -b "$NIXPKGS_CONSOLIDATED_BRANCH" \
254    "$NIXPKGS_WORKTREE_PATH" \
255    "$NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH" 2>&1 | tee -a "$LOG_FILE"; then
256    log "Worktree created successfully"
257  else
258    log "ERROR: Failed to create worktree"
259    exit 1
260  fi
261}
262
263consolidate_branches() {
264  cd "$NIXPKGS_WORKTREE_PATH"
265
266  for branch in "${WIP_BRANCHES[@]}"; do
267    log "Processing WIP branch: $branch"
268    FAILED_BRANCH="$branch"
269
270    # Check if branch exists on fork remote
271    if ! git rev-parse --verify "$NIXPKGS_FORK_REMOTE/$branch" >/dev/null 2>&1; then
272      log "ERROR: Branch $branch not found on remote $NIXPKGS_FORK_REMOTE"
273      exit 1
274    fi
275
276    # Count commits before rebase (for logging)
277    local commits_before
278    commits_before=$(git rev-list --count HEAD)
279
280    # Rebase the WIP branch onto wip-consolidated
281    # This automatically drops empty commits (already in base)
282    log "Rebasing $NIXPKGS_FORK_REMOTE/$branch onto wip-consolidated..."
283    if ! git -c commit.gpgsign=false rebase "$NIXPKGS_FORK_REMOTE/$branch" 2>&1 | tee -a "$LOG_FILE"; then
284      log "ERROR: Rebase failed for branch $branch"
285      log "Aborting rebase..."
286      git rebase --abort 2>&1 | tee -a "$LOG_FILE" || true
287      exit 1
288    fi
289
290    # Count commits after rebase
291    local commits_after
292    commits_after=$(git rev-list --count HEAD)
293    local commits_added=$((commits_after - commits_before))
294
295    log "Successfully rebased $branch: added $commits_added commit(s)"
296    COMPLETED_BRANCHES+=("$branch")
297    FAILED_BRANCH=""
298    TOTAL_COMMITS=$((TOTAL_COMMITS + commits_added))
299  done
300
301  log "Total commits added: $TOTAL_COMMITS"
302}
303
304main() {
305  log "=== Starting nixpkgs branch consolidation ==="
306  log "Repository: $NIXPKGS_REPO_PATH"
307  log "Worktree path: $NIXPKGS_WORKTREE_PATH"
308  log "Fork remote: $NIXPKGS_FORK_REMOTE ($NIXPKGS_FORK_URL)"
309  log "Upstream remote: $NIXPKGS_UPSTREAM_REMOTE ($NIXPKGS_UPSTREAM_URL)"
310  log "Consolidated branch: $NIXPKGS_CONSOLIDATED_BRANCH"
311  log "Configuration: $NIXPKGS_CONFIG_FILE"
312  log "Dry run: $DRY_RUN"
313
314  # Parse configuration
315  parse_config
316
317  # Ensure repository exists
318  ensure_repo
319
320  # Fetch latest from remotes
321  log "Fetching from $NIXPKGS_UPSTREAM_REMOTE..."
322  cd "$NIXPKGS_REPO_PATH"
323  git fetch "$NIXPKGS_UPSTREAM_REMOTE" 2>&1 | tee -a "$LOG_FILE"
324
325  log "Fetching from $NIXPKGS_FORK_REMOTE..."
326  git fetch "$NIXPKGS_FORK_REMOTE" 2>&1 | tee -a "$LOG_FILE"
327
328  # List available branches on fork remote
329  log "Available branches on $NIXPKGS_FORK_REMOTE:"
330  git branch -r | grep "^  $NIXPKGS_FORK_REMOTE/" | sed "s|^  $NIXPKGS_FORK_REMOTE/||" | while read -r branch; do
331    log "  - $branch"
332  done
333
334  # Verify base branch exists
335  if ! git rev-parse --verify "$NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH" >/dev/null 2>&1; then
336    log "ERROR: Base branch $BASE_BRANCH not found on remote $NIXPKGS_UPSTREAM_REMOTE"
337    exit 1
338  fi
339
340  # Setup worktree for consolidation work
341  setup_worktree
342
343  # Cherry-pick WIP branches (happens in worktree)
344  consolidate_branches
345
346  # Show summary
347  log "=== Consolidation Summary ==="
348  log "Base branch: $BASE_BRANCH"
349  log "WIP branches consolidated: ${#COMPLETED_BRANCHES[@]}"
350  for branch in "${COMPLETED_BRANCHES[@]}"; do
351    log "  - $branch"
352  done
353  log "Total commits: $TOTAL_COMMITS"
354
355  # Push to remote (from worktree)
356  cd "$NIXPKGS_WORKTREE_PATH"
357
358  if [ "$DRY_RUN" = "true" ]; then
359    log "DRY RUN: Would push $NIXPKGS_CONSOLIDATED_BRANCH to $NIXPKGS_FORK_REMOTE"
360    log "Current HEAD: $(git rev-parse HEAD)"
361    log "Branch differs from upstream/$BASE_BRANCH by $TOTAL_COMMITS commits"
362  else
363    log "Force pushing $NIXPKGS_CONSOLIDATED_BRANCH to $NIXPKGS_FORK_REMOTE (with --force-with-lease)..."
364    if git push --force-with-lease "$NIXPKGS_FORK_REMOTE" "$NIXPKGS_CONSOLIDATED_BRANCH:$NIXPKGS_CONSOLIDATED_BRANCH" 2>&1 | tee -a "$LOG_FILE"; then
365      log "Successfully pushed to $NIXPKGS_FORK_REMOTE/$NIXPKGS_CONSOLIDATED_BRANCH"
366    else
367      log "ERROR: Push failed. This might mean the remote branch was updated by someone else."
368      log "Check the remote branch and try again."
369      exit 1
370    fi
371  fi
372
373  # Success notification
374  local branches_str
375  branches_str=$(IFS=$'\n'; echo "${COMPLETED_BRANCHES[*]}")
376  local success_msg="Consolidated ${#COMPLETED_BRANCHES[@]} branch(es) into $NIXPKGS_CONSOLIDATED_BRANCH
377
378Base: $BASE_BRANCH
379Branches:
380$branches_str
381
382Total commits: $TOTAL_COMMITS
383
384${DRY_RUN:+[DRY RUN] }Pushed to $NIXPKGS_FORK_REMOTE/$NIXPKGS_CONSOLIDATED_BRANCH"
385
386  notify "default" \
387    "✅ Nixpkgs Consolidation Success" \
388    "$success_msg" \
389    "white_check_mark,git,nixpkgs"
390
391  log "=== Consolidation completed successfully ==="
392}
393
394main "$@"