flake-update-20260201
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 "$@"