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}