Persist image submissions with fixed idempotency keys, leased execution, provider identity, reference snapshots and staged results. Integrate DIY, assisted assembly and non-integrated white-background sessions without holding generation in a browser request. Recover ambiguous submissions through owner-scoped reads and stop a new eight-view batch when restoring an earlier request. Add the NativeImageGenerationJob migration and a dedicated worker to both release entry points. Protect job inputs/results from storage GC. Existing production configuration and business data are not changed by this commit. Validation: 256 focused Jest tests, isolated PostgreSQL migration/lease/transaction checks (8), deployment guard tests (9), type-check, production build and desktop/ mobile browser component scenarios. Live native 2K reference-image request worked; reusing its idempotency key returned the same task. Build uses local placeholders for otherwise absent Drawing System configuration. Three unrelated material tests also fail with the original mainline test file; they remain outside this change. Architecture/visual/root-cause review: passed with warnings. Independent review completed for the integration and shared input/storage paths; worker additionally reviewed by the primary agent. Production merge/deployment requires authorization.
144 lines
5.1 KiB
Bash
144 lines
5.1 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
|
|
}
|
|
|
|
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
|
|
}
|