Created
July 2, 2026 14:44
-
-
Save EClaesson/0759e780a1b84a462152ab8352a43438 to your computer and use it in GitHub Desktop.
Rootless Podman Git repository reconciler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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