From a9c7a1129b70c1f0b196b91ab55969c3af6ec38c Mon Sep 17 00:00:00 2001 From: joshua Date: Fri, 23 Oct 2026 16:15:29 +0200 Subject: [PATCH 1/2] Deploy: latest main without a tag; per-install PM2 config out of git - scripts/deploy.sh with no tag deploys the latest main (detached origin/main); with a tag, that release. - ecosystem.config.js is no longer in git (now ecosystem.config.example.js; the deploy creates the install's copy from it once). The deploy keeps the install's copy safe across checkouts and rollbacks, including releases that still tracked it. - The script runs from a temp copy, so checking out a new deploy.sh can't change it mid-run; set -E so failures in functions still roll back. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 + ...m.config.js => ecosystem.config.example.js | 3 + scripts/deploy.sh | 74 +++++++++++++++---- 3 files changed, 67 insertions(+), 13 deletions(-) rename ecosystem.config.js => ecosystem.config.example.js (61%) diff --git a/.gitignore b/.gitignore index 984eb01..49441fa 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ yarn-error.log* .DS_Store Thumbs.db +# this install's PM2 config (made from ecosystem.config.example.js; deploys never touch it) +/ecosystem.config.js + # misc scratch / generated files temp/ backend/public/uploads/ diff --git a/ecosystem.config.js b/ecosystem.config.example.js similarity index 61% rename from ecosystem.config.js rename to ecosystem.config.example.js index b3e3ec6..78afb25 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.example.js @@ -1,3 +1,6 @@ +// PM2 config template. Each install keeps its own copy as ecosystem.config.js (not in git, +// so deploys never change it); scripts/deploy.sh creates it from this file the first time. +// Change the app names (unique per install on a server) and the frontend port to suit. module.exports = { apps: [ { diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 113ebeb..faf6237 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,31 +1,79 @@ #!/usr/bin/env bash -# Updates this install to a tagged release: back up the database, check out the tag, -# install, migrate, build, and reload PM2. If anything fails before the reload, the -# previous version keeps running and the checkout goes back to it. +# 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 v1.34.0 -# CI runs this over SSH when a v* tag is pushed (see .gitea/workflows/ci.yml). -set -euo pipefail +# 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 -TAG="${1:?Usage: scripts/deploy.sh }" -cd "$(dirname "$0")/.." +# 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)" -echo "==> Deploying $TAG (currently $PREVIOUS)" +# 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() { 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 -git fetch --tags --quiet origin -git checkout -q "$TAG" +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) @@ -35,6 +83,6 @@ echo "==> Frontend: install and build" trap - ERR echo "==> Reloading PM2" -pm2 reload ecosystem.config.js --update-env +pm2 reload "$PM2_CONFIG" --update-env -echo "==> Deployed $TAG" +echo "==> Deployed $TARGET ($DEPLOYED)" From 96adf20cf041845d1eedb5d7aaaa339bfa0af32f Mon Sep 17 00:00:00 2001 From: joshua Date: Fri, 23 Oct 2026 16:20:22 +0200 Subject: [PATCH 2/2] Deploy script: roll back once (not inside subshells); docs; bump to 1.37.4 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++++++ README.md | 14 ++++++++++++-- backend/package-lock.json | 4 ++-- backend/package.json | 2 +- docs/HANDOVER.md | 8 +++++++- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- scripts/deploy.sh | 2 ++ 10 files changed, 36 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb15ad..c7231ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [1.37.4] - 2026-10-23 + +### Fixed + +- **Deploy script** (`scripts/deploy.sh`): without a tag it now deploys the latest `main`. The PM2 config is per install: `ecosystem.config.js` is no longer in git (the repo has `ecosystem.config.example.js`), so deploys stop overwriting the install's app names and port; the script keeps the install's copy across checkouts and rollbacks, and creates it from the example on a fresh install. The script also runs from a temporary copy (so checking out a new version of it mid-deploy is safe), and a failure rolls back once. + ## [1.37.3] - 2026-10-23 ### Fixed diff --git a/README.md b/README.md index 1b73838..d1ca9d9 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,18 @@ Documentation: see Frontend README for environment and scripts. ## Deployment -To update a running install to a release: `bash scripts/deploy.sh v1.34.0` on the server -(backs up the database, migrates, builds, reloads PM2). +To update a running install, on the server in the repo folder: + +- `bash scripts/deploy.sh` — the latest `main` +- `bash scripts/deploy.sh v1.37.4` — a tagged release + +It backs up the database, checks the version out, installs, migrates, builds and reloads PM2. +If anything fails before the reload, it goes back to the previous version (the running app +isn't touched). + +PM2 uses the install's own `ecosystem.config.js` (app names, frontend port). It isn't in git, +so deploys never change it; the first deploy creates it from `ecosystem.config.example.js` — +set names that match your PM2 apps (`pm2 ls`) and the port you use. ### Backend Deployment diff --git a/backend/package-lock.json b/backend/package-lock.json index 55f10aa..b9eaa8e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "event-management-backend", - "version": "1.37.3", + "version": "1.37.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "event-management-backend", - "version": "1.37.3", + "version": "1.37.4", "hasInstallScript": true, "dependencies": { "@prisma/adapter-pg": "^7.10.1", diff --git a/backend/package.json b/backend/package.json index 41431c0..7139c65 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "event-management-backend", - "version": "1.37.3", + "version": "1.37.4", "description": "Event Management System Backend", "main": "src/index.js", "scripts": { diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index c466a92..606ed4b 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -1,6 +1,6 @@ # Handover — where things stand and how to carry on -Last updated: 2026-10-23, after **1.37.3**. The plan itself is in +Last updated: 2026-10-23, after **1.37.4**. The plan itself is in [ROADMAP-v2.md](./ROADMAP-v2.md) (decisions table at the end); the previous round of features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md). @@ -12,6 +12,7 @@ features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md). | 1.33.0 | Multi-day events phase A — sessions, late joining + late pricing, per-session check-in, attendance register, session reminders, certificates (4 templates, verify page) | | 1.34.0 | Background jobs on pg-boss (admin Background jobs page), Playwright e2e tests, Gitea CI workflow, `scripts/deploy.sh` | | 1.35.0 | Multi-day events phase B — "pick sessions" (per-session tickets and prices, session capacity) | +| 1.37.4 | Deploy script: no tag = latest main; per-install `ecosystem.config.js` out of git (`ecosystem.config.example.js`) | | 1.37.3 | Fix: the production build failed on unescaped quotes in a help text | | 1.37.2 | Payment status checks (PayFast transaction history; job every 15 min; Payments → Reconcile → Waiting for payment), PayFast Test connection | | 1.37.1 | "Pay online" hidden when no provider is on or under R15; SnapScan/Zapper dropped from the roadmap | @@ -44,6 +45,11 @@ features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md). need a migration. - **Server Node version:** the deployment notes install Node 20, so dependencies must support Node 20 (that's why pg-boss is pinned to 10.x). +- **Deploying:** `bash scripts/deploy.sh [tag]` on the server (no tag = latest main). Each install + keeps its own `ecosystem.config.js` (PM2 app names, port), which is not in git. The script + runs from a temp copy of itself and rolls back on failure before the PM2 reload. It was + tested by running it against a throwaway clone with npm/pm2 stubbed: across the tracked → + untracked change for the PM2 file, deploying main, failed builds and a bad tag. - **Pushing:** pushing from some shells fails with "Failed to authenticate user" (a stale saved credential); pushing from your own terminal works. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6e4eb42..6b59bde 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "hope-events-frontend", - "version": "1.37.3", + "version": "1.37.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hope-events-frontend", - "version": "1.37.3", + "version": "1.37.4", "dependencies": { "@dnd-kit/core": "^6.3.1", "@hookform/resolvers": "^5.2.1", diff --git a/frontend/package.json b/frontend/package.json index 40e17eb..0eb71f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "hope-events-frontend", - "version": "1.37.3", + "version": "1.37.4", "private": true, "scripts": { "dev": "next dev --turbopack", diff --git a/package-lock.json b/package-lock.json index 9ccce9b..2c2935d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hope-events", - "version": "1.37.3", + "version": "1.37.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hope-events", - "version": "1.37.3", + "version": "1.37.4", "license": "ISC", "devDependencies": { "@playwright/test": "^1.64.0", diff --git a/package.json b/package.json index afb66dd..3fbdfb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hope-events", - "version": "1.37.3", + "version": "1.37.4", "main": "index.js", "scripts": { "dev:backend": "cd backend && npm run dev", diff --git a/scripts/deploy.sh b/scripts/deploy.sh index faf6237..d0a550c 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -58,6 +58,8 @@ 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"