Skip to content

Instantly share code, notes, and snippets.

@EClaesson
Created July 2, 2026 14:44
Show Gist options
  • Select an option

  • Save EClaesson/0759e780a1b84a462152ab8352a43438 to your computer and use it in GitHub Desktop.

Select an option

Save EClaesson/0759e780a1b84a462152ab8352a43438 to your computer and use it in GitHub Desktop.
Rootless Podman Git repository reconciler
# update_infra.sh — pull-based GitOps reconciler for a single-user, rootless
# systemd/Quadlet setup. Syncs a git repo to the filesystem and reconciles the
# affected `systemd --user` units. Intended to run on a schedule (cron/timer).
#
# Behaviour:
# - Repo lives at ~/infra and tracks origin/main. Each run fetches; if
# origin/main advanced it does `git reset --hard origin/main` (local repo
# changes are discarded) and applies only the diff since the last run.
# - Path mapping — ONLY these two prefixes are synced, everything else ignored:
# <user>/... -> ~/... (e.g. <user>/.config/foo -> ~/.config/foo)
# data/<user>/... -> /data/<user>/...
# - Adds/modifies/renames are copied (perms preserved via `cp -a`); deletes removed.
#
# systemd reconciliation (units under ~/.config/...):
# - Quadlet *.container -> (re)starts <name>.service
# - *.timer -> enable + (re)start; removal stops + disables
# - *.service WITHOUT a sibling .timer -> enable + (re)start
# (a .service that has a matching .timer is left for the timer to trigger)
# - .config/services/<name>/... -> restarts <name>.service
# - `daemon-reload` runs once if any unit file changed.
#
# Usage:
# ./update_infra.sh # no-op unless origin/main has new commits
# ./update_infra.sh --force # re-apply since last applied rev even with no new commits
#!/usr/bin/env bash
set -euo pipefail
script_owner_uid=$(stat -c '%u' "${BASH_SOURCE[0]}")
if [ "$(id -u)" != "$script_owner_uid" ]; then
echo "This script must be run by its owner." >&2
exit 1
fi
REPO_DIR="$HOME/infra"
cd "$REPO_DIR"
OLD_REV=$(git rev-parse HEAD)
git fetch --quiet origin
NEW_REV=$(git rev-parse origin/main)
if [ "$OLD_REV" = "$NEW_REV" ]; then
if [[ "${1:-}" != "--force" ]]; then
exit 0
fi
echo "Forcing rerun"
if [ -r .last_applied_rev ] && git cat-file -e "$(cat .last_applied_rev)^{commit}" 2>/dev/null; then
OLD_REV=$(cat .last_applied_rev)
else
OLD_REV=$(git rev-parse HEAD~1)
fi
fi
echo "Updating infra $OLD_REV -> $NEW_REV"
git reset --hard origin/main
map_path() {
local f="$1"
case "$f" in
"$USER"/*) echo "$HOME/${f#"$USER"/}" ;;
data/"$USER"/*) echo "/$f" ;;
*) echo "" ;;
esac
}
has_matching_timer() {
local svc_path="$1"
local timer_path="${svc_path%.service}.timer"
[ -f "$REPO_DIR/$timer_path" ]
}
reload_systemd=0
restart_services=()
stop_services=()
enable_units=()
disable_units=()
while IFS=$'\t' read -r status src dst; do
case "$status" in
A|M|C)
target=$(map_path "$src")
[ -z "$target" ] && continue
echo "+ $src -> $target"
mkdir -p "$(dirname "$target")"
cp -a "$REPO_DIR/$src" "$target"
affected="$src"
;;
D)
target=$(map_path "$src")
[ -z "$target" ] && continue
echo "- $target"
rm -f "$target"
affected="$src"
;;
R*)
old_target=$(map_path "$src")
new_target=$(map_path "$dst")
[ -n "$old_target" ] && rm -f "$old_target"
if [ -n "$new_target" ]; then
echo "~ $old_target -> $new_target"
mkdir -p "$(dirname "$new_target")"
cp -a "$REPO_DIR/$dst" "$new_target"
fi
affected="$dst"
;;
*)
continue
;;
esac
[ -z "$(map_path "${dst:-$src}")" ] && continue
case "$affected" in
"$USER"/.config/containers/systemd/*|"$USER"/.config/systemd/user/*)
reload_systemd=1
;;
esac
if [ "$status" = "D" ]; then
case "$affected" in
"$USER"/.config/containers/systemd/*.container)
stop_services+=("$(basename "$affected" .container).service")
;;
"$USER"/.config/systemd/user/*.timer)
unit="$(basename "$affected")"
stop_services+=("$unit")
disable_units+=("$unit")
;;
"$USER"/.config/systemd/user/*.service)
if ! has_matching_timer "$affected"; then
unit="$(basename "$affected")"
stop_services+=("$unit")
disable_units+=("$unit")
fi
;;
esac
continue
fi
case "$affected" in
"$USER"/.config/containers/systemd/*.container)
restart_services+=("$(basename "$affected" .container).service")
;;
"$USER"/.config/systemd/user/*.timer)
unit="$(basename "$affected")"
enable_units+=("$unit")
restart_services+=("$unit")
;;
"$USER"/.config/systemd/user/*.service)
if ! has_matching_timer "$affected"; then
unit="$(basename "$affected")"
enable_units+=("$unit")
restart_services+=("$unit")
fi
;;
"$USER"/.config/services/*)
svc_name="${affected#"$USER"/.config/services/}"
svc_name="${svc_name%%/*}"
restart_services+=("${svc_name}.service")
;;
esac
done < <(git diff --name-status "$OLD_REV" "$NEW_REV")
if [ ${#stop_services[@]} -gt 0 ]; then
printf '%s\n' "${stop_services[@]}" | sort -u | while read -r svc; do
if systemctl --user is-active --quiet "$svc"; then
echo "Stopping $svc"
systemctl --user stop "$svc"
fi
done
fi
if [ ${#disable_units[@]} -gt 0 ]; then
printf '%s\n' "${disable_units[@]}" | sort -u | while read -r unit; do
if systemctl --user is-enabled --quiet "$unit" 2>/dev/null; then
echo "Disabling $unit"
systemctl --user disable "$unit"
fi
done
fi
if [ "$reload_systemd" = 1 ]; then
echo "Reloading daemon"
systemctl --user daemon-reload
fi
if [ ${#enable_units[@]} -gt 0 ]; then
printf '%s\n' "${enable_units[@]}" | sort -u | while read -r unit; do
if ! systemctl --user is-enabled --quiet "$unit" 2>/dev/null; then
echo "Enabling $unit"
systemctl --user enable "$unit"
fi
done
fi
if [ ${#restart_services[@]} -gt 0 ]; then
printf '%s\n' "${restart_services[@]}" | sort -u | while read -r svc; do
echo "(Re)starting $svc"
systemctl --user restart "$svc"
done
fi
echo -n "$OLD_REV" > .last_applied_rev
echo "Done"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment