#!/usr/bin/env bash # Updates this install: back up the database, check out the release, install, migrate, # build, and reload PM2. If anything fails before the reload, the previous version keeps # running and the checkout goes back to it. # # Usage (on the server, from the repo folder): # bash scripts/deploy.sh # the latest main # bash scripts/deploy.sh v1.37.3 # a tagged release # CI runs it with the tag over SSH when a v* tag is pushed (see .gitea/workflows/ci.yml). # # PM2 uses this install's own ecosystem.config.js (app names, ports). It isn't in git, so # deploys never change it; the first deploy creates it from ecosystem.config.example.js. 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 "${HOPE_DEPLOY_COPY:-}" ]; then COPY="$(mktemp)" cp "$0" "$COPY" HOPE_DEPLOY_COPY=1 HOPE_DEPLOY_DIR="$(cd "$(dirname "$0")/.." && pwd)" exec bash "$COPY" "$@" fi cd "$HOPE_DEPLOY_DIR" TARGET="${1:-main}" PREVIOUS="$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)" # Keep this install's PM2 config safe across the checkout (older releases tracked it in git). PM2_CONFIG="ecosystem.config.js" PM2_KEEP="$(mktemp)" HAVE_PM2_CONFIG=0 if [ -f "$PM2_CONFIG" ]; then cp "$PM2_CONFIG" "$PM2_KEEP"; HAVE_PM2_CONFIG=1; fi trap 'rm -f "$0" "$PM2_KEEP"' EXIT # Out of the way before a checkout: back to the committed version if git tracks it in this # release, otherwise removed (a release that still tracks it would refuse to overwrite it). clear_pm2_config() { git checkout -q -- "$PM2_CONFIG" 2>/dev/null || rm -f "$PM2_CONFIG" } restore_pm2_config() { if [ "$HAVE_PM2_CONFIG" = 1 ]; then cp "$PM2_KEEP" "$PM2_CONFIG"; fi } # Check out a tag, or the latest main (detached, so local main is never in the way). checkout() { clear_pm2_config if [ "$1" = "main" ]; then git fetch --quiet origin main git checkout -q --detach origin/main else git fetch --tags --quiet origin git checkout -q "$1" fi restore_pm2_config } echo "==> Deploying $TARGET (currently $PREVIOUS)" echo "==> Backing up the database" (cd backend && node -e "require('dotenv').config(); require('./src/utils/backupUtils').runBackup().then(r => console.log(' ', r.filename)).catch(e => { console.error(' Backup failed:', e.message); process.exit(1); })") rollback() { # set -E passes this trap into ( … ) subshells too; only the script itself rolls back. if [ "$BASH_SUBSHELL" -gt 0 ]; then return; fi echo "!!> Deploy failed — going back to $PREVIOUS (the running version was not touched)" clear_pm2_config git checkout -q "$PREVIOUS" restore_pm2_config (cd backend && npm ci --silent) || true (cd frontend && npm ci --silent && npm run build --silent) || true } trap rollback ERR checkout "$TARGET" DEPLOYED="$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)" if [ ! -f "$PM2_CONFIG" ]; then cp ecosystem.config.example.js "$PM2_CONFIG" echo " Created $PM2_CONFIG from the example — check its app names and port." fi echo "==> Backend: install and migrate" (cd backend && npm ci && npm run prisma:deploy) echo "==> Frontend: install and build" (cd frontend && npm ci && npm run build) trap - ERR echo "==> Reloading PM2" pm2 reload "$PM2_CONFIG" --update-env echo "==> Deployed $TARGET ($DEPLOYED)"