diff --git a/README.md b/README.md index e87bb17..0477f58 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,157 @@ -# KiteStacks Homelab +╔══════════════════════════════════════════════════════════════════╗ +║ KiteStacks Homelab — Auto-Sync Setup Guide ║ +╚══════════════════════════════════════════════════════════════════╝ -Private GitOps repository for the KiteStacks homelab. +WHAT THIS DOES +────────────── +Watches your Docker app directories for any file changes. +On each change it automatically: -## Cluster + 1. Copies the changed file into the correct repo folder + ~/docker/homepage/ → apps/homepage/ + ~/docker// → apps// + /etc/kitestacks/ → clusters/assassin/ -- K3s -- FluxCD (planned) -- Longhorn (planned) + 2. Bumps the version in README.md + -## Applications + 3. Creates a new versioned doc file + docs/KiteStacks-Homelab-Documentation-v1.3.3.md -- Homepage -- Kavita -- Linkding -- Forgejo -- Grafana -- Prometheus -- Authentik -- Open WebUI -- LiteLLM + 4. Appends an entry to CHANGELOG.md -## Documentation + 5. Commits with your existing style: + "Automated update: 2026-06-06 03:36:00" -docs/KiteStacks-Homelab-Documentation-v1.2.md + 6. Pushes to Forgejo at gitforge.kitestacks.com + +It pushes to a TEST repo first so you can verify before +flipping to kenpat/kitestacks-homelab. + + +FILES +───── + setup.sh ← Run first (as root) + promote.sh ← Run to switch test → main repo + status.sh ← Health check + config/ + settings.conf.example + settings.conf ← Your config (fill this in first) + scripts/ + kitestacks-watcher.sh ← The daemon (started by systemd) + + +STEP-BY-STEP +════════════ + +1. GET A FORGEJO TOKEN + gitforge.kitestacks.com → User Settings → Applications + → Generate New Token → name it "autosync" + → Scopes: ✓ repository (read + write) + Copy it — you only see it once. + +2. CONFIGURE + cp config/settings.conf.example config/settings.conf + nano config/settings.conf + + Required changes: + FORGEJO_TOKEN="your-token-here" + WATCH_DIRS="/home/kenpat/docker /etc/kitestacks" + ↑ adjust to match where your app files live + +3. RUN SETUP + sudo bash setup.sh + + This will: + • Install git, inotify-tools, curl, jq + • Create kitestacks-homelab-autosync-test on Forgejo + • Clone it to /opt/kitestacks-autosync/ + • Bootstrap README.md and docs/ at v1.3.0 + • Install and start the systemd service + +4. VERIFY + bash status.sh + journalctl -u kitestacks-autosync -f + + Trigger a test change: + touch /home/kenpat/docker/homepage/test-autosync.txt + + Check gitforge.kitestacks.com/kenpat/kitestacks-homelab-autosync-test + You should see: + • A new commit: "Automated update: 2026-06-06 ..." + • apps/homepage/test-autosync.txt added + • README.md version bumped: 1.3.0 → 1.3.1 + • docs/KiteStacks-Homelab-Documentation-v1.3.1.md created + • CHANGELOG.md entry added + + Clean up the test file: + rm /home/kenpat/docker/homepage/test-autosync.txt + +5. PROMOTE TO MAIN REPO + sudo bash promote.sh + + Type "promote" to confirm. From this point on, all changes + go to kenpat/kitestacks-homelab. + + +HOW THE README.md VERSIONING WORKS +════════════════════════════════════ +The script tracks versions using an HTML comment tag that is +invisible when the README renders: + + # KiteStacks Homelab + + + + Private GitOps repository for the KiteStacks homelab. + ... + +This tag is injected automatically on first run (it won't +change how your README looks in the browser). + +The docs/ reference line is also kept current: + docs/KiteStacks-Homelab-Documentation-v1.3.4.md + + +DIRECTORY MAPPING +═════════════════ +When a file changes, it's placed in the repo like this: + + Server path Repo path + ───────────────────────────────── ───────────────────────────────── + ~/docker/homepage/config/… apps/homepage/config/… + ~/docker/kavita-docker-automation/… apps/kavita-docker-automation/… + /etc/kitestacks/… clusters/assassin/… + + +MANAGING THE SERVICE +════════════════════ + bash status.sh # health check + journalctl -u kitestacks-autosync -f # live logs + sudo systemctl restart kitestacks-autosync # restart + sudo systemctl stop kitestacks-autosync # stop + + +DEBOUNCE +════════ +Default: 15 seconds after the last change before committing. +This means if you save 10 files in 5 seconds, you get ONE commit +not 10. Adjust DEBOUNCE_SECONDS in settings.conf. + + +TROUBLESHOOTING +═══════════════ +Push fails with auth error + → Check FORGEJO_TOKEN is correct in settings.conf + → Re-run: sudo bash setup.sh + +"Config not found" on start + → sudo bash setup.sh (it will copy the example for you) + +Service crashes + → journalctl -u kitestacks-autosync -n 50 + → Check WATCH_DIRS path(s) exist + +Files not appearing in apps/ subfolder + → Check WATCH_DIRS in settings.conf matches your actual + Docker compose directory (e.g. /home/kenpat/docker) diff --git a/promote.sh b/promote.sh new file mode 100644 index 0000000..2ee45ef --- /dev/null +++ b/promote.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# ============================================================================= +# promote.sh +# Flips the watcher from TEST repo → kenpat/kitestacks-homelab (main). +# Run after verifying the test repo looks correct in Forgejo. +# ============================================================================= + +set -e + +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" + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +[[ $EUID -ne 0 ]] && error "Run as root: sudo bash promote.sh" +source "$CONFIG_FILE" + +CURRENT="$(cat "$STATE_FILE" 2>/dev/null || echo "test")" +if [[ "$CURRENT" == "main" ]]; then + warn "Already targeting kenpat/kitestacks-homelab. Nothing to do." + exit 0 +fi + +echo "" +warn "┌──────────────────────────────────────────────────────────────┐" +warn "│ PROMOTE: test repo → kenpat/kitestacks-homelab │" +warn "│ │" +warn "│ From: ${FORGEJO_URL}/${FORGEJO_USER}/${TEST_REPO}" +warn "│ To : ${FORGEJO_URL}/${FORGEJO_USER}/${MAIN_REPO}" +warn "│ │" +warn "│ All future auto-commits will go to the MAIN repo. │" +warn "└──────────────────────────────────────────────────────────────┘" +echo "" +read -rp "Have you verified the test repo looks correct? Type 'promote' to confirm: " CONFIRM +[[ "$CONFIRM" != "promote" ]] && { info "Aborted."; exit 0; } + +# ── Set up remote prefix ────────────────────────────────────────────────────── +if [[ "$AUTH_METHOD" == "ssh" ]]; then + REMOTE_BASE="git@gitforge.kitestacks.com:${FORGEJO_USER}" +else + REMOTE_BASE="https://gitforge.kitestacks.com/${FORGEJO_USER}" +fi + +# ── Clone main repo if not already present ──────────────────────────────────── +MAIN_DIR="$WORK_DIR/$MAIN_REPO" +if [[ ! -d "$MAIN_DIR/.git" ]]; then + info "Cloning kenpat/kitestacks-homelab..." + git clone "${REMOTE_BASE}/${MAIN_REPO}.git" "$MAIN_DIR" +fi + +cd "$MAIN_DIR" +git config user.email "$GIT_EMAIL" +git config user.name "$GIT_NAME" +git pull --rebase origin HEAD 2>/dev/null || true + +# ── Add version tag to main README.md if not already there ─────────────────── +if ! grep -q '|}" "$MAIN_DIR/README.md" + + # Update the docs reference line + sed -i "s|docs/KiteStacks-Homelab-Documentation-v.*\.md|docs/KiteStacks-Homelab-Documentation-v${TEST_VER}.md|" "$MAIN_DIR/README.md" || true + + # Copy CHANGELOG.md and docs from test repo if main doesn't have them + if [[ ! -f "$MAIN_DIR/CHANGELOG.md" ]]; then + cp "$WORK_DIR/$TEST_REPO/CHANGELOG.md" "$MAIN_DIR/CHANGELOG.md" 2>/dev/null || true + fi + if [[ ! -d "$MAIN_DIR/docs" ]]; then + cp -r "$WORK_DIR/$TEST_REPO/docs" "$MAIN_DIR/docs" 2>/dev/null || true + fi + + git add -A + git commit -m "chore: inject autosync version tracking — promoted from test" + git push origin HEAD + info "Main repo prepared for autosync." +fi + +# ── Flip state ──────────────────────────────────────────────────────────────── +echo "main" > "$STATE_FILE" +info "Active target → main (kenpat/kitestacks-homelab)" + +# ── Restart service ─────────────────────────────────────────────────────────── +info "Restarting kitestacks-autosync service..." +systemctl restart kitestacks-autosync +sleep 2 +systemctl status kitestacks-autosync --no-pager + +echo "" +echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +info "Promotion complete!" +info " Now watching → ${FORGEJO_URL}/${FORGEJO_USER}/${MAIN_REPO}" +echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" diff --git a/scripts/kitestacks-watcher-sh b/scripts/kitestacks-watcher-sh new file mode 100644 index 0000000..16c7e27 --- /dev/null +++ b/scripts/kitestacks-watcher-sh @@ -0,0 +1,319 @@ +#!/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: 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 '||" "$readme" + else + # Inject version tag after the first H1 line + sed -i "0,/^# /{s|^# \(.*\)|# \1\n\n|}" "$readme" + fi + + log "README.md version tag: $old_ver → $new_ver" +} + +# ── Docs changelog update ───────────────────────────────────────────────────── +# Updates docs/KiteStacks-Homelab-Documentation-v.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" </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 < "$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// +# 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 diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..16a1fd0 --- /dev/null +++ b/setup.sh @@ -0,0 +1,244 @@ +#!/bin/bash +# ============================================================================= +# KiteStacks HomelaB — Auto-Sync Setup +# Run once as root: sudo bash setup.sh +# ============================================================================= + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="$SCRIPT_DIR/config/settings.conf" +WORK_DIR="/opt/kitestacks-autosync" +STATE_FILE="$WORK_DIR/.active_target" + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } +step() { echo -e "\n${CYAN}══ $1 ══${NC}"; } + +[[ $EUID -ne 0 ]] && error "Run as root: sudo bash setup.sh" + +# ── Config check ───────────────────────────────────────────────────────────── +if [[ ! -f "$CONFIG_FILE" ]]; then + info "Copying example config..." + cp "$SCRIPT_DIR/config/settings.conf.example" "$CONFIG_FILE" + warn "Please edit $CONFIG_FILE and set FORGEJO_TOKEN and WATCH_DIRS, then re-run." + exit 0 +fi + +source "$CONFIG_FILE" + +[[ "$FORGEJO_TOKEN" == "PASTE_YOUR_TOKEN_HERE" ]] && \ + error "You haven't set FORGEJO_TOKEN in config/settings.conf yet." +[[ -z "$FORGEJO_TOKEN" ]] && error "FORGEJO_TOKEN is empty in config/settings.conf." + +# ── Dependencies ───────────────────────────────────────────────────────────── +step "Installing dependencies" +apt-get update -qq +apt-get install -y -qq git inotify-tools curl jq +info "Dependencies installed." + +# ── Configure git credentials ───────────────────────────────────────────────── +step "Configuring git credentials" +if [[ "$AUTH_METHOD" == "ssh" ]]; then + if [[ ! -f "$SSH_KEY_PATH" ]]; then + info "Generating SSH key at $SSH_KEY_PATH..." + ssh-keygen -t ed25519 -C "kitestacks-autosync@$(hostname)" -f "$SSH_KEY_PATH" -N "" + echo "" + warn "Add this public key to Forgejo:" + warn " gitforge.kitestacks.com → User Settings → SSH / GPG Keys → Add Key" + echo "────────────────────────────────────────────────────────" + cat "${SSH_KEY_PATH}.pub" + echo "────────────────────────────────────────────────────────" + read -rp "Press Enter after adding the key to Forgejo..." + fi + REMOTE_BASE="git@gitforge.kitestacks.com:${FORGEJO_USER}" +else + # HTTPS token auth + git config --global credential.helper store + # Write credential (idempotent — grep prevents duplicates) + local CRED_LINE="https://${FORGEJO_USER}:${FORGEJO_TOKEN}@gitforge.kitestacks.com" + grep -qF "$CRED_LINE" ~/.git-credentials 2>/dev/null || echo "$CRED_LINE" >> ~/.git-credentials + REMOTE_BASE="https://gitforge.kitestacks.com/${FORGEJO_USER}" +fi +info "Credentials configured (method: $AUTH_METHOD)" + +# ── Verify Forgejo API ──────────────────────────────────────────────────────── +step "Verifying Forgejo connectivity" +HTTP=$(curl -s -o /dev/null -w "%{http_code}" --max-time 8 \ + -H "Authorization: token $FORGEJO_TOKEN" \ + "${FORGEJO_URL}/api/v1/user") +[[ "$HTTP" != "200" ]] && error "Forgejo API returned HTTP $HTTP — check FORGEJO_URL and FORGEJO_TOKEN." +info "Forgejo API reachable ✓" + +# ── Create test repo on Forgejo if needed ───────────────────────────────────── +step "Setting up test repo: $TEST_REPO" +HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token $FORGEJO_TOKEN" \ + "${FORGEJO_URL}/api/v1/repos/${FORGEJO_USER}/${TEST_REPO}") + +if [[ "$HTTP" == "404" ]]; then + info "Creating test repo on Forgejo..." + RESULT=$(curl -s -X POST \ + -H "Authorization: token $FORGEJO_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"${TEST_REPO}\", + \"description\": \"AutoSync test — KiteStacks Homelab\", + \"private\": true, + \"auto_init\": true, + \"default_branch\": \"main\" + }" \ + "${FORGEJO_URL}/api/v1/user/repos") + URL=$(echo "$RESULT" | jq -r '.html_url // empty') + [[ -n "$URL" ]] && info "Test repo created: $URL" || warn "Repo creation response was unexpected — check Forgejo." +else + info "Test repo already exists." +fi + +# ── Clone repos ─────────────────────────────────────────────────────────────── +step "Cloning repos into $WORK_DIR" +mkdir -p "$WORK_DIR" + +# Test repo +TEST_DIR="$WORK_DIR/$TEST_REPO" +if [[ ! -d "$TEST_DIR/.git" ]]; then + info "Cloning test repo..." + git clone "${REMOTE_BASE}/${TEST_REPO}.git" "$TEST_DIR" +else + info "Test repo already cloned." +fi + +cd "$TEST_DIR" +git config user.email "$GIT_EMAIL" +git config user.name "$GIT_NAME" + +# ── Bootstrap README.md from main repo content ─────────────────────────────── +step "Bootstrapping README.md in test repo" +if [[ ! -f "$TEST_DIR/README.md" ]] || ! grep -q 'version:' "$TEST_DIR/README.md" 2>/dev/null; then + cat > "$TEST_DIR/README.md" <<'EOF' +# KiteStacks Homelab + + + +Private GitOps repository for the KiteStacks homelab. + +## Cluster + +- K3s +- FluxCD (planned) +- Longhorn (planned) + +## Applications + +- Homepage +- Kavita +- Linkding +- Forgejo +- Grafana +- Prometheus +- Authentik +- Open WebUI +- LiteLLM + +## Documentation + +docs/KiteStacks-Homelab-Documentation-v1.3.0.md +EOF + + mkdir -p "$TEST_DIR/docs" + cat > "$TEST_DIR/docs/KiteStacks-Homelab-Documentation-v1.3.0.md" <<'EOF' +# KiteStacks Homelab Documentation v1.3.0 + +**Version:** 1.3.0 +**Updated:** Initial autosync bootstrap + +--- + +## 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/ | +EOF + + cat > "$TEST_DIR/CHANGELOG.md" <<'EOF' +# Changelog + +All notable changes to KiteStacks Homelab are documented here. + +## [v1.3.0] — Initial autosync bootstrap + +- Automated sync system installed +EOF + + git add -A + git commit -m "chore: bootstrap autosync README and docs v1.3.0" + git push origin HEAD + info "Test repo bootstrapped with v1.3.0 content." +else + info "README.md already has version tag — skipping bootstrap." +fi + +# ── Set initial state to test ───────────────────────────────────────────────── +echo "test" > "$STATE_FILE" + +# ── Install systemd service ─────────────────────────────────────────────────── +step "Installing systemd service" +cat > /etc/systemd/system/kitestacks-autosync.service </dev/null || { echo "Config not found at $CONFIG_FILE"; exit 1; } + +echo -e "\n${BOLD}╔══ KiteStacks AutoSync Status ════════════════════════════════╗${NC}" + +# ── Service ─────────────────────────────────────────────────────────────────── +echo -e "\n${BOLD}Service${NC}" +if systemctl is-active --quiet kitestacks-autosync 2>/dev/null; then + ok "kitestacks-autosync is running" +else + fail "kitestacks-autosync is NOT running" + echo " → sudo systemctl start kitestacks-autosync" +fi +systemctl is-enabled --quiet kitestacks-autosync 2>/dev/null \ + && ok "Enabled at boot" || info "Not enabled at boot" + +# ── Active target ───────────────────────────────────────────────────────────── +echo -e "\n${BOLD}Active Target${NC}" +TARGET="$(cat "$STATE_FILE" 2>/dev/null || echo "test")" +if [[ "$TARGET" == "main" ]]; then + ok "→ MAIN: ${FORGEJO_URL}/${FORGEJO_USER}/${MAIN_REPO}" +else + info "→ TEST: ${FORGEJO_URL}/${FORGEJO_USER}/${TEST_REPO}" + info " (run 'sudo bash promote.sh' when you're happy with the test repo)" +fi + +# ── Workspace ───────────────────────────────────────────────────────────────── +echo -e "\n${BOLD}Workspace${NC}" +RNAME="$([[ "$TARGET" == "main" ]] && echo "$MAIN_REPO" || echo "$TEST_REPO")" +RDIR="$WORK_DIR/$RNAME" + +if [[ -d "$RDIR/.git" ]]; then + ok "Cloned: $RDIR" + VER=$(grep -oP '(?<=version:\s)[\d.]+' "$RDIR/README.md" 2>/dev/null | head -1 || echo "unknown") + info "Current version : $VER" + LAST=$(cd "$RDIR" && git log -1 --format="%h %s (%ar)" 2>/dev/null) + info "Last commit : $LAST" + DOC_COUNT=$(ls "$RDIR/docs/"KiteStacks-Homelab-Documentation-v*.md 2>/dev/null | wc -l) + info "Doc versions : $DOC_COUNT file(s) in docs/" +else + fail "Workspace not found — run setup.sh first" +fi + +# ── Forgejo API ─────────────────────────────────────────────────────────────── +echo -e "\n${BOLD}Forgejo Connectivity${NC}" +HTTP=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \ + -H "Authorization: token $FORGEJO_TOKEN" \ + "${FORGEJO_URL}/api/v1/user" 2>/dev/null) +[[ "$HTTP" == "200" ]] && ok "API reachable ($FORGEJO_URL)" || fail "API returned HTTP $HTTP" + +# ── Watch dirs ──────────────────────────────────────────────────────────────── +echo -e "\n${BOLD}Watched Directories${NC}" +for d in $WATCH_DIRS; do + if [[ -d "$d" ]]; then + COUNT=$(find "$d" -type f 2>/dev/null | wc -l) + ok "$d ($COUNT files)" + else + fail "$d (does not exist)" + fi +done + +# ── Recent logs ─────────────────────────────────────────────────────────────── +echo -e "\n${BOLD}Recent Log Entries (last 10)${NC}" +journalctl -u kitestacks-autosync -n 10 --no-pager -q 2>/dev/null \ + | sed 's/^/ /' \ + || info "No journal entries yet." + +echo -e "\n${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}" +echo -e " Live logs: ${CYAN}journalctl -u kitestacks-autosync -f${NC}\n"