mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-07 07:18:18 +00:00
A shared zconfig.json (one file used from a Windows checkout and from WSL via the symlink) holds Windows paths like "F:\evomedia.net\app". The bash port passed those to local file ops verbatim, so every path-using command failed on WSL (e.g. zbackup: "Root not found: F:\evomedia.net\evomedia-docs"). Add a z_path helper that converts drive-letter paths to the host's native form (wslpath, with a /mnt fallback) and is a no-op for Unix paths and empty strings — so a bash-native config is unaffected. Route every LOCAL path through it: localRoot (new zproj_root accessor), paths.* (temp, backupsLocal, backupsEc2, scriptsRoot, oneDriveBackups), ec2.pemKey (zec2_pem), and ztokens.dataDir. Server-side paths (remote.path, composeDir, stackRoot, certsSource) are left verbatim. Verified on WSL: all 11 project roots, all paths.*, and the C:/G: pem and OneDrive paths resolve to existing dirs; zbackup evodocs (the failing case) now runs clean end-to-end.
496 lines
25 KiB
Bash
496 lines
25 KiB
Bash
#!/usr/bin/env bash
|
|
# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers
|
|
# Created by Kelly Michels · dev@evomedia.net
|
|
# Licensed under the MIT License. See LICENSE.
|
|
#
|
|
# zdeploy — deploy any project defined in zconfig.json to the server.
|
|
# Each project runs its own docker compose stack; the handler is picked by the
|
|
# project's "kind": python | vite | nextjs | edge | docker.
|
|
#
|
|
# Usage:
|
|
# zdeploy <project> [<project> ...] [--note "message"]
|
|
# zdeploy all # every project (edge kinds first), stop at first failure
|
|
#
|
|
# Flow (python/vite/nextjs): zip source -> free server disk space -> scp up ->
|
|
# unzip into remote.path (preserving server-side .env) -> docker compose build + up
|
|
# -> verify the live site reports the new build version. Zips are always deleted.
|
|
|
|
set -uo pipefail
|
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
|
z_track_start "$@"
|
|
z_need_config
|
|
z_require ssh scp zip curl jq
|
|
|
|
projects=(); NOTE="Build deployed"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--note) NOTE="${2:-Build deployed}"; shift 2 ;;
|
|
*) projects+=("${1#-}"); shift ;; # tolerate switch-style keys (zdeploy -myproject)
|
|
esac
|
|
done
|
|
|
|
if [ "${#projects[@]}" -eq 0 ]; then
|
|
printf '\n'; warn "Usage: zdeploy <project> [<project> ...] | all [--note \"message\"]"
|
|
dim " Projects in zconfig.json: $(zproj_csv)"
|
|
dim " 'all' deploys everything (edge kinds first) and stops at the first failure."
|
|
exit 1
|
|
fi
|
|
|
|
EC2_IP="$(zec2_ip)"
|
|
STACK_ROOT="$(zq '.ec2.stackRoot')"
|
|
REMOTE_HOME="/home/$(zec2_user)"
|
|
EC2_USER="$(zec2_user)"
|
|
TEMP_ROOT="$(z_path "$(zq '.paths.temp')")"
|
|
{ [ -z "$TEMP_ROOT" ] || [ "$TEMP_ROOT" = "null" ]; } && TEMP_ROOT="${TMPDIR:-/tmp}"
|
|
mkdir -p "$TEMP_ROOT"
|
|
|
|
# 'all' -> every project, edge kinds first (the proxy must route before apps ship).
|
|
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
|
mapfile -t edge_keys < <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.kind == "edge") | .key' "$ZCONFIG")
|
|
mapfile -t rest_keys < <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.kind != "edge") | .key' "$ZCONFIG")
|
|
projects=("${edge_keys[@]+"${edge_keys[@]}"}" "${rest_keys[@]+"${rest_keys[@]}"}")
|
|
info "Deploying all projects: $(printf '%s ' "${projects[@]}")"
|
|
fi
|
|
|
|
deploy_zip_name() { # <key>
|
|
local zn; zn="$(zproj "$1" .deploy.zipName)"
|
|
[ -n "$zn" ] && printf '%s' "$zn" || printf '%sDeploy.zip' "$1"
|
|
}
|
|
|
|
# Pre-upload cleanup: remove stale deploy zips, prune docker, truncate big logs,
|
|
# fail if under 1.5 GB free.
|
|
preflight_cleanup() { # <extra_zip_to_remove...>
|
|
printf '\n'; info "--- [Preflight] Freeing disk space on the server ---"
|
|
local rm_clause="true"
|
|
[ $# -gt 0 ] && rm_clause="rm -f $*"
|
|
local cmd
|
|
cmd="echo '--- df / before cleanup ---'; df -h /"
|
|
cmd+="; echo '--- removing stale deploy artifacts ---'; $rm_clause"
|
|
cmd+="; echo '--- pruning docker build cache + dangling images + stopped containers ---'"
|
|
cmd+="; sudo docker container prune -f >/dev/null 2>&1 || true"
|
|
cmd+="; sudo docker builder prune -f >/dev/null 2>&1 || true"
|
|
cmd+="; sudo docker image prune -af >/dev/null 2>&1 || true"
|
|
cmd+="; echo '--- truncating large container logs ---'"
|
|
cmd+="; sudo find /var/lib/docker/containers/ -name '*-json.log' -size +50M -exec truncate -s 0 {} + 2>/dev/null || true"
|
|
cmd+="; echo '--- df / after cleanup ---'; df -h /"
|
|
cmd+='; avail_mb=$(df --output=avail -BM / | tail -n 1 | tr -dc 0-9)'
|
|
cmd+='; [ -z "$avail_mb" ] && avail_mb=0'
|
|
cmd+='; echo available_mb=$avail_mb'
|
|
cmd+='; if [ "$avail_mb" -lt 1500 ]; then echo "ERROR: less than 1.5 GB free on /. Grow the root volume or run: sudo docker system prune -af" >&2; exit 11; fi'
|
|
zssh "$cmd" || { err "Server pre-flight cleanup failed. Root volume too full (need ~1.5 GB free, ideally 3+)."; return 1; }
|
|
}
|
|
|
|
# Post-deploy cleanup: prune what this deploy created; running stacks untouched.
|
|
post_cleanup() { # <label>
|
|
printf '\n'; info "--- [Post-deploy] Reclaiming disk space ($1) ---"
|
|
local cmd
|
|
cmd="sudo docker container prune -f >/dev/null 2>&1 || true"
|
|
cmd+="; sudo docker builder prune -f >/dev/null 2>&1 || true"
|
|
cmd+="; sudo docker image prune -af >/dev/null 2>&1 || true"
|
|
cmd+="; sudo find /var/lib/docker/containers/ -name '*-json.log' -size +50M -exec truncate -s 0 {} + 2>/dev/null || true"
|
|
cmd+="; df -h /"
|
|
zssh "$cmd" || warn " Post-deploy cleanup returned non-zero; continuing."
|
|
}
|
|
|
|
send_zip() { # <local_zip> <zip_name>
|
|
scp -i "$(zec2_pem)" "$1" "$(zec2_target):$REMOTE_HOME/" \
|
|
|| { err "SCP upload failed. Likely server disk space. Try: rm -f $REMOTE_HOME/$2"; return 1; }
|
|
}
|
|
|
|
remote_unzip() { # <zip_name> <dest_path>
|
|
zec2_step "unzip $1" \
|
|
"test -f $REMOTE_HOME/$1 || { echo 'missing $REMOTE_HOME/$1'; exit 2; }; unzip -t $REMOTE_HOME/$1 || exit 3; unzip -o $REMOTE_HOME/$1 -d $2; uc=\$?; if [ \$uc -gt 1 ]; then exit \$uc; fi; exit 0"
|
|
}
|
|
|
|
# ── Deploy verification (build-version match, not just HTTP 200 — a 200 can be
|
|
# a stale cached build; the version match proves the new build is live) ─────
|
|
|
|
wait_verify_api() { # <key> <expected_label> <timeout_sec>
|
|
local key="$1" expected="$2" timeout="${3:-60}" domain live deadline
|
|
domain="$(zproj "$key" .domain)"
|
|
printf '\n'; info "--- [$key] Live build verification (expect $expected) ---"
|
|
deadline=$((SECONDS + timeout))
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
live="$(http_get "http://$EC2_IP/api/build-version" "$domain" | jq -r '.build_version // empty' 2>/dev/null)"
|
|
if [ -n "$live" ]; then
|
|
if [ "$live" = "$expected" ]; then
|
|
ok " PASS - live build $live matches expected."
|
|
return 0
|
|
fi
|
|
warn " Live build is $live, expected $expected - waiting..."
|
|
else
|
|
dim " /api/build-version not ready yet - waiting..."
|
|
fi
|
|
sleep 3
|
|
done
|
|
warn " WARNING: live build did not match $expected within ${timeout}s (a stale build may be cached)."
|
|
return 1
|
|
}
|
|
|
|
wait_verify_static() { # <key> <pre_product_version> <pre_build_number>
|
|
local key="$1" pv="$2" bn="$3" expected container domain live deadline raw
|
|
if [ -z "$pv" ] || [ -z "$bn" ]; then
|
|
printf '\n'; warn "--- [$key version] SKIPPED (no local build-version.json - see 'Enabling deploy verification' in README) ---"
|
|
return 0
|
|
fi
|
|
expected="v${pv}.$((bn + 1))"
|
|
container="$(zproj "$key" .remote.containerName)"
|
|
domain="$(zproj "$key" .domain)"
|
|
printf '\n'; info "--- [$key] Live build verification (expect $expected) ---"
|
|
deadline=$((SECONDS + 45))
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
if [ -n "$container" ]; then
|
|
# build-version.json may be blocked externally by the edge proxy;
|
|
# read it inside the running container instead.
|
|
raw="$(zssh "sudo docker exec $container cat /usr/share/nginx/html/build-version.json 2>/dev/null" 2>/dev/null)"
|
|
else
|
|
raw="$(http_get "http://$EC2_IP/build-version.json" "$domain")"
|
|
fi
|
|
live="$(printf '%s' "$raw" | jq -r '"v\(.productVersion).\(.buildNumber)"' 2>/dev/null)"
|
|
if [ -n "$live" ] && [ "$live" != "null" ] && [ "$live" != "v." ]; then
|
|
if [ "$live" = "$expected" ]; then
|
|
ok " PASS - live build $live matches expected."
|
|
return 0
|
|
fi
|
|
warn " Live build is $live, expected $expected - waiting..."
|
|
else
|
|
dim " Container not ready yet - waiting..."
|
|
fi
|
|
sleep 3
|
|
done
|
|
warn " WARNING: live build did not match $expected within 45s (upload or Docker build may have failed, or a stale build is cached)."
|
|
}
|
|
|
|
# Fast-forward the checkout before a deploy when deploy.gitPull is set — zdeploy
|
|
# zips the working tree, so a stale checkout would ship stale code while still
|
|
# bumping the build number. Abort rather than deploy uncertain code.
|
|
deploy_git_pull() { # <key>
|
|
local key="$1" root branch
|
|
[ "$(zproj "$key" .deploy.gitPull)" = "true" ] || return 0
|
|
root="$(zproj_root "$key")"
|
|
if [ ! -d "$root/.git" ]; then
|
|
warn " gitPull set but '$root' is not a git repo - skipping pull."
|
|
return 0
|
|
fi
|
|
printf '\n'; info "--- [0] git pull --ff-only ---"
|
|
branch="$(cd "$root" && git rev-parse --abbrev-ref HEAD)"
|
|
dim " Branch: $branch"
|
|
(cd "$root" && git pull --ff-only) || {
|
|
err "git pull --ff-only failed in '$root' (branch '$branch'). Resolve it, then re-run - refusing to deploy possibly-stale code."
|
|
return 1
|
|
}
|
|
dim " Now at: $(cd "$root" && git log -1 --oneline)"
|
|
}
|
|
|
|
# ── Kind handlers ────────────────────────────────────────────────────────────
|
|
|
|
deploy_python() { # <key>
|
|
local key="$1" start=$SECONDS root remote_path compose_dir app_svc zip_name zip_local
|
|
local bv="" has_version_tool=0 excl=() attempt out label domain
|
|
root="$(zproj_root "$key")"
|
|
remote_path="$(zproj "$key" .remote.path)"
|
|
compose_dir="$(zremote_compose_dir "$key")"
|
|
app_svc="$(zproj "$key" .remote.appService)"; [ -n "$app_svc" ] || app_svc="app"
|
|
zip_name="$(deploy_zip_name "$key")"
|
|
zip_local="$TEMP_ROOT/$zip_name"
|
|
label="$(zproj "$key" .label)"
|
|
domain="$(zproj "$key" .domain)"
|
|
[ -f "$root/scripts/build_version_tool.py" ] && has_version_tool=1
|
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
|
|
|
printf '\n'; info "=== $label deploy (python) ==="
|
|
dim "Local zip: $zip_local"
|
|
|
|
printf '\n'; info "--- [1] Zipping $label ---"
|
|
find "$root" -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null
|
|
mapfile -t excl < <(z_archive_excludes "$key" 0)
|
|
z_archive "$root" "$zip_local" 0 "-" "${excl[@]}" || return 1
|
|
|
|
preflight_cleanup "$REMOTE_HOME/$zip_name" || { rm -f "$zip_local"; return 1; }
|
|
|
|
printf '\n'; info "--- [2] Uploading zip ---"
|
|
send_zip "$zip_local" "$zip_name" || { rm -f "$zip_local"; return 1; }
|
|
rm -f "$zip_local"
|
|
|
|
printf '\n'; info "--- [3] Unzipping and rebuilding on the server ---"
|
|
zec2_step "apt-get install unzip" "sudo apt-get update -qq && sudo apt-get install -y unzip" || return 1
|
|
zec2_step "ensure stack root" "sudo mkdir -p $STACK_ROOT && sudo chown ${EC2_USER}:${EC2_USER} $STACK_ROOT" || return 1
|
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
|
zec2_step "backup .env if present" "if [ -f $remote_path/.env ]; then cp $remote_path/.env $REMOTE_HOME/.env.${key}_bak; fi" || return 1
|
|
zec2_step "replace project directory" "sudo rm -rf $remote_path && sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
|
remote_unzip "$zip_name" "$remote_path" || return 1
|
|
zec2_step "restore .env from backup" "if [ -f $REMOTE_HOME/.env.${key}_bak ]; then cp $REMOTE_HOME/.env.${key}_bak $remote_path/.env; fi" || return 1
|
|
zec2_step "require compose directory" "test -d $compose_dir" || return 1
|
|
zec2_step "docker compose build $app_svc" "cd $compose_dir && sudo COMPOSE_BAKE=false docker compose build $app_svc" || return 1
|
|
zec2_step "docker compose up -d" "cd $compose_dir && sudo COMPOSE_BAKE=false docker compose up -d" || return 1
|
|
zec2_step "record deploy time; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remote_path/.last_deploy_utc > /dev/null && rm -f $REMOTE_HOME/$zip_name" || return 1
|
|
|
|
if [ "$has_version_tool" -eq 1 ]; then
|
|
printf '\n'; info "--- [4] Incrementing build version ---"
|
|
for attempt in 1 2 3 4 5; do
|
|
out="$(zssh "cd $compose_dir && sudo docker compose exec -T $app_svc python scripts/build_version_tool.py bump" 2>/dev/null | tail -1)"
|
|
[ -n "$out" ] && { bv="$out"; break; }
|
|
warn " Attempt $attempt failed, retrying in 3s..."
|
|
sleep 3
|
|
done
|
|
[ -n "$bv" ] || { err "Build version bump failed after 5 attempts"; return 1; }
|
|
|
|
(cd "$root" && { python3 scripts/build_version_tool.py set "$bv" >/dev/null 2>&1 || python scripts/build_version_tool.py set "$bv" >/dev/null 2>&1; })
|
|
zssh "echo '$bv' | sudo tee $remote_path/.build_version > /dev/null"
|
|
if [ -f "$root/scripts/build_changelog_tool.py" ]; then
|
|
(cd "$root" && { python3 scripts/build_changelog_tool.py append --version "$bv" --note "$NOTE" >/dev/null 2>&1 \
|
|
|| python scripts/build_changelog_tool.py append --version "$bv" --note "$NOTE" >/dev/null 2>&1; })
|
|
fi
|
|
|
|
printf '\n'; info "--- [5] Restarting app to pick up new version ---"
|
|
zssh "cd $compose_dir && sudo COMPOSE_BAKE=false docker compose restart $app_svc" \
|
|
|| { err "App restart after build bump failed"; return 1; }
|
|
|
|
wait_verify_api "$key" "$bv" 30 || true
|
|
else
|
|
printf '\n'; info "--- [4] Basic reachability check (no build_version_tool - see 'Enabling deploy verification' in README) ---"
|
|
local deadline=$((SECONDS + 30)) code up=0
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
sleep 3
|
|
code="$(http_code "http://$EC2_IP/" "$domain")"
|
|
if [ "$code" != "000" ] && [ "$code" -lt 500 ]; then up=1; break; fi
|
|
dim " App not ready yet - waiting..."
|
|
done
|
|
if [ "$up" -eq 1 ]; then ok " App is responding."; else warn " WARNING: app did not respond within 30s."; fi
|
|
fi
|
|
|
|
post_cleanup "$key"
|
|
|
|
local elapsed=$((SECONDS - start))
|
|
printf '\n'; ok "--- [Done] $label deployed! ---"
|
|
[ -n "$domain" ] && warn "Site: https://$domain"
|
|
[ -n "$bv" ] && note "Build Version: $bv"
|
|
info "Change Note: $NOTE"
|
|
dim "Deploy Time: $(printf '%02d:%02d' $((elapsed / 60)) $((elapsed % 60))) (${elapsed}s)"
|
|
}
|
|
|
|
deploy_vite() { # <key>
|
|
local key="$1" start=$SECONDS root remote_path zip_name zip_local excl=()
|
|
local pre_pv="" pre_bn="" label domain edge_pc
|
|
root="$(zproj_root "$key")"
|
|
remote_path="$(zproj "$key" .remote.path)"
|
|
zip_name="$(deploy_zip_name "$key")"
|
|
zip_local="$TEMP_ROOT/$zip_name"
|
|
label="$(zproj "$key" .label)"
|
|
domain="$(zproj "$key" .domain)"
|
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
|
|
|
printf '\n'; info "=== $label deploy (vite/static) ==="
|
|
dim "Local zip: $zip_local"
|
|
|
|
printf '\n'; info "--- [1] Zipping site ---"
|
|
if [ -f "$root/build-version.json" ]; then
|
|
pre_pv="$(jq -r '.productVersion // empty' "$root/build-version.json")"
|
|
pre_bn="$(jq -r '.buildNumber // empty' "$root/build-version.json")"
|
|
[ -n "$pre_pv" ] && dim " Pre-zip build label: v${pre_pv}.${pre_bn} (server-side build will bump +1)"
|
|
fi
|
|
mapfile -t excl < <(z_archive_excludes "$key" 0)
|
|
z_archive "$root" "$zip_local" 0 "-" "${excl[@]}" || return 1
|
|
|
|
preflight_cleanup "$REMOTE_HOME/$zip_name" || { rm -f "$zip_local"; return 1; }
|
|
|
|
printf '\n'; info "--- [2] Uploading zip ---"
|
|
send_zip "$zip_local" "$zip_name" || { rm -f "$zip_local"; return 1; }
|
|
rm -f "$zip_local"
|
|
|
|
printf '\n'; info "--- [3] Unzipping and rebuilding on the server ---"
|
|
zec2_step "apt-get install unzip" "sudo apt-get update -qq && sudo apt-get install -y unzip" || return 1
|
|
zec2_step "ensure stack root" "sudo mkdir -p $STACK_ROOT && sudo chown ${EC2_USER}:${EC2_USER} $STACK_ROOT" || return 1
|
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
|
zec2_step "replace project directory" "sudo rm -rf $remote_path && sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
|
remote_unzip "$zip_name" "$remote_path" || return 1
|
|
zec2_step "require compose file" "test -f $remote_path/docker-compose.yml" || return 1
|
|
zec2_step "docker compose build" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose build" || return 1
|
|
zec2_step "docker compose up -d" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose up -d" || return 1
|
|
|
|
local ek; ek="$(zedge_key)"
|
|
if [ -n "$ek" ]; then
|
|
edge_pc="$(zproj "$ek" .proxyContainer)"
|
|
[ -n "$edge_pc" ] && { zec2_step "reload edge nginx (flush DNS cache for new container IP)" "sudo docker exec $edge_pc nginx -s reload" || return 1; }
|
|
fi
|
|
zec2_step "record deploy time; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remote_path/.last_deploy_utc > /dev/null && rm -f $REMOTE_HOME/$zip_name" || return 1
|
|
|
|
wait_verify_static "$key" "$pre_pv" "$pre_bn"
|
|
post_cleanup "$key"
|
|
|
|
local elapsed=$((SECONDS - start))
|
|
printf '\n'; ok "--- [Done] $label deployed! ---"
|
|
[ -n "$domain" ] && warn "Site: https://$domain"
|
|
info "Change Note: $NOTE"
|
|
dim "Deploy Time: $(printf '%02d:%02d' $((elapsed / 60)) $((elapsed % 60))) (${elapsed}s)"
|
|
}
|
|
|
|
deploy_next() { # <key>
|
|
local key="$1" start=$SECONDS root remote_path app_svc zip_name zip_local excl=()
|
|
local pre_pv="" pre_bn="" label domain db_user db_name prod_port
|
|
root="$(zproj_root "$key")"
|
|
remote_path="$(zproj "$key" .remote.path)"
|
|
app_svc="$(zproj "$key" .remote.appService)"; [ -n "$app_svc" ] || app_svc="web"
|
|
zip_name="$(deploy_zip_name "$key")"
|
|
zip_local="$TEMP_ROOT/$zip_name"
|
|
label="$(zproj "$key" .label)"
|
|
domain="$(zproj "$key" .domain)"
|
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
|
|
|
printf '\n'; info "=== $label deploy (nextjs) ==="
|
|
dim "Local zip: $zip_local"
|
|
|
|
if [ -f "$root/public/build-version.json" ]; then
|
|
pre_pv="$(jq -r '.productVersion // empty' "$root/public/build-version.json")"
|
|
pre_bn="$(jq -r '.buildNumber // empty' "$root/public/build-version.json")"
|
|
[ -n "$pre_pv" ] && dim " Pre-zip build label: v${pre_pv}.${pre_bn} (server-side build will bump +1)"
|
|
fi
|
|
|
|
printf '\n'; info "--- [1] Zipping project files ---"
|
|
mapfile -t excl < <(z_archive_excludes "$key" 0)
|
|
z_archive "$root" "$zip_local" 0 "-" "${excl[@]}" || return 1
|
|
|
|
preflight_cleanup "$REMOTE_HOME/$zip_name" || { rm -f "$zip_local"; return 1; }
|
|
|
|
printf '\n'; info "--- [2] Uploading zip ---"
|
|
send_zip "$zip_local" "$zip_name" || { rm -f "$zip_local"; return 1; }
|
|
rm -f "$zip_local"
|
|
|
|
printf '\n'; info "--- [3] Unzipping and rebuilding on the server ---"
|
|
zec2_step "ensure unzip installed" "sudo apt-get update -qq && sudo apt-get install -y unzip" || return 1
|
|
zec2_step "ensure stack root" "sudo mkdir -p $STACK_ROOT && sudo chown ${EC2_USER}:${EC2_USER} $STACK_ROOT" || return 1
|
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
|
zec2_step "backup .env if present" "if [ -f $remote_path/.env ]; then cp $remote_path/.env $REMOTE_HOME/.env.${key}_bak; fi" || return 1
|
|
zec2_step "replace project directory" "sudo rm -rf $remote_path && sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
|
remote_unzip "$zip_name" "$remote_path" || return 1
|
|
zec2_step "restore .env from backup" "if [ -f $REMOTE_HOME/.env.${key}_bak ]; then cp $REMOTE_HOME/.env.${key}_bak $remote_path/.env; fi" || return 1
|
|
|
|
printf '\n'; info "--- [4] Docker compose rebuild ---"
|
|
zec2_step "docker compose down" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose down" || return 1
|
|
zec2_step "docker compose build" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose build" || return 1
|
|
zec2_step "docker compose up -d" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose up -d" || return 1
|
|
|
|
db_user="$(zproj "$key" .db.user)"; db_name="$(zproj "$key" .db.name)"
|
|
if [ -n "$db_user" ] && [ -n "$db_name" ]; then
|
|
zec2_step "wait for postgres ready" \
|
|
"cd $remote_path && for i in \$(seq 1 30); do sudo docker compose exec -T db pg_isready -U $db_user -d $db_name >/dev/null 2>&1 && break; sleep 2; done" || return 1
|
|
fi
|
|
if [ "$(zproj "$key" .migrations)" = "prisma" ]; then
|
|
zec2_step "apply prisma migrations" "cd $remote_path && sudo docker compose exec -T $app_svc npx prisma migrate deploy" || return 1
|
|
fi
|
|
zec2_step "record deploy timestamp; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remote_path/.last_deploy_utc > /dev/null && rm -f $REMOTE_HOME/$zip_name" || return 1
|
|
|
|
printf '\n'; info "--- [5] Verifying deployment ---"
|
|
prod_port="$(zproj "$key" .ports.prod)"
|
|
if [[ "$prod_port" =~ ^[0-9]+$ ]]; then
|
|
local direct="http://${EC2_IP}:${prod_port}/" deadline=$((SECONDS + 60)) code verified=0
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
sleep 4
|
|
code="$(http_code "$direct")"
|
|
if [ "$code" = "200" ]; then
|
|
ok " PASS - app is responding at $direct"
|
|
verified=1; break
|
|
fi
|
|
dim " App not ready yet - waiting..."
|
|
done
|
|
[ "$verified" -eq 1 ] || warn " WARNING: no response at $direct within 60s (is the port open in the security group?)."
|
|
fi
|
|
if [ -n "$pre_pv" ]; then
|
|
wait_verify_api "$key" "v${pre_pv}.$((pre_bn + 1))" 60 || true
|
|
else
|
|
warn " (No public/build-version.json - version verification skipped. See 'Enabling deploy verification' in README.)"
|
|
fi
|
|
|
|
post_cleanup "$key"
|
|
|
|
local elapsed=$((SECONDS - start))
|
|
printf '\n'; ok "--- [Done] $label deployed! ---"
|
|
[ -n "$domain" ] && warn "Site: https://$domain"
|
|
info "Change Note: $NOTE"
|
|
dim "Deploy Time: $(printf '%02d:%02d' $((elapsed / 60)) $((elapsed % 60))) (${elapsed}s)"
|
|
}
|
|
|
|
deploy_edge() { # <key>
|
|
local key="$1" root remote_path pc label cert_mount="" rm_stale="" f
|
|
root="$(zproj_root "$key")"
|
|
remote_path="$(zproj "$key" .remote.path)"
|
|
pc="$(zproj "$key" .proxyContainer)"
|
|
label="$(zproj "$key" .label)"
|
|
|
|
printf '\n'; info "=== $label deploy (edge nginx ingress) ==="
|
|
[ -d "$root" ] || { err "Edge root not found: $root"; return 1; }
|
|
for f in docker-compose.yml nginx.conf; do
|
|
[ -f "$root/$f" ] || { err "Missing $root/$f"; return 1; }
|
|
done
|
|
|
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
|
zec2_step "ensure edge dir" "sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
|
|
|
# Ship every top-level file in the edge folder — nginx.conf, compose, css,
|
|
# htpasswd, whatever the proxy serves. Subdirectories (logs, certs) stay put.
|
|
while IFS= read -r -d '' f; do
|
|
info " >> uploading $(basename "$f")"
|
|
scp -i "$(zec2_pem)" "$f" "$(zec2_target):$remote_path/" \
|
|
|| { err "SCP failed for $(basename "$f")"; return 1; }
|
|
done < <(find "$root" -maxdepth 1 -type f ! -name nul -print0)
|
|
|
|
local certs; certs="$(zproj "$key" .certsSource)"
|
|
[ -n "$certs" ] && cert_mount="-v $certs:/etc/letsencrypt/:ro "
|
|
zec2_step "validate new nginx.conf" \
|
|
"sudo docker run --rm -v $remote_path/nginx.conf:/etc/nginx/nginx.conf:ro ${cert_mount}nginx:1.27-alpine nginx -t -c /etc/nginx/nginx.conf" || return 1
|
|
|
|
# A container from an older compose project may still hold the proxy name;
|
|
# docker refuses a second create with the same name, so remove it first.
|
|
[ -n "$pc" ] && rm_stale="; sudo docker rm -f $pc 2>/dev/null || true"
|
|
zec2_step "edge: compose down + remove stale proxy" "cd $remote_path && sudo docker compose down 2>/dev/null || true$rm_stale" || return 1
|
|
zec2_step "edge compose up -d" "cd $remote_path && sudo docker compose up -d" || return 1
|
|
[ -n "$pc" ] && { zec2_step "edge nginx reload" "sudo docker exec $pc nginx -s reload || true" || return 1; }
|
|
zec2_step "fix nginx-logs permissions (if present)" \
|
|
"if [ -d $remote_path/nginx-logs ]; then sudo chmod 777 $remote_path/nginx-logs; sudo chmod 666 $remote_path/nginx-logs/*.log 2>/dev/null || true; fi" || return 1
|
|
ok "--- [Done] Edge proxy deploy finished ---"
|
|
}
|
|
|
|
deploy_docker() { # <key>
|
|
local key="$1" root remote_path label f domain
|
|
root="$(zproj_root "$key")"
|
|
remote_path="$(zproj "$key" .remote.path)"
|
|
label="$(zproj "$key" .label)"
|
|
domain="$(zproj "$key" .domain)"
|
|
|
|
printf '\n'; info "=== $label deploy (docker compose) ==="
|
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
|
[ -f "$root/docker-compose.yml" ] || { err "Missing $root/docker-compose.yml"; return 1; }
|
|
|
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
|
zec2_step "ensure project dir" "sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
|
|
|
while IFS= read -r -d '' f; do
|
|
info " >> uploading $(basename "$f")"
|
|
scp -i "$(zec2_pem)" "$f" "$(zec2_target):$remote_path/" \
|
|
|| { err "SCP failed for $(basename "$f")"; return 1; }
|
|
done < <(find "$root" -maxdepth 1 -type f ! -name nul -print0)
|
|
|
|
zec2_step "docker compose pull" "cd $remote_path && sudo docker compose pull" || return 1
|
|
zec2_step "docker compose up -d" "cd $remote_path && sudo docker compose up -d" || return 1
|
|
|
|
post_cleanup "$key"
|
|
printf '\n'; ok "--- [Done] $label deploy finished ---"
|
|
[ -n "$domain" ] && warn "Site: https://$domain"
|
|
}
|
|
|
|
# ── Dispatch ─────────────────────────────────────────────────────────────────
|
|
|
|
for key in "${projects[@]}"; do
|
|
zproj_require "$key"
|
|
deploy_git_pull "$key" || exit 1
|
|
kind="$(zproj "$key" .kind)"
|
|
case "$kind" in
|
|
python) deploy_python "$key" || exit 1 ;;
|
|
vite) deploy_vite "$key" || exit 1 ;;
|
|
nextjs) deploy_next "$key" || exit 1 ;;
|
|
edge) deploy_edge "$key" || exit 1 ;;
|
|
docker) deploy_docker "$key" || exit 1 ;;
|
|
*) err "No deploy handler for kind '$kind' (project '$key'). Add a deploy_<kind> function in zdeploy."; exit 1 ;;
|
|
esac
|
|
done
|