{ config, lib, pkgs, ... }: let cfg = config.atelier.services.knot-sync; in { options.atelier.services.knot-sync = { enable = lib.mkEnableOption "Knot to GitHub sync service"; repoDir = lib.mkOption { type = lib.types.str; default = "/home/git/did:plc:krxbvxvis5skq7jj6eot23ul"; description = "Directory containing git repositories"; }; githubUsername = lib.mkOption { type = lib.types.str; default = "taciturnaxolotl"; description = "GitHub username"; }; secretsFile = lib.mkOption { type = lib.types.path; description = "Path to secrets file containing GITHUB_TOKEN"; }; logFile = lib.mkOption { type = lib.types.str; default = "/home/git/knot-sync.log"; description = "Log file location"; }; interval = lib.mkOption { type = lib.types.str; default = "*/5 * * * *"; description = "Cron schedule for sync (default: every 5 minutes)"; }; }; config = lib.mkIf cfg.enable { systemd.services.knot-sync = { description = "Sync Knot repositories to GitHub"; serviceConfig = { Type = "oneshot"; User = "git"; EnvironmentFile = cfg.secretsFile; ExecStart = pkgs.writeShellScript "knot-sync" '' set -euo pipefail # Variables REPO_DIR="${cfg.repoDir}" GITHUB_USERNAME="${cfg.githubUsername}" LOG_FILE="${cfg.logFile}" # Log function log() { echo "$(date +'%Y-%m-%d %H:%M:%S'): $1" >> "$LOG_FILE"; } # Create the post-receive hook template cat <<'EOF' > /tmp/post-receive.template #!${pkgs.bash}/bin/bash # post-receive hook to sync to GitHub - AUTOGENERATED # Load environment variables from secrets file if [ -f "${cfg.secretsFile}" ]; then source "${cfg.secretsFile}" fi # Variables GITHUB_USERNAME="${cfg.githubUsername}" LOG_FILE="${cfg.logFile}" REPO_NAME=$(basename $(pwd)) # Log function log() { echo "$(date +'%Y-%m-%d %H:%M:%S'): $1" >> "''${LOG_FILE}"; } # Check for nosync marker if [ -f "$(pwd)/.nosync" ]; then log "Skipping sync for $REPO_NAME (nosync marker present)" exit 0 fi # Function to sync to GitHub sync_to_github() { log "Syncing $REPO_NAME to GitHub" expected_url="https://''${GITHUB_USERNAME}:''${GITHUB_TOKEN}@github.com/''${GITHUB_USERNAME}/''${REPO_NAME}.git" current_url=$(${pkgs.git}/bin/git remote get-url origin 2>/dev/null || echo "") if [ -z "$current_url" ]; then log "Adding origin remote" ${pkgs.git}/bin/git remote add origin "$expected_url" elif [ "$current_url" != "$expected_url" ]; then log "Updating origin remote URL" ${pkgs.git}/bin/git remote set-url origin "$expected_url" fi # Mirror push everything (refs, tags, branches) if ${pkgs.git}/bin/git push --mirror origin 2>&1 | tee -a "''${LOG_FILE}"; then log "Sync succeeded for $REPO_NAME" return 0 else log "Sync failed for $REPO_NAME" return 1 fi } # Main while read oldrev newrev refname; do log "Received push for ref '$refname' (old revision: $oldrev, new revision: $newrev)" sync_to_github done EOF HOOK_TEMPLATE="/tmp/post-receive.template" # Create the post-receive hook create_hook() { local new_repo_path="$1" local hook_path="$new_repo_path/hooks/post-receive.d/forward" local nosync_marker="$new_repo_path/.nosync" # Skip if .nosync marker exists if [ -f "$nosync_marker" ]; then log "Skipping $new_repo_path (nosync marker present)" return 0 fi if [ -d "$new_repo_path" ] && [ ! -f "$hook_path" ]; then # Check that it's a git repository, specifically a bare repo if [ -f "$new_repo_path/config" ]; then # Create hooks directory if it doesn't exist mkdir -p "$(dirname "$hook_path")" # Create hook from the template file, substituting variables. if cat "$HOOK_TEMPLATE" > "$hook_path" && chmod +x "$hook_path"; then log "Created hook for $new_repo_path" # Check if repo has any commits before pushing if (cd "$new_repo_path" && ${pkgs.git}/bin/git rev-parse HEAD >/dev/null 2>&1); then # Auto push by simulating a post-receive hook trigger log "Triggering initial push for $new_repo_path" (cd "$new_repo_path" && \ echo "0000000000000000000000000000000000000000 $(${pkgs.git}/bin/git rev-parse HEAD) refs/heads/main" | \ "$hook_path") fi else log "Hook creation failed for $new_repo_path" fi fi fi } # Keep track of hooks created hooks_created=0 # Find all directories that look like bare Git repos without a post-receive hook ${pkgs.findutils}/bin/find "$REPO_DIR" -mindepth 1 -maxdepth 1 -type d \! -name ".*" -print0 | while IFS= read -r -d $'\0' repo_path; do create_hook "$repo_path" if [ $? -eq 0 ]; then hooks_created=$((hooks_created + 1)) fi done # Only log completion if hooks were actually created if [ $hooks_created -gt 0 ]; then log "Sync job complete - Created $hooks_created hooks" fi ''; }; }; systemd.paths.knot-sync = { description = "Watch for new Knot repositories"; wantedBy = [ "multi-user.target" ]; pathConfig = { PathModified = cfg.repoDir; Unit = "knot-sync.service"; MakeDirectory = true; }; }; }; }