Commit baf0194de2a5

Vincent Demeester <vincent@sbr.pm>
2026-01-08 11:40:38
feat(nixpkgs): Add automated WIP branch consolidation tool
- Enable testing multiple nixpkgs changes via single consolidated branch - Automate cherry-picking workflow with conflict detection and ntfy alerts - Use git worktrees to avoid interfering with main repository work - Provide simpler flake integration than managing multiple overlays Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 2c7e4eb
Changed files (3)
tools/nixpkgs-consolidate/branches.conf.example
@@ -0,0 +1,72 @@
+# Nixpkgs Branch Consolidation Configuration
+#
+# This file defines which branches to consolidate into a single branch.
+# Copy this file to ~/.config/nixpkgs-automation/branches.conf and customize.
+#
+# Usage:
+#   mkdir -p ~/.config/nixpkgs-automation
+#   cp tools/nixpkgs-consolidate/branches.conf.example \
+#      ~/.config/nixpkgs-automation/branches.conf
+#   vim ~/.config/nixpkgs-automation/branches.conf
+
+# Base Branch Configuration
+# -------------------------
+# The first non-comment line starting with "base:" defines the upstream base branch.
+# This is the branch that your WIP branches will be cherry-picked on top of.
+#
+# Common choices:
+#   - nixos-unstable: Rolling release, most up-to-date packages
+#   - master: Main development branch
+#   - nixos-24.11: Stable release branch
+
+base: nixos-unstable
+
+# WIP Branches to Consolidate
+# ----------------------------
+# List your work-in-progress branches below, one per line.
+# Branches will be cherry-picked in the order listed.
+#
+# Branch names should match branches in your fork remote (typically 'origin').
+# These branches should exist at: git@github.com:yourusername/nixpkgs.git
+#
+# Naming conventions (recommended):
+#   - wip-<feature-name>: Work in progress features
+#   - pr-<number>-<description>: Branches for open pull requests
+#   - fix-<issue>: Bug fixes
+#
+# Examples:
+
+# wip-package-update-foo
+# wip-fix-bar-build
+# pr-12345-add-new-service
+# fix-python-package-build
+
+# Guidelines:
+# -----------
+# 1. Order matters: Branches are cherry-picked in listed order
+# 2. Keep it clean: Remove branches after PRs are merged upstream
+# 3. Test first: Use DRY_RUN=true to test before actual push
+# 4. Watch for conflicts: Script will abort if cherry-pick conflicts occur
+#
+# Environment Variables:
+# ---------------------
+# You can override defaults with environment variables:
+#
+#   NIXPKGS_REPO_PATH           - Where nixpkgs is cloned (default: ~/src/nixpkgs)
+#   NIXPKGS_WORKTREE_PATH       - Worktree for consolidation work
+#   NIXPKGS_UPSTREAM_REMOTE     - Upstream remote name (default: upstream)
+#   NIXPKGS_FORK_REMOTE         - Your fork remote name (default: origin)
+#   NIXPKGS_CONSOLIDATED_BRANCH - Output branch name (default: wip-consolidated)
+#   DRY_RUN                     - Set to "true" to test without pushing
+#
+# Example with custom settings:
+#   NIXPKGS_CONSOLIDATED_BRANCH=personal-main \
+#   DRY_RUN=true \
+#   nixpkgs-consolidate
+
+# Your WIP Branches
+# -----------------
+# Add your branches below (uncomment and modify as needed):
+
+# wip-my-feature
+# pr-54321-awesome-package
tools/nixpkgs-consolidate/nixpkgs-consolidate.sh
@@ -0,0 +1,347 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Nixpkgs Branch Consolidation Tool
+# Consolidates multiple WIP/PR branches into a single consolidated branch via cherry-picking
+# Uses git worktrees to avoid interfering with main repository
+
+# Configuration from environment or defaults
+NIXPKGS_REPO_PATH="${NIXPKGS_REPO_PATH:-/home/vincent/src/nixpkgs}"
+NIXPKGS_WORKTREE_PATH="${NIXPKGS_WORKTREE_PATH:-/home/vincent/src/nixpkgs-consolidate-work}"
+NIXPKGS_UPSTREAM_REMOTE="${NIXPKGS_UPSTREAM_REMOTE:-upstream}"
+NIXPKGS_FORK_REMOTE="${NIXPKGS_FORK_REMOTE:-origin}"
+NIXPKGS_CONSOLIDATED_BRANCH="${NIXPKGS_CONSOLIDATED_BRANCH:-wip-consolidated}"
+NIXPKGS_CONFIG_FILE="${NIXPKGS_CONFIG_FILE:-$HOME/.config/nixpkgs-automation/branches.conf}"
+NTFY_SERVER="${NTFY_SERVER:-https://ntfy.sbr.pm}"
+NTFY_TOPIC="${NTFY_TOPIC:-git-builds}"
+NTFY_TOKEN_FILE="${NTFY_TOKEN_FILE:-/run/agenix/ntfy-token}"
+LOG_DIR="${LOG_DIR:-/var/log/nixpkgs-consolidate}"
+DRY_RUN="${DRY_RUN:-false}"
+
+# Global state variables
+BASE_BRANCH=""
+WIP_BRANCHES=()
+TOTAL_COMMITS=0
+COMPLETED_BRANCHES=()
+FAILED_BRANCH=""
+FAILED_COMMIT=""
+WORKTREE_CREATED=false
+
+# Setup logging
+LOG_FILE="$LOG_DIR/$(date +%Y%m%d-%H%M%S).log"
+mkdir -p "$LOG_DIR"
+
+log() {
+  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
+}
+
+notify() {
+  local priority="$1"
+  local title="$2"
+  local message="$3"
+  local tags="$4"
+
+  if [ -f "$NTFY_TOKEN_FILE" ]; then
+    curl -s \
+      -H "Authorization: Bearer $(tr -d '\n' < "$NTFY_TOKEN_FILE")" \
+      -H "Title: $title" \
+      -H "Priority: $priority" \
+      -H "Tags: $tags" \
+      -d "$message" \
+      "$NTFY_SERVER/$NTFY_TOPIC" || true
+  else
+    log "WARNING: ntfy token file not found at $NTFY_TOKEN_FILE, skipping notification"
+  fi
+}
+
+cleanup() {
+  local exit_code=$?
+
+  # Clean up worktree if it was created
+  if [ "$WORKTREE_CREATED" = true ]; then
+    log "Cleaning up worktree..."
+    cd "$NIXPKGS_REPO_PATH"
+    git worktree remove "$NIXPKGS_WORKTREE_PATH" --force 2>&1 | tee -a "$LOG_FILE" || true
+  fi
+
+  if [ $exit_code -ne 0 ]; then
+    log "ERROR: Consolidation failed with exit code $exit_code"
+
+    # Build completed branches string
+    local completed_str=""
+    if [ ${#COMPLETED_BRANCHES[@]} -gt 0 ]; then
+      completed_str=$(IFS=", "; echo "${COMPLETED_BRANCHES[*]}")
+    else
+      completed_str="(none)"
+    fi
+
+    # Send failure notification
+    local failure_msg="Cherry-pick conflict or error during consolidation
+
+Base: $BASE_BRANCH
+Completed: $completed_str
+Failed at: ${FAILED_BRANCH:-unknown}
+${FAILED_COMMIT:+Commit: $FAILED_COMMIT}
+
+Logs: $LOG_FILE"
+
+    notify "high" \
+      "❌ Nixpkgs Consolidation Failed" \
+      "$failure_msg" \
+      "x,warning,git,nixpkgs"
+  fi
+}
+
+trap cleanup EXIT
+
+parse_config() {
+  log "Reading configuration from $NIXPKGS_CONFIG_FILE"
+
+  if [ ! -f "$NIXPKGS_CONFIG_FILE" ]; then
+    log "ERROR: Configuration file not found: $NIXPKGS_CONFIG_FILE"
+    echo "Please create a configuration file at $NIXPKGS_CONFIG_FILE"
+    echo "See tools/nixpkgs-consolidate/branches.conf.example for an example"
+    exit 1
+  fi
+
+  # Parse configuration file
+  while IFS= read -r line || [ -n "$line" ]; do
+    # Skip comments and empty lines
+    [[ "$line" =~ ^[[:space:]]*# ]] && continue
+    [[ -z "${line// }" ]] && continue
+
+    # Extract base branch (line starting with "base:")
+    if [[ "$line" =~ ^base:[[:space:]]*(.*) ]]; then
+      BASE_BRANCH="${BASH_REMATCH[1]}"
+      log "Base branch: $BASE_BRANCH"
+      continue
+    fi
+
+    # Otherwise it's a WIP branch
+    branch=$(echo "$line" | tr -d ' ')
+    if [ -n "$branch" ]; then
+      WIP_BRANCHES+=("$branch")
+    fi
+  done < "$NIXPKGS_CONFIG_FILE"
+
+  # Validate configuration
+  if [ -z "$BASE_BRANCH" ]; then
+    log "ERROR: No base branch defined in config (should start with 'base:')"
+    exit 1
+  fi
+
+  if [ ${#WIP_BRANCHES[@]} -eq 0 ]; then
+    log "WARNING: No WIP branches defined in config"
+    log "Nothing to consolidate, exiting"
+    exit 0
+  fi
+
+  log "WIP branches (${#WIP_BRANCHES[@]}): ${WIP_BRANCHES[*]}"
+}
+
+ensure_repo() {
+  if [ ! -d "$NIXPKGS_REPO_PATH" ]; then
+    log "Repository not found at $NIXPKGS_REPO_PATH"
+    log "Cloning NixOS/nixpkgs..."
+
+    mkdir -p "$(dirname "$NIXPKGS_REPO_PATH")"
+    git clone git@github.com:NixOS/nixpkgs.git "$NIXPKGS_REPO_PATH"
+
+    cd "$NIXPKGS_REPO_PATH"
+
+    # Setup remotes if needed
+    if ! git remote | grep -q "^${NIXPKGS_UPSTREAM_REMOTE}$"; then
+      log "Adding upstream remote: $NIXPKGS_UPSTREAM_REMOTE"
+      git remote add "$NIXPKGS_UPSTREAM_REMOTE" git@github.com:NixOS/nixpkgs.git
+    fi
+
+    log "Repository cloned successfully"
+  else
+    log "Repository exists at $NIXPKGS_REPO_PATH"
+  fi
+
+  cd "$NIXPKGS_REPO_PATH"
+
+  # Verify remotes exist
+  if ! git remote | grep -q "^${NIXPKGS_UPSTREAM_REMOTE}$"; then
+    log "ERROR: Remote '$NIXPKGS_UPSTREAM_REMOTE' not found"
+    log "Available remotes: $(git remote | tr '\n' ' ')"
+    exit 1
+  fi
+
+  if ! git remote | grep -q "^${NIXPKGS_FORK_REMOTE}$"; then
+    log "ERROR: Remote '$NIXPKGS_FORK_REMOTE' not found"
+    log "Available remotes: $(git remote | tr '\n' ' ')"
+    exit 1
+  fi
+}
+
+setup_worktree() {
+  cd "$NIXPKGS_REPO_PATH"
+
+  # Remove existing worktree if it exists
+  if [ -d "$NIXPKGS_WORKTREE_PATH" ]; then
+    log "Removing existing worktree at $NIXPKGS_WORKTREE_PATH"
+    git worktree remove "$NIXPKGS_WORKTREE_PATH" --force 2>&1 | tee -a "$LOG_FILE" || true
+  fi
+
+  # Delete local consolidated branch if it exists (we'll recreate it fresh)
+  if git show-ref --verify --quiet "refs/heads/$NIXPKGS_CONSOLIDATED_BRANCH"; then
+    log "Deleting existing local branch $NIXPKGS_CONSOLIDATED_BRANCH"
+    git branch -D "$NIXPKGS_CONSOLIDATED_BRANCH" 2>&1 | tee -a "$LOG_FILE" || true
+  fi
+
+  # Create new worktree with consolidated branch from base
+  log "Creating worktree at $NIXPKGS_WORKTREE_PATH"
+  log "Branching from $NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH"
+
+  if git worktree add -b "$NIXPKGS_CONSOLIDATED_BRANCH" \
+    "$NIXPKGS_WORKTREE_PATH" \
+    "$NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH" 2>&1 | tee -a "$LOG_FILE"; then
+    WORKTREE_CREATED=true
+    log "Worktree created successfully"
+  else
+    log "ERROR: Failed to create worktree"
+    exit 1
+  fi
+}
+
+consolidate_branches() {
+  local commit_count=0
+  cd "$NIXPKGS_WORKTREE_PATH"
+
+  for branch in "${WIP_BRANCHES[@]}"; do
+    log "Processing WIP branch: $branch"
+    FAILED_BRANCH="$branch"
+
+    # Check if branch exists on fork remote
+    if ! git rev-parse --verify "$NIXPKGS_FORK_REMOTE/$branch" >/dev/null 2>&1; then
+      log "ERROR: Branch $branch not found on remote $NIXPKGS_FORK_REMOTE"
+      exit 1
+    fi
+
+    # Get commits that are in WIP branch but not in base
+    log "Finding commits to cherry-pick from $branch..."
+    COMMITS=$(git log --reverse --pretty=format:%H \
+      "$NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH..$NIXPKGS_FORK_REMOTE/$branch" 2>/dev/null || true)
+
+    if [ -z "$COMMITS" ]; then
+      log "No commits to cherry-pick from $branch (already in base or branch is behind base)"
+      COMPLETED_BRANCHES+=("$branch")
+      continue
+    fi
+
+    # Count commits
+    local branch_commit_count=$(echo "$COMMITS" | wc -l)
+    log "Found $branch_commit_count commit(s) to cherry-pick"
+
+    # Cherry-pick each commit
+    for commit in $COMMITS; do
+      FAILED_COMMIT="$commit"
+      local commit_msg=$(git log -1 --pretty=format:%s "$commit")
+      log "Cherry-picking $commit: $commit_msg"
+
+      if ! git cherry-pick "$commit" 2>&1 | tee -a "$LOG_FILE"; then
+        log "ERROR: Cherry-pick failed for commit $commit in branch $branch"
+        log "Aborting cherry-pick..."
+        git cherry-pick --abort || true
+        exit 1
+      fi
+
+      ((commit_count++))
+    done
+
+    log "Successfully cherry-picked $branch_commit_count commit(s) from $branch"
+    COMPLETED_BRANCHES+=("$branch")
+    FAILED_BRANCH=""
+    FAILED_COMMIT=""
+  done
+
+  TOTAL_COMMITS=$commit_count
+  log "Total commits cherry-picked: $TOTAL_COMMITS"
+}
+
+main() {
+  log "=== Starting nixpkgs branch consolidation ==="
+  log "Repository: $NIXPKGS_REPO_PATH"
+  log "Worktree path: $NIXPKGS_WORKTREE_PATH"
+  log "Upstream remote: $NIXPKGS_UPSTREAM_REMOTE"
+  log "Fork remote: $NIXPKGS_FORK_REMOTE"
+  log "Consolidated branch: $NIXPKGS_CONSOLIDATED_BRANCH"
+  log "Configuration: $NIXPKGS_CONFIG_FILE"
+  log "Dry run: $DRY_RUN"
+
+  # Parse configuration
+  parse_config
+
+  # Ensure repository exists
+  ensure_repo
+
+  # Fetch latest from remotes
+  log "Fetching from $NIXPKGS_UPSTREAM_REMOTE..."
+  cd "$NIXPKGS_REPO_PATH"
+  git fetch "$NIXPKGS_UPSTREAM_REMOTE" 2>&1 | tee -a "$LOG_FILE"
+
+  log "Fetching from $NIXPKGS_FORK_REMOTE..."
+  git fetch "$NIXPKGS_FORK_REMOTE" 2>&1 | tee -a "$LOG_FILE"
+
+  # Verify base branch exists
+  if ! git rev-parse --verify "$NIXPKGS_UPSTREAM_REMOTE/$BASE_BRANCH" >/dev/null 2>&1; then
+    log "ERROR: Base branch $BASE_BRANCH not found on remote $NIXPKGS_UPSTREAM_REMOTE"
+    exit 1
+  fi
+
+  # Setup worktree for consolidation work
+  setup_worktree
+
+  # Cherry-pick WIP branches (happens in worktree)
+  consolidate_branches
+
+  # Show summary
+  log "=== Consolidation Summary ==="
+  log "Base branch: $BASE_BRANCH"
+  log "WIP branches consolidated: ${#COMPLETED_BRANCHES[@]}"
+  for branch in "${COMPLETED_BRANCHES[@]}"; do
+    log "  - $branch"
+  done
+  log "Total commits: $TOTAL_COMMITS"
+
+  # Push to remote (from worktree)
+  cd "$NIXPKGS_WORKTREE_PATH"
+
+  if [ "$DRY_RUN" = "true" ]; then
+    log "DRY RUN: Would push $NIXPKGS_CONSOLIDATED_BRANCH to $NIXPKGS_FORK_REMOTE"
+    log "Current HEAD: $(git rev-parse HEAD)"
+    log "Branch differs from upstream/$BASE_BRANCH by $TOTAL_COMMITS commits"
+  else
+    log "Force pushing $NIXPKGS_CONSOLIDATED_BRANCH to $NIXPKGS_FORK_REMOTE (with --force-with-lease)..."
+    if git push --force-with-lease "$NIXPKGS_FORK_REMOTE" "$NIXPKGS_CONSOLIDATED_BRANCH" 2>&1 | tee -a "$LOG_FILE"; then
+      log "Successfully pushed to $NIXPKGS_FORK_REMOTE/$NIXPKGS_CONSOLIDATED_BRANCH"
+    else
+      log "ERROR: Push failed. This might mean the remote branch was updated by someone else."
+      log "Check the remote branch and try again."
+      exit 1
+    fi
+  fi
+
+  # Success notification
+  local branches_str=$(IFS=$'\n'; echo "${COMPLETED_BRANCHES[*]}")
+  local success_msg="Consolidated ${#COMPLETED_BRANCHES[@]} branch(es) into $NIXPKGS_CONSOLIDATED_BRANCH
+
+Base: $BASE_BRANCH
+Branches:
+$branches_str
+
+Total commits: $TOTAL_COMMITS
+
+${DRY_RUN:+[DRY RUN] }Pushed to $NIXPKGS_FORK_REMOTE/$NIXPKGS_CONSOLIDATED_BRANCH"
+
+  notify "default" \
+    "✅ Nixpkgs Consolidation Success" \
+    "$success_msg" \
+    "white_check_mark,git,nixpkgs"
+
+  log "=== Consolidation completed successfully ==="
+}
+
+main "$@"
tools/nixpkgs-consolidate/README.md
@@ -0,0 +1,513 @@
+# Nixpkgs Branch Consolidation Tool
+
+Automatically consolidate multiple WIP/PR branches from your nixpkgs fork into a single consolidated branch using cherry-picking.
+
+## Overview
+
+This tool helps manage multiple work-in-progress (WIP) branches in your nixpkgs fork by:
+
+1. Fetching latest from upstream nixpkgs
+2. Creating a fresh consolidated branch from the base (e.g., `nixos-unstable`)
+3. Cherry-picking commits from each WIP branch in order
+4. Force-pushing the result to your fork
+5. Sending notifications on success/failure
+
+**Key Features:**
+
+- Uses git worktrees to avoid interfering with your main nixpkgs checkout
+- Cherry-pick strategy (no merge commits)
+- Handles conflicts gracefully with detailed error reporting
+- Integrates with ntfy for notifications
+- Dry-run mode for safe testing
+- Comprehensive logging
+
+## Use Case
+
+Perfect for nixpkgs contributors who:
+
+- Maintain multiple WIP branches for different packages/features
+- Want to test all changes together in a single branch
+- Need an automated way to keep a consolidated branch up-to-date with upstream
+- Want to reference one branch in their NixOS flake instead of managing overlays
+
+## Installation
+
+### Manual Usage
+
+```bash
+# Run directly from the tools directory
+./tools/nixpkgs-consolidate/nixpkgs-consolidate.sh
+```
+
+### Nix Package (Recommended)
+
+```bash
+# Build and install
+nix build .#nixpkgs-consolidate
+nix profile install .#nixpkgs-consolidate
+
+# Run
+nixpkgs-consolidate
+```
+
+### Automated with NixOS Module
+
+Add to your system configuration (e.g., `systems/aomi/extra.nix`):
+
+```nix
+{
+  services.nixpkgs-consolidate = {
+    enable = true;
+    user = "vincent";
+    schedule = "daily";  # or "Mon,Thu *-*-* 02:00:00"
+    configFile = "/home/vincent/.config/nixpkgs-automation/branches.conf";
+  };
+}
+```
+
+## Configuration
+
+### Setup
+
+1. **Create configuration directory:**
+   ```bash
+   mkdir -p ~/.config/nixpkgs-automation
+   ```
+
+2. **Copy example configuration:**
+   ```bash
+   cp tools/nixpkgs-consolidate/branches.conf.example \
+      ~/.config/nixpkgs-automation/branches.conf
+   ```
+
+3. **Edit configuration:**
+   ```bash
+   vim ~/.config/nixpkgs-automation/branches.conf
+   ```
+
+### Configuration File Format
+
+`~/.config/nixpkgs-automation/branches.conf`:
+
+```bash
+# Base branch (required)
+base: nixos-unstable
+
+# WIP branches to consolidate (one per line)
+wip-package-foo
+wip-fix-bar
+pr-12345-add-service
+```
+
+**Rules:**
+
+- First line starting with `base:` defines the upstream base branch
+- Other non-comment, non-empty lines are treated as WIP branch names
+- Branches are cherry-picked in the order listed
+- Comments start with `#`
+
+### Environment Variables
+
+Override defaults with environment variables:
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `NIXPKGS_REPO_PATH` | `~/src/nixpkgs` | Path to nixpkgs repository |
+| `NIXPKGS_WORKTREE_PATH` | `~/src/nixpkgs-consolidate-work` | Worktree for consolidation |
+| `NIXPKGS_UPSTREAM_REMOTE` | `upstream` | Upstream remote name |
+| `NIXPKGS_FORK_REMOTE` | `origin` | Your fork remote name |
+| `NIXPKGS_CONSOLIDATED_BRANCH` | `wip-consolidated` | Output branch name |
+| `NIXPKGS_CONFIG_FILE` | `~/.config/nixpkgs-automation/branches.conf` | Config file path |
+| `NTFY_SERVER` | `https://ntfy.sbr.pm` | ntfy server URL |
+| `NTFY_TOPIC` | `git-builds` | ntfy notification topic |
+| `NTFY_TOKEN_FILE` | `/run/agenix/ntfy-token` | ntfy auth token file |
+| `LOG_DIR` | `/var/log/nixpkgs-consolidate` | Log directory |
+| `DRY_RUN` | `false` | Set to `true` for dry-run |
+
+## Usage
+
+### Basic Usage
+
+```bash
+# Run with defaults
+nixpkgs-consolidate
+
+# Or directly:
+./tools/nixpkgs-consolidate/nixpkgs-consolidate.sh
+```
+
+### Dry Run (Recommended First)
+
+Test without actually pushing:
+
+```bash
+DRY_RUN=true nixpkgs-consolidate
+```
+
+This will:
+- Parse configuration
+- Fetch from remotes
+- Create worktree
+- Cherry-pick all commits
+- Show what would be pushed
+- NOT actually push to remote
+
+### Custom Branch Name
+
+```bash
+NIXPKGS_CONSOLIDATED_BRANCH=personal-main nixpkgs-consolidate
+```
+
+### Different Base Branch
+
+```bash
+# Edit config file to change base:
+echo "base: master" > ~/.config/nixpkgs-automation/branches.conf
+# Then run:
+nixpkgs-consolidate
+```
+
+## Workflow
+
+### Initial Setup
+
+1. **Fork nixpkgs on GitHub** (if you haven't already)
+
+2. **Clone nixpkgs locally:**
+   ```bash
+   git clone git@github.com:NixOS/nixpkgs.git ~/src/nixpkgs
+   cd ~/src/nixpkgs
+   git remote add upstream git@github.com:NixOS/nixpkgs.git
+   # Assumes 'origin' points to your fork
+   ```
+
+3. **Create WIP branches:**
+   ```bash
+   git checkout -b wip-my-feature nixos-unstable
+   # Make changes...
+   git push origin wip-my-feature
+   ```
+
+4. **Configure consolidation:**
+   ```bash
+   mkdir -p ~/.config/nixpkgs-automation
+   cat > ~/.config/nixpkgs-automation/branches.conf <<EOF
+   base: nixos-unstable
+   wip-my-feature
+   wip-another-feature
+   EOF
+   ```
+
+5. **Run consolidation:**
+   ```bash
+   DRY_RUN=true nixpkgs-consolidate  # Test first
+   nixpkgs-consolidate                # Actually push
+   ```
+
+### Regular Usage
+
+1. **Work on WIP branches as usual:**
+   ```bash
+   git checkout wip-my-feature
+   # Make changes...
+   git commit -m "Update package foo"
+   git push origin wip-my-feature
+   ```
+
+2. **Run consolidation** (manually or via systemd timer):
+   ```bash
+   nixpkgs-consolidate
+   ```
+
+3. **Use consolidated branch in your flake:**
+   ```nix
+   {
+     inputs = {
+       nixpkgs.url = "github:vdemeester/nixpkgs/wip-consolidated";
+     };
+   }
+   ```
+
+4. **Clean up when PRs land:**
+   - Remove merged branches from `branches.conf`
+   - Delete remote branches: `git push origin --delete wip-merged-feature`
+
+## Git Workflow Details
+
+### Worktree Usage
+
+The script uses git worktrees to keep your main repository clean:
+
+1. **Main repository** (`~/src/nixpkgs`):
+   - Stays on whatever branch you're working on
+   - Not affected by consolidation process
+
+2. **Consolidation worktree** (`~/src/nixpkgs-consolidate-work`):
+   - Created automatically
+   - Used for all cherry-picking operations
+   - Removed automatically after completion
+
+### Cherry-Pick Strategy
+
+For each WIP branch:
+
+```bash
+# Get commits unique to WIP branch
+commits = $(git log upstream/nixos-unstable..origin/wip-feature)
+
+# Cherry-pick each commit
+for commit in commits; do
+  git cherry-pick $commit
+done
+```
+
+**Why cherry-pick instead of merge?**
+
+- Clean linear history
+- No merge commits
+- Easy to see what changed
+- Better for testing package updates
+
+### Conflict Handling
+
+If a cherry-pick conflicts:
+
+1. Script aborts immediately
+2. Sends detailed notification with:
+   - Which branch caused the conflict
+   - Which commit failed
+   - What was successfully completed
+3. Cleans up worktree
+4. Returns non-zero exit code
+
+**Resolution:**
+
+- Fix conflicts in the WIP branch
+- Rebase WIP branch on latest upstream
+- Run consolidation again
+
+## Notifications
+
+### Success Notification
+
+Sent to `ntfy.sbr.pm/git-builds`:
+
+```
+✅ Nixpkgs Consolidation Success
+
+Consolidated 3 branch(es) into wip-consolidated
+
+Base: nixos-unstable
+Branches:
+wip-package-foo
+wip-fix-bar
+pr-12345-add-service
+
+Total commits: 15
+
+Pushed to origin/wip-consolidated
+```
+
+### Failure Notification
+
+```
+❌ Nixpkgs Consolidation Failed
+
+Cherry-pick conflict in branch: wip-fix-bar
+Commit: abc123def456
+
+Base: nixos-unstable
+Completed: wip-package-foo
+Failed at: wip-fix-bar
+
+Logs: /var/log/nixpkgs-consolidate/20260108-103015.log
+```
+
+## Logging
+
+Logs are written to:
+
+```
+/var/log/nixpkgs-consolidate/YYYYMMDD-HHMMSS.log
+```
+
+**Log format:**
+
+```
+[2026-01-08 10:30:15] Starting nixpkgs branch consolidation
+[2026-01-08 10:30:16] Reading configuration from ~/.config/nixpkgs-automation/branches.conf
+[2026-01-08 10:30:17] Base branch: nixos-unstable
+[2026-01-08 10:30:17] WIP branches (3): wip-foo wip-bar pr-12345
+[2026-01-08 10:30:20] Fetching from upstream...
+[2026-01-08 10:30:25] Creating worktree at ~/src/nixpkgs-consolidate-work
+[2026-01-08 10:30:26] Processing WIP branch: wip-foo
+[2026-01-08 10:30:27] Found 5 commit(s) to cherry-pick
+[2026-01-08 10:30:28] Cherry-picking abc123: Update foo to 1.2.3
+...
+```
+
+## Troubleshooting
+
+### "Configuration file not found"
+
+**Problem:** `~/.config/nixpkgs-automation/branches.conf` doesn't exist
+
+**Solution:**
+```bash
+mkdir -p ~/.config/nixpkgs-automation
+cp tools/nixpkgs-consolidate/branches.conf.example \
+   ~/.config/nixpkgs-automation/branches.conf
+vim ~/.config/nixpkgs-automation/branches.conf
+```
+
+### "Remote 'upstream' not found"
+
+**Problem:** nixpkgs repository doesn't have upstream remote configured
+
+**Solution:**
+```bash
+cd ~/src/nixpkgs
+git remote add upstream git@github.com:NixOS/nixpkgs.git
+git fetch upstream
+```
+
+### "Branch not found on remote origin"
+
+**Problem:** WIP branch listed in config doesn't exist in your fork
+
+**Solution:**
+- Check branch exists: `git ls-remote origin | grep wip-feature`
+- Push branch to fork: `git push origin wip-feature`
+- Or remove from config if no longer needed
+
+### Cherry-pick conflicts
+
+**Problem:** Commits conflict when cherry-picked
+
+**Solution:**
+
+1. Check the logs to see which branch caused the conflict
+2. Rebase that branch on latest upstream:
+   ```bash
+   git checkout wip-problematic-branch
+   git fetch upstream
+   git rebase upstream/nixos-unstable
+   git push --force-with-lease origin wip-problematic-branch
+   ```
+3. Run consolidation again
+
+### Force push rejected
+
+**Problem:** `git push --force-with-lease` fails
+
+**Cause:** Someone else (or another process) updated the consolidated branch
+
+**Solution:**
+- If it was you from another machine: Re-run consolidation
+- If unexpected: Check remote branch with `git log origin/wip-consolidated`
+
+## Integration with NixOS Flake
+
+Once consolidation is running, reference the consolidated branch in your flake:
+
+```nix
+{
+  inputs = {
+    # Use your consolidated branch instead of official nixpkgs
+    nixpkgs.url = "github:vdemeester/nixpkgs/wip-consolidated";
+
+    # Or for testing with local changes:
+    # nixpkgs.url = "path:/home/vincent/src/nixpkgs";
+  };
+
+  outputs = { nixpkgs, ... }: {
+    # Your configuration...
+  };
+}
+```
+
+**Benefits:**
+
+- Test all your WIP packages together
+- Single flake input instead of multiple overlays
+- Automatically updated as you run consolidation
+
+## Automation with Systemd
+
+### Manual Trigger
+
+```bash
+systemctl start nixpkgs-consolidate.service
+```
+
+### Check Status
+
+```bash
+systemctl status nixpkgs-consolidate.service
+journalctl -u nixpkgs-consolidate.service -f
+```
+
+### List Timer Schedule
+
+```bash
+systemctl list-timers | grep nixpkgs
+```
+
+### Modify Schedule
+
+Edit your system configuration:
+
+```nix
+services.nixpkgs-consolidate = {
+  enable = true;
+  schedule = "Mon,Thu *-*-* 02:00:00";  # Monday and Thursday at 2 AM
+};
+```
+
+## Best Practices
+
+1. **Test with dry-run first:**
+   ```bash
+   DRY_RUN=true nixpkgs-consolidate
+   ```
+
+2. **Keep WIP branches rebased:**
+   ```bash
+   git checkout wip-feature
+   git fetch upstream
+   git rebase upstream/nixos-unstable
+   git push --force-with-lease origin wip-feature
+   ```
+
+3. **Clean up merged branches:**
+   - Remove from `branches.conf`
+   - Delete from fork: `git push origin --delete wip-merged`
+
+4. **Use descriptive branch names:**
+   - `wip-update-python-312` ✅
+   - `wip-stuff` ❌
+
+5. **Order matters in config:**
+   - Put independent changes first
+   - Put dependent changes later
+
+6. **Monitor notifications:**
+   - Subscribe to ntfy topic `git-builds`
+   - Fix conflicts promptly
+
+## Security
+
+- **Git Authentication:** Uses existing SSH keys (`~/.ssh/`)
+- **ntfy Token:** Stored in agenix (`/run/agenix/ntfy-token`)
+- **Logs:** Written to `/var/log` with restricted permissions
+- **Systemd:** Service runs with security hardening (ProtectSystem, ProtectHome, etc.)
+
+## See Also
+
+- [Nixpkgs Contributing Guide](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md)
+- [Git Worktrees Documentation](https://git-scm.com/docs/git-worktree)
+- [ntfy Documentation](https://ntfy.sh)
+
+## License
+
+MIT