Files
CurrenciCombo/scripts/deployment/deploy-currencicombo-8604.sh
Devin AI 361776ab2e
Some checks failed
CI / Frontend Lint (pull_request) Failing after 7s
CI / Frontend Type Check (pull_request) Failing after 6s
CI / Frontend Build (pull_request) Failing after 8s
CI / Frontend E2E Tests (pull_request) Failing after 8s
CI / Orchestrator Build (pull_request) Failing after 7s
CI / Orchestrator Unit Tests (pull_request) Failing after 6s
CI / Orchestrator E2E (Testcontainers) (pull_request) Has been skipped
CI / Contracts Compile (pull_request) Failing after 6s
CI / Contracts Test (pull_request) Failing after 7s
Code Quality / SonarQube Analysis (pull_request) Failing after 19s
Code Quality / Code Quality Checks (pull_request) Failing after 6s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 4s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 5s
PR AA: Phoenix / systemd deployment scaffolding (migrate Phoenix off Next.js stub)
Closes the gap between Gitea main (b48eb2a, Vite portal + Node
orchestrator, 29 PRs merged, 167 tests) and what's actually serving
curucombo.xn--vov0g.com (Next.js 'ISO-20022 Combo Flow' app from an
unpushed local b118b2b checkout). After this PR is merged and the
runbook in scripts/deployment/README.md is followed on CT 8604, the
Phoenix deployment will serve d-bis/CurrenciCombo main.

Artifacts (all under scripts/deployment/):
- systemd/currencicombo-orchestrator.service  - Node orchestrator,
  EnvironmentFile=/etc/currencicombo/orchestrator.env, full systemd
  hardening (ProtectSystem=strict, PrivateTmp, no caps).
- systemd/currencicombo-webapp.service        - nginx serving Vite
  SPA on :3000 via RuntimeDirectory=/run/currencicombo-webapp.
