This repository has been archived on 2026-06-19. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
kitestacks-homelab-autosync.../scripts/kitestacks-watcher-sh
2026-06-06 04:20:01 -05:00

319 lines
11 KiB
Bash

#!/bin/bash
# =============================================================================
# kitestacks-watcher.sh
# Runs as a systemd service. Watches configured directories, then on change:
# 1. Pulls latest from Forgejo (avoid conflicts)
# 2. Copies changed files into the correct apps/ or clusters/ folder
# 3. Bumps the version in README.md and the docs/ changelog file
# 4. Commits with a descriptive message
# 5. Pushes to active repo (test first, then main after promote.sh)
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/../config/settings.conf"
STATE_FILE="/opt/kitestacks-autosync/.active_target"
WORK_DIR="/opt/kitestacks-autosync"
log() { echo "[autosync] $(date '+%Y-%m-%d %H:%M:%S') INFO $*"; }
warn() { echo "[autosync] $(date '+%Y-%m-%d %H:%M:%S') WARN $*"; }
err() { echo "[autosync] $(date '+%Y-%m-%d %H:%M:%S') ERROR $*" >&2; }
# ── Load config ──────────────────────────────────────────────────────────────
[[ ! -f "$CONFIG_FILE" ]] && { err "Config not found: $CONFIG_FILE"; exit 1; }
source "$CONFIG_FILE"
# ── Helpers ───────────────────────────────────────────────────────────────────
active_target() { cat "$STATE_FILE" 2>/dev/null || echo "test"; }
active_repo() {
[[ "$(active_target)" == "main" ]] && echo "$MAIN_REPO" || echo "$TEST_REPO"
}
repo_dir() { echo "$WORK_DIR/$(active_repo)"; }
# ── Version management ────────────────────────────────────────────────────────
read_version() {
local readme="$(repo_dir)/README.md"
# Look for a line like: <!-- version: 1.3.2 --> or **Version:** 1.3.2
local ver
ver=$(grep -oP '(?<=version:\s)[\d.]+' "$readme" 2>/dev/null | head -1)
[[ -z "$ver" ]] && ver=$(grep -oP '\d+\.\d+\.\d+' "$readme" 2>/dev/null | head -1)
echo "${ver:-$VERSION_SEED}"
}
bump_version() {
local ver="$1"
IFS='.' read -ra p <<< "$ver"
local maj="${p[0]:-1}" min="${p[1]:-3}" pat="${p[2]:-0}"
case "${VERSION_BUMP:-patch}" in
major) echo "$((maj+1)).0.0" ;;
minor) echo "${maj}.$((min+1)).0" ;;
*) echo "${maj}.${min}.$((pat+1))" ;;
esac
}
# ── README.md update ──────────────────────────────────────────────────────────
# Maintains the KiteStacks README.md format with version tracking.
update_readme() {
local dir="$1" new_ver="$2" old_ver="$3"
local readme="$dir/README.md"
local ts; ts="$(date '+%Y-%m-%d %H:%M:%S')"
# If version comment tag exists, update it; otherwise append one after title
if grep -q '<!-- version:' "$readme" 2>/dev/null; then
sed -i "s|<!-- version:.*-->|<!-- version: $new_ver -->|" "$readme"
else
# Inject version tag after the first H1 line
sed -i "0,/^# /{s|^# \(.*\)|# \1\n\n<!-- version: $new_ver -->|}" "$readme"
fi
log "README.md version tag: $old_ver$new_ver"
}
# ── Docs changelog update ─────────────────────────────────────────────────────
# Updates docs/KiteStacks-Homelab-Documentation-v<version>.md
# Creates a new versioned doc file and a CHANGELOG.md entry.
update_docs() {
local dir="$1" new_ver="$2" changed_files="$3"
local ts; ts="$(date '+%Y-%m-%d %H:%M:%S')"
local docs_dir="$dir/docs"
mkdir -p "$docs_dir"
# ── New versioned doc file ────────────────────────────────────────────────
local doc_file="$docs_dir/KiteStacks-Homelab-Documentation-v${new_ver}.md"
cat > "$doc_file" <<EOF
# KiteStacks Homelab Documentation v${new_ver}
**Version:** ${new_ver}
**Updated:** ${ts}
**Previous:** [v${2%.*}.$(( ${new_ver##*.} - 1 )) docs](KiteStacks-Homelab-Documentation-v${2%.*}.$(( ${new_ver##*.} - 1 )).md)
---
## Change Summary
$(echo "$changed_files" | sed 's/^/- /')
---
## Cluster
| Component | Status |
|-----------|--------|
| K3s | Active |
| FluxCD | Planned |
| Longhorn | Planned |
## Applications
| App | Path |
|-----|------|
| Homepage | apps/homepage/ |
| Kavita | apps/kavita-docker-automation/ |
| Linkding | apps/linkding/ |
| Forgejo | apps/forgejo/ |
| Grafana | apps/grafana/ |
| Prometheus | apps/prometheus/ |
| Authentik | apps/authentik/ |
| Open WebUI | apps/open-webui/ |
| LiteLLM | apps/litellm/ |
## All Documentation Versions
$(ls "$docs_dir"/KiteStacks-Homelab-Documentation-v*.md 2>/dev/null \
| sort -V \
| while read f; do
v=$(basename "$f" .md | grep -oP '[\d.]+$')
echo "- [v${v}]($(basename "$f"))"
done)
EOF
log "Created doc file: $(basename "$doc_file")"
# ── CHANGELOG.md ─────────────────────────────────────────────────────────
local changelog="$dir/CHANGELOG.md"
if [[ ! -f "$changelog" ]]; then
echo -e "# Changelog\n\nAll notable changes to KiteStacks Homelab are documented here.\n" > "$changelog"
fi
# Prepend new entry after the header (after line 3)
local entry
entry=$(cat <<EOF
## [v${new_ver}] — ${ts}
### Changed
$(echo "$changed_files" | sed 's/^/- /')
EOF
)
# Insert after line 3 of the changelog
local tmp; tmp=$(mktemp)
head -3 "$changelog" > "$tmp"
echo "$entry" >> "$tmp"
tail -n +4 "$changelog" >> "$tmp"
mv "$tmp" "$changelog"
log "CHANGELOG.md updated with v${new_ver}"
}
# ── Map a filesystem path to a repo subfolder ─────────────────────────────────
# Files from ~/docker/homepage → apps/homepage/
# Files from ~/docker/ → apps/<dirname>/
# Files from /etc/kitestacks/ → clusters/assassin/
map_to_repo_path() {
local src="$1"
local rel=""
for watch in $WATCH_DIRS; do
if [[ "$src" == "$watch"* ]]; then
rel="${src#$watch/}"
# Top-level dir under watch becomes the app folder
local top; top=$(echo "$rel" | cut -d'/' -f1)
if [[ "$watch" == *docker* ]]; then
echo "apps/${rel}"
else
echo "clusters/assassin/${rel}"
fi
return
fi
done
# Fallback
echo "server-files/${src#/}"
}
# ── Sync changed files into the workspace ─────────────────────────────────────
sync_files() {
local repo="$1"; shift
local files=("$@")
local synced=()
for src in "${files[@]}"; do
local dest_rel; dest_rel="$(map_to_repo_path "$src")"
local dest="$repo/$dest_rel"
mkdir -p "$(dirname "$dest")"
if [[ -f "$src" ]]; then
cp -p "$src" "$dest"
synced+=("$dest_rel")
log "Synced: $dest_rel"
else
rm -f "$dest"
synced+=("DELETED: $dest_rel")
log "Removed: $dest_rel"
fi
done
printf '%s\n' "${synced[@]}"
}
# ── Commit and push ───────────────────────────────────────────────────────────
commit_and_push() {
local dir="$1" version="$2" file_count="$3"
cd "$dir"
git add -A
if git diff --cached --quiet; then
log "Nothing to commit."
return 0
fi
local target; target="$(active_target)"
local msg="Automated update: $(date '+%Y-%m-%d %H:%M:%S')"
# Match the commit style already in your repo: "Automated update: YYYY-MM-DD HH:MM:SS"
git commit -m "$msg"
log "Pushing to ${target} repo ($(active_repo))..."
if git push origin HEAD 2>&1; then
log "✓ Push OK — v${version}"
else
err "Push failed. Will retry on next change."
return 1
fi
}
# ── Should this path be skipped? ─────────────────────────────────────────────
is_excluded() {
local path="$1"
for pat in $EXCLUDE_PATTERNS; do
case "$path" in $pat) return 0 ;; esac
done
return 1
}
# ════════════════════════════════════════════════════════════════════════════
# MAIN
# ════════════════════════════════════════════════════════════════════════════
mkdir -p "$WORK_DIR"
log "KiteStacks AutoSync starting up"
log "Active target : $(active_target)$(active_repo)"
log "Watching : $WATCH_DIRS"
log "Debounce : ${DEBOUNCE_SECONDS}s"
# Pull latest before we start watching
RDIR="$(repo_dir)"
if [[ -d "$RDIR/.git" ]]; then
cd "$RDIR"
git pull --rebase origin HEAD 2>/dev/null && log "Pulled latest from remote." || warn "Pull failed — continuing anyway."
fi
declare -A PENDING
LAST_EVENT=0
# Build the inotifywait argument list from WATCH_DIRS
IFS=' ' read -ra WATCH_ARRAY <<< "$WATCH_DIRS"
inotifywait -m -r \
-e modify,create,delete,moved_to,moved_from \
--format '%w%f' \
--exclude '\.git' \
"${WATCH_ARRAY[@]}" 2>/dev/null |
while IFS= read -r changed; do
is_excluded "$changed" && continue
PENDING["$changed"]=1
LAST_EVENT=$(date +%s)
# ── Debounce loop ─────────────────────────────────────────────────────────
while true; do
sleep 1
ELAPSED=$(( $(date +%s) - LAST_EVENT ))
[[ $ELAPSED -ge ${DEBOUNCE_SECONDS:-15} ]] && break
# Drain any additional events that arrived during sleep
while IFS= read -r -t 0.1 extra; do
is_excluded "$extra" && continue
PENDING["$extra"]=1
LAST_EVENT=$(date +%s)
done
done
[[ ${#PENDING[@]} -eq 0 ]] && continue
log "━━ Batch of ${#PENDING[@]} change(s) detected ━━"
RDIR="$(repo_dir)"
OLD_VER="$(read_version)"
NEW_VER="$(bump_version "$OLD_VER")"
# Pull before committing
cd "$RDIR"
git pull --rebase origin HEAD 2>/dev/null || warn "Pre-commit pull failed."
# Sync files and capture the list of repo-relative paths
CHANGED_LIST="$(sync_files "$RDIR" "${!PENDING[@]}")"
update_readme "$RDIR" "$NEW_VER" "$OLD_VER"
update_docs "$RDIR" "$NEW_VER" "$CHANGED_LIST"
commit_and_push "$RDIR" "$NEW_VER" "${#PENDING[@]}"
# Clear batch
unset PENDING
declare -A PENDING
done