commit eaad237c760efe648c241879758088f3e5a46a0b
parent de158dfad01b94e31ed96cdab01748dda83c5fa2
Author: DAEMON <zer0sec.xp@icloud.com>
Date: Sat, 10 Oct 2026 12:35:58 +0100
feat(git-site): sync git.daemon-sec.xyz automatically
- git-site-sync: auto (sync when a mirrored repo's main moved on GitLab),
now, status, log. One git ls-remote per repo, so checking is cheap; the
build and upload run only on a change. flock keeps runs from overlapping;
a failure raises a desktop notification.
- A systemd user timer runs it every 15 minutes (persistent across sleep).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat:
3 files changed, 160 insertions(+), 0 deletions(-)
diff --git a/modules/home/default.nix b/modules/home/default.nix
@@ -22,6 +22,7 @@
prompt # starship prompt and fastfetch card, Rosé Pine, NixOS logo
fan # `fan`: status/watch without root, max/auto with a clamp watchdog
htb-shell # $TARGET/$BOX in every terminal, and in the prompt
+ git-site # git-site-sync: keep git.daemon-sec.xyz in step with GitLab (15-min timer)
ssh # the ssh key (sops) and ~/.ssh/config
gpg # the GPG main key (public in-repo, secret via sops) and gpg.conf
git # git identity, signing with the main key, delta
diff --git a/modules/home/git-site.nix b/modules/home/git-site.nix
@@ -0,0 +1,68 @@
+# modules/home/git-site.nix — keep git.daemon-sec.xyz in step with GitLab,
+# automatically.
+#
+# git-site-sync status last sync, GitLab's heads, the next timer run
+# git-site-sync now force a sync (after a theme or build change)
+# git-site-sync log what the last runs did
+#
+# A systemd user timer runs `git-site-sync auto` every 15 minutes while you
+# are logged in. It asks GitLab for the head of `main` in each mirrored repo
+# (NixDaemon, daemon-sec-cheatsheet, daemon-sec-lotl — the list is
+# ~/git/daemon-sec/script/git-site/repos.json) and only when one has moved
+# runs `npm run git-site:sync`, which rebuilds the pages and uploads what
+# changed to the daemon-sec-git bucket. Failures raise a desktop notification.
+#
+# It uses what an interactive shell has: the ssh config (the private
+# cheatsheet repo is fetched over SSH with the sops key, see ssh.nix) and
+# wrangler's own login in ~/.config/.wrangler. If wrangler's login ever
+# expires, `npx wrangler login` in ~/git/daemon-sec fixes it.
+{ ... }:
+{
+ flake.homeModules.git-site =
+ { pkgs, lib, ... }:
+ let
+ tool = pkgs.writeShellApplication {
+ name = "git-site-sync";
+ runtimeInputs = with pkgs; [
+ git
+ openssh
+ jq
+ nodejs
+ python3
+ util-linux # flock
+ coreutils
+ diffutils
+ gnused
+ libnotify # notify-send
+ systemd
+ ];
+ text = builtins.readFile ./git-site/git-site-sync.sh;
+ };
+ in
+ {
+ home.packages = [ tool ];
+
+ systemd.user.services.git-site-sync = {
+ Unit = {
+ Description = "Sync git.daemon-sec.xyz from GitLab when a mirrored repo changed";
+ After = [ "network-online.target" ];
+ };
+ Service = {
+ Type = "oneshot";
+ ExecStart = "${lib.getExe tool} auto";
+ Nice = 10;
+ IOSchedulingClass = "idle";
+ };
+ };
+
+ systemd.user.timers.git-site-sync = {
+ Unit.Description = "Check the mirrored repos every 15 minutes";
+ Timer = {
+ OnCalendar = "*:0/15";
+ Persistent = true; # catch up after the laptop was asleep
+ RandomizedDelaySec = "90";
+ };
+ Install.WantedBy = [ "timers.target" ];
+ };
+ };
+}
diff --git a/modules/home/git-site/git-site-sync.sh b/modules/home/git-site/git-site-sync.sh
@@ -0,0 +1,91 @@
+# git-site-sync — keep git.daemon-sec.xyz in step with GitLab.
+#
+# git-site-sync same as `auto`
+# git-site-sync auto sync only if a mirrored repo's main moved (the timer runs this)
+# git-site-sync now sync regardless (after editing the theme or build script)
+# git-site-sync status last sync, what GitLab has now, the timer
+# git-site-sync log the last runs, from the journal
+#
+# The repos are whatever ~/git/daemon-sec/script/git-site/repos.json lists
+# (NixDaemon, daemon-sec-cheatsheet, daemon-sec-lotl). Checking is one
+# `git ls-remote` per repo — a few hundred bytes — so the timer can run
+# often; the build and upload (`npm run git-site:sync`, which itself only
+# sends changed files) happen only when a head actually moved.
+
+SITE_REPO=${GIT_SITE_REPO:-$HOME/git/daemon-sec}
+CONFIG=$SITE_REPO/script/git-site/repos.json
+STATE=${XDG_STATE_HOME:-$HOME/.local/state}/git-site
+mkdir -p "$STATE"
+
+# The npm script calls `nix shell` and `npx`: give a systemd user service the
+# system and per-user profiles that an interactive shell would have.
+export PATH="$PATH:/run/current-system/sw/bin:/etc/profiles/per-user/$USER/bin"
+
+die() { echo "git-site-sync: $*" >&2; exit 1; }
+[ -f "$CONFIG" ] || die "no $CONFIG (set GIT_SITE_REPO if the site repo moved)"
+
+# "<name> <commit>" per repo, from GitLab. Fails loudly if any repo cannot be
+# read, so a network blip never looks like "nothing changed".
+heads() {
+ local name url sha
+ jq -r '.repos[] | "\(.name) \(.fetch)"' "$CONFIG" | while read -r name url; do
+ sha=$(git ls-remote "$url" refs/heads/main | cut -f1) || return 1
+ [ -n "$sha" ] || { echo "git-site-sync: no main on $url" >&2; return 1; }
+ printf '%s %s\n' "$name" "$sha"
+ done
+}
+
+notify() {
+ command -v notify-send >/dev/null 2>&1 && notify-send -a git-site-sync -u "${2:-normal}" "git.daemon-sec.xyz" "$1" 2>/dev/null || true
+}
+
+sync() {
+ local new=$1
+ echo "git-site-sync: syncing…"
+ if (cd "$SITE_REPO" && npm run -s git-site:sync); then
+ printf '%s\n' "$new" > "$STATE/heads"
+ date -Is > "$STATE/last-sync"
+ echo "git-site-sync: done"
+ else
+ notify "Sync failed — git-site-sync log" critical
+ die "sync failed (git-site-sync log for the details)"
+ fi
+}
+
+# One run at a time: the timer and a manual `now` must not interleave uploads.
+exec 9>"$STATE/lock"
+case "${1:-auto}" in
+ auto | now)
+ flock -n 9 || { echo "git-site-sync: another sync is running"; exit 0; }
+ new=$(heads) || die "could not read GitLab (offline?); nothing changed"
+ if [ "${1:-auto}" = auto ] && [ "$new" = "$(cat "$STATE/heads" 2>/dev/null)" ]; then
+ echo "git-site-sync: up to date"
+ exit 0
+ fi
+ if [ -f "$STATE/heads" ]; then
+ diff <(cat "$STATE/heads") <(printf '%s\n' "$new") | sed -n 's/^> / moved: /p' || true
+ fi
+ sync "$new"
+ ;;
+ status)
+ echo "last sync: $(cat "$STATE/last-sync" 2>/dev/null || echo never)"
+ echo "synced heads:"
+ sed 's/^/ /' "$STATE/heads" 2>/dev/null || echo " (none yet)"
+ if new=$(heads); then
+ if [ "$new" = "$(cat "$STATE/heads" 2>/dev/null)" ]; then echo "GitLab: no changes since"; else
+ echo "GitLab now:"; printf '%s\n' "$new" | sed 's/^/ /'; fi
+ fi
+ systemctl --user list-timers git-site-sync.timer --no-pager 2>/dev/null | head -2
+ ;;
+ log)
+ journalctl --user -u git-site-sync.service -n "${2:-60}" --no-pager
+ ;;
+ -h | --help | help)
+ echo "usage: git-site-sync [auto|now|status|log [n]]"
+ echo " auto sync if a mirrored repo's main moved (what the timer runs)"
+ echo " now sync regardless"
+ echo " status last sync, GitLab's heads, the timer"
+ echo " log the last runs from the journal"
+ ;;
+ *) die "unknown command '$1' (auto, now, status, log)" ;;
+esac