phoenix: automate CurrenciCombo e2e deploys
Some checks failed
Deploy to Phoenix / validate (push) Successful in 13s
Deploy to Phoenix / deploy (push) Successful in 37s
phoenix-deploy Phoenix deployment in progress
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Failing after 30s
Deploy to Phoenix / cloudflare (push) Has been skipped
Some checks failed
Deploy to Phoenix / validate (push) Successful in 13s
Deploy to Phoenix / deploy (push) Successful in 37s
phoenix-deploy Phoenix deployment in progress
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Failing after 30s
Deploy to Phoenix / cloudflare (push) Has been skipped
This commit is contained in:
@@ -7,13 +7,11 @@
|
||||
"repo": "d-bis/proxmox",
|
||||
"branch": "main",
|
||||
"target": "default",
|
||||
"description": "Deploy the Phoenix deploy API bundle to the dev VM on Proxmox.",
|
||||
"description": "Install the Phoenix deploy API locally on the dev VM from the synced repo workspace.",
|
||||
"cwd": "${PHOENIX_REPO_ROOT}",
|
||||
"command": [
|
||||
"bash",
|
||||
"scripts/deployment/deploy-phoenix-deploy-api-to-dev-vm.sh",
|
||||
"--apply",
|
||||
"--start-ct"
|
||||
"phoenix-deploy-api/scripts/install-systemd.sh"
|
||||
],
|
||||
"required_env": [
|
||||
"PHOENIX_REPO_ROOT"
|
||||
@@ -80,6 +78,29 @@
|
||||
"timeout_ms": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"repo": "d-bis/CurrenciCombo",
|
||||
"branch": "main",
|
||||
"target": "default",
|
||||
"description": "Deploy CurrenciCombo from the staged Gitea workspace into Phoenix CT 8604 and verify the public hostname end to end.",
|
||||
"cwd": "${PHOENIX_REPO_ROOT}",
|
||||
"command": [
|
||||
"bash",
|
||||
"scripts/deployment/phoenix-deploy-currencicombo-from-workspace.sh"
|
||||
],
|
||||
"required_env": [
|
||||
"PHOENIX_REPO_ROOT",
|
||||
"PHOENIX_DEPLOY_WORKSPACE"
|
||||
],
|
||||
"healthcheck": {
|
||||
"url": "https://curucombo.xn--vov0g.com/api/ready",
|
||||
"expect_status": 200,
|
||||
"expect_body_includes": "\"ready\":true",
|
||||
"attempts": 12,
|
||||
"delay_ms": 5000,
|
||||
"timeout_ms": 15000
|
||||
}
|
||||
},
|
||||
{
|
||||
"repo": "d-bis/proxmox",
|
||||
"branch": "main",
|
||||
@@ -106,13 +127,11 @@
|
||||
"repo": "d-bis/proxmox",
|
||||
"branch": "master",
|
||||
"target": "default",
|
||||
"description": "Deploy the Phoenix deploy API bundle to the dev VM on Proxmox.",
|
||||
"description": "Install the Phoenix deploy API locally on the dev VM from the synced repo workspace.",
|
||||
"cwd": "${PHOENIX_REPO_ROOT}",
|
||||
"command": [
|
||||
"bash",
|
||||
"scripts/deployment/deploy-phoenix-deploy-api-to-dev-vm.sh",
|
||||
"--apply",
|
||||
"--start-ct"
|
||||
"phoenix-deploy-api/scripts/install-systemd.sh"
|
||||
],
|
||||
"required_env": [
|
||||
"PHOENIX_REPO_ROOT"
|
||||
@@ -200,6 +219,29 @@
|
||||
"delay_ms": 5000,
|
||||
"timeout_ms": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"repo": "d-bis/CurrenciCombo",
|
||||
"branch": "master",
|
||||
"target": "default",
|
||||
"description": "Deploy CurrenciCombo from the staged Gitea workspace into Phoenix CT 8604 and verify the public hostname end to end.",
|
||||
"cwd": "${PHOENIX_REPO_ROOT}",
|
||||
"command": [
|
||||
"bash",
|
||||
"scripts/deployment/phoenix-deploy-currencicombo-from-workspace.sh"
|
||||
],
|
||||
"required_env": [
|
||||
"PHOENIX_REPO_ROOT",
|
||||
"PHOENIX_DEPLOY_WORKSPACE"
|
||||
],
|
||||
"healthcheck": {
|
||||
"url": "https://curucombo.xn--vov0g.com/api/ready",
|
||||
"expect_status": 200,
|
||||
"expect_body_includes": "\"ready\":true",
|
||||
"attempts": 12,
|
||||
"delay_ms": 5000,
|
||||
"timeout_ms": 15000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import https from 'https';
|
||||
import path from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { execFile as execFileCallback } from 'child_process';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import express from 'express';
|
||||
|
||||
@@ -31,6 +31,13 @@ const PORT = parseInt(process.env.PORT || '4001', 10);
|
||||
const GITEA_URL = (process.env.GITEA_URL || 'https://gitea.d-bis.org').replace(/\/$/, '');
|
||||
const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
|
||||
const WEBHOOK_SECRET = process.env.PHOENIX_DEPLOY_SECRET || '';
|
||||
const PHOENIX_REPO_ROOT_DEFAULT = (process.env.PHOENIX_REPO_ROOT_DEFAULT || '/srv/projects/proxmox').trim();
|
||||
const ATOMIC_SWAP_REPO = (process.env.PHOENIX_ATOMIC_SWAP_REPO || 'd-bis/atomic-swap-dapp').trim();
|
||||
const ATOMIC_SWAP_REF = (process.env.PHOENIX_ATOMIC_SWAP_REF || 'main').trim();
|
||||
const CROSS_CHAIN_PMM_LPS_REPO = (process.env.PHOENIX_CROSS_CHAIN_PMM_LPS_REPO || '').trim();
|
||||
const CROSS_CHAIN_PMM_LPS_REF = (process.env.PHOENIX_CROSS_CHAIN_PMM_LPS_REF || 'main').trim();
|
||||
const SMOM_DBIS_138_REPO = (process.env.PHOENIX_SMOM_DBIS_138_REPO || '').trim();
|
||||
const SMOM_DBIS_138_REF = (process.env.PHOENIX_SMOM_DBIS_138_REF || 'main').trim();
|
||||
|
||||
const PROXMOX_HOST = process.env.PROXMOX_HOST || '';
|
||||
const PROXMOX_PORT = parseInt(process.env.PROXMOX_PORT || '8006', 10);
|
||||
@@ -47,9 +54,13 @@ const PARTNER_KEYS = (process.env.PHOENIX_PARTNER_KEYS || '').split(',').map((k)
|
||||
const WEBHOOK_DEPLOY_ENABLED = process.env.PHOENIX_WEBHOOK_DEPLOY_ENABLED === '1' || process.env.PHOENIX_WEBHOOK_DEPLOY_ENABLED === 'true';
|
||||
const execFile = promisify(execFileCallback);
|
||||
|
||||
function expandEnvTokens(value) {
|
||||
function expandEnvTokens(value, env = process.env) {
|
||||
if (typeof value !== 'string') return value;
|
||||
return value.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, key) => process.env[key] || '');
|
||||
return value.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, key) => env[key] || '');
|
||||
}
|
||||
|
||||
function resolvePhoenixRepoRoot() {
|
||||
return (process.env.PHOENIX_REPO_ROOT || PHOENIX_REPO_ROOT_DEFAULT || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,25 +166,151 @@ async function verifyHealthCheck(healthcheck) {
|
||||
throw new Error(`Health check failed for ${healthcheck.url}: ${lastError?.message || 'unknown error'}`);
|
||||
}
|
||||
|
||||
async function runDeployTarget(definition, configDefaults, context) {
|
||||
async function downloadRepoArchive({ owner, repo, ref, archivePath, authToken }) {
|
||||
const archiveRef = `${ref}.tar.gz`;
|
||||
const url = `${GITEA_URL}/api/v1/repos/${owner}/${repo}/archive/${archiveRef}`;
|
||||
const headers = {};
|
||||
if (authToken) headers.Authorization = `token ${authToken}`;
|
||||
const res = await fetch(url, { headers });
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download archive ${owner}/${repo}@${ref}: HTTP ${res.status}`);
|
||||
}
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
writeFileSync(archivePath, buffer);
|
||||
}
|
||||
|
||||
function syncExtractedTree({ sourceRoot, destRoot, entries = null }) {
|
||||
mkdirSync(destRoot, { recursive: true });
|
||||
const selectedEntries = Array.isArray(entries) ? entries : readdirSync(sourceRoot);
|
||||
for (const entry of selectedEntries) {
|
||||
const sourcePath = path.join(sourceRoot, entry);
|
||||
if (!existsSync(sourcePath)) continue;
|
||||
const destPath = path.join(destRoot, entry);
|
||||
rmSync(destPath, { recursive: true, force: true });
|
||||
cpSync(sourcePath, destPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function syncRepoArchive({ owner, repo, ref, destRoot, entries = null, authToken = '' }) {
|
||||
const tempDir = mkdtempSync('/tmp/phoenix-archive-');
|
||||
const archivePath = path.join(tempDir, 'repo.tar.gz');
|
||||
const extractDir = path.join(tempDir, 'extract');
|
||||
mkdirSync(extractDir, { recursive: true });
|
||||
try {
|
||||
await downloadRepoArchive({ owner, repo, ref, archivePath, authToken });
|
||||
await execFile('tar', ['-xzf', archivePath, '-C', extractDir]);
|
||||
const [rootDir] = readdirSync(extractDir);
|
||||
if (!rootDir) {
|
||||
throw new Error(`Archive for ${owner}/${repo}@${ref} was empty`);
|
||||
}
|
||||
syncExtractedTree({
|
||||
sourceRoot: path.join(extractDir, rootDir),
|
||||
destRoot,
|
||||
entries,
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareDeployWorkspace({ repo, branch, sha, target }) {
|
||||
const repoRoot = resolvePhoenixRepoRoot();
|
||||
if (!repoRoot) {
|
||||
throw new Error('PHOENIX_REPO_ROOT is not configured');
|
||||
}
|
||||
|
||||
const [owner, repoName] = repo.includes('/') ? repo.split('/') : ['d-bis', repo];
|
||||
const externalWorkspaceRoot = path.join(repoRoot, '.phoenix-deploy-workspaces', owner, repoName);
|
||||
|
||||
// Manual smoke tests can target the already-staged local workspace without
|
||||
// forcing an archive sync from Gitea.
|
||||
if (sha === 'HEAD' || sha === 'local') {
|
||||
mkdirSync(repoRoot, { recursive: true });
|
||||
if (repo !== 'd-bis/proxmox') {
|
||||
mkdirSync(externalWorkspaceRoot, { recursive: true });
|
||||
}
|
||||
return {
|
||||
PHOENIX_REPO_ROOT: repoRoot,
|
||||
PROXMOX_REPO_ROOT: repoRoot,
|
||||
PHOENIX_DEPLOY_WORKSPACE: repo === 'd-bis/proxmox' ? repoRoot : externalWorkspaceRoot,
|
||||
};
|
||||
}
|
||||
|
||||
const ref = sha || branch || 'main';
|
||||
|
||||
if (repo === 'd-bis/proxmox') {
|
||||
await syncRepoArchive({
|
||||
owner,
|
||||
repo: repoName,
|
||||
ref,
|
||||
destRoot: repoRoot,
|
||||
entries: ['config', 'phoenix-deploy-api', 'reports', 'scripts', 'token-lists'],
|
||||
authToken: GITEA_TOKEN,
|
||||
});
|
||||
} else {
|
||||
await syncRepoArchive({
|
||||
owner,
|
||||
repo: repoName,
|
||||
ref,
|
||||
destRoot: externalWorkspaceRoot,
|
||||
authToken: GITEA_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
if (repo === 'd-bis/proxmox' && target === 'atomic-swap-dapp-live') {
|
||||
const [swapOwner, swapRepo] = ATOMIC_SWAP_REPO.includes('/')
|
||||
? ATOMIC_SWAP_REPO.split('/')
|
||||
: ['d-bis', ATOMIC_SWAP_REPO];
|
||||
await syncRepoArchive({
|
||||
owner: swapOwner,
|
||||
repo: swapRepo,
|
||||
ref: ATOMIC_SWAP_REF,
|
||||
destRoot: path.join(repoRoot, 'atomic-swap-dapp'),
|
||||
authToken: GITEA_TOKEN,
|
||||
});
|
||||
|
||||
if (CROSS_CHAIN_PMM_LPS_REPO) {
|
||||
const [lpsOwner, lpsRepo] = CROSS_CHAIN_PMM_LPS_REPO.includes('/')
|
||||
? CROSS_CHAIN_PMM_LPS_REPO.split('/')
|
||||
: ['d-bis', CROSS_CHAIN_PMM_LPS_REPO];
|
||||
await syncRepoArchive({
|
||||
owner: lpsOwner,
|
||||
repo: lpsRepo,
|
||||
ref: CROSS_CHAIN_PMM_LPS_REF,
|
||||
destRoot: path.join(repoRoot, 'cross-chain-pmm-lps'),
|
||||
authToken: GITEA_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
if (SMOM_DBIS_138_REPO) {
|
||||
const [smomOwner, smomRepo] = SMOM_DBIS_138_REPO.includes('/')
|
||||
? SMOM_DBIS_138_REPO.split('/')
|
||||
: ['d-bis', SMOM_DBIS_138_REPO];
|
||||
await syncRepoArchive({
|
||||
owner: smomOwner,
|
||||
repo: smomRepo,
|
||||
ref: SMOM_DBIS_138_REF,
|
||||
destRoot: path.join(repoRoot, 'smom-dbis-138'),
|
||||
authToken: GITEA_TOKEN,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
PHOENIX_REPO_ROOT: repoRoot,
|
||||
PROXMOX_REPO_ROOT: repoRoot,
|
||||
PHOENIX_DEPLOY_WORKSPACE: repo === 'd-bis/proxmox' ? repoRoot : externalWorkspaceRoot,
|
||||
};
|
||||
}
|
||||
|
||||
async function runDeployTarget(definition, configDefaults, context, envOverrides = {}) {
|
||||
if (!Array.isArray(definition.command) || definition.command.length === 0) {
|
||||
throw new Error('Deploy target is missing a command array');
|
||||
}
|
||||
|
||||
const cwd = expandEnvTokens(definition.cwd || configDefaults.cwd || process.cwd());
|
||||
const timeoutSeconds = Number(definition.timeout_sec || configDefaults.timeout_sec || 1800);
|
||||
const timeout = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 1800 * 1000;
|
||||
const command = definition.command.map((part) => expandEnvTokens(part));
|
||||
const missingEnv = (definition.required_env || []).filter((key) => !process.env[key]);
|
||||
if (missingEnv.length > 0) {
|
||||
throw new Error(`Missing required env for deploy target: ${missingEnv.join(', ')}`);
|
||||
}
|
||||
if (!existsSync(cwd)) {
|
||||
throw new Error(`Deploy working directory does not exist: ${cwd}`);
|
||||
}
|
||||
|
||||
const childEnv = {
|
||||
...process.env,
|
||||
...envOverrides,
|
||||
PHOENIX_DEPLOY_REPO: context.repo,
|
||||
PHOENIX_DEPLOY_BRANCH: context.branch,
|
||||
PHOENIX_DEPLOY_SHA: context.sha || '',
|
||||
@@ -181,6 +318,18 @@ async function runDeployTarget(definition, configDefaults, context) {
|
||||
PHOENIX_DEPLOY_TRIGGER: context.trigger,
|
||||
};
|
||||
|
||||
const cwd = expandEnvTokens(definition.cwd || configDefaults.cwd || process.cwd(), childEnv);
|
||||
const timeoutSeconds = Number(definition.timeout_sec || configDefaults.timeout_sec || 1800);
|
||||
const timeout = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 1800 * 1000;
|
||||
const command = definition.command.map((part) => expandEnvTokens(part, childEnv));
|
||||
const missingEnv = (definition.required_env || []).filter((key) => !childEnv[key]);
|
||||
if (missingEnv.length > 0) {
|
||||
throw new Error(`Missing required env for deploy target: ${missingEnv.join(', ')}`);
|
||||
}
|
||||
if (!existsSync(cwd)) {
|
||||
throw new Error(`Deploy working directory does not exist: ${cwd}`);
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execFile(command[0], command.slice(1), {
|
||||
cwd,
|
||||
env: childEnv,
|
||||
@@ -237,15 +386,22 @@ async function executeDeploy({ repo, branch = 'main', target = 'default', sha =
|
||||
|
||||
let deployResult = null;
|
||||
let deployError = null;
|
||||
let envOverrides = {};
|
||||
|
||||
try {
|
||||
envOverrides = await prepareDeployWorkspace({
|
||||
repo,
|
||||
branch,
|
||||
sha: commitSha,
|
||||
target: wantedTarget,
|
||||
});
|
||||
deployResult = await runDeployTarget(match, config.defaults, {
|
||||
repo,
|
||||
branch,
|
||||
sha: commitSha,
|
||||
target: wantedTarget,
|
||||
trigger,
|
||||
});
|
||||
}, envOverrides);
|
||||
if (commitSha && GITEA_TOKEN) {
|
||||
await setGiteaCommitStatus(owner, repoName, commitSha, 'success', `Deployed to ${wantedTarget}`);
|
||||
}
|
||||
@@ -286,6 +442,7 @@ async function executeDeploy({ repo, branch = 'main', target = 'default', sha =
|
||||
success: Boolean(deployResult),
|
||||
command: deployResult?.command,
|
||||
cwd: deployResult?.cwd,
|
||||
phoenix_repo_root: envOverrides.PHOENIX_REPO_ROOT || null,
|
||||
error: deployError?.message || null,
|
||||
};
|
||||
const body = JSON.stringify(payload);
|
||||
|
||||
244
scripts/deployment/phoenix-deploy-currencicombo-from-workspace.sh
Executable file
244
scripts/deployment/phoenix-deploy-currencicombo-from-workspace.sh
Executable file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
source "$PROJECT_ROOT/scripts/lib/load-project-env.sh"
|
||||
source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true
|
||||
|
||||
PHOENIX_DEPLOY_WORKSPACE="${PHOENIX_DEPLOY_WORKSPACE:-}"
|
||||
PROXMOX_HOST="${PROXMOX_HOST_R630_01:-192.168.11.11}"
|
||||
PROXMOX_SSH_USER="${PROXMOX_SSH_USER:-root}"
|
||||
VMID="${CURRENCICOMBO_PHOENIX_VMID:-8604}"
|
||||
CT_IP="${IP_CURRENCICOMBO_PHOENIX:-10.160.0.14}"
|
||||
CT_REPO_DIR="${CT_REPO_DIR:-/var/lib/currencicombo/repo}"
|
||||
PUBLIC_URL="${PUBLIC_URL:-https://curucombo.xn--vov0g.com}"
|
||||
PUBLIC_DOMAIN="${PUBLIC_DOMAIN:-curucombo.xn--vov0g.com}"
|
||||
NPM_URL="${NPM_URL:-https://${IP_NPMPLUS:-192.168.11.167}:81}"
|
||||
NPM_EMAIL="${NPM_EMAIL:-}"
|
||||
NPM_PASSWORD="${NPM_PASSWORD:-}"
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: phoenix-deploy-currencicombo-from-workspace.sh [--dry-run]
|
||||
|
||||
Requires:
|
||||
PHOENIX_DEPLOY_WORKSPACE Full staged CurrenciCombo checkout prepared by phoenix-deploy-api
|
||||
|
||||
This script:
|
||||
1. Packs the staged repo workspace.
|
||||
2. Pushes it into CT 8604 on r630-01.
|
||||
3. Ensures host prerequisites, install.sh, prune cron, and deploy script run in-CT.
|
||||
4. Updates the public NPMplus host so /api/* preserves the full path and supports SSE.
|
||||
5. Verifies the public portal + /api/ready end to end.
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
log() { printf '[currencicombo-phoenix] %s\n' "$*" >&2; }
|
||||
die() { printf '[currencicombo-phoenix][FATAL] %s\n' "$*" >&2; exit 1; }
|
||||
run() { if [[ "$DRY_RUN" -eq 1 ]]; then printf '[dry-run] %s\n' "$*" >&2; else eval "$*"; fi; }
|
||||
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; }
|
||||
|
||||
for cmd in ssh scp tar curl jq mktemp; do
|
||||
need_cmd "$cmd"
|
||||
done
|
||||
|
||||
[[ -n "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "PHOENIX_DEPLOY_WORKSPACE is required"
|
||||
[[ -d "$PHOENIX_DEPLOY_WORKSPACE" ]] || die "staged workspace missing: $PHOENIX_DEPLOY_WORKSPACE"
|
||||
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
[[ -n "$NPM_EMAIL" ]] || die "NPM_EMAIL is required"
|
||||
[[ -n "$NPM_PASSWORD" ]] || die "NPM_PASSWORD is required"
|
||||
fi
|
||||
|
||||
SSH_TARGET="${PROXMOX_SSH_USER}@${PROXMOX_HOST}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new)
|
||||
TMP_DIR="$(mktemp -d /tmp/currencicombo-phoenix-XXXXXX)"
|
||||
ARCHIVE_PATH="${TMP_DIR}/currencicombo-workspace.tgz"
|
||||
REMOTE_ARCHIVE="/tmp/$(basename "$ARCHIVE_PATH")"
|
||||
CT_ARCHIVE="/root/$(basename "$ARCHIVE_PATH")"
|
||||
NPM_COOKIE_JAR="${TMP_DIR}/npm-cookies.txt"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
ssh_remote() {
|
||||
local cmd="$1"
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
printf '[dry-run] ssh %q %q\n' "$SSH_TARGET" "$cmd" >&2
|
||||
else
|
||||
ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "$cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
pct_exec_script() {
|
||||
local local_script="$1"
|
||||
local remote_script
|
||||
local ct_script
|
||||
remote_script="/tmp/$(basename "$local_script")"
|
||||
ct_script="/root/$(basename "$local_script")"
|
||||
run "scp ${SSH_OPTS[*]} '$local_script' '${SSH_TARGET}:${remote_script}'"
|
||||
ssh_remote "pct push ${VMID} '${remote_script}' '${ct_script}' --perms 0755 && rm -f '${remote_script}' && pct exec ${VMID} -- bash '${ct_script}' && pct exec ${VMID} -- rm -f '${ct_script}'"
|
||||
}
|
||||
|
||||
log "packing staged workspace from ${PHOENIX_DEPLOY_WORKSPACE}"
|
||||
run "tar -C '$PHOENIX_DEPLOY_WORKSPACE' --exclude='.git' --exclude='node_modules' --exclude='dist' --exclude='orchestrator/node_modules' --exclude='orchestrator/dist' -czf '$ARCHIVE_PATH' ."
|
||||
|
||||
log "ensuring CT ${VMID} is running on ${PROXMOX_HOST}"
|
||||
ssh_remote "pct start ${VMID} >/dev/null 2>&1 || true"
|
||||
|
||||
log "uploading staged archive to CT ${VMID}"
|
||||
run "scp ${SSH_OPTS[*]} '$ARCHIVE_PATH' '${SSH_TARGET}:${REMOTE_ARCHIVE}'"
|
||||
ssh_remote "pct push ${VMID} '${REMOTE_ARCHIVE}' '${CT_ARCHIVE}' && rm -f '${REMOTE_ARCHIVE}'"
|
||||
|
||||
CT_SCRIPT="${TMP_DIR}/currencicombo-ct-deploy.sh"
|
||||
cat > "$CT_SCRIPT" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
ARCHIVE_PATH="__CT_ARCHIVE__"
|
||||
REPO_DIR="__CT_REPO_DIR__"
|
||||
|
||||
need_pkg() {
|
||||
dpkg -s "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
apt-get update -qq
|
||||
for pkg in ca-certificates curl git jq postgresql redis-server rsync build-essential; do
|
||||
need_pkg "$pkg" || apt-get install -y -qq "$pkg"
|
||||
done
|
||||
|
||||
if ! command -v node >/dev/null 2>&1 || ! node -v 2>/dev/null | grep -q '^v20\.'; then
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y -qq nodejs
|
||||
fi
|
||||
|
||||
systemctl enable --now postgresql >/dev/null 2>&1 || true
|
||||
systemctl enable --now redis-server >/dev/null 2>&1 || true
|
||||
|
||||
if [[ ! -f /root/currencicombo-prephoenix-archive.tgz && -d /opt/currencicombo ]]; then
|
||||
tar -czf /root/currencicombo-prephoenix-archive.tgz /opt/currencicombo /etc/currencicombo 2>/dev/null || true
|
||||
fi
|
||||
|
||||
install -d -o root -g root -m 0755 "$(dirname "$REPO_DIR")"
|
||||
rm -rf "$REPO_DIR"
|
||||
mkdir -p "$REPO_DIR"
|
||||
tar -xzf "$ARCHIVE_PATH" -C "$REPO_DIR"
|
||||
rm -f "$ARCHIVE_PATH"
|
||||
|
||||
bash "$REPO_DIR/scripts/deployment/install.sh"
|
||||
bash "$REPO_DIR/scripts/deployment/install-prune-cron.sh"
|
||||
CC_GIT_REF=local bash "$REPO_DIR/scripts/deployment/deploy-currencicombo-8604.sh"
|
||||
systemctl is-active currencicombo-orchestrator.service currencicombo-webapp.service
|
||||
curl -fsS http://127.0.0.1:8080/ready
|
||||
curl -fsS http://127.0.0.1:3000/ >/dev/null
|
||||
EOF
|
||||
perl -0pi -e "s|__CT_ARCHIVE__|${CT_ARCHIVE//|/\\|}|g; s|__CT_REPO_DIR__|${CT_REPO_DIR//|/\\|}|g" "$CT_SCRIPT"
|
||||
|
||||
log "running install + deploy inside CT ${VMID}"
|
||||
pct_exec_script "$CT_SCRIPT"
|
||||
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
log "updating NPMplus proxy host for ${PUBLIC_DOMAIN}"
|
||||
AUTH_JSON="$(jq -nc --arg identity "$NPM_EMAIL" --arg secret "$NPM_PASSWORD" '{identity:$identity,secret:$secret}')"
|
||||
TOKEN_RESPONSE="$(curl -sk -X POST "$NPM_URL/api/tokens" -H 'Content-Type: application/json' -d "$AUTH_JSON" -c "$NPM_COOKIE_JAR")"
|
||||
TOKEN="$(echo "$TOKEN_RESPONSE" | jq -r '.token // .accessToken // .access_token // .data.token // empty' 2>/dev/null)"
|
||||
USE_COOKIE_AUTH=0
|
||||
if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
|
||||
if echo "$TOKEN_RESPONSE" | jq -e '.expires' >/dev/null 2>&1; then
|
||||
USE_COOKIE_AUTH=1
|
||||
else
|
||||
die "NPMplus authentication failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
npm_api() {
|
||||
if [[ "$USE_COOKIE_AUTH" -eq 1 ]]; then
|
||||
curl -sk -b "$NPM_COOKIE_JAR" "$@"
|
||||
else
|
||||
curl -sk -H "Authorization: Bearer $TOKEN" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
HOSTS_JSON="$(npm_api -X GET "$NPM_URL/api/nginx/proxy-hosts")"
|
||||
HOST_ID="$(echo "$HOSTS_JSON" | jq -r --arg domain "$PUBLIC_DOMAIN" '
|
||||
(if type == "array" then . elif .data != null then .data elif .result != null then .result else [] end)
|
||||
| map(select(.domain_names | type == "array"))
|
||||
| map(select(any(.domain_names[]; . == $domain)))
|
||||
| .[0].id // empty
|
||||
')"
|
||||
[[ -n "$HOST_ID" ]] || die "NPMplus proxy host not found for ${PUBLIC_DOMAIN}"
|
||||
|
||||
ADVANCED_CONFIG="$(cat <<CFG
|
||||
location ^~ /api/ {
|
||||
proxy_pass http://${CT_IP}:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_set_header Connection \"\";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 24h;
|
||||
proxy_send_timeout 24h;
|
||||
add_header Cache-Control \"no-cache\";
|
||||
}
|
||||
CFG
|
||||
)"
|
||||
|
||||
PAYLOAD="$(echo "$HOSTS_JSON" | jq -c --arg domain "$PUBLIC_DOMAIN" --arg host "$CT_IP" --arg advanced "$ADVANCED_CONFIG" '
|
||||
(if type == "array" then . elif .data != null then .data elif .result != null then .result else [] end)
|
||||
| map(select(.domain_names | type == "array"))
|
||||
| map(select(any(.domain_names[]; . == $domain)))
|
||||
| .[0]
|
||||
| {
|
||||
domain_names,
|
||||
forward_scheme: (.forward_scheme // "http"),
|
||||
forward_host: $host,
|
||||
forward_port: 3000,
|
||||
access_list_id,
|
||||
certificate_id,
|
||||
ssl_forced,
|
||||
caching_enabled,
|
||||
block_exploits,
|
||||
advanced_config: $advanced,
|
||||
allow_websocket_upgrade,
|
||||
http2_support,
|
||||
hsts_enabled,
|
||||
hsts_subdomains,
|
||||
enabled
|
||||
}
|
||||
')"
|
||||
[[ -n "$PAYLOAD" && "$PAYLOAD" != "null" ]] || die "failed to build NPMplus update payload"
|
||||
UPDATE_RESPONSE="$(npm_api -X PUT "$NPM_URL/api/nginx/proxy-hosts/${HOST_ID}" -H 'Content-Type: application/json' -d "$PAYLOAD")"
|
||||
echo "$UPDATE_RESPONSE" | jq -e '.id != null' >/dev/null 2>&1 || die "NPMplus proxy host update failed"
|
||||
|
||||
log "running public smoke checks"
|
||||
HEADERS="$(curl -skI "$PUBLIC_URL/")"
|
||||
echo "$HEADERS" | grep -q '^HTTP/2 200' || die "public root is not HTTP 200"
|
||||
if echo "$HEADERS" | grep -qi '^x-nextjs-prerender:'; then
|
||||
die "old Next.js headers still present on public root"
|
||||
fi
|
||||
|
||||
curl -sk "$PUBLIC_URL/" | grep -F '<title>Solace Bank Group PLC — Treasury Management Portal</title>' >/dev/null || die "public title mismatch"
|
||||
READY_BODY="$(curl -sk "$PUBLIC_URL/api/ready")"
|
||||
echo "$READY_BODY" | grep -F '"ready":true' >/dev/null || die "public /api/ready failed"
|
||||
curl -skN --max-time 5 -H 'Accept: text/event-stream' "$PUBLIC_URL/api/plans/demo-pay-014/status/stream" | grep -F '"type":"connected"' >/dev/null || die "public SSE smoke failed"
|
||||
|
||||
log "capturing EXT-* blocker summary"
|
||||
ssh_remote "pct exec ${VMID} -- journalctl -u currencicombo-orchestrator.service -n 200 --no-pager | grep -E 'ExternalBlockers|EXT-' || true"
|
||||
fi
|
||||
|
||||
log "CurrenciCombo Phoenix deploy completed from ${PHOENIX_DEPLOY_WORKSPACE}"
|
||||
Reference in New Issue
Block a user