Kieran's opinionated (and probably slightly dumb) nix config
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.atelier.services.knot-sync; 10in 11{ 12 options.atelier.services.knot-sync = { 13 enable = lib.mkEnableOption "Knot to GitHub sync service"; 14 15 repoDir = lib.mkOption { 16 type = lib.types.str; 17 default = "/home/git/did:plc:krxbvxvis5skq7jj6eot23ul"; 18 description = "Directory containing git repositories"; 19 }; 20 21 githubUsername = lib.mkOption { 22 type = lib.types.str; 23 default = "taciturnaxolotl"; 24 description = "GitHub username"; 25 }; 26 27 secretsFile = lib.mkOption { 28 type = lib.types.path; 29 description = "Path to secrets file containing GITHUB_TOKEN"; 30 }; 31 32 logFile = lib.mkOption { 33 type = lib.types.str; 34 default = "/home/git/knot-sync.log"; 35 description = "Log file location"; 36 }; 37 38 interval = lib.mkOption { 39 type = lib.types.str; 40 default = "*/5 * * * *"; 41 description = "Cron schedule for sync (default: every 5 minutes)"; 42 }; 43 }; 44 45 config = lib.mkIf cfg.enable { 46 systemd.services.knot-sync = { 47 description = "Sync Knot repositories to GitHub"; 48 serviceConfig = { 49 Type = "oneshot"; 50 User = "git"; 51 EnvironmentFile = cfg.secretsFile; 52 ExecStart = pkgs.writeShellScript "knot-sync" '' 53 set -euo pipefail 54 55 # Variables 56 REPO_DIR="${cfg.repoDir}" 57 GITHUB_USERNAME="${cfg.githubUsername}" 58 LOG_FILE="${cfg.logFile}" 59 60 # Log function 61 log() { echo "$(date +'%Y-%m-%d %H:%M:%S'): $1" >> "$LOG_FILE"; } 62 63 # Create the post-receive hook template 64 cat <<'EOF' > /tmp/post-receive.template 65 #!${pkgs.bash}/bin/bash 66 # post-receive hook to sync to GitHub - AUTOGENERATED 67 68 # Load environment variables from secrets file 69 if [ -f "${cfg.secretsFile}" ]; then 70 source "${cfg.secretsFile}" 71 fi 72 73 # Variables 74 GITHUB_USERNAME="${cfg.githubUsername}" 75 LOG_FILE="${cfg.logFile}" 76 REPO_NAME=$(basename $(pwd)) 77 78 # Log function 79 log() { echo "$(date +'%Y-%m-%d %H:%M:%S'): $1" >> "''${LOG_FILE}"; } 80 81 # Check for nosync marker 82 if [ -f "$(pwd)/.nosync" ]; then 83 log "Skipping sync for $REPO_NAME (nosync marker present)" 84 exit 0 85 fi 86 87 # Function to sync to GitHub 88 sync_to_github() { 89 log "Syncing $REPO_NAME to GitHub" 90 expected_url="https://''${GITHUB_USERNAME}:''${GITHUB_TOKEN}@github.com/''${GITHUB_USERNAME}/''${REPO_NAME}.git" 91 current_url=$(${pkgs.git}/bin/git remote get-url origin 2>/dev/null || echo "") 92 93 if [ -z "$current_url" ]; then 94 log "Adding origin remote" 95 ${pkgs.git}/bin/git remote add origin "$expected_url" 96 elif [ "$current_url" != "$expected_url" ]; then 97 log "Updating origin remote URL" 98 ${pkgs.git}/bin/git remote set-url origin "$expected_url" 99 fi 100 101 # Mirror push everything (refs, tags, branches) 102 if ${pkgs.git}/bin/git push --mirror origin 2>&1 | tee -a "''${LOG_FILE}"; then 103 log "Sync succeeded for $REPO_NAME" 104 return 0 105 else 106 log "Sync failed for $REPO_NAME" 107 return 1 108 fi 109 } 110 111 # Main 112 while read oldrev newrev refname; do 113 log "Received push for ref '$refname' (old revision: $oldrev, new revision: $newrev)" 114 sync_to_github 115 done 116 EOF 117 118 HOOK_TEMPLATE="/tmp/post-receive.template" 119 120 # Create the post-receive hook 121 create_hook() { 122 local new_repo_path="$1" 123 local hook_path="$new_repo_path/hooks/post-receive.d/forward" 124 local nosync_marker="$new_repo_path/.nosync" 125 126 # Skip if .nosync marker exists 127 if [ -f "$nosync_marker" ]; then 128 log "Skipping $new_repo_path (nosync marker present)" 129 return 0 130 fi 131 132 if [ -d "$new_repo_path" ] && [ ! -f "$hook_path" ]; then 133 # Check that it's a git repository, specifically a bare repo 134 if [ -f "$new_repo_path/config" ]; then 135 # Create hooks directory if it doesn't exist 136 mkdir -p "$(dirname "$hook_path")" 137 # Create hook from the template file, substituting variables. 138 if cat "$HOOK_TEMPLATE" > "$hook_path" && chmod +x "$hook_path"; then 139 log "Created hook for $new_repo_path" 140 # Check if repo has any commits before pushing 141 if (cd "$new_repo_path" && ${pkgs.git}/bin/git rev-parse HEAD >/dev/null 2>&1); then 142 # Auto push by simulating a post-receive hook trigger 143 log "Triggering initial push for $new_repo_path" 144 (cd "$new_repo_path" && \ 145 echo "0000000000000000000000000000000000000000 $(${pkgs.git}/bin/git rev-parse HEAD) refs/heads/main" | \ 146 "$hook_path") 147 fi 148 else 149 log "Hook creation failed for $new_repo_path" 150 fi 151 fi 152 fi 153 } 154 155 # Keep track of hooks created 156 hooks_created=0 157 158 # Find all directories that look like bare Git repos without a post-receive hook 159 ${pkgs.findutils}/bin/find "$REPO_DIR" -mindepth 1 -maxdepth 1 -type d \! -name ".*" -print0 | 160 while IFS= read -r -d $'\0' repo_path; do 161 create_hook "$repo_path" 162 if [ $? -eq 0 ]; then 163 hooks_created=$((hooks_created + 1)) 164 fi 165 done 166 167 # Only log completion if hooks were actually created 168 if [ $hooks_created -gt 0 ]; then 169 log "Sync job complete - Created $hooks_created hooks" 170 fi 171 ''; 172 }; 173 }; 174 175 systemd.paths.knot-sync = { 176 description = "Watch for new Knot repositories"; 177 wantedBy = [ "multi-user.target" ]; 178 pathConfig = { 179 PathModified = cfg.repoDir; 180 Unit = "knot-sync.service"; 181 MakeDirectory = true; 182 }; 183 }; 184 }; 185}