@jaspermayone.com's dotfiles
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}