273 lines
7.3 KiB
Bash
273 lines
7.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Apply a Nexus release tarball on the production host.
|
|
#
|
|
# Usage:
|
|
# sudo bash deploy/apply-release-bundle.sh \
|
|
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
|
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1 \
|
|
# --deploy-path /opt/nexus
|
|
#
|
|
# Validate only (no backup, no rsync, no rebuild):
|
|
# bash deploy/apply-release-bundle.sh --validate-only \
|
|
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
|
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1
|
|
#
|
|
# This script intentionally preserves runtime secrets and data:
|
|
# - .env
|
|
# - docker/.env.prod
|
|
# - web/data
|
|
|
|
set -euo pipefail
|
|
|
|
TARBALL=""
|
|
EXPECTED_SHA256=""
|
|
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
|
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/opt/nexus-backups}"
|
|
SKIP_REBUILD=0
|
|
DRY_RUN=0
|
|
VALIDATE_ONLY=0
|
|
APPLY_TMP=""
|
|
|
|
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
|
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
|
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
|
|
|
usage() {
|
|
sed -n '1,26p' "$0"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--tarball)
|
|
TARBALL="${2:-}"
|
|
shift 2
|
|
;;
|
|
--sha256)
|
|
EXPECTED_SHA256="${2:-}"
|
|
shift 2
|
|
;;
|
|
--deploy-path)
|
|
DEPLOY_PATH="${2:-}"
|
|
shift 2
|
|
;;
|
|
--backup-dir)
|
|
BACKUP_DIR="${2:-}"
|
|
shift 2
|
|
;;
|
|
--skip-rebuild)
|
|
SKIP_REBUILD=1
|
|
shift
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=1
|
|
shift
|
|
;;
|
|
--validate-only)
|
|
VALIDATE_ONLY=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
error "Unknown argument: $1"
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || {
|
|
error "Missing required command: $1"
|
|
exit 127
|
|
}
|
|
}
|
|
|
|
require_runtime_cmds() {
|
|
require_cmd tar
|
|
require_cmd sha256sum
|
|
if [[ "$VALIDATE_ONLY" == 1 ]]; then
|
|
return 0
|
|
fi
|
|
require_cmd rsync
|
|
require_cmd curl
|
|
}
|
|
|
|
run() {
|
|
echo "+ $*"
|
|
if [[ "$DRY_RUN" == 1 ]]; then
|
|
return 0
|
|
fi
|
|
"$@"
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ -n "${APPLY_TMP:-}" && -d "$APPLY_TMP" ]]; then
|
|
rm -rf "$APPLY_TMP"
|
|
fi
|
|
}
|
|
|
|
detect_runtime() {
|
|
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
|
|
echo docker
|
|
return
|
|
fi
|
|
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
|
echo supervisor
|
|
return
|
|
fi
|
|
echo unknown
|
|
}
|
|
|
|
verify_sha256() {
|
|
if [[ -z "$EXPECTED_SHA256" ]]; then
|
|
warn "No --sha256 provided; skipping checksum verification"
|
|
return 0
|
|
fi
|
|
local actual
|
|
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
|
|
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
|
|
error "SHA256 mismatch"
|
|
error "expected: $EXPECTED_SHA256"
|
|
error "actual: $actual"
|
|
exit 1
|
|
fi
|
|
info "SHA256 OK: $actual"
|
|
}
|
|
|
|
validate_extracted_tree() {
|
|
local src="$1"
|
|
for required in server deploy Dockerfile.prod frontend-v2 web/app-v2; do
|
|
if [[ ! -e "${src}/${required}" ]]; then
|
|
error "Release bundle missing required path: ${required}"
|
|
exit 1
|
|
fi
|
|
done
|
|
for forbidden in .env docker/.env.prod web/data .venv-py312-codex frontend-v2/node_modules 2025.2 =2025.2; do
|
|
if [[ -e "${src}/${forbidden}" ]]; then
|
|
error "Release bundle contains forbidden path: ${forbidden}"
|
|
exit 1
|
|
fi
|
|
done
|
|
info "Extracted release tree validation OK"
|
|
}
|
|
|
|
backup_current() {
|
|
if [[ ! -d "$DEPLOY_PATH" ]]; then
|
|
warn "Deploy path does not exist yet: $DEPLOY_PATH"
|
|
return 0
|
|
fi
|
|
local stamp backup
|
|
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
backup="${BACKUP_DIR}/nexus-before-${stamp}.tar.gz"
|
|
run mkdir -p "$BACKUP_DIR"
|
|
info "Backup current deploy path -> $backup"
|
|
run tar czf "$backup" \
|
|
--exclude='./web/data' \
|
|
--exclude='./.venv*' \
|
|
--exclude='./venv' \
|
|
--exclude='./frontend/node_modules' \
|
|
--exclude='./frontend-v2/node_modules' \
|
|
-C "$(dirname "$DEPLOY_PATH")" "$(basename "$DEPLOY_PATH")"
|
|
}
|
|
|
|
sync_release() {
|
|
local src="$1"
|
|
run mkdir -p "$DEPLOY_PATH"
|
|
info "Sync release tree -> $DEPLOY_PATH"
|
|
run rsync -a --delete \
|
|
--exclude='.env' \
|
|
--exclude='docker/.env.prod' \
|
|
--exclude='web/data' \
|
|
"${src}/" "${DEPLOY_PATH}/"
|
|
}
|
|
|
|
rebuild_or_restart() {
|
|
if [[ "$SKIP_REBUILD" == 1 ]]; then
|
|
warn "--skip-rebuild set; not rebuilding/restarting Nexus"
|
|
return 0
|
|
fi
|
|
|
|
local runtime
|
|
runtime="$(detect_runtime)"
|
|
info "Runtime: $runtime"
|
|
case "$runtime" in
|
|
docker)
|
|
if [[ -x "${DEPLOY_PATH}/deploy/nexus-1panel.sh" || -f "${DEPLOY_PATH}/deploy/nexus-1panel.sh" ]]; then
|
|
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/nexus-1panel.sh" upgrade --skip-git
|
|
else
|
|
error "Docker runtime detected but deploy/nexus-1panel.sh missing"
|
|
exit 1
|
|
fi
|
|
if [[ -f "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" ]]; then
|
|
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh"
|
|
fi
|
|
;;
|
|
supervisor)
|
|
run supervisorctl restart nexus
|
|
;;
|
|
*)
|
|
error "Unknown runtime. Rebuild/restart manually."
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
verify_local_endpoints() {
|
|
local port health app app_v2
|
|
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
|
port="${port:-8600}"
|
|
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
|
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
|
|
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
|
|
echo " /health -> ${health}"
|
|
echo " /app/ -> ${app}"
|
|
echo " /app-v2/ -> ${app_v2}"
|
|
if [[ "$health" != "ok" || "$app" != "200" || "$app_v2" != "200" ]]; then
|
|
error "Local endpoint verification failed"
|
|
exit 1
|
|
fi
|
|
info "Local endpoint verification OK"
|
|
}
|
|
|
|
main() {
|
|
if [[ -z "$TARBALL" ]]; then
|
|
error "--tarball is required"
|
|
usage
|
|
exit 2
|
|
fi
|
|
[[ -f "$TARBALL" ]] || {
|
|
error "Tarball not found: $TARBALL"
|
|
exit 1
|
|
}
|
|
|
|
require_runtime_cmds
|
|
|
|
info "Tarball: $TARBALL"
|
|
info "Deploy path: $DEPLOY_PATH"
|
|
info "Backup dir: $BACKUP_DIR"
|
|
verify_sha256
|
|
|
|
local extracted
|
|
APPLY_TMP="$(mktemp -d /tmp/nexus-release-apply.XXXXXX)"
|
|
trap cleanup EXIT
|
|
run tar xzf "$TARBALL" -C "$APPLY_TMP"
|
|
extracted="${APPLY_TMP}/nexus-release"
|
|
validate_extracted_tree "$extracted"
|
|
if [[ "$VALIDATE_ONLY" == 1 ]]; then
|
|
info "Validate-only mode: tarball checksum and content validation passed"
|
|
return 0
|
|
fi
|
|
backup_current
|
|
sync_release "$extracted"
|
|
rebuild_or_restart
|
|
if [[ "$DRY_RUN" != 1 && "$SKIP_REBUILD" != 1 ]]; then
|
|
verify_local_endpoints
|
|
fi
|
|
info "Release applied successfully"
|
|
}
|
|
|
|
main "$@"
|