CCP restore/tunnel/upgrade + upgrade.sh release-mode fixes + volunteer dashboard polish

- Add instance restore model, routes, and agent backup/restore endpoints
- Add Pangolin tunnel service (subdomain prefix, teardown action, CCP client)
- Add slug mutex for concurrent operation safety in agent
- Expand upgrade service with remote driver orchestration
- Fix upgrade.sh to properly handle release-mode installs (no git operations)
- Add CCP registration flags to config.sh (--ccp-url, --ccp-invite-code, --ccp-agent-url)
- Auto-detect JVB advertise IP in non-interactive mode
- Polish volunteer dashboard ActionStepsList with highlighted step component
- Add ticketed event description field + volunteer dashboard query refinements

Bunker Admin
This commit is contained in:
2026-04-12 11:09:46 -06:00
parent 29d1f3998a
commit 26ec925d9b
35 changed files with 4191 additions and 329 deletions

View File

@@ -103,7 +103,8 @@ cp "$PROJECT_DIR/api/prisma/init-nocodb-db.sh" "$STAGE_DIR/scripts/"
cp "$PROJECT_DIR/api/prisma/init-gancio-db.sh" "$STAGE_DIR/scripts/"
# Runtime scripts
for script in nocodb-init.sh gitea-init.sh mkdocs-entrypoint.sh backup.sh \
for script in nocodb-init.sh gitea-init.sh mkdocs-entrypoint.sh \
backup.sh restore.sh \
upgrade.sh upgrade-check.sh upgrade-watcher.sh \
uninstall.sh test-deployment.sh; do
if [[ -f "$PROJECT_DIR/scripts/$script" ]]; then

View File

@@ -294,7 +294,7 @@ if [[ "$START_SERVICES" =~ ^[Yy]$ ]]; then
info " Database migrations and seeding run automatically on first boot."
echo ""
CORE_SERVICES=("v2-postgres" "redis" "api" "admin")
CORE_SERVICES=("v2-postgres" "redis" "api" "admin" "nginx")
ELAPSED=0
ALL_HEALTHY=false

View File

