#!/usr/bin/env bash set -euo pipefail SRC="${1:-}" APP_DIR="/srv/www/devops-platform-api" BACKUP_DIR="/srv/www/devops-platform-api-backups" ENV_FILE="/etc/devops-platform-api.env" PM2_NAME="devops-platform-api" HEALTH_URL="http://127.0.0.1:4300/health" TS="$(date +%Y%m%d%H%M%S)" if [ -z "$SRC" ] || [ ! -d "$SRC" ]; then echo "Usage: deploy-devops-platform-api-from-jenkins " exit 2 fi for required in "$SRC/dist/main.js" "$SRC/package.json" "$SRC/pnpm-lock.yaml" "$SRC/prisma" "$ENV_FILE"; do if [ ! -e "$required" ]; then echo "Missing $required" exit 3 fi done run_prisma_migrations() { if [ "${PRISMA_MIGRATE_DEPLOY_ENABLED:-true}" != "true" ]; then echo "Skipping Prisma migrations because PRISMA_MIGRATE_DEPLOY_ENABLED is not true" return fi set -a . ./.env.production set +a local migration_state migration_state="$(node <<'NODE' const { PrismaClient } = require('@prisma/client'); const requiredTables = [ 'agent_invocations', 'audit_logs', 'deploy_jobs', 'deploy_runs', 'environments', 'outbox_messages', 'platform_messages', 'project_member_permissions', 'projects', 'run_steps', 'secrets', 'users', ]; const baselineMigrations = [ '20260611000000_baseline_current_schema', '20260611120000_auth_members_messages', ]; const prisma = new PrismaClient(); async function main() { const tables = await prisma.$queryRaw` select table_name from information_schema.tables where table_schema = database() `; const existingTables = new Set(tables.map((row) => row.TABLE_NAME || row.table_name)); if (existingTables.size === 0) { console.log('fresh'); return; } const missingTables = requiredTables.filter((name) => !existingTables.has(name)); if (missingTables.length > 0) { console.error(`Existing database schema is incomplete; missing tables: ${missingTables.join(', ')}`); process.exit(12); } const hasMigrationTable = existingTables.has('_prisma_migrations'); const appliedMigrations = hasMigrationTable ? await prisma.$queryRaw`select migration_name from _prisma_migrations` : []; const appliedNames = new Set( appliedMigrations.map((row) => row.MIGRATION_NAME || row.migration_name), ); const missingMigrations = baselineMigrations.filter((name) => !appliedNames.has(name)); if (missingMigrations.length > 0) { console.log(`bootstrap:${missingMigrations.join(',')}`); return; } console.log('ready'); } main() .finally(() => prisma.$disconnect()); NODE )" if [[ "$migration_state" == bootstrap:* ]]; then local migrations_csv="${migration_state#bootstrap:}" IFS=',' read -r -a missing_migrations <<< "$migrations_csv" for migration in "${missing_migrations[@]}"; do echo "Marking existing production schema migration as applied: $migration" pnpm prisma migrate resolve --schema prisma/schema.prisma --applied "$migration" done else echo "Prisma migration bootstrap state: $migration_state" fi echo "Running Prisma migrate deploy" pnpm prisma migrate deploy --schema prisma/schema.prisma } mkdir -p "$BACKUP_DIR" TMP="$(mktemp -d /srv/www/devops-platform-api.deploy.XXXXXX)" OLD="$APP_DIR.old.$TS" cp -a "$SRC/dist" "$TMP/dist" cp -a "$SRC/prisma" "$TMP/prisma" cp -a "$SRC/package.json" "$TMP/package.json" cp -a "$SRC/pnpm-lock.yaml" "$TMP/pnpm-lock.yaml" cp -a "$ENV_FILE" "$TMP/.env.production" cd "$TMP" corepack enable corepack prepare pnpm@9.15.4 --activate pnpm install --frozen-lockfile pnpm prisma:generate run_prisma_migrations if [ -d "$APP_DIR" ]; then tar -C "$APP_DIR" -czf "$BACKUP_DIR/$TS.tgz" . mv "$APP_DIR" "$OLD" fi mv "$TMP" "$APP_DIR" cd "$APP_DIR" set -a . ./.env.production set +a if ! pm2 restart "$PM2_NAME" --update-env; then pm2 start dist/main.js --name "$PM2_NAME" --update-env fi sleep 8 if ! curl -fsS "$HEALTH_URL" >/dev/null; then echo "Health check failed; rolling back $PM2_NAME" pm2 stop "$PM2_NAME" || true rm -rf "$APP_DIR.failed.$TS" mv "$APP_DIR" "$APP_DIR.failed.$TS" if [ -d "$OLD" ]; then mv "$OLD" "$APP_DIR" cd "$APP_DIR" set -a . ./.env.production set +a pm2 restart "$PM2_NAME" --update-env || pm2 start dist/main.js --name "$PM2_NAME" --update-env fi exit 5 fi rm -rf "$OLD" pm2 save >/dev/null echo "Deployed devops-platform-api from $SRC to $APP_DIR"