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