#!/usr/bin/env bash # Deploy latest code from git and rebuild containers. # Usage: ./deploy.sh (rebuilds site, api, workers) # ./deploy.sh site (rebuilds only site) # ./deploy.sh api (rebuilds only api) # ./deploy.sh erpnext (rebuild + migrate ERPNext, re-extract assets) # ./deploy.sh api workers (rebuild a custom set) set -euo pipefail # cd to this script's own directory so the deploy operates on the clone it lives # in. Previously this was hardcoded `cd /opt/performancewest`, which meant # running /opt/performancewest-dev/deploy.sh silently rebuilt PROD instead of # dev (the dev script is an identical copy). Resolving the real path makes the # same script correct in every clone; docker compose then derives the project # name + auto-loads docker-compose.override.yml from this directory, so dev # (project performancewest-dev, dev port remaps) and prod stay isolated. cd "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" SERVICES="${@:-site api workers proxy-relay listmonk-hc}" # proxy-relay and listmonk-hc are upstream images (no build context). Build # everything else, but always include them in the `up` set so the healthcare # proxy sidecar and the healthcare-stream Listmonk run. # NB: listmonk-hc needs a one-time DB setup the first time it is deployed: # docker compose exec api-postgres psql -U pw -d postgres -c 'CREATE DATABASE listmonk_hc OWNER pw;' # docker compose run --rm --entrypoint /bin/sh listmonk-hc -c './listmonk --install --idempotent --yes --config /listmonk/config.toml' # then configure its 3 SMTP servers (hc ports 2526/2527/2528). See # docs/healthcare-email-stream-plan.md. BUILD_SERVICES="$(echo "$SERVICES" | tr ' ' '\n' | grep -vE '^(proxy-relay|listmonk-hc)$' | tr '\n' ' ')" echo "=== Pulling latest from git ===" # deploy steps below (sync_nav.py, gen-service-catalog.py) rewrite generated # files under site/public + site/src in place, leaving the tree dirty. That dirty # tree makes `git pull` abort ("local changes would be overwritten"), silently # stranding new commits on an old checkout. Discard those generated changes first # so the pull always fast-forwards. (Only generated paths are reset.) git checkout -- site/public site/src 2>/dev/null || true git fetch origin main # Hard-reset the tracked tree to origin/main: the deploy box is a pure mirror of # origin (all real changes land via git), so any other tracked-file drift is also # generated/stale and must not be allowed to abort the pull. Untracked files # (data/*, .secrets/) are preserved. This makes "stranded on an old commit" # impossible — the previous `git pull` could silently abort, this cannot. git reset --hard origin/main # Assert we actually advanced to the just-fetched origin tip; fail LOUDLY (not # masked by a `| tail` in the caller) if somehow we did not. LOCAL_HEAD="$(git rev-parse HEAD)" ORIGIN_HEAD="$(git rev-parse origin/main)" if [ "$LOCAL_HEAD" != "$ORIGIN_HEAD" ]; then echo "FATAL: working tree is at $LOCAL_HEAD but origin/main is $ORIGIN_HEAD — deploy aborting." >&2 exit 1 fi echo "Deploying commit $LOCAL_HEAD" # Single source of truth for the site header: rewrite every static page's #