Commit 577de508ed44
Changed files (4)
dots
modules
nix-flake-updater
systems
aomi
tools
nix-flake-update
dots/Makefile
@@ -53,11 +53,13 @@ lazypr : ~/.config/lazypr/config.toml
all += gh-news
gh-news : ~/.config/gh-news/config.toml
-all += git-template copilot-hooks opencode-plugin pi-agent
+all += git-template copilot-hooks opencode-plugin pi-agent agent-skills agent-skill-manager
git-template : ~/.config/git/template
copilot-hooks : ~/.config/copilot-hooks
opencode-plugin : ~/.config/opencode/plugin
pi-agent : ~/.pi/agent/extensions ~/.pi/agent/AGENTS.md ~/.pi/agent/README.md
+agent-skills : ~/.config/agent-skills
+agent-skill-manager : ~/bin/agent-skill-manager
# Backward compatibility: symlink ~/.claude to ~/.config/claude
~/.claude : force
modules/nix-flake-updater/default.nix
@@ -10,115 +10,154 @@ with lib;
let
cfg = config.services.nix-flake-updater;
- # Use the nix-flake-update package
- updateScript = pkgs.writeShellScript "nix-flake-update-wrapper" ''
- export REPO_PATH="${cfg.repoPath}"
- export FLAKE_PATH="${cfg.flakePath}"
- export GIT_REMOTE="${cfg.gitRemote}"
- export BRANCH_PREFIX="${cfg.branchPrefix}"
- export NTFY_TOPIC="${cfg.ntfyTopic}"
- export NTFY_SERVER="${cfg.ntfyServer}"
- export BUILD_SYSTEMS="${toString cfg.buildSystems}"
- export DRY_RUN="${toString cfg.dryRun}"
- ${optionalString (cfg.ntfyTokenFile != null) ''export NTFY_TOKEN_FILE="${cfg.ntfyTokenFile}"''}
+ instanceOpts =
+ { config, ... }:
+ {
+ options = {
+ enable = mkEnableOption "this flake updater instance";
- # Execute the packaged update script (already has tools in PATH)
- exec ${pkgs.nix-flake-update}/bin/nix-flake-update
- '';
+ repoPath = mkOption {
+ type = types.str;
+ example = "/home/user/nixos-config";
+ description = "Path to the git repository containing the flake";
+ };
-in
-{
- options.services.nix-flake-updater = {
- enable = mkEnableOption "automated Nix flake.lock updates";
+ flakePath = mkOption {
+ type = types.str;
+ default = config.repoPath;
+ example = "/home/user/nixos-config";
+ description = "Path to the flake (usually same as repoPath)";
+ };
- repoPath = mkOption {
- type = types.str;
- example = "/home/user/nixos-config";
- description = "Path to the git repository containing the flake";
+ gitRemote = mkOption {
+ type = types.str;
+ default = "origin";
+ description = "Git remote name to push to";
+ };
+
+ mainBranch = mkOption {
+ type = types.str;
+ default = "main";
+ description = "Main branch name (for auto-merge)";
+ };
+
+ branchPrefix = mkOption {
+ type = types.str;
+ default = "flake-update-";
+ description = "Prefix for update branches";
+ };
+
+ flakeInputs = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [
+ "chick-group"
+ "chapeau-rouge"
+ ];
+ description = "List of specific flake inputs to update (empty = all)";
+ };
+
+ autoMerge = mkOption {
+ type = types.bool;
+ default = false;
+ description = "If true, automatically merge to main branch on successful build";
+ };
+
+ inboxOrg = mkOption {
+ type = types.str;
+ default = "/home/${config.user}/desktop/org/inbox.org";
+ example = "/home/user/org/inbox.org";
+ description = "Path to org-mode inbox file for TODO entries on failure";
+ };
+
+ buildSystems = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [
+ "aomi"
+ "sakhalin"
+ ];
+ description = "List of NixOS systems to build for verification";
+ };
+
+ schedule = mkOption {
+ type = types.str;
+ default = "weekly";
+ example = "Mon *-*-* 02:00:00";
+ description = "Systemd timer schedule (OnCalendar format or 'weekly'/'daily')";
+ };
+
+ ntfyTopic = mkOption {
+ type = types.str;
+ default = "nix-updates";
+ description = "ntfy topic for notifications";
+ };
+
+ ntfyServer = mkOption {
+ type = types.str;
+ default = "https://ntfy.sh";
+ example = "http://ntfy.sbr.pm";
+ description = "ntfy server URL";
+ };
+
+ ntfyTokenFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = "Path to file containing ntfy authentication token (optional)";
+ };
+
+ dryRun = mkOption {
+ type = types.bool;
+ default = false;
+ description = "If true, don't push to remote (testing mode)";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "root";
+ description = "User to run the update as";
+ };
+
+ randomizedDelaySec = mkOption {
+ type = types.int;
+ default = 3600;
+ description = "Random delay in seconds before starting (0-value)";
+ };
+ };
};
- flakePath = mkOption {
- type = types.str;
- default = cfg.repoPath;
- example = "/home/user/nixos-config";
- description = "Path to the flake (usually same as repoPath)";
- };
+ mkUpdateScript =
+ name: instanceCfg:
+ pkgs.writeShellScript "nix-flake-update-${name}" ''
+ export REPO_PATH="${instanceCfg.repoPath}"
+ export FLAKE_PATH="${instanceCfg.flakePath}"
+ export GIT_REMOTE="${instanceCfg.gitRemote}"
+ export MAIN_BRANCH="${instanceCfg.mainBranch}"
+ export BRANCH_PREFIX="${instanceCfg.branchPrefix}"
+ export NTFY_TOPIC="${instanceCfg.ntfyTopic}"
+ export NTFY_SERVER="${instanceCfg.ntfyServer}"
+ export BUILD_SYSTEMS="${toString instanceCfg.buildSystems}"
+ export DRY_RUN="${toString instanceCfg.dryRun}"
+ export FLAKE_INPUTS="${toString instanceCfg.flakeInputs}"
+ export AUTO_MERGE="${toString instanceCfg.autoMerge}"
+ export INBOX_ORG="${instanceCfg.inboxOrg}"
+ ${optionalString (
+ instanceCfg.ntfyTokenFile != null
+ ) ''export NTFY_TOKEN_FILE="${instanceCfg.ntfyTokenFile}"''}
- gitRemote = mkOption {
- type = types.str;
- default = "origin";
- description = "Git remote name to push to";
- };
+ # Execute the packaged update script (already has tools in PATH)
+ exec ${pkgs.nix-flake-update}/bin/nix-flake-update
+ '';
- branchPrefix = mkOption {
- type = types.str;
- default = "flake-update-";
- description = "Prefix for update branches";
- };
-
- buildSystems = mkOption {
- type = types.listOf types.str;
- default = [ ];
- example = [
- "aomi"
- "sakhalin"
- ];
- description = "List of NixOS systems to build for verification";
- };
-
- schedule = mkOption {
- type = types.str;
- default = "weekly";
- example = "Mon *-*-* 02:00:00";
- description = "Systemd timer schedule (OnCalendar format or 'weekly'/'daily')";
- };
-
- ntfyTopic = mkOption {
- type = types.str;
- default = "nix-updates";
- description = "ntfy topic for notifications";
- };
-
- ntfyServer = mkOption {
- type = types.str;
- default = "https://ntfy.sh";
- example = "http://ntfy.sbr.pm";
- description = "ntfy server URL";
- };
-
- ntfyTokenFile = mkOption {
- type = types.nullOr types.path;
- default = null;
- description = "Path to file containing ntfy authentication token (optional)";
- };
-
- dryRun = mkOption {
- type = types.bool;
- default = false;
- description = "If true, don't push to remote (testing mode)";
- };
-
- user = mkOption {
- type = types.str;
- default = "root";
- description = "User to run the update as";
- };
-
- randomizedDelaySec = mkOption {
- type = types.int;
- default = 3600;
- description = "Random delay in seconds before starting (0-value)";
- };
- };
-
- config = mkIf cfg.enable {
- systemd.services.nix-flake-updater = {
- description = "Automated Nix flake.lock updater";
+ mkService =
+ name: instanceCfg:
+ nameValuePair "nix-flake-updater-${name}" {
+ description = "Automated Nix flake.lock updater (${name})";
serviceConfig = {
Type = "oneshot";
- User = cfg.user;
- ExecStart = "${updateScript}";
+ User = instanceCfg.user;
+ ExecStart = "${mkUpdateScript name instanceCfg}";
Environment = ''"GIT_SSH_COMMAND=ssh -o ControlMaster=no"'';
# Security hardening
@@ -126,19 +165,21 @@ in
ProtectSystem = "strict";
ProtectHome = "read-only";
ReadWritePaths = [
- cfg.repoPath
+ instanceCfg.repoPath
"/var/log/nix-flake-updater"
# Worktree location (script creates worktrees in ~/tmp)
- "/home/${cfg.user}/tmp"
+ "/home/${instanceCfg.user}/tmp"
# Nix cache for flake fetcher
- "/home/${cfg.user}/.cache/nix"
+ "/home/${instanceCfg.user}/.cache/nix"
+ # Org inbox for TODOs
+ (dirOf instanceCfg.inboxOrg)
];
NoNewPrivileges = true;
# Logging
StandardOutput = "journal";
StandardError = "journal";
- SyslogIdentifier = "nix-flake-updater";
+ SyslogIdentifier = "nix-flake-updater-${name}";
};
# Don't fail if update fails (e.g., no changes, build failures)
@@ -147,20 +188,39 @@ in
};
};
- systemd.timers.nix-flake-updater = {
- description = "Timer for automated Nix flake.lock updates";
+ mkTimer =
+ name: instanceCfg:
+ nameValuePair "nix-flake-updater-${name}" {
+ description = "Timer for automated Nix flake.lock updates (${name})";
wantedBy = [ "timers.target" ];
timerConfig = {
- OnCalendar = cfg.schedule;
- RandomizedDelaySec = cfg.randomizedDelaySec;
+ OnCalendar = instanceCfg.schedule;
+ RandomizedDelaySec = instanceCfg.randomizedDelaySec;
Persistent = true;
};
};
- # Ensure log directory exists
+in
+{
+ options.services.nix-flake-updater = mkOption {
+ type = types.attrsOf (types.submodule instanceOpts);
+ default = { };
+ description = "Automated Nix flake.lock updater instances";
+ };
+
+ config = mkIf (cfg != { }) {
+ systemd.services = listToAttrs (
+ mapAttrsToList (name: instanceCfg: mkService name instanceCfg) (filterAttrs (_: v: v.enable) cfg)
+ );
+
+ systemd.timers = listToAttrs (
+ mapAttrsToList (name: instanceCfg: mkTimer name instanceCfg) (filterAttrs (_: v: v.enable) cfg)
+ );
+
+ # Ensure log directory exists (shared by all instances)
systemd.tmpfiles.rules = [
- "d /var/log/nix-flake-updater 0750 ${cfg.user} ${config.users.users.${cfg.user}.group} -"
+ "d /var/log/nix-flake-updater 0750 root root -"
];
};
}
systems/aomi/extra.nix
@@ -151,42 +151,88 @@
# Automated flake.lock updates with build verification
services.nix-flake-updater = {
- enable = true;
- repoPath = "/home/vincent/src/home";
+ # Weekly updates for all inputs
+ weekly = {
+ enable = true;
+ repoPath = "/home/vincent/src/home";
- # Build systems across both architectures for verification
- buildSystems = [
- # x86_64-linux systems
- "aomi" # Self (laptop/build server)
- "kyushu" # Work laptop
- "sakhalin" # Server
- "kerkouane" # VPS server
+ # Build systems across both architectures for verification
+ buildSystems = [
+ # x86_64-linux systems
+ "aomi" # Self (laptop/build server)
+ "kyushu" # Work laptop
+ "sakhalin" # Server
+ "kerkouane" # VPS server
- # aarch64-linux systems
- "rhea" # Main media server
- "aion" # XMPP/podcast server
- "athena" # Raspberry Pi 4
- "demeter" # Raspberry Pi 4
- "aix" # Raspberry Pi 4
- ];
+ # aarch64-linux systems
+ "rhea" # Main media server
+ "aion" # XMPP/podcast server
+ "athena" # Raspberry Pi 4
+ "demeter" # Raspberry Pi 4
+ "aix" # Raspberry Pi 4
+ ];
- # Run weekly on Sunday at 2 AM
- schedule = "Sun *-*-* 02:00:00";
+ # Run weekly on Sunday at 2 AM
+ schedule = "Sun *-*-* 02:00:00";
- # Notifications via ntfy
- ntfyServer = "https://ntfy.sbr.pm";
- ntfyTopic = "nix-updates";
- ntfyTokenFile = config.age.secrets."ntfy-token".path;
+ # Notifications via ntfy
+ ntfyServer = "https://ntfy.sbr.pm";
+ ntfyTopic = "nix-updates";
+ ntfyTokenFile = config.age.secrets."ntfy-token".path;
- # Git settings
- gitRemote = "origin";
- branchPrefix = "flake-update-";
+ # Git settings
+ gitRemote = "origin";
+ branchPrefix = "flake-update-";
- # Run as vincent (has git push access)
- user = "vincent";
+ # Run as vincent (has git push access)
+ user = "vincent";
- # Add randomized delay to avoid conflicts
- randomizedDelaySec = 1800; # 0-30 min delay
+ # Add randomized delay to avoid conflicts
+ randomizedDelaySec = 1800; # 0-30 min delay
+ };
+
+ # Daily automated updates for chick-group and chapeau-rouge with auto-merge
+ daily = {
+ enable = true;
+ repoPath = "/home/vincent/src/home";
+
+ # Update only personal repos
+ flakeInputs = [
+ "chick-group"
+ "chapeau-rouge"
+ ];
+
+ # Auto-merge to main on successful build
+ autoMerge = true;
+
+ # Build fewer systems for faster daily updates
+ buildSystems = [
+ "aomi" # Self (x86_64-linux)
+ "kyushu" # Work laptop (x86_64-linux)
+ ];
+
+ # Run daily at 4 AM
+ schedule = "*-*-* 04:00:00";
+
+ # Notifications via ntfy (same topic as weekly)
+ ntfyServer = "https://ntfy.sbr.pm";
+ ntfyTopic = "nix-updates";
+ ntfyTokenFile = config.age.secrets."ntfy-token".path;
+
+ # Git settings
+ gitRemote = "origin";
+ mainBranch = "main";
+ branchPrefix = "auto-update-daily-";
+
+ # Org inbox for failure TODOs
+ inboxOrg = "/home/vincent/desktop/org/inbox.org";
+
+ # Run as vincent (has git push access)
+ user = "vincent";
+
+ # Smaller delay for daily updates
+ randomizedDelaySec = 600; # 0-10 min delay
+ };
};
services = {
tools/nix-flake-update/nix-flake-update.sh
@@ -8,12 +8,16 @@ set -euo pipefail
REPO_PATH="${REPO_PATH:-/home/vincent/src/home}"
FLAKE_PATH="${FLAKE_PATH:-$REPO_PATH}"
GIT_REMOTE="${GIT_REMOTE:-origin}"
+MAIN_BRANCH="${MAIN_BRANCH:-main}"
BRANCH_PREFIX="${BRANCH_PREFIX:-flake-update-}"
NTFY_TOPIC="${NTFY_TOPIC:-nix-updates}"
NTFY_SERVER="${NTFY_SERVER:-https://ntfy.sh}"
NTFY_TOKEN_FILE="${NTFY_TOKEN_FILE:-}"
BUILD_SYSTEMS="${BUILD_SYSTEMS:-}"
DRY_RUN="${DRY_RUN:-false}"
+FLAKE_INPUTS="${FLAKE_INPUTS:-}" # Space-separated list of inputs to update (empty = all)
+AUTO_MERGE="${AUTO_MERGE:-false}" # If true, merge to main on success
+INBOX_ORG="${INBOX_ORG:-$HOME/desktop/org/inbox.org}" # Path to org-mode inbox
LOG_FILE="/var/log/nix-flake-updater/$(date +%Y%m%d-%H%M%S).log"
mkdir -p "$(dirname "$LOG_FILE")"
@@ -52,6 +56,28 @@ notify() {
fi
}
+add_todo_to_inbox() {
+ local title="$1"
+ local details="$2"
+
+ if [ -f "$INBOX_ORG" ]; then
+ log "Adding TODO to $INBOX_ORG"
+ cat >> "$INBOX_ORG" <<EOF
+* TODO $title
+ SCHEDULED: <$(date '+%Y-%m-%d %a')>
+ :PROPERTIES:
+ :CREATED: [$(date '+%Y-%m-%d %a %H:%M')]
+ :END:
+
+$details
+
+Log file: $LOG_FILE
+EOF
+ else
+ log "WARNING: Inbox file not found: $INBOX_ORG"
+ fi
+}
+
cleanup() {
local exit_code=$?
@@ -65,8 +91,20 @@ cleanup() {
if [ $exit_code -ne 0 ]; then
log "ERROR: Update process failed with exit code $exit_code"
+
+ # Add TODO to inbox on failure
+ local input_desc="all inputs"
+ if [ -n "$FLAKE_INPUTS" ]; then
+ input_desc="inputs: $FLAKE_INPUTS"
+ fi
+
+ add_todo_to_inbox "Fix flake update failure" \
+ "Flake update failed for $input_desc.
+Build systems: $BUILD_SYSTEMS
+Auto-merge: $AUTO_MERGE"
+
notify "high" "❌ Flake Update Failed" \
- "Build failed. See logs: $LOG_FILE" \
+ "Build failed for $input_desc. TODO added to inbox. See logs: $LOG_FILE" \
"warning,flake"
fi
}
@@ -101,7 +139,16 @@ git checkout -b "$BRANCH_NAME"
# Update flake.lock (work in worktree, flake is at root)
log "Updating flake.lock"
-nix flake update 2>&1 | tee -a "$LOG_FILE"
+if [ -n "$FLAKE_INPUTS" ]; then
+ log "Updating specific inputs: $FLAKE_INPUTS"
+ for input in $FLAKE_INPUTS; do
+ log "Updating input: $input"
+ nix flake lock --update-input "$input" 2>&1 | tee -a "$LOG_FILE"
+ done
+else
+ log "Updating all inputs"
+ nix flake update 2>&1 | tee -a "$LOG_FILE"
+fi
# Check if there are changes
if ! git diff --quiet flake.lock; then
@@ -131,7 +178,12 @@ if ! git diff --quiet flake.lock; then
git add flake.lock
# Generate commit message with changed inputs
- COMMIT_MSG="chore(flake): update flake.lock
+ input_desc="all inputs"
+ if [ -n "$FLAKE_INPUTS" ]; then
+ input_desc="$FLAKE_INPUTS"
+ fi
+
+ COMMIT_MSG="chore(flake): update $input_desc
$(nix flake metadata . --json 2>/dev/null | \
jq -r '.locks.nodes | to_entries[] | select(.key != "root") | "- \(.key): \(.value.locked.rev // .value.locked.narHash // "updated")"' 2>/dev/null || echo "Updated flake inputs")
@@ -143,16 +195,52 @@ Built systems: $BUILD_SYSTEMS
git commit -m "$COMMIT_MSG"
if [ "$DRY_RUN" = "false" ]; then
- # Push to remote
- log "Pushing to $GIT_REMOTE/$BRANCH_NAME"
- git push "$GIT_REMOTE" "$BRANCH_NAME"
+ if [ "$AUTO_MERGE" = "true" ]; then
+ # Auto-merge: rebase onto main and push directly
+ log "Auto-merge enabled: rebasing onto $GIT_REMOTE/$MAIN_BRANCH"
+
+ # Fetch latest main
+ git fetch "$GIT_REMOTE" "$MAIN_BRANCH"
+
+ # Rebase our commit onto main
+ if git rebase "$GIT_REMOTE/$MAIN_BRANCH"; then
+ log "Rebase successful, pushing to $GIT_REMOTE/$MAIN_BRANCH"
+
+ # Push directly to main
+ git push "$GIT_REMOTE" "HEAD:$MAIN_BRANCH"
+
+ # Notify success
+ notify "default" "✅ Flake Auto-Updated & Merged" \
+ "Updates for $input_desc merged to $MAIN_BRANCH. All builds passed: $BUILD_SYSTEMS" \
+ "white_check_mark,flake,merged"
+
+ log "SUCCESS: Flake updated and merged to $MAIN_BRANCH"
+ else
+ log "ERROR: Rebase failed, main branch may have moved"
+ git rebase --abort || true
+
+ add_todo_to_inbox "Flake update rebase conflict" \
+ "Auto-merge failed due to rebase conflict.
+Inputs: $input_desc
+Branch: $BRANCH_NAME (in worktree, needs manual rebase)"
+
+ notify "high" "⚠️ Flake Update Rebase Failed" \
+ "Could not rebase $input_desc onto $MAIN_BRANCH. TODO added to inbox." \
+ "warning,flake,conflict"
+ exit 1
+ fi
+ else
+ # Branch mode: push to feature branch
+ log "Pushing to $GIT_REMOTE/$BRANCH_NAME"
+ git push "$GIT_REMOTE" "$BRANCH_NAME"
- # Notify success
- notify "default" "✅ Flake Updated Successfully" \
- "Branch $BRANCH_NAME created and pushed. All builds passed: $BUILD_SYSTEMS" \
- "white_check_mark,flake"
+ # Notify success
+ notify "default" "✅ Flake Updated Successfully" \
+ "Branch $BRANCH_NAME created and pushed. All builds passed: $BUILD_SYSTEMS" \
+ "white_check_mark,flake"
- log "SUCCESS: Flake updated and pushed to $BRANCH_NAME"
+ log "SUCCESS: Flake updated and pushed to $BRANCH_NAME"
+ fi
else
log "DRY RUN: Would push to $GIT_REMOTE/$BRANCH_NAME"
notify "low" "🧪 Flake Update (Dry Run)" \
@@ -162,8 +250,19 @@ Built systems: $BUILD_SYSTEMS
else
log "Build failed, not committing changes"
+
+ input_desc="all inputs"
+ if [ -n "$FLAKE_INPUTS" ]; then
+ input_desc="$FLAKE_INPUTS"
+ fi
+
+ add_todo_to_inbox "Flake update build failure" \
+ "Build failed after updating $input_desc.
+Build systems tested: $BUILD_SYSTEMS
+Auto-merge: $AUTO_MERGE"
+
notify "high" "❌ Flake Update Build Failed" \
- "Builds failed for updated flake.lock. Check logs: $LOG_FILE" \
+ "Builds failed for updated $input_desc. TODO added to inbox. Check logs: $LOG_FILE" \
"x,flake,warning"
# Clean up failed branch in main repo