- webapp-nginx.conf                            - self-contained nginx
  config; intentionally 421s on /api/* and /events/* so an NPMplus
  misconfig fails loudly instead of silently returning index.html.
- .env.prod.example                            - template for
  /etc/currencicombo/orchestrator.env. Documents every EXT-* blocker
  env var 1:1 with the Proxmox repo's check-external-dependencies.sh.
- install.sh                                   - idempotent host setup:
  user, dirs, nginx, fresh Postgres role/DB (--force-recreate-db to
  wipe), Redis autodetect, env file with auto-generated
  EVENT_SIGNING_SECRET + 3 API keys, systemd units enabled but not
  started. --dry-run supported.
- deploy-currencicombo-8604.sh                 - build-and-swap deploy
  driver (the script deploy-targets.json / phoenix-deploy-api calls):
  git fetch/reset, orchestrator tsc build, portal vite build with
  VITE_ORCHESTRATOR_URL baked in, migrations, timestamped backup,
  systemctl stop, rsync, systemctl start, smoke /ready + portal /,
  grep EXT-* from journalctl. --ref, --dry-run, --skip-migrate,
  --skip-build, --rollback.
- README.md                                    - architecture diagram,
  first-time setup (8 steps), NPMplus ingress rule table, subsequent-
  deploy one-liner, rollback, troubleshooting table, cutover-from-
  pre-existing-Next.js sequence, explicit list of Proxmox-side
  follow-ups.

Target-agnostic: no IP / hostname / VLAN hardcoded. The only file that
embeds the public hostname is README.md (for documentation) and the
default VITE_ORCHESTRATOR_URL in deploy-currencicombo-8604.sh (which
is overridable via env).

Single-origin NPMplus routing (confirmed with user):
  curucombo.\xe6\x9b\xbc\xe6\x9d\x8e.com/api/*     -> 10.160.0.14:8080  (orchestrator)
  curucombo.\xe6\x9b\xbc\xe6\x9d\x8e.com/events/*  -> 10.160.0.14:8080  (SSE)
  curucombo.\xe6\x9b\xbc\xe6\x9d\x8e.com/*         -> 10.160.0.14:3000  (Vite SPA)

Verified on this box (headless):
- shellcheck --severity=warning: clean on both scripts.
- bash -n: clean on both scripts.
- systemd-analyze verify: both unit files parse cleanly (only complaint
  is /usr/sbin/nginx not being executable, expected -- nginx is
  installed at deploy time).
- install.sh --dry-run: fails fast with the expected FATAL on hosts
  without psql (build box). On CT 8604 with Postgres+Redis already
  installed, it walks through every step.
- deploy-currencicombo-8604.sh --help: prints the usage.

No runtime code changes. Non-UI. Complements PR #30 (docker-compose
sandbox) which remains the local-dev path.

Proxmox-side follow-up (separate commit on /home/intlc/projects/proxmox
after this PR merges and cutover runs cleanly):
- Update phoenix-deploy-api/deploy-targets.json to point at
  scripts/deployment/deploy-currencicombo-8604.sh.
- Retire the inaccurate "Next.js webapp with ignoreBuildErrors"
  language in EXTERNAL_DEPENDENCY_BLOCKERS.md.

Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
2026-04-22 23:05:18 +00:00

198 lines
8.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# deploy-currencicombo-8604.sh — build-and-swap deploy for CurrenciCombo.
#
# Runs on a systemd host that has already had `install.sh` applied once.
# This is the script referenced by the Proxmox repo's
# `phoenix-deploy-api/deploy-targets.json` tuple
# (repo=d-bis/CurrenciCombo, branch=main, target=default).
#
# Steps (each idempotent, each can be --dry-run'd):
# 1. git clone/pull /var/lib/currencicombo/repo to the target ref.
# 2. Build orchestrator (npm ci + npm run build).
# 3. Build portal/webapp (npm ci + npm run build), baking
# VITE_ORCHESTRATOR_URL into the bundle.
# 4. Run DB migrations (npm run migrate in orchestrator/).
# 5. Stop systemd units.
# 6. rsync build output into /opt/currencicombo/{orchestrator,webapp}.
# 7. Start systemd units.
# 8. Smoke-test /ready + portal / + print EXT-* blocker summary.
#
# Rollback: `--rollback` restores the previous backup under
# /var/lib/currencicombo/backups/<timestamp>.
#
# CT 8604 is in the filename for ops-grep-ability; the script itself is
# host-agnostic. Override paths via env vars if you run it elsewhere.
set -euo pipefail
# ----- defaults (override via env) ------------------------------------
: "${CC_GIT_REMOTE:=https://gitea.d-bis.org/d-bis/CurrenciCombo.git}"
: "${CC_GIT_REF:=main}"
: "${CC_REPO_DIR:=/var/lib/currencicombo/repo}"
: "${CC_APP_HOME:=/opt/currencicombo}"
: "${CC_BACKUP_DIR:=/var/lib/currencicombo/backups}"
: "${CC_USER:=currencicombo}"
# Portal build-time env. The NPMplus ingress path-routes /api/* and
# /events/* to the orchestrator, so same-origin works.
: "${VITE_ORCHESTRATOR_URL:=https://curucombo.xn--vov0g.com}"
: "${ORCHESTRATOR_UNIT:=currencicombo-orchestrator.service}"
: "${WEBAPP_UNIT:=currencicombo-webapp.service}"
: "${CC_HEALTH_URL:=http://127.0.0.1:8080/ready}"
: "${CC_PORTAL_URL:=http://127.0.0.1:3000/}"
: "${CC_HEALTH_TIMEOUT_SECS:=60}"
# ----- flags ----------------------------------------------------------
DRY_RUN=0
SKIP_MIGRATE=0
SKIP_BUILD=0
DO_ROLLBACK=0
usage() {
cat <<'USAGE'
Usage: sudo ./deploy-currencicombo-8604.sh [flags]
Flags:
--ref=<git-ref> Override CC_GIT_REF (default: main)
--dry-run Print commands, don't run them
--skip-migrate Skip `npm run migrate` step (use for hotfix
deploys where schema hasn't changed)
--skip-build Reuse the existing build in CC_REPO_DIR/dist
(useful after `--dry-run --skip-build=no` from
the previous run)
--rollback Restore the most recent backup and restart.
Does not run git/build/migrate.
-h, --help This help
Env overrides:
CC_GIT_REMOTE, CC_GIT_REF, CC_REPO_DIR, CC_APP_HOME, CC_BACKUP_DIR,
CC_USER, VITE_ORCHESTRATOR_URL, ORCHESTRATOR_UNIT, WEBAPP_UNIT,
CC_HEALTH_URL, CC_PORTAL_URL, CC_HEALTH_TIMEOUT_SECS
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--ref=*) CC_GIT_REF="${1#*=}"; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--skip-migrate) SKIP_MIGRATE=1; shift ;;
--skip-build) SKIP_BUILD=1; shift ;;
--rollback) DO_ROLLBACK=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
log() { printf '[deploy] %s\n' "$*" >&2; }
warn() { printf '[deploy][WARN] %s\n' "$*" >&2; }
die() { printf '[deploy][FATAL] %s\n' "$*" >&2; exit 1; }
run() { if [[ "${DRY_RUN}" -eq 1 ]]; then printf '[deploy][dry-run] %s\n' "$*" >&2; else eval "$*"; fi; }
runcc() { if [[ "${DRY_RUN}" -eq 1 ]]; then printf '[deploy][dry-run][as %s] %s\n' "${CC_USER}" "$*" >&2; else sudo -u "${CC_USER}" -H bash -lc "$*"; fi; }
[[ "$EUID" -eq 0 ]] || die "must run as root (sudo)"
# ----- rollback fast-path ---------------------------------------------
if [[ "${DO_ROLLBACK}" -eq 1 ]]; then
LATEST="$(ls -1dt "${CC_BACKUP_DIR}"/* 2>/dev/null | head -1 || true)"
[[ -n "${LATEST}" ]] || die "no backup under ${CC_BACKUP_DIR}"
log "rolling back to ${LATEST}"
run "systemctl stop '${WEBAPP_UNIT}' '${ORCHESTRATOR_UNIT}'"
run "rsync -a --delete '${LATEST}/orchestrator/' '${CC_APP_HOME}/orchestrator/'"
run "rsync -a --delete '${LATEST}/webapp/' '${CC_APP_HOME}/webapp/'"
run "systemctl start '${ORCHESTRATOR_UNIT}' '${WEBAPP_UNIT}'"
log "rollback applied. systemctl status ${ORCHESTRATOR_UNIT} to verify."
exit 0
fi
# ----- 1. git ---------------------------------------------------------
if [[ ! -d "${CC_REPO_DIR}/.git" ]]; then
log "cloning ${CC_GIT_REMOTE}${CC_REPO_DIR}"
run "install -d -o '${CC_USER}' -g '${CC_USER}' -m 0755 '${CC_REPO_DIR}'"
runcc "git clone '${CC_GIT_REMOTE}' '${CC_REPO_DIR}'"
fi
runcc "cd '${CC_REPO_DIR}' && git fetch --prune origin"
runcc "cd '${CC_REPO_DIR}' && git reset --hard 'origin/${CC_GIT_REF}'"
REF_SHA="$(sudo -u "${CC_USER}" git -C "${CC_REPO_DIR}" rev-parse --short HEAD 2>/dev/null || echo unknown)"
log "repo at ${CC_GIT_REF} = ${REF_SHA}"
# ----- 2. orchestrator build -----------------------------------------
if [[ "${SKIP_BUILD}" -eq 0 ]]; then
log "building orchestrator"
runcc "cd '${CC_REPO_DIR}/orchestrator' && npm ci --no-audit --no-fund"
runcc "cd '${CC_REPO_DIR}/orchestrator' && npm run build"
log "building portal (VITE_ORCHESTRATOR_URL=${VITE_ORCHESTRATOR_URL})"
runcc "cd '${CC_REPO_DIR}' && npm ci --include=optional --no-audit --no-fund || npm ci --include=optional --force --no-audit --no-fund"
runcc "cd '${CC_REPO_DIR}' && VITE_ORCHESTRATOR_URL='${VITE_ORCHESTRATOR_URL}' npm run build"
else
log "skipping builds (--skip-build)"
fi
# ----- 3. migrations --------------------------------------------------
if [[ "${SKIP_MIGRATE}" -eq 0 ]]; then
log "running DB migrations"
runcc "cd '${CC_REPO_DIR}/orchestrator' && npm run migrate"
else
log "skipping migrations (--skip-migrate)"
fi
# ----- 4. backup previous install ------------------------------------
TS="$(date +%Y%m%d-%H%M%S)"
BACKUP="${CC_BACKUP_DIR}/${TS}"
if [[ -d "${CC_APP_HOME}/orchestrator/dist" || -d "${CC_APP_HOME}/webapp/dist" ]]; then
log "backing up current install → ${BACKUP}"
run "install -d -o root -g root -m 0700 '${BACKUP}/orchestrator' '${BACKUP}/webapp'"
run "rsync -a '${CC_APP_HOME}/orchestrator/' '${BACKUP}/orchestrator/'"
run "rsync -a '${CC_APP_HOME}/webapp/' '${BACKUP}/webapp/'"
fi
# ----- 5. stop units --------------------------------------------------
log "stopping systemd units"
run "systemctl stop '${WEBAPP_UNIT}' || true"
run "systemctl stop '${ORCHESTRATOR_UNIT}' || true"
# ----- 6. swap in new build ------------------------------------------
log "rsyncing new build into ${CC_APP_HOME}"
# Orchestrator: dist/ + node_modules/ + package.json + package-lock.json
runcc "rsync -a --delete '${CC_REPO_DIR}/orchestrator/dist/' '${CC_APP_HOME}/orchestrator/dist/'"
runcc "rsync -a '${CC_REPO_DIR}/orchestrator/node_modules/' '${CC_APP_HOME}/orchestrator/node_modules/'"
runcc "cp '${CC_REPO_DIR}/orchestrator/package.json' '${CC_APP_HOME}/orchestrator/package.json'"
runcc "cp '${CC_REPO_DIR}/orchestrator/package-lock.json' '${CC_APP_HOME}/orchestrator/package-lock.json'"
# Webapp: dist/
runcc "rsync -a --delete '${CC_REPO_DIR}/dist/' '${CC_APP_HOME}/webapp/dist/'"
# ----- 7. start units ------------------------------------------------
log "starting systemd units"
run "systemctl start '${ORCHESTRATOR_UNIT}'"
run "systemctl start '${WEBAPP_UNIT}'"
# ----- 8. smoke -------------------------------------------------------
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "dry-run: skipping smoke test"
exit 0
fi
log "waiting up to ${CC_HEALTH_TIMEOUT_SECS}s for orchestrator ${CC_HEALTH_URL}"
SECS=0
until curl -sfL --max-time 3 "${CC_HEALTH_URL}" >/dev/null 2>&1; do
SECS=$((SECS + 2))
if [[ "${SECS}" -ge "${CC_HEALTH_TIMEOUT_SECS}" ]]; then
journalctl -u "${ORCHESTRATOR_UNIT}" -n 80 --no-pager || true
die "orchestrator did not become ready after ${CC_HEALTH_TIMEOUT_SECS}s"
fi
sleep 2
done
log "orchestrator ready: $(curl -sf "${CC_HEALTH_URL}")"
log "probing portal ${CC_PORTAL_URL}"
PORTAL_CODE="$(curl -s -o /dev/null -w '%{http_code}' "${CC_PORTAL_URL}" || echo ERR)"
[[ "${PORTAL_CODE}" =~ ^2 ]] || die "portal returned HTTP ${PORTAL_CODE}"
log "portal OK (HTTP ${PORTAL_CODE})"
log "EXT-* blocker summary from orchestrator boot log:"
journalctl -u "${ORCHESTRATOR_UNIT}" --no-pager -n 200 \
| grep -E 'ExternalBlockers|EXT-[A-Z0-9-]+' | tail -20 || true
log "deploy complete. ref=${CC_GIT_REF} sha=${REF_SHA} ts=${TS}"