@@ -359,9 +359,13 @@ trap on_failure EXIT
acquire_lock
load_env
# Determine branch
# Determine branch (source mode only — release installs have no git)
if [[ -z "$BRANCH" ]]; then
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$INSTALL_MODE" == "release" ]]; then
BRANCH="release"
else
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
fi
fi
# =============================================================================
@@ -461,13 +465,15 @@ else
exit 1
fi
# Remote reachable
info "Checking git remote..."
if timeout 10 git ls-remote origin HEAD &>/dev/null 2>&1; then
success "Git remote reachable"
else
error "Cannot reach git remote. Check your network or remote configuration."
exit 1
# Remote reachable (source mode only — release mode pulls from Gitea API later)
if [[ "$INSTALL_MODE" == "source" ]]; then
info "Checking git remote..."
if timeout 10 git ls-remote origin HEAD &>/dev/null 2>&1; then
success "Git remote reachable"
else
error "Cannot reach git remote. Check your network or remote configuration."
exit 1
fi
fi
# Working directory checks
@@ -490,9 +496,16 @@ fi
success "Disk space: ${AVAILABLE_MB}MB available"
# Record pre-upgrade state
PRE_UPGRADE_COMMIT="$(git rev-parse HEAD)"
PRE_UPGRADE_SHORT="$(git rev-parse --short HEAD)"
info "Current commit: $PRE_UPGRADE_SHORT ($(git log -1 --format='%s' HEAD))"
if [[ "$INSTALL_MODE" == "source" ]]; then
PRE_UPGRADE_COMMIT="$(git rev-parse HEAD)"
PRE_UPGRADE_SHORT="$(git rev-parse --short HEAD)"
info "Current commit: $PRE_UPGRADE_SHORT ($(git log -1 --format='%s' HEAD))"
else
# Release mode: derive "commit" from VERSION file (format: <tag>\n<sha>)
PRE_UPGRADE_COMMIT="$(head -2 "$PROJECT_DIR/VERSION" 2>/dev/null | tail -1 || echo "release")"
PRE_UPGRADE_SHORT="$(head -1 "$PROJECT_DIR/VERSION" 2>/dev/null || echo "release")"
info "Current version: $PRE_UPGRADE_SHORT"
fi
info "Target branch: $BRANCH"
# Record running containers (for restoring monitoring profile later)
@@ -502,31 +515,36 @@ if docker ps --format '{{.Names}}' | grep -q 'prometheus-changemaker'; then
info "Monitoring stack detected (will restart after upgrade)"
fi
# Warn about uncommitted changes in project-owned paths
PROJECT_OWNED_PATHS="api/ admin/ docker-compose.yml"
DIRTY_PROJECT_FILES="$(git diff --name-only HEAD -- $PROJECT_OWNED_PATHS 2>/dev/null || true)"
if [[ -n "$DIRTY_PROJECT_FILES" ]]; then
warn "Uncommitted changes in project-owned files:"
echo "$DIRTY_PROJECT_FILES" | while read -r f; do echo " $f"; done
if [[ "$FORCE" != "true" ]]; then
error "Commit or stash these changes first, or use --force to continue."
exit 1
# Source-mode-only checks: dirty files + upstream commit comparison
if [[ "$INSTALL_MODE" == "source" ]]; then
# Warn about uncommitted changes in project-owned paths
PROJECT_OWNED_PATHS="api/ admin/ docker-compose.yml"
DIRTY_PROJECT_FILES="$(git diff --name-only HEAD -- $PROJECT_OWNED_PATHS 2>/dev/null || true)"
if [[ -n "$DIRTY_PROJECT_FILES" ]]; then
warn "Uncommitted changes in project-owned files:"
echo "$DIRTY_PROJECT_FILES" | while read -r f; do echo " $f"; done
if [[ "$FORCE" != "true" ]]; then
error "Commit or stash these changes first, or use --force to continue."
exit 1
fi
warn "Continuing with --force (changes will be stashed)"
fi
warn "Continuing with --force (changes will be stashed)"
fi
# Check for available updates
LOCAL_HEAD="$(git rev-parse HEAD)"
REMOTE_HEAD="$(git ls-remote origin "$BRANCH" | cut -f1)"
if [[ "$LOCAL_HEAD" == "$REMOTE_HEAD" ]]; then
info "Already up to date ($PRE_UPGRADE_SHORT). No upstream changes."
if [[ "$FORCE" != "true" ]]; then
success "Nothing to upgrade."
release_lock
exit 0
# Check for available updates
LOCAL_HEAD="$(git rev-parse HEAD)"
REMOTE_HEAD="$(git ls-remote origin "$BRANCH" | cut -f1)"
if [[ "$LOCAL_HEAD" == "$REMOTE_HEAD" ]]; then
info "Already up to date ($PRE_UPGRADE_SHORT). No upstream changes."
if [[ "$FORCE" != "true" ]]; then
success "Nothing to upgrade."
release_lock
exit 0
fi
warn "Continuing with --force despite no upstream changes."
fi
warn "Continuing with --force despite no upstream changes."
fi
# Release mode: the upstream-version comparison happens later in the
# release-mode block (line ~597) which queries the Gitea Releases API.
# =============================================================================
# Phase 2: Backup
@@ -669,100 +687,105 @@ elif [[ "$DRY_RUN" == "true" ]]; then
exit 0
fi
# Step 0: Save user-modifiable paths before any git operations
save_user_paths
# Source-mode git pull flow. Release mode handles its update via tarball
# download in the block above and skips this entire section.
if [[ "$INSTALL_MODE" == "source" ]]; then
# Step 0: Save user-modifiable paths before any git operations
save_user_paths
# Step 0b: Clear skip-worktree flags that prevent merge (e.g., repo-data JSON files)
SKIP_WORKTREE_FILES="$(git ls-files -v | grep '^S ' | awk '{print $2}' || true)"
if [[ -n "$SKIP_WORKTREE_FILES" ]]; then
info "Clearing skip-worktree flags on $(echo "$SKIP_WORKTREE_FILES" | wc -l | xargs) file(s)..."
echo "$SKIP_WORKTREE_FILES" | xargs git update-index --no-skip-worktree
success "Skip-worktree flags cleared"
fi
# Step 0c: Fix Docker-owned directories that block git checkout
for owned_dir in api/upgrade api/uploads api/configs; do
if [[ -d "$PROJECT_DIR/$owned_dir" ]] && [[ ! -w "$PROJECT_DIR/$owned_dir" ]]; then
info "Fixing permissions on $owned_dir..."
docker run --rm -v "$PROJECT_DIR/$owned_dir:/fix" alpine chown -R "$(id -u):$(id -g)" /fix 2>/dev/null || true
# Step 0b: Clear skip-worktree flags that prevent merge (e.g., repo-data JSON files)
SKIP_WORKTREE_FILES="$(git ls-files -v | grep '^S ' | awk '{print $2}' || true)"
if [[ -n "$SKIP_WORKTREE_FILES" ]]; then
info "Clearing skip-worktree flags on $(echo "$SKIP_WORKTREE_FILES" | wc -l | xargs) file(s)..."
echo "$SKIP_WORKTREE_FILES" | xargs git update-index --no-skip-worktree
success "Skip-worktree flags cleared"
fi
done
# Step 1: Stash user changes if any exist
HAS_CHANGES=false
if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
HAS_CHANGES=true
STASH_NAME="upgrade-${TIMESTAMP}"
info "Stashing local changes as '$STASH_NAME'..."
git stash push --include-untracked -m "$STASH_NAME"
success "Local changes stashed"
fi
# Step 0c: Fix Docker-owned directories that block git checkout
for owned_dir in api/upgrade api/uploads api/configs; do
if [[ -d "$PROJECT_DIR/$owned_dir" ]] && [[ ! -w "$PROJECT_DIR/$owned_dir" ]]; then
info "Fixing permissions on $owned_dir..."
docker run --rm -v "$PROJECT_DIR/$owned_dir:/fix" alpine chown -R "$(id -u):$(id -g)" /fix 2>/dev/null || true
fi
done
# Step 3: Pull updates
info "Pulling updates from origin/$BRANCH..."
if ! git pull origin "$BRANCH" --no-edit 2>&1; then
error "git pull failed. This may indicate upstream force-push or branch issues."
# Step 1: Stash user changes if any exist
HAS_CHANGES=false
if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
HAS_CHANGES=true
STASH_NAME="upgrade-${TIMESTAMP}"
info "Stashing local changes as '$STASH_NAME'..."
git stash push --include-untracked -m "$STASH_NAME"
success "Local changes stashed"
fi
# Step 3: Pull updates
info "Pulling updates from origin/$BRANCH..."
if ! git pull origin "$BRANCH" --no-edit 2>&1; then
error "git pull failed. This may indicate upstream force-push or branch issues."
if [[ "$HAS_CHANGES" == "true" ]]; then
warn "Your stashed changes can be recovered with: git stash pop"
fi
exit 1
fi
POST_PULL_COMMIT="$(git rev-parse --short HEAD)"
success "Updated to $POST_PULL_COMMIT"
# Step 4: Pop stash and handle conflicts
if [[ "$HAS_CHANGES" == "true" ]]; then
warn "Your stashed changes can be recovered with: git stash pop"
fi
exit 1
fi
info "Restoring local changes..."
if git stash pop 2>&1; then
success "Local changes restored cleanly"
else
warn "Merge conflicts detected during stash pop"
POST_PULL_COMMIT="$(git rev-parse --short HEAD)"
success "Updated to $POST_PULL_COMMIT"
# Auto-resolve user-modifiable paths by keeping user's version
RESOLVED_COUNT=0
for user_path in "${USER_PATHS[@]}"; do
CONFLICTED="$(git diff --name-only --diff-filter=U -- "$user_path" 2>/dev/null || true)"
if [[ -n "$CONFLICTED" ]]; then
while IFS= read -r cf; do
info " Auto-resolving (keeping yours): $cf"
git checkout --theirs "$cf" 2>/dev/null || true
git add "$cf"
RESOLVED_COUNT=$((RESOLVED_COUNT + 1))
done < <(echo "$CONFLICTED")
fi
done
# Step 4: Pop stash and handle conflicts
if [[ "$HAS_CHANGES" == "true" ]]; then
info "Restoring local changes..."
if git stash pop 2>&1; then
success "Local changes restored cleanly"
else
warn "Merge conflicts detected during stash pop"
# Auto-resolve user-modifiable paths by keeping user's version
RESOLVED_COUNT=0
for user_path in "${USER_PATHS[@]}"; do
CONFLICTED="$(git diff --name-only --diff-filter=U -- "$user_path" 2>/dev/null || true)"
if [[ -n "$CONFLICTED" ]]; then
while IFS= read -r cf; do
info " Auto-resolving (keeping yours): $cf"
git checkout --theirs "$cf" 2>/dev/null || true
git add "$cf"
RESOLVED_COUNT=$((RESOLVED_COUNT + 1))
done < <(echo "$CONFLICTED")
# Check if any conflicts remain in project-owned files
REMAINING_CONFLICTS="$(git diff --name-only --diff-filter=U 2>/dev/null || true)"
if [[ -n "$REMAINING_CONFLICTS" ]]; then
error "Unresolved conflicts in project-owned files:"
echo "$REMAINING_CONFLICTS" | while read -r f; do echo " $f"; done
echo ""
error "These files have upstream changes that conflict with your edits."
error "Resolve manually, then run the upgrade again."
info "Your pre-upgrade commit: $PRE_UPGRADE_COMMIT"
info "To abort: git merge --abort OR git checkout $PRE_UPGRADE_COMMIT"
exit 1
fi
done
# Check if any conflicts remain in project-owned files
REMAINING_CONFLICTS="$(git diff --name-only --diff-filter=U 2>/dev/null || true)"
if [[ -n "$REMAINING_CONFLICTS" ]]; then
error "Unresolved conflicts in project-owned files:"
echo "$REMAINING_CONFLICTS" | while read -r f; do echo " $f"; done
echo ""
error "These files have upstream changes that conflict with your edits."
error "Resolve manually, then run the upgrade again."
info "Your pre-upgrade commit: $PRE_UPGRADE_COMMIT"
info "To abort: git merge --abort OR git checkout $PRE_UPGRADE_COMMIT"
exit 1
fi
if [[ $RESOLVED_COUNT -gt 0 ]]; then
success "Auto-resolved $RESOLVED_COUNT user-modifiable path(s) (kept your versions)"
if [[ $RESOLVED_COUNT -gt 0 ]]; then
success "Auto-resolved $RESOLVED_COUNT user-modifiable path(s) (kept your versions)"
fi
fi
fi
fi
# Step 4b: Restore user-modifiable paths (unconditionally overwrites with saved copies)
restore_user_paths
# Step 4b: Restore user-modifiable paths (unconditionally overwrites with saved copies)
restore_user_paths
# Step 4c: Restore any tracked files accidentally deleted by restore_user_paths
# (can happen when save_user_paths can't read root-owned files in user paths)
DELETED_TRACKED="$(git ls-files --deleted 2>/dev/null || true)"
if [[ -n "$DELETED_TRACKED" ]]; then
info "Restoring $(echo "$DELETED_TRACKED" | wc -l | xargs) tracked file(s) deleted during restore..."
echo "$DELETED_TRACKED" | xargs git checkout HEAD -- 2>/dev/null || true
success "Tracked files restored from HEAD"
# Step 4c: Restore any tracked files accidentally deleted by restore_user_paths
# (can happen when save_user_paths can't read root-owned files in user paths)
DELETED_TRACKED="$(git ls-files --deleted 2>/dev/null || true)"
if [[ -n "$DELETED_TRACKED" ]]; then
info "Restoring $(echo "$DELETED_TRACKED" | wc -l | xargs) tracked file(s) deleted during restore..."
echo "$DELETED_TRACKED" | xargs git checkout HEAD -- 2>/dev/null || true
success "Tracked files restored from HEAD"
fi
fi
# End of source-mode git pull flow
# Step 5: Detect new env vars
info "Checking for new environment variables..."
@@ -791,24 +814,30 @@ if [[ -f "$PROJECT_DIR/.env.example" ]] && [[ -f "$PROJECT_DIR/.env" ]]; then
fi
fi
# Step 6: Print update summary
COMMIT_RANGE="${PRE_UPGRADE_SHORT}..${POST_PULL_COMMIT}"
COMMIT_COUNT="$(git log --oneline "$PRE_UPGRADE_COMMIT..HEAD" 2>/dev/null | wc -l | xargs)"
echo ""
info "Update summary: $COMMIT_COUNT commit(s) ($COMMIT_RANGE)"
git log --oneline "$PRE_UPGRADE_COMMIT..HEAD" 2>/dev/null | head -20
if [[ "$COMMIT_COUNT" -gt 20 ]]; then
info " ... and $((COMMIT_COUNT - 20)) more"
fi
# Flag commits that may require manual attention
BREAKING_COMMITS="$(git log --oneline "$PRE_UPGRADE_COMMIT..HEAD" --grep="BREAKING" --grep="\[manual\]" 2>/dev/null || true)"
if [[ -n "$BREAKING_COMMITS" ]]; then
# Step 6: Print update summary (source mode only — release mode has no commit range)
COMMIT_COUNT=0
if [[ "$INSTALL_MODE" == "source" ]]; then
COMMIT_RANGE="${PRE_UPGRADE_SHORT}..${POST_PULL_COMMIT}"
# Use || true and check pipefail-safe to survive git failures
COMMIT_COUNT="$(git log --oneline "$PRE_UPGRADE_COMMIT..HEAD" 2>/dev/null | wc -l | xargs || echo 0)"
echo ""
warn "Commits requiring manual attention:"
echo "$BREAKING_COMMITS" | while read -r line; do
echo -e " ${YELLOW}$line${NC}"
done
info "Update summary: $COMMIT_COUNT commit(s) ($COMMIT_RANGE)"
git log --oneline "$PRE_UPGRADE_COMMIT..HEAD" 2>/dev/null | head -20 || true
if [[ "$COMMIT_COUNT" -gt 20 ]]; then
info " ... and $((COMMIT_COUNT - 20)) more"
fi
# Flag commits that may require manual attention
BREAKING_COMMITS="$(git log --oneline "$PRE_UPGRADE_COMMIT..HEAD" --grep="BREAKING" --grep="\[manual\]" 2>/dev/null || true)"
if [[ -n "$BREAKING_COMMITS" ]]; then
echo ""
warn "Commits requiring manual attention:"
echo "$BREAKING_COMMITS" | while read -r line; do
echo -e " ${YELLOW}$line${NC}"
done
fi
else
info "Update summary: ${PRE_UPGRADE_SHORT} → release"
fi
# =============================================================================
@@ -1135,7 +1164,10 @@ verify_service_health() {
done
warn "$name: not responding after ${max_wait}s"
VERIFY_FAILED=true
return 1
# Always return 0 — under set -e a non-zero return from this helper would
# exit the script before write_result runs. The VERIFY_FAILED flag is the
# signal the caller actually checks.
return 0
}
# API health (with polling — may still be running migrations)
@@ -1194,7 +1226,11 @@ fi
# =============================================================================
ELAPSED="$(elapsed)"
FINAL_COMMIT="$(git rev-parse --short HEAD)"
if [[ "$INSTALL_MODE" == "source" ]]; then
FINAL_COMMIT="$(git rev-parse --short HEAD)"
else
FINAL_COMMIT="$(head -1 "$PROJECT_DIR/VERSION" 2>/dev/null || echo "release")"
fi
# Collect warnings for API mode result
UPGRADE_WARNINGS="[]"
@@ -1211,7 +1247,11 @@ echo -e "${BOLD}${GREEN} Upgrade Complete${NC}"
echo -e "${BOLD}${GREEN}══════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}Previous:${NC} $PRE_UPGRADE_SHORT"
echo -e " ${BOLD}Current:${NC} $FINAL_COMMIT ($(git log -1 --format='%s' HEAD))"
if [[ "$INSTALL_MODE" == "source" ]]; then
echo -e " ${BOLD}Current:${NC} $FINAL_COMMIT ($(git log -1 --format='%s' HEAD 2>/dev/null || echo "$FINAL_COMMIT"))"
else
echo -e " ${BOLD}Current:${NC} $FINAL_COMMIT"
fi
echo -e " ${BOLD}Commits:${NC} $COMMIT_COUNT"
echo -e " ${BOLD}Duration:${NC} $ELAPSED"
echo -e " ${BOLD}Log:${NC} $LOG_FILE"