Route remaining sudo call sites through hostd-call.sh / hostd:
- scripts/destroy-jails.sh: bastille stop/destroy via hostd-call.sh
- scripts/docs-sync.cron.sh: nginx reload via service-restart op
- scripts/heartbeat.sh: bastille list via hostd-call.sh
- src/startup-report.ts: drop sudo bastille/pkg fallbacks; tighten
buildStartupReport signature now that hostdData is always supplied
Relies on 537c613 (non-interactive bastille-destroy) so the
yes-pipe in destroy-jails.sh is no longer needed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
346 lines
15 KiB
Bash
Executable file
346 lines
15 KiB
Bash
Executable file
#!/bin/sh
|
|
# docs-sync.cron.sh: Documentation sync orchestrator
|
|
#
|
|
# Automated cron job (runs daily @ 05:00 UTC):
|
|
# 1. Pull latest markdown from git
|
|
# 2. Compile markdown → HTML (with .docignore filtering)
|
|
# 3. Validate output
|
|
# 4. Deploy via atomic symlink swap (zero-downtime)
|
|
# 5. Cleanup old versions (30-day retention)
|
|
#
|
|
# Logs:
|
|
# /var/log/clawdie-docs-sync.log — detailed output
|
|
# .sync-metadata.json — deployment metadata
|
|
#
|
|
# Lock file prevents concurrent syncs:
|
|
# <repo>/tmp/clawdie-docs-sync.lock
|
|
|
|
set -e
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Configuration
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
DOCS_DIR="${REPO_DIR}/docs/public"
|
|
DOCS_POLICY="${REPO_DIR}/docs/internal/DOCUMENTATION-POLICY.md"
|
|
SYNC_METADATA="${DOCS_DIR}/.sync-metadata.json"
|
|
COMPILE_SCRIPT="${SCRIPT_DIR}/docs-compile.sh"
|
|
LOG_FILE="${LOG_FILE:-/var/log/clawdie-docs-sync.log}"
|
|
LOCK_FILE="${LOCK_FILE:-${REPO_DIR}/tmp/clawdie-docs-sync.lock}"
|
|
RETENTION_DAYS="${RETENTION_DAYS:-30}"
|
|
LANGUAGES="${LANGUAGES:-sl,en,de,hr,sr,ru}" # Slovenian primary; English source; German, Croatian, Serbian, Russian
|
|
DATE_FORMAT_SCRIPT="${REPO_DIR}/scripts/date-format.sh"
|
|
|
|
# Colors (for manual runs; cron will strip these)
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# Deployment targets (from .sync-metadata.json)
|
|
TARGETS="docs-clawdie-si osa-smilepowered.org"
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Logging & Locking
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg() {
|
|
local msg="$1"
|
|
echo "$(bash "$DATE_FORMAT_SCRIPT" display-ts) [docs-sync] $msg" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
log_err() {
|
|
local msg="$1"
|
|
echo "$(bash "$DATE_FORMAT_SCRIPT" display-ts) ${RED}[ERROR]${NC} $msg" | tee -a "$LOG_FILE" >&2
|
|
}
|
|
|
|
log_ok() {
|
|
local msg="$1"
|
|
echo "$(bash "$DATE_FORMAT_SCRIPT" display-ts) ${GREEN}[OK]${NC} $msg" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
acquire_lock() {
|
|
# Prevent concurrent syncs
|
|
if [ -f "$LOCK_FILE" ]; then
|
|
local lock_age=$(($(date +%s) - $(stat -f%m "$LOCK_FILE" 2>/dev/null || echo 0)))
|
|
if [ "$lock_age" -lt 3600 ]; then
|
|
log_err "Sync already in progress (lock age: ${lock_age}s)"
|
|
exit 1
|
|
else
|
|
log_msg "Stale lock removed (age: ${lock_age}s)"
|
|
rm -f "$LOCK_FILE"
|
|
fi
|
|
fi
|
|
mkdir -p "$(dirname "$LOCK_FILE")"
|
|
touch "$LOCK_FILE"
|
|
trap 'rm -f "$LOCK_FILE"' EXIT
|
|
}
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Initialization
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg "════════════════════════════════════════════════════════════════════════"
|
|
log_msg "Documentation Sync Started"
|
|
log_msg "Repository: $REPO_DIR"
|
|
log_msg "Docs source: $DOCS_DIR"
|
|
|
|
acquire_lock
|
|
|
|
# Verify dependencies
|
|
for cmd in git pandoc jq rsync; do
|
|
command -v "$cmd" >/dev/null 2>&1 || { log_err "$cmd not found"; exit 1; }
|
|
done
|
|
|
|
cd "$REPO_DIR" || exit 1
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Step 1: Pull latest markdown from git
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg "Step 1: Fetching latest changes from git..."
|
|
|
|
if git fetch origin 2>&1 | grep -q "fatal\|error"; then
|
|
log_err "git fetch failed"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if docs/public has changes
|
|
CHANGES=$(git diff HEAD origin/implementation --name-only -- docs/public 2>/dev/null | wc -l || echo 0)
|
|
LAST_COMMIT_LOCAL=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
LAST_COMMIT_REMOTE=$(git rev-parse origin/implementation 2>/dev/null || echo "unknown")
|
|
|
|
if [ "$CHANGES" -gt 0 ]; then
|
|
log_msg "Found $CHANGES changes in docs/public — pulling..."
|
|
git pull origin implementation --ff-only 2>&1 | grep -E "^(Already|Fast-forward|Merge)" || true
|
|
else
|
|
log_msg "No changes in docs/public — skipping pull"
|
|
fi
|
|
|
|
CURRENT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
log_ok "Git sync complete (commit: ${CURRENT_COMMIT:0:7})"
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Step 2: Compile markdown → HTML for each target
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg "Step 2: Compiling markdown to HTML..."
|
|
|
|
# Read targets from .sync-metadata.json
|
|
# For now, hardcode targets; eventually read from metadata
|
|
COMPILE_DIRS=""
|
|
for target in $TARGETS; do
|
|
case "$target" in
|
|
docs-clawdie-si)
|
|
COMPILE_DIRS="$COMPILE_DIRS /usr/local/www/docs.clawdie.si"
|
|
;;
|
|
osa-smilepowered.org)
|
|
COMPILE_DIRS="$COMPILE_DIRS /usr/local/www/osa.smilepowered.org"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
SEMVER=$(git describe --tags --match 'v*' 2>/dev/null | sed 's/^v//' | cut -d'-' -f1 || echo "0.0.0")
|
|
BUILD_DATE=$(bash "$DATE_FORMAT_SCRIPT" display-date | tr 'A-Z' 'a-z')
|
|
|
|
for out_dir in $COMPILE_DIRS; do
|
|
if [ ! -d "$out_dir" ]; then
|
|
log_err "Output directory not found: $out_dir"
|
|
# Non-fatal: skip this target, continue with others
|
|
continue
|
|
fi
|
|
|
|
log_msg " Compiling to: $out_dir"
|
|
|
|
# Compile each language
|
|
for lang in $(echo "$LANGUAGES" | tr ',' ' '); do
|
|
log_msg " Language: $lang"
|
|
|
|
if [ "$lang" = "en" ]; then
|
|
if ! "$COMPILE_SCRIPT" \
|
|
--semver "$SEMVER" \
|
|
--date "$BUILD_DATE" \
|
|
--filter "$DOCS_DIR/.docignore" \
|
|
--source "$DOCS_DIR" \
|
|
"$out_dir" 2>&1 | tee -a "$LOG_FILE"; then
|
|
log_err "Compilation failed for $out_dir (language: $lang)"
|
|
exit 1
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if [ ! -d "$DOCS_DIR/$lang" ]; then
|
|
log_msg " Language directory missing: $DOCS_DIR/$lang (skipping)"
|
|
continue
|
|
fi
|
|
|
|
if ! "$COMPILE_SCRIPT" \
|
|
--semver "$SEMVER" \
|
|
--date "$BUILD_DATE" \
|
|
--filter "$DOCS_DIR/.docignore" \
|
|
--source "$DOCS_DIR" \
|
|
--language "$lang" \
|
|
"$out_dir" 2>&1 | tee -a "$LOG_FILE"; then
|
|
log_err "Compilation failed for $out_dir (language: $lang)"
|
|
exit 1
|
|
fi
|
|
done
|
|
done
|
|
|
|
log_ok "Compilation complete (v${SEMVER}, ${BUILD_DATE})"
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Step 3: Validate compiled output
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg "Step 3: Validating output..."
|
|
|
|
VALIDATION_OK=1
|
|
for out_dir in $COMPILE_DIRS; do
|
|
[ ! -d "$out_dir" ] && continue
|
|
|
|
VERSION_DIR="${out_dir}/docs-v${SEMVER}_${BUILD_DATE}"
|
|
|
|
# Check essential files exist
|
|
[ ! -f "${VERSION_DIR}/index.html" ] && {
|
|
log_err "Missing index.html in $VERSION_DIR"
|
|
VALIDATION_OK=0
|
|
}
|
|
|
|
# Count HTML files
|
|
HTML_COUNT=$(find "$VERSION_DIR" -name "*.html" | wc -l)
|
|
[ "$HTML_COUNT" -lt 3 ] && {
|
|
log_err "Too few HTML files ($HTML_COUNT) in $VERSION_DIR"
|
|
VALIDATION_OK=0
|
|
}
|
|
|
|
log_msg " $out_dir: $HTML_COUNT HTML files ✓"
|
|
done
|
|
|
|
if [ "$VALIDATION_OK" -ne 1 ]; then
|
|
log_err "Validation failed — aborting deployment"
|
|
exit 1
|
|
fi
|
|
|
|
log_ok "Validation passed"
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Step 4: Deploy via atomic symlink swap
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg "Step 4: Deploying with atomic symlink swap..."
|
|
|
|
NGINX_RELOAD_NEEDED=0
|
|
|
|
for out_dir in $COMPILE_DIRS; do
|
|
[ ! -d "$out_dir" ] && continue
|
|
|
|
SYMLINK_NAME="docs-current"
|
|
case "$(basename "$out_dir")" in
|
|
*osa*) SYMLINK_NAME="en-current" ;;
|
|
*) SYMLINK_NAME="docs-current" ;;
|
|
esac
|
|
|
|
VERSION_DIR="docs-v${SEMVER}_${BUILD_DATE}"
|
|
SYMLINK_PATH="${out_dir}/${SYMLINK_NAME}"
|
|
|
|
# Atomic swap (ln -sfn is atomic on modern filesystems)
|
|
if ln -sfn "$VERSION_DIR" "$SYMLINK_PATH" 2>&1; then
|
|
log_ok " Symlink swap: $SYMLINK_NAME → $VERSION_DIR"
|
|
NGINX_RELOAD_NEEDED=1
|
|
else
|
|
log_err "Failed to update symlink: $SYMLINK_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify symlink points to correct directory
|
|
CURRENT=$(readlink "$SYMLINK_PATH" 2>/dev/null || echo "")
|
|
if [ "$CURRENT" = "$VERSION_DIR" ]; then
|
|
log_ok " Verified: $SYMLINK_PATH → $CURRENT"
|
|
else
|
|
log_err "Symlink verification failed: expected $VERSION_DIR, got $CURRENT"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Reload nginx to serve new content
|
|
if [ "$NGINX_RELOAD_NEEDED" -eq 1 ]; then
|
|
log_msg "Reloading nginx..."
|
|
if "${SCRIPT_DIR:-$(dirname "$0")}/hostd-call.sh" service-restart '{"name":"nginx","jail":"cms"}' 2>&1 | tee -a "$LOG_FILE"; then
|
|
log_ok "Nginx reloaded (serving v${SEMVER})"
|
|
else
|
|
log_err "Nginx reload failed"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
log_ok "Deployment complete (zero-downtime swap)"
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Step 5: Cleanup old versions (30-day retention)
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg "Step 5: Cleaning up old versions (retention: ${RETENTION_DAYS} days)..."
|
|
|
|
CLEANUP_COUNT=0
|
|
for out_dir in $COMPILE_DIRS; do
|
|
[ ! -d "$out_dir" ] && continue
|
|
|
|
# Find version directories older than RETENTION_DAYS
|
|
find "$out_dir" -maxdepth 1 -name "docs-v*" -o -name "*-v*" -type d 2>/dev/null | while read old_dir; do
|
|
# Skip current version
|
|
SYMLINK_TARGET=$(readlink "${out_dir}/docs-current" 2>/dev/null || echo "")
|
|
[ "$(basename "$old_dir")" = "$SYMLINK_TARGET" ] && continue
|
|
|
|
# Check age
|
|
AGE_DAYS=$(($(date +%s) - $(stat -f%m "$old_dir" 2>/dev/null)))
|
|
AGE_DAYS=$((AGE_DAYS / 86400))
|
|
|
|
if [ "$AGE_DAYS" -gt "$RETENTION_DAYS" ]; then
|
|
log_msg " Removing $(basename "$old_dir") (${AGE_DAYS} days old)"
|
|
rm -rf "$old_dir"
|
|
CLEANUP_COUNT=$((CLEANUP_COUNT + 1))
|
|
fi
|
|
done
|
|
done
|
|
|
|
log_ok "Cleanup complete ($CLEANUP_COUNT old versions removed)"
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Update metadata
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg "Updating metadata..."
|
|
|
|
NEXT_SYNC=$(date -d "+1 day" -u +"%Y-%m-%dT05:00:00Z" 2>/dev/null || date -u -v+1d +"%Y-%m-%dT05:00:00Z")
|
|
|
|
jq \
|
|
--arg last_sync "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
--arg last_commit "$CURRENT_COMMIT" \
|
|
--arg next_sync "$NEXT_SYNC" \
|
|
'.last_sync = $last_sync | .last_commit = $last_commit | .next_scheduled_sync = $next_sync' \
|
|
"$SYNC_METADATA" > "${SYNC_METADATA}.tmp" && mv "${SYNC_METADATA}.tmp" "$SYNC_METADATA" || true
|
|
|
|
log_ok "Metadata updated"
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Final Summary
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
|
|
log_msg "════════════════════════════════════════════════════════════════════════"
|
|
log_msg "${GREEN}✓ SYNC COMPLETE${NC}"
|
|
log_msg "Version: v${SEMVER}"
|
|
log_msg "Built: ${BUILD_DATE}"
|
|
log_msg "Commit: ${CURRENT_COMMIT:0:7}"
|
|
log_msg "Targets: $TARGETS"
|
|
log_msg "Cleanup: $CLEANUP_COUNT old versions removed"
|
|
log_msg ""
|
|
log_msg "Live URLs:"
|
|
log_msg " 📚 docs.clawdie.si"
|
|
log_msg " 🌐 osa.smilepowered.org"
|
|
log_msg ""
|
|
|
|
exit 0
|