black/apricot homelan died 2026-06-27. Point everything at the DO store tier: - @lilith npm registry: forge.black.lan/npm.black.lan -> cocotte-forge Verdaccio (134.199.243.61:4873) across bunfig.toml scopes, all deploy.sh .npmrc writers, and package.json publishConfig. - Forgejo URL (git/CI): forge.black.lan -> 134.199.243.61:3000 / :2222. - quinn.www prod.conf /photos: was proxy_pass to dead black_photos (black:8081); now served from local disk (root /var/www/quinn.www/dist). Prevents a future deploy from re-breaking photos. (Phase G: repoint to DO Spaces/CDN later.) Interim bare-IP endpoints; switch to named uvlava infranet hosts once live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
226 lines
No EOL
9.8 KiB
Bash
Executable file
226 lines
No EOL
9.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# Deploy quinn.admin dev preview to black (admin.quinn.black.lan).
|
|
#
|
|
# Replaces admin.quinn.apricot.lan while apricot is down. Serves the latest
|
|
# main-branch build over LAN HTTPS without SSO (DEV_AUTH_SKIP_HOSTS=*.black.lan).
|
|
#
|
|
# Usage:
|
|
# ./run deploy:admin-dev --from-local # syncs origin/main via ./run first
|
|
# bash deployments/@domains/quinn.admin/deploy-black-dev.sh
|
|
# bash deployments/@domains/quinn.admin/deploy-black-dev.sh --skip-build
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../" && pwd)"
|
|
|
|
REMOTE="${REMOTE_HOST:-black}"
|
|
# shellcheck source=../../ci/local-remote.sh
|
|
source "$REPO_ROOT/deployments/ci/local-remote.sh"
|
|
init_deploy_local
|
|
|
|
# host-nginx container bind-mounts /bigdisk/next only — not /var/www/quinn.admin
|
|
REMOTE_DIST_HOST="/bigdisk/next/quinn-admin/dist"
|
|
REMOTE_DIST_CONTAINER="/var/www/next/quinn-admin/dist"
|
|
REMOTE_API="/opt/quinn-admin-api"
|
|
REMOTE_NGINX_CONF="/bigdisk/nginx/nginx.conf"
|
|
NGINX_SNIPPET_SRC="$SCRIPT_DIR/nginx/black-dev.conf"
|
|
DNS_SNIPPET_SRC="$REPO_ROOT/infrastructure/dns/quinn-black-lan.conf"
|
|
API_BUNDLE="dist/server.node.js"
|
|
TIMESTAMP="$(date '+%Y%m%d_%H%M%S')"
|
|
MARKER_BEGIN="# BEGIN quinn.admin black-dev"
|
|
MARKER_END="# END quinn.admin black-dev"
|
|
DEV_AUTH_SKIP='*.black.lan,*.apricot.lan,localhost'
|
|
|
|
SKIP_BUILD=false
|
|
for arg in "$@"; do
|
|
[[ "$arg" == "--skip-build" ]] && SKIP_BUILD=true
|
|
done
|
|
|
|
# ─── sync origin/main (direct invocation; ./run and CI sync earlier) ─────────
|
|
if [[ "$SKIP_BUILD" != "true" && "${DEPLOY_SYNC_MAIN_DONE:-}" != "1" ]]; then
|
|
# shellcheck source=../../../scripts/run/git-sync-main.sh
|
|
source "$REPO_ROOT/scripts/run/git-sync-main.sh"
|
|
sync_main "$REPO_ROOT"
|
|
fi
|
|
|
|
# ─── build (unless --skip-build) ─────────────────────────────────────────────
|
|
if [[ "$SKIP_BUILD" != "true" ]]; then
|
|
echo "==> Building admin frontend..."
|
|
cd "$REPO_ROOT/codebase/@features/admin/frontend-public" && bun run build
|
|
|
|
echo "==> Building @features/api monolith..."
|
|
cd "$REPO_ROOT/codebase/@features/api"
|
|
bun run typecheck
|
|
_bi_ver="$(cat "${REPO_ROOT}/VERSION.txt" | head -1)"
|
|
_bi_bc="$(cat "${REPO_ROOT}/BUILD_COUNT" 2>/dev/null | head -1)"
|
|
_bi_sha="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
|
_bi_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
bun build --target=node --external better-sqlite3 --external sharp \
|
|
--define "__BUILD_INFO__={\"version\":\"${_bi_ver}\",\"buildCount\":\"${_bi_bc}\",\"sha\":\"${_bi_sha}\",\"builtAt\":\"${_bi_time}\"}" \
|
|
src/app/server.ts --outfile="${API_BUNDLE}" --sourcemap=none
|
|
cd "$SCRIPT_DIR"
|
|
fi
|
|
|
|
if [[ ! -d "$REPO_ROOT/codebase/@features/admin/frontend-public/dist" ]]; then
|
|
echo "ERROR: missing admin frontend dist — run build first." >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "$REPO_ROOT/codebase/@features/api/${API_BUNDLE}" ]]; then
|
|
echo "ERROR: missing API bundle at codebase/@features/api/${API_BUNDLE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ─── frontend dist ───────────────────────────────────────────────────────────
|
|
echo "==> Deploying admin frontend dist to ${REMOTE}:${REMOTE_DIST_HOST}..."
|
|
run_remote_cmd "mkdir -p ${REMOTE_DIST_HOST}"
|
|
rsync_to_remote \
|
|
"$REPO_ROOT/codebase/@features/admin/frontend-public/dist/" \
|
|
"${REMOTE_DIST_HOST}/" \
|
|
-avz --delete
|
|
|
|
# ─── API bundle ──────────────────────────────────────────────────────────────
|
|
echo "==> Deploying API bundle to ${REMOTE}:${REMOTE_API}..."
|
|
run_remote_cmd "mkdir -p ${REMOTE_API}/dist"
|
|
rsync_to_remote \
|
|
"$REPO_ROOT/codebase/@features/api/${API_BUNDLE}" \
|
|
"${REMOTE_API}/dist/server.node.js" \
|
|
-avz
|
|
|
|
# ─── node_modules (native deps — always resolve on black for correct arch) ───
|
|
echo "==> Installing API runtime node_modules on ${REMOTE}..."
|
|
copy_to_remote "$REPO_ROOT/codebase/@features/api/package.json" /tmp/quinn-admin-api.package.json
|
|
run_remote <<'ENDSSH'
|
|
DEPS_DIR="$(mktemp -d)"
|
|
node -e '
|
|
const fs = require("fs");
|
|
const pkg = JSON.parse(fs.readFileSync("/tmp/quinn-admin-api.package.json", "utf8"));
|
|
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
if (!pkg[section]) continue;
|
|
for (const [name, ver] of Object.entries(pkg[section])) {
|
|
if (typeof ver === "string" && ver.startsWith("workspace:")) delete pkg[section][name];
|
|
}
|
|
}
|
|
fs.writeFileSync(process.argv[1], JSON.stringify(pkg, null, 2));
|
|
' "$DEPS_DIR/package.json"
|
|
echo '@lilith:registry=http://134.199.243.61:4873/' > "$DEPS_DIR/.npmrc"
|
|
(cd "$DEPS_DIR" && npm install --omit=dev --legacy-peer-deps 2>&1 | tail -3)
|
|
rsync -a --delete "$DEPS_DIR/node_modules/" /opt/quinn-admin-api/node_modules/
|
|
rm -rf "$DEPS_DIR" /tmp/quinn-admin-api.package.json
|
|
ENDSSH
|
|
|
|
# ─── DEV_AUTH_SKIP_HOSTS in secrets ──────────────────────────────────────────
|
|
echo "==> Ensuring DEV_AUTH_SKIP_HOSTS in /etc/quinn-admin-api/secrets.env..."
|
|
run_remote <<ENDSSH
|
|
SECRETS=/etc/quinn-admin-api/secrets.env
|
|
if ! sudo test -f "\$SECRETS"; then
|
|
echo "ERROR: missing \$SECRETS on ${REMOTE}" >&2
|
|
exit 1
|
|
fi
|
|
if sudo grep -q '^DEV_AUTH_SKIP_HOSTS=' "\$SECRETS"; then
|
|
sudo sed -i 's|^DEV_AUTH_SKIP_HOSTS=.*|DEV_AUTH_SKIP_HOSTS=${DEV_AUTH_SKIP}|' "\$SECRETS"
|
|
else
|
|
echo "DEV_AUTH_SKIP_HOSTS=${DEV_AUTH_SKIP}" | sudo tee -a "\$SECRETS" >/dev/null
|
|
fi
|
|
sudo chmod 600 "\$SECRETS"
|
|
sudo chown quinn-api:quinn-api "\$SECRETS"
|
|
|
|
# /api/admin/* routes proxy to quinn-api (:3030) — it needs the same skip list.
|
|
API_SECRETS=/etc/quinn-api/secrets.env
|
|
if sudo test -f "\$API_SECRETS"; then
|
|
if sudo grep -q '^DEV_AUTH_SKIP_HOSTS=' "\$API_SECRETS"; then
|
|
sudo sed -i 's|^DEV_AUTH_SKIP_HOSTS=.*|DEV_AUTH_SKIP_HOSTS=${DEV_AUTH_SKIP}|' "\$API_SECRETS"
|
|
else
|
|
echo "DEV_AUTH_SKIP_HOSTS=${DEV_AUTH_SKIP}" | sudo tee -a "\$API_SECRETS" >/dev/null
|
|
fi
|
|
sudo chmod 600 "\$API_SECRETS"
|
|
fi
|
|
ENDSSH
|
|
|
|
# ─── DNS: *.quinn.black.lan → black ────────────────────────────────────────
|
|
if [[ -f "$DNS_SNIPPET_SRC" ]]; then
|
|
echo "==> Installing dnsmasq snippet for *.quinn.black.lan..."
|
|
copy_to_remote "$DNS_SNIPPET_SRC" /tmp/quinn-black-lan.conf
|
|
run_remote_cmd "sudo cp /tmp/quinn-black-lan.conf /etc/dnsmasq.d/quinn-black-lan.conf && sudo systemctl reload dnsmasq"
|
|
fi
|
|
|
|
# ─── nginx vhost in host-nginx config ──────────────────────────────────────
|
|
echo "==> Installing admin.quinn.black.lan vhost into ${REMOTE_NGINX_CONF}..."
|
|
copy_to_remote "$NGINX_SNIPPET_SRC" /tmp/quinn-admin-black-dev.conf
|
|
run_remote <<'ENDSSH'
|
|
set -euo pipefail
|
|
NGINX_CONF="/bigdisk/nginx/nginx.conf"
|
|
SNIPPET="/tmp/quinn-admin-black-dev.conf"
|
|
BEGIN_MARK="# BEGIN quinn.admin black-dev"
|
|
END_MARK="# END quinn.admin black-dev"
|
|
BACKUP="${NGINX_CONF}.bak-$(date +%Y%m%d_%H%M%S)"
|
|
|
|
if [[ ! -f "$NGINX_CONF" ]]; then
|
|
echo "ERROR: missing $NGINX_CONF" >&2
|
|
exit 1
|
|
fi
|
|
|
|
sudo cp "$NGINX_CONF" "$BACKUP"
|
|
|
|
python3 - "$NGINX_CONF" "$SNIPPET" "$BEGIN_MARK" "$END_MARK" <<'PY'
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
conf_path, snippet_path, begin, end = sys.argv[1:5]
|
|
conf = Path(conf_path).read_text()
|
|
snippet = Path(snippet_path).read_text().rstrip() + "\n"
|
|
block = f"{begin}\n{snippet}{end}\n"
|
|
|
|
# Strip every prior install (handles duplicate/orphan blocks from bad inserts).
|
|
conf = re.sub(
|
|
re.escape(begin) + r"[\s\S]*?" + re.escape(end) + r"\n?",
|
|
"",
|
|
conf,
|
|
)
|
|
|
|
marker = "\n}\n\n\nstream {"
|
|
idx = conf.find(marker)
|
|
if idx == -1:
|
|
raise SystemExit("could not find http/stream boundary")
|
|
conf = conf[:idx] + "\n" + block + conf[idx:]
|
|
|
|
Path(conf_path).write_text(conf)
|
|
PY
|
|
|
|
rm -f "$SNIPPET"
|
|
sudo docker exec host-nginx nginx -t
|
|
sudo docker exec host-nginx nginx -s reload
|
|
echo " nginx reloaded (backup: $BACKUP)"
|
|
ENDSSH
|
|
|
|
# ─── restart API + smoke ───────────────────────────────────────────────────
|
|
echo "==> Restarting quinn-admin-api + quinn-api..."
|
|
run_remote_cmd "sudo systemctl restart quinn-admin-api.service quinn-api.service 2>/dev/null || sudo systemctl restart quinn-admin-api.service"
|
|
sleep 3
|
|
|
|
echo "==> Smoke-testing https://admin.quinn.black.lan ..."
|
|
run_remote <<'ENDSSH'
|
|
set -euo pipefail
|
|
UA='Mozilla/5.0'
|
|
health="$(curl -sk -A "$UA" https://admin.quinn.black.lan/health)"
|
|
echo " /health → ${health}"
|
|
echo "$health" | grep -q '"ok":true' || { echo "FAIL: health check" >&2; exit 1; }
|
|
|
|
code="$(curl -sk -o /dev/null -w '%{http_code}' -A "$UA" https://admin.quinn.black.lan/)"
|
|
echo " / → HTTP ${code}"
|
|
[[ "$code" == "200" ]] || { echo "FAIL: frontend not 200" >&2; exit 1; }
|
|
|
|
api_code="$(curl -sk -o /tmp/quinn-admin-site-text.json -w '%{http_code}' -A "$UA" https://admin.quinn.black.lan/api/admin/site-text)"
|
|
api="$(head -c 120 /tmp/quinn-admin-site-text.json)"
|
|
echo " /api/admin/site-text → HTTP ${api_code} ${api}"
|
|
if [[ "$api_code" != "200" ]] || grep -q '"authentication_required"' /tmp/quinn-admin-site-text.json 2>/dev/null; then
|
|
echo "FAIL: API still requires SSO — check DEV_AUTH_SKIP_HOSTS on :3023 and :3030" >&2
|
|
exit 1
|
|
fi
|
|
rm -f /tmp/quinn-admin-site-text.json
|
|
ENDSSH
|
|
|
|
echo ""
|
|
echo "✓ quinn.admin dev preview live at https://admin.quinn.black.lan ($(date '+%Y-%m-%d %H:%M:%S %Z'))" |