#!/usr/bin/env bash # Updates the Hub: back up the database, check out the release, install, migrate, build, # restart PM2 and check it answers. If any step fails, it goes back to the previous version: # the code, its dependencies and build, and the database too if new migrations had run. # # Usage (on the server, as the site user, from the repo folder): # bash scripts/deploy.sh # the latest main # bash scripts/deploy.sh v1.2.0 # a tag (or any commit) # # Optional settings (environment variables): # HUB_BACKUP_DIR where database dumps go (default ~/backups/events-hub; the newest 10 are kept) # HUB_PM2_BACKEND PM2 name of the backend (default events-hub-backend) # HUB_PM2_FRONTEND PM2 name of the frontend (default events-hub-frontend) set -Eeuo pipefail # The checkout below replaces this file, and bash reads a script as it runs — so run from a # copy that can't change underneath us. if [ -z "${HUB_DEPLOY_COPY:-}" ]; then COPY="$(mktemp)" cp "$0" "$COPY" HUB_DEPLOY_COPY=1 HUB_DEPLOY_DIR="$(cd "$(dirname "$0")/.." && pwd)" exec bash "$COPY" "$@" fi trap 'rm -f "$0"' EXIT cd "$HUB_DEPLOY_DIR" TARGET="${1:-main}" BACKUP_DIR="${HUB_BACKUP_DIR:-$HOME/backups/events-hub}" PM2_BACKEND="${HUB_PM2_BACKEND:-events-hub-backend}" PM2_FRONTEND="${HUB_PM2_FRONTEND:-events-hub-frontend}" FRONTEND_PORT=3200 # matches "start" in frontend/package.json describe() { git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD; } PREVIOUS="$(git rev-parse HEAD)" PREVIOUS_NAME="$(describe)" if [ -n "$(git status --porcelain --untracked-files=no)" ]; then echo "!!> There are local changes to tracked files; deploy would overwrite them:" git status --short --untracked-files=no exit 1 fi # The database URL from backend/.env, without Prisma-only options (pg_dump rejects ?schema=). DB_URL="$(cd backend && node -e "require('dotenv').config(); const u = new URL(process.env.DATABASE_URL); u.search = ''; console.log(u.toString())")" BACKEND_PORT="$(cd backend && node -e "require('dotenv').config(); console.log(process.env.PORT || 5200)")" echo "==> Deploying $TARGET (currently $PREVIOUS_NAME)" echo "==> Backing up the database" mkdir -p "$BACKUP_DIR" BACKUP="$BACKUP_DIR/events_hub-$(date +%Y%m%d-%H%M%S)-$PREVIOUS_NAME.dump" pg_dump --dbname="$DB_URL" -Fc -f "$BACKUP" echo " $BACKUP" ls -1t "$BACKUP_DIR"/events_hub-*.dump | tail -n +11 | xargs -r rm -f MIGRATED=0 RESTARTED=0 rollback() { # set -E passes this trap into ( … ) subshells too; only the script itself rolls back. if [ "$BASH_SUBSHELL" -gt 0 ]; then return; fi trap - ERR set +e echo "!!> Deploy failed — going back to $PREVIOUS_NAME" git checkout -q -- . 2>/dev/null git checkout -q "$PREVIOUS" if [ "$MIGRATED" = 1 ]; then echo "!!> Restoring the database from $BACKUP" pg_restore --clean --if-exists --no-owner --single-transaction --dbname="$DB_URL" "$BACKUP" \ || echo "!!> Restore failed. Restore by hand: pg_restore --clean --if-exists --no-owner -d \"\" $BACKUP" fi (cd backend && npm ci --silent) (cd frontend && npm ci --silent && npm run build --silent) if [ "$RESTARTED" = 1 ]; then pm2 restart "$PM2_BACKEND" "$PM2_FRONTEND" --update-env; fi echo "!!> Back on $PREVIOUS_NAME. Look at the output above for what failed." exit 1 } trap rollback ERR # A tag or commit as is; main as the latest origin/main (detached, so a local main is never # in the way). if [ "$TARGET" = "main" ]; then git fetch --quiet origin main git checkout -q --detach origin/main else git fetch --tags --quiet origin git checkout -q "$TARGET" fi DEPLOYED="$(describe)" echo "==> Backend: install, migrate and generate" (cd backend && npm ci) if [ -n "$(git diff --name-only "$PREVIOUS" HEAD -- backend/prisma/migrations)" ]; then MIGRATED=1; fi (cd backend && npm run prisma:deploy) echo "==> Frontend: install and build" (cd frontend && npm ci && npm run build) echo "==> Restarting PM2" RESTARTED=1 pm2 restart "$PM2_BACKEND" "$PM2_FRONTEND" --update-env echo "==> Checking it answers" check() { for _ in $(seq 1 30); do if curl -fsS -o /dev/null "$1"; then return 0; fi sleep 1 done echo "!!> No answer from $1 after 30 seconds" return 1 } check "http://127.0.0.1:$BACKEND_PORT/api/health" check "http://127.0.0.1:$FRONTEND_PORT/" trap - ERR pm2 save --force >/dev/null echo "==> Deployed $TARGET ($DEPLOYED). Backup: $BACKUP"