- Rewrote RUNBOOK.md and DEBUG-DOCUMENTATION.md in simple 5th-grade language with real-world analogies for every technical concept - Updated README.md with current service inventory and folder map - Added cloud-migration/ subdirectory (from kitestacks-cloud-migration repo) - Added autosync/ subdirectory (from kitestacks-homelab-autosync-test repo) - Added osticket/ subdirectory (from OSTicketSystem repo) - Added cloud/ placeholder for future cloud configs - Excluded binary DB/postgres files from autosync subdirectory Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
319 lines
11 KiB
Bash
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
|