Add browser-based system upgrade UI with file-based IPC
API container writes trigger files to a shared volume (data/upgrade/), and a systemd path watcher on the host detects them and runs the upgrade scripts. This avoids giving the container Docker socket access. - Add upgrade-check.sh (git fetch + compare + write status.json) - Add upgrade-watcher.sh (systemd bridge, dispatches check/upgrade) - Add systemd path/service units with placeholder substitution - Modify upgrade.sh with --api-mode flag (progress.json + result.json) - Add API upgrade module (service + routes, SUPER_ADMIN only) - Add System tab to Settings page with version info, changelog, progress steps, and upgrade confirmation modal - Add upgrade watcher installation to config.sh wizard - Add data/upgrade/ shared volume to api service in docker-compose Bunker Admin
This commit is contained in:
10
scripts/systemd/changemaker-upgrade.path
Normal file
10
scripts/systemd/changemaker-upgrade.path
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Watch for Changemaker Lite upgrade triggers
|
||||
Documentation=https://docs.cmlite.org/docs/admin/services/
|
||||
|
||||
[Path]
|
||||
PathExists=__PROJECT_DIR__/data/upgrade/trigger.json
|
||||
MakeDirectory=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
13
scripts/systemd/changemaker-upgrade.service
Normal file
13
scripts/systemd/changemaker-upgrade.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Changemaker Lite upgrade dispatcher
|
||||
Documentation=https://docs.cmlite.org/docs/admin/services/
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=__USER__
|
||||
Group=__USER__
|
||||
WorkingDirectory=__PROJECT_DIR__
|
||||
ExecStart=__PROJECT_DIR__/scripts/upgrade-watcher.sh
|
||||
TimeoutStartSec=900
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
105
scripts/upgrade-check.sh
Executable file
105
scripts/upgrade-check.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Changemaker Lite V2 — Upgrade Check Script
|
||||
# Checks for available updates and writes status to data/upgrade/status.json.
|
||||
# Safe to run via cron or on-demand via file trigger.
|
||||
# Usage: ./scripts/upgrade-check.sh [--branch BRANCH]
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
UPGRADE_DIR="${PROJECT_DIR}/data/upgrade"
|
||||
STATUS_FILE="${UPGRADE_DIR}/status.json"
|
||||
BRANCH=""
|
||||
|
||||
# --- Parse Arguments ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--branch) BRANCH="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
mkdir -p "$UPGRADE_DIR"
|
||||
|
||||
# Determine branch
|
||||
if [[ -z "$BRANCH" ]]; then
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
fi
|
||||
|
||||
# Write an error status and exit
|
||||
write_error() {
|
||||
local msg="$1"
|
||||
cat > "$STATUS_FILE" <<EOF
|
||||
{
|
||||
"branch": "${BRANCH}",
|
||||
"currentCommit": "$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")",
|
||||
"currentCommitFull": "$(git rev-parse HEAD 2>/dev/null || echo "unknown")",
|
||||
"currentMessage": "$(git log -1 --format='%s' HEAD 2>/dev/null | sed 's/"/\\"/g' || echo "")",
|
||||
"currentDate": "$(git log -1 --format='%aI' HEAD 2>/dev/null || echo "")",
|
||||
"remoteCommit": null,
|
||||
"commitsBehind": 0,
|
||||
"changelog": [],
|
||||
"checkedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"error": "${msg}"
|
||||
}
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Fetch latest from remote
|
||||
if ! timeout 30 git fetch origin "$BRANCH" 2>/dev/null; then
|
||||
write_error "Failed to reach git remote"
|
||||
fi
|
||||
|
||||
# Gather info
|
||||
CURRENT_COMMIT="$(git rev-parse HEAD)"
|
||||
CURRENT_SHORT="$(git rev-parse --short HEAD)"
|
||||
CURRENT_MSG="$(git log -1 --format='%s' HEAD | sed 's/"/\\"/g')"
|
||||
CURRENT_DATE="$(git log -1 --format='%aI' HEAD)"
|
||||
REMOTE_COMMIT="$(git rev-parse "origin/${BRANCH}" 2>/dev/null || echo "")"
|
||||
REMOTE_SHORT="$(git rev-parse --short "origin/${BRANCH}" 2>/dev/null || echo "")"
|
||||
|
||||
if [[ -z "$REMOTE_COMMIT" ]]; then
|
||||
write_error "Remote branch origin/${BRANCH} not found"
|
||||
fi
|
||||
|
||||
# Count commits behind
|
||||
COMMITS_BEHIND=0
|
||||
if [[ "$CURRENT_COMMIT" != "$REMOTE_COMMIT" ]]; then
|
||||
COMMITS_BEHIND="$(git rev-list --count HEAD..origin/"${BRANCH}" 2>/dev/null || echo "0")"
|
||||
fi
|
||||
|
||||
# Build changelog (last 30 commits we're behind)
|
||||
CHANGELOG="[]"
|
||||
if [[ "$COMMITS_BEHIND" -gt 0 ]]; then
|
||||
CHANGELOG="$(git log --oneline --format='{"hash":"%h","message":"%s","date":"%aI","author":"%an"}' HEAD..origin/"${BRANCH}" 2>/dev/null | head -30 | while IFS= read -r line; do
|
||||
# Escape any double quotes in the message that aren't already escaped
|
||||
echo "$line"
|
||||
done | paste -sd ',' | sed 's/^/[/' | sed 's/$/]/')"
|
||||
# Fallback if jq-less approach fails
|
||||
if [[ -z "$CHANGELOG" ]] || [[ "$CHANGELOG" == "[]" ]]; then
|
||||
CHANGELOG="[]"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write status
|
||||
cat > "$STATUS_FILE" <<EOF
|
||||
{
|
||||
"branch": "${BRANCH}",
|
||||
"currentCommit": "${CURRENT_SHORT}",
|
||||
"currentCommitFull": "${CURRENT_COMMIT}",
|
||||
"currentMessage": "${CURRENT_MSG}",
|
||||
"currentDate": "${CURRENT_DATE}",
|
||||
"remoteCommit": "${REMOTE_SHORT}",
|
||||
"remoteCommitFull": "${REMOTE_COMMIT}",
|
||||
"commitsBehind": ${COMMITS_BEHIND},
|
||||
"changelog": ${CHANGELOG},
|
||||
"checkedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"error": null
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Update check complete: ${COMMITS_BEHIND} commit(s) behind on ${BRANCH}"
|
||||
88
scripts/upgrade-watcher.sh
Executable file
88
scripts/upgrade-watcher.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Changemaker Lite V2 — Upgrade Watcher (systemd bridge)
|
||||
# Called by systemd path unit when data/upgrade/trigger.json is created.
|
||||
# Reads the trigger, dispatches to the appropriate script, cleans up.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
UPGRADE_DIR="${PROJECT_DIR}/data/upgrade"
|
||||
TRIGGER_FILE="${UPGRADE_DIR}/trigger.json"
|
||||
LOG_DIR="${PROJECT_DIR}/logs"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "${LOG_DIR}/upgrade-watcher.log"
|
||||
}
|
||||
|
||||
# Bail if no trigger file
|
||||
if [[ ! -f "$TRIGGER_FILE" ]]; then
|
||||
log "No trigger file found, exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Read trigger (minimal JSON parsing with grep/sed — no jq dependency)
|
||||
TRIGGER_CONTENT="$(cat "$TRIGGER_FILE")"
|
||||
ACTION="$(echo "$TRIGGER_CONTENT" | grep -o '"action"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"action"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//')"
|
||||
|
||||
if [[ -z "$ACTION" ]]; then
|
||||
log "ERROR: Could not parse action from trigger file"
|
||||
rm -f "$TRIGGER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Received trigger: action=${ACTION}"
|
||||
|
||||
# Remove trigger immediately to prevent re-execution
|
||||
rm -f "$TRIGGER_FILE"
|
||||
|
||||
case "$ACTION" in
|
||||
check)
|
||||
log "Running update check..."
|
||||
# Extract optional branch
|
||||
BRANCH="$(echo "$TRIGGER_CONTENT" | grep -o '"branch"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//' || true)"
|
||||
ARGS=()
|
||||
if [[ -n "$BRANCH" ]]; then
|
||||
ARGS+=(--branch "$BRANCH")
|
||||
fi
|
||||
"$SCRIPT_DIR/upgrade-check.sh" "${ARGS[@]}" 2>&1 | tee -a "${LOG_DIR}/upgrade-watcher.log"
|
||||
log "Update check complete."
|
||||
;;
|
||||
|
||||
upgrade)
|
||||
log "Running upgrade..."
|
||||
# Parse options from trigger
|
||||
ARGS=(--api-mode)
|
||||
|
||||
SKIP_BACKUP="$(echo "$TRIGGER_CONTENT" | grep -o '"skipBackup"[[:space:]]*:[[:space:]]*true' || true)"
|
||||
if [[ -n "$SKIP_BACKUP" ]]; then
|
||||
ARGS+=(--skip-backup --force)
|
||||
fi
|
||||
|
||||
PULL_SERVICES="$(echo "$TRIGGER_CONTENT" | grep -o '"pullServices"[[:space:]]*:[[:space:]]*true' || true)"
|
||||
if [[ -n "$PULL_SERVICES" ]]; then
|
||||
ARGS+=(--pull-services)
|
||||
fi
|
||||
|
||||
DRY_RUN="$(echo "$TRIGGER_CONTENT" | grep -o '"dryRun"[[:space:]]*:[[:space:]]*true' || true)"
|
||||
if [[ -n "$DRY_RUN" ]]; then
|
||||
ARGS+=(--dry-run)
|
||||
fi
|
||||
|
||||
BRANCH="$(echo "$TRIGGER_CONTENT" | grep -o '"branch"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//' || true)"
|
||||
if [[ -n "$BRANCH" ]]; then
|
||||
ARGS+=(--branch "$BRANCH")
|
||||
fi
|
||||
|
||||
"$SCRIPT_DIR/upgrade.sh" "${ARGS[@]}" 2>&1 | tee -a "${LOG_DIR}/upgrade-watcher.log"
|
||||
log "Upgrade complete."
|
||||
;;
|
||||
|
||||
*)
|
||||
log "ERROR: Unknown action '${ACTION}'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -46,6 +46,7 @@ DRY_RUN=false
|
||||
FORCE=false
|
||||
BRANCH=""
|
||||
ROLLBACK=false
|
||||
API_MODE=false
|
||||
|
||||
# --- Colors (respects NO_COLOR convention) ---
|
||||
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
|
||||
@@ -73,6 +74,52 @@ phase() {
|
||||
echo ""
|
||||
}
|
||||
|
||||
# --- API mode: JSON progress/result writing ---
|
||||
UPGRADE_DIR="${PROJECT_DIR}/data/upgrade"
|
||||
PROGRESS_FILE="${UPGRADE_DIR}/progress.json"
|
||||
RESULT_FILE="${UPGRADE_DIR}/result.json"
|
||||
|
||||
write_progress() {
|
||||
[[ "$API_MODE" != "true" ]] && return
|
||||
local phase_num="$1" phase_name="$2" pct="$3" msg="$4"
|
||||
mkdir -p "$UPGRADE_DIR"
|
||||
cat > "$PROGRESS_FILE" <<PEOF
|
||||
{
|
||||
"phase": ${phase_num},
|
||||
"phaseName": "${phase_name}",
|
||||
"percentage": ${pct},
|
||||
"message": "$(echo "$msg" | sed 's/"/\\"/g')",
|
||||
"lastUpdate": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
}
|
||||
PEOF
|
||||
}
|
||||
|
||||
write_result() {
|
||||
[[ "$API_MODE" != "true" ]] && return
|
||||
local success="$1" msg="$2"
|
||||
local duration_secs=$((SECONDS - START_TIME))
|
||||
local warnings_json="${3:-[]}"
|
||||
mkdir -p "$UPGRADE_DIR"
|
||||
cat > "$RESULT_FILE" <<REOF
|
||||
{
|
||||
"success": ${success},
|
||||
"message": "$(echo "$msg" | sed 's/"/\\"/g')",
|
||||
"previousCommit": "${PRE_UPGRADE_SHORT:-unknown}",
|
||||
"newCommit": "$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")",
|
||||
"commitCount": ${COMMIT_COUNT:-0},
|
||||
"durationSeconds": ${duration_secs},
|
||||
"warnings": ${warnings_json},
|
||||
"completedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
}
|
||||
REOF
|
||||
# Clean up progress file
|
||||
rm -f "$PROGRESS_FILE"
|
||||
# Update status.json with new commit info
|
||||
if [[ -x "$SCRIPT_DIR/upgrade-check.sh" ]]; then
|
||||
"$SCRIPT_DIR/upgrade-check.sh" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
elapsed() {
|
||||
local secs=$((SECONDS - START_TIME))
|
||||
printf '%dm %ds' $((secs / 60)) $((secs % 60))
|
||||
@@ -213,6 +260,7 @@ on_failure() {
|
||||
release_lock
|
||||
if [[ $exit_code -ne 0 ]] && [[ "$DRY_RUN" != "true" ]]; then
|
||||
error "Upgrade failed at line ${BASH_LINENO[0]} (exit code $exit_code)"
|
||||
write_result "false" "Upgrade failed at line ${BASH_LINENO[0]} (exit code ${exit_code})"
|
||||
print_rollback_help
|
||||
info "Log file: $LOG_FILE"
|
||||
fi
|
||||
@@ -235,6 +283,7 @@ Options:
|
||||
--force Continue past non-critical warnings
|
||||
--branch BRANCH Git branch to pull (default: current branch)
|
||||
--rollback Rollback to pre-upgrade commit
|
||||
--api-mode Write progress/result JSON for admin UI
|
||||
--help Show this help message
|
||||
|
||||
Examples:
|
||||
@@ -254,6 +303,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--force) FORCE=true; shift ;;
|
||||
--branch) BRANCH="$2"; shift 2 ;;
|
||||
--rollback) ROLLBACK=true; shift ;;
|
||||
--api-mode) API_MODE=true; shift ;;
|
||||
--help|-h) show_help ;;
|
||||
*) error "Unknown option: $1"; echo "Run with --help for usage."; exit 1 ;;
|
||||
esac
|
||||
@@ -350,6 +400,7 @@ fi
|
||||
# =============================================================================
|
||||
|
||||
phase "1" "Pre-flight Checks"
|
||||
write_progress 1 "Pre-flight Checks" 5 "Verifying system requirements..."
|
||||
|
||||
# Docker
|
||||
if command -v docker &>/dev/null; then
|
||||
@@ -454,6 +505,7 @@ fi
|
||||
# =============================================================================
|
||||
|
||||
phase "2" "Backup"
|
||||
write_progress 2 "Backup" 15 "Creating backup..."
|
||||
|
||||
if [[ "$SKIP_BACKUP" == "true" ]]; then
|
||||
warn "Backup skipped (--skip-backup --force)"
|
||||
@@ -512,6 +564,7 @@ fi
|
||||
# =============================================================================
|
||||
|
||||
phase "3" "Code Update"
|
||||
write_progress 3 "Code Update" 30 "Pulling latest code..."
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would fetch and show incoming changes:"
|
||||
@@ -640,6 +693,7 @@ fi
|
||||
# =============================================================================
|
||||
|
||||
phase "4" "Container Rebuild"
|
||||
write_progress 4 "Container Rebuild" 50 "Rebuilding containers..."
|
||||
|
||||
# Always rebuild source-built containers
|
||||
info "Rebuilding source containers: $SOURCE_CONTAINERS"
|
||||
@@ -683,6 +737,7 @@ fi
|
||||
# =============================================================================
|
||||
|
||||
phase "5" "Service Restart"
|
||||
write_progress 5 "Service Restart" 70 "Restarting services..."
|
||||
|
||||
# Stop application containers
|
||||
info "Stopping application containers..."
|
||||
@@ -773,6 +828,7 @@ fi
|
||||
# =============================================================================
|
||||
|
||||
phase "6" "Post-Upgrade Verification"
|
||||
write_progress 6 "Verification" 90 "Running health checks..."
|
||||
|
||||
VERIFY_FAILED=false
|
||||
|
||||
@@ -846,6 +902,15 @@ fi
|
||||
ELAPSED="$(elapsed)"
|
||||
FINAL_COMMIT="$(git rev-parse --short HEAD)"
|
||||
|
||||
# Collect warnings for API mode result
|
||||
UPGRADE_WARNINGS="[]"
|
||||
if [[ "$VERIFY_FAILED" == "true" ]]; then
|
||||
UPGRADE_WARNINGS='["Some health checks failed after upgrade — services may still be starting"]'
|
||||
fi
|
||||
|
||||
write_progress 6 "Verification" 100 "Upgrade complete!"
|
||||
write_result "true" "Upgraded ${PRE_UPGRADE_SHORT} → ${FINAL_COMMIT} (${COMMIT_COUNT} commits)" "$UPGRADE_WARNINGS"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}${GREEN}══════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD}${GREEN} Upgrade Complete${NC}"
|
||||
|
||||
Reference in New Issue
Block a user