#!/usr/bin/env bash set -Eeuo pipefail # Run on the actual ces-control host, with root or passwordless sudo. if [ "$(id -u)" -ne 0 ]; then exec sudo -n bash "$0" "$@" fi RELEASE_SHA="${1:?Release SHA is required}" NODE_SOURCE="${2:?Node executable path is required}" [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo 'Invalid release SHA'; exit 1; } test -x "$NODE_SOURCE" test -d /run/systemd/system || { echo 'Deployment requires the host systemd environment'; exit 1; } systemctl show-environment >/dev/null test -f .next/standalone/server.js APP_ROOT=/opt/prompt DATA_ROOT=/var/lib/prompt SERVICE=prompt.service SERVICE_USER=prompt PORT=18131 RELEASE_DIR="$APP_ROOT/releases/$RELEASE_SHA" mkdir -p "$APP_ROOT/releases" exec 9>"$APP_ROOT/.deploy.lock" flock -w 300 9 || { echo 'Another PROMPT deployment is running'; exit 1; } # Do not replace the release of a newer main commit after a slower old run. if [ "$(git -c safe.directory="$PWD" rev-parse HEAD)" != "$RELEASE_SHA" ]; then echo 'Checkout does not match release SHA'; exit 1 fi LATEST_MAIN=$(git -c safe.directory="$PWD" ls-remote origin refs/heads/main | cut -f1) if [ "$LATEST_MAIN" != "$RELEASE_SHA" ]; then echo 'Skipping deployment: main has a newer commit' exit 0 fi PREVIOUS="" if [ -L "$APP_ROOT/current" ] && [ -d "$APP_ROOT/current" ]; then PREVIOUS=$(readlink -f "$APP_ROOT/current") fi if [ -z "$PREVIOUS" ]; then "$NODE_SOURCE" -e "const s=require('net').createServer();s.once('error',()=>process.exit(1));s.listen($PORT,'0.0.0.0',()=>s.close());" || { echo "Port $PORT is already occupied"; exit 1; } fi if ! id "$SERVICE_USER" >/dev/null 2>&1; then useradd --system --home "$DATA_ROOT" --shell /usr/sbin/nologin "$SERVICE_USER" fi install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0750 "$DATA_ROOT" bash scripts/infra.sh "$NODE_SOURCE" "$RELEASE_SHA" set -a . /etc/prompt.env set +a "$NODE_SOURCE" node_modules/tsx/dist/cli.mjs scripts/bootstrap.ts # Keep runtime data outside the checkout and releases; future modules can use it. if [ ! -d "$RELEASE_DIR" ]; then STAGING=$(mktemp -d "$APP_ROOT/releases/.staging.XXXXXX") cp -a .next/standalone/. "$STAGING/" mkdir -p "$STAGING/.next" "$STAGING/bin" cp -a .next/static "$STAGING/.next/static" if [ -d public ]; then cp -a public "$STAGING/public"; fi install -m 0755 "$NODE_SOURCE" "$STAGING/bin/node" install -m 0644 dist/worker.cjs "$STAGING/worker.cjs" printf 'RELEASE_SHA=%s\n' "$RELEASE_SHA" > "$STAGING/release.env" chmod -R a+rX "$STAGING" chmod 0755 "$STAGING" mv "$STAGING" "$RELEASE_DIR" fi cat > /etc/systemd/system/prompt.service < /etc/systemd/system/prompt-worker.service <{const d=await r.json();if(!r.ok||!s.ok||d.service!=='prompt'||d.release!=='$RELEASE_SHA')process.exit(1)}).catch(()=>process.exit(1))"; then HEALTHY=1 break fi sleep 2 done if [ "$HEALTHY" -ne 1 ]; then echo 'Health check failed; rolling back' false fi systemctl is-enabled "$SERVICE" systemctl is-active "$SERVICE" systemctl is-active prompt-worker.service echo "Persistent deployment verified: service=$SERVICE port=$PORT release=$RELEASE_SHA" echo "Host: $(hostname)" hostname -I || true