furniture-3d-system-Wu/scripts/yydd-prod-release-guards.sh

149 lines
5.3 KiB
Bash

# Embedded by both official deployment entry points; no remote helper dependency.
ensure_ai_image_worker_secret() {
local app_root="$1"
[ -f "$app_root/scripts/run-ai-image-worker.mjs" ] || return 0
if ! grep -qE '^AI_IMAGE_WORKER_SECRET=.+$' "$SHARED/.env"; then
local secret
secret="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))")" || return 1
[ -n "$secret" ] || return 1
(umask 077; printf '\nAI_IMAGE_WORKER_SECRET=%s\n' "$secret" >> "$SHARED/.env")
chmod 600 "$SHARED/.env"
fi
}
start_ai_image_worker() {
local app_root="$1"
local name="${PM2_NAME}-ai-image-worker"
pm2 delete "$name" >/dev/null 2>&1 || true
# Rolling back to an older release pauses the new queue without losing persisted jobs.
[ -f "$app_root/scripts/run-ai-image-worker.mjs" ] || return 0
local command="cd '$app_root' && set -a && source '$SHARED/.env' && set +a && exec env PORT=$APP_PORT node scripts/run-ai-image-worker.mjs"
pm2 start /bin/bash --name "$name" -- -lc "$command"
}
assert_release_ready() {
local app_root="$1"
local runtime="$app_root/.next/standalone"
if [ ! -s "$runtime/server.js" ] || [ ! -d "$runtime/.next/static" ]; then
echo "Incomplete standalone release: $app_root" >&2
return 1
fi
if [ -z "$(find "$runtime/.next/static" -type f -size +0c -print -quit)" ]; then
echo "Empty standalone static assets: $app_root" >&2
return 1
fi
if [ ! -s "$app_root/scripts/run-supply-chain-discontinuation-daily.mjs" ] || \
[ ! -s "$app_root/node_modules/tsx/dist/cli.mjs" ]; then
echo "Missing discontinuation regeneration runtime: $app_root" >&2
return 1
fi
}
prepare_standalone_runtime() {
local app_root="$1"
local runtime="$app_root/.next/standalone"
if [ ! -s "$runtime/server.js" ] || [ ! -d "$app_root/.next/static" ]; then
echo "Missing build output: $app_root" >&2
return 1
fi
if [ -z "$(find "$app_root/.next/static" -type f -size +0c -print -quit)" ]; then
echo "Empty build static assets: $app_root" >&2
return 1
fi
mkdir -p "$runtime/.next"
rm -rf -- "$runtime/.next/static" "$runtime/public"
cp -a "$app_root/.next/static" "$runtime/.next/static"
if [ -d "$app_root/public" ]; then
cp -a "$app_root/public" "$runtime/public"
fi
# Turbopack standalone tracing may omit onnxruntime-node native binaries;
# restore them or @huggingface/transformers fails to load libonnxruntime at runtime.
local ort_real="$app_root/node_modules/onnxruntime-node"
local ort_runtime="$runtime/node_modules/onnxruntime-node"
if [ -d "$ort_real/bin" ] && [ ! -L "$ort_runtime" ]; then
mkdir -p "$ort_runtime"
rm -rf -- "$ort_runtime/bin"
cp -a "$ort_real/bin" "$ort_runtime/bin"
fi
assert_release_ready "$app_root"
}
acquire_deploy_lock() {
command -v python3 >/dev/null || return 1
exec 9>"$BASE/.deploy.lock"
if ! flock -n 9; then
echo "Another production deployment is running" >&2
return 1
fi
}
verify_release_http() {
python3 - "$1" <<'PY'
import json
import sys
import time
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse
from urllib.request import urlopen
base = sys.argv[1].rstrip('/')
def get(url):
with urlopen(url, timeout=15) as response:
if response.status != 200:
raise RuntimeError('HTTP %s: %s' % (response.status, url))
return response.read(), response.headers.get_content_type(), response.url
for attempt in range(10):
try:
payload, _, _ = get(base + '/api/health')
result = json.loads(payload)
health = result.get('data', {}).get('services', {})
if not result.get('success'):
raise RuntimeError('Health request failed')
if health.get('database') != 'connected' or health.get('api') != 'running':
raise RuntimeError('Unhealthy database or API')
break
except Exception:
if attempt == 9:
raise
time.sleep(2)
class Assets(HTMLParser):
def __init__(self):
super().__init__()
self.urls = set()
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if tag == 'script' and attrs.get('src'):
self.urls.add(attrs['src'])
if tag == 'link' and 'stylesheet' in attrs.get('rel', '').split() and attrs.get('href'):
self.urls.add(attrs['href'])
assets = set()
for path in ['/', '/installation-demo-workbench']:
content, kind, final_url = get(base + path)
if kind != 'text/html':
raise RuntimeError('Expected HTML: ' + path)
parser = Assets()
parser.feed(content.decode('utf-8'))
found = {urljoin(final_url, item) for item in parser.urls}
found = {item for item in found if urlparse(item).path.startswith('/_next/static/')}
if not found:
raise RuntimeError('No Next static references: ' + path)
assets.update(found)
for url in sorted(assets):
content, kind, _ = get(url)
if not content or kind == 'text/html':
raise RuntimeError('Invalid static asset: ' + url)
print('200 ' + url, flush=True)
if not any(urlparse(url).path.endswith('.css') for url in assets):
raise RuntimeError('No stylesheet verified')
if not any(urlparse(url).path.endswith('.js') for url in assets):
raise RuntimeError('No JavaScript verified')
print('Health and %d static assets verified at %s' % (len(assets), base), flush=True)
PY
}