Files
hope-events/scripts/deploy.sh
T
joshuaandClaude Opus 5.5 51992814d2 Deploy: stop first when files in the install were changed by hand (1.52.2)
A setup wizard run on one server added files to backend/src that the
backend build picked up, so the deploy failed part-way and rolled back.
deploy.sh now lists files git reports as changed or added (ignored files
and the PM2 config don't count) and stops before backing up or checking
out. --check runs only that; ALLOW_LOCAL_CHANGES=1 deploys anyway; the
script also runs from a copy outside the install (the folder you're in).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:03:12 +02:00

137 lines
5.9 KiB
Bash

#!/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
# bash scripts/deploy.sh --check # only check for files changed by hand (changes nothing)
#
# 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.
#
# A deploy stops before touching anything when files in the repo folder were changed or added
# by hand: they'd be built into the release (or block the checkout). ALLOW_LOCAL_CHANGES=1
# deploys anyway. Ignored files (.env, uploads, backups, the PM2 config) never count.
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
DIR="${HOPE_DEPLOY_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
# Run from a copy outside the repo (e.g. downloaded to /tmp): the folder you're in.
if [ ! -d "$DIR/.git" ] || [ ! -d "$DIR/backend" ]; then DIR="$PWD"; fi
COPY="$(mktemp)"
cp "$0" "$COPY"
HOPE_DEPLOY_COPY=1 HOPE_DEPLOY_DIR="$DIR" exec bash "$COPY" "$@"
fi
cd "$HOPE_DEPLOY_DIR"
if [ ! -d .git ] || [ ! -d backend ]; then
echo "!!> $PWD isn't a Cross Events install. Run this from the install's folder (e.g. /var/www/hope-events)."
exit 1
fi
CHECK_ONLY=0
if [ "${1:-}" = "--check" ]; then CHECK_ONLY=1; shift; fi
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
}
# Files changed or added by hand (anything git reports, apart from this install's PM2 config,
# which releases before 1.37.4 tracked). Ignored files — .env, uploads, backups — aren't listed.
local_changes() {
git status --porcelain --untracked-files=all | awk -v keep="$PM2_CONFIG" 'substr($0, 4) != keep'
}
CHANGES="$(local_changes)"
if [ -n "$CHANGES" ]; then
echo "!!> Files in $PWD were changed or added by hand:"
echo "$CHANGES" | sed 's/^/ /'
echo " A release would build them in (or refuse to replace them), so the deploy can fail."
echo " Move added files (??) somewhere outside this folder, and put changed ones ( M) back with"
echo " \"git checkout -- <file>\" — after checking nothing in them is needed."
if [ "$CHECK_ONLY" = 1 ]; then exit 1; fi
if [ "${ALLOW_LOCAL_CHANGES:-}" != 1 ]; then
echo " Nothing was changed. To deploy anyway: ALLOW_LOCAL_CHANGES=1 bash scripts/deploy.sh $TARGET"
exit 1
fi
echo " ALLOW_LOCAL_CHANGES=1 — deploying anyway."
elif [ "$CHECK_ONLY" = 1 ]; then
echo "==> No files changed by hand in $PWD — ready to deploy."
fi
if [ "$CHECK_ONLY" = 1 ]; then exit 0; fi
echo "==> Deploying $TARGET (currently $PREVIOUS)"
# Node 20 reached end of life in April 2026 (no more security fixes). Releases still run on it,
# but the server should move to Node 24 LTS — see DEPLOYMENT.md, "Upgrading Node".
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
if [ "$NODE_MAJOR" -lt 22 ]; then
echo "!!> This server runs Node $(node -v), which no longer gets security fixes. Upgrade to Node 24 LTS (DEPLOYMENT.md, \"Upgrading Node\")."
fi
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
# Releases from 1.51.0 on have a TypeScript build; older ones run src/ as it is.
(cd backend && npm ci --include=dev --silent && { ! grep -q '"build":' package.json || npm run build --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, migrate and build"
# --include=dev: the TypeScript build needs the type packages even where NODE_ENV=production.
(cd backend && npm ci --include=dev && npm run prisma:deploy && npm run build)
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)"