#!/bin/bash # # Deploy script for the production host (WebApp-Testing-live). # # The app runs there as a PM2 process ("my-weekly-todo") from /root/My-Weekly-ToDo-List. # It is NOT run under Docker — deploy.sh and docker-compose.yml in this repo describe an # older Portainer setup that is not what serves todo.martin-bierschenk.de. # # This is the single update path. It is used both ways: # - by hand: /root/My-Weekly-ToDo-List/scripts/update-app.sh [--force] # - at boot: my-weekly-todo-update.service, before pm2-root.service starts the app # # There used to be a second, near-identical boot-update.sh. The two drifted (the npm ci fix # from 1.114.1 only ever landed in this one), so boot-update.sh is now a shim that calls # this script. Keep it that way: one script, one set of fixes. # # The whole body is wrapped in a function so bash parses it in one go: this script lives # inside the very checkout it updates, and `git pull` rewriting it mid-run would otherwise # make bash resume at a stale file offset. main() { # pipefail matters because everything below is written through a `tee` redirect: without # it the shell sees tee's exit status (always 0) and a failed build looks like success. set -euo pipefail local APP_DIR="/root/My-Weekly-ToDo-List" local PM2_APP="my-weekly-todo" local LOG_FILE="/var/log/my-weekly-todo-update.log" local LOCK_FILE="/var/lock/my-weekly-todo-update.lock" local FORCE="${1:-}" # One redirect for the whole run: goes to the log and, when run by hand, to the console. exec > >(tee -a "$LOG_FILE") 2>&1 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; } trap 'log "!!! Update FAILED at line $LINENO (exit $?) — the running build is untouched"' ERR # Fail fast instead of hanging on a credential prompt when run without a terminal # (systemd has no tty). The host authenticates via a read-only SSH deploy key. export GIT_TERMINAL_PROMPT=0 # Never let a boot run and a manual run install into the same tree at once: a half-written # node_modules is what leaves npm unable to clean up afterwards (ENOTEMPTY on rmdir). exec 9>"$LOCK_FILE" if ! flock -n 9; then log "Another update is already running — nothing to do." return 0 fi cd "$APP_DIR" log "=== Update started ===" log "--- Ensuring main branch ---" git checkout main log "--- Fetching from git ---" git fetch origin main local LOCAL REMOTE LOCAL=$(git rev-parse HEAD) REMOTE=$(git rev-parse origin/main) # Rebuild when the code moved, when there is no build to serve, or when asked to. local NEEDS_BUILD=0 [ "$LOCAL" != "$REMOTE" ] && NEEDS_BUILD=1 [ ! -f "$APP_DIR/.next/standalone/server.js" ] && NEEDS_BUILD=1 [ "$FORCE" = "--force" ] && NEEDS_BUILD=1 if [ "$NEEDS_BUILD" -eq 0 ]; then log "Already up to date and a build is in place. Nothing to do." log "=== Update finished (no changes) ===" return 0 fi if [ "$LOCAL" != "$REMOTE" ]; then log "--- Pulling latest changes ($LOCAL -> $REMOTE) ---" git pull origin main # npm ci, not npm install: it installs exactly what package-lock.json specifies and # never rewrites it. npm install rewrites the lockfile, which leaves the working tree # dirty and makes the *next* git pull abort. npm ci clears node_modules itself; if # that ever fails with ENOTEMPTY, remove the directory by hand and rerun. log "--- Installing dependencies ---" npm ci fi log "--- Updating database schema (safe migrate) ---" npx prisma migrate deploy || { log "migrate deploy failed, falling back to db push..." npx prisma db push --accept-data-loss } log "--- Regenerating Prisma client ---" npx prisma generate # Der Standard-Heap von Node richtet sich nach dem Arbeitsspeicher und landet auf # diesem Container bei rund 1,5 GB. Das reichte fuer `next build` nicht mehr, der Build # starb mit "JavaScript heap out of memory". 2 GB lassen genug Luft fuer den Rest. log "--- Building application ---" NODE_OPTIONS="--max-old-space-size=2048" npm run build # next.config.js sets output: 'standalone', and Next.js does NOT copy public/ or # .next/static/ into .next/standalone — that is a documented manual step. Skipping it # ships new server code whose HTML references new chunk hashes while the old chunks sit # in the standalone tree: they 404, React never hydrates, and every page hangs on its # server-rendered shell ("Loading session...") while the API still answers 200. log "--- Copying static assets into standalone output ---" cp -r public .next/standalone/ mkdir -p .next/standalone/.next/static cp -r .next/static .next/standalone/.next/ cp .env .next/standalone/ 2>/dev/null || true # At boot this script runs before pm2-root.service, so the PM2 daemon is not up yet and # there is nothing to restart — pm2-root.service resurrects the app right after. Probing # with `pm2 describe` would be wrong here: any pm2 command spawns a daemon, and a stray # one started from this unit would race the real service. local PM2_PID_FILE="${HOME:-/root}/.pm2/pm2.pid" if [ -f "$PM2_PID_FILE" ] && kill -0 "$(cat "$PM2_PID_FILE")" 2>/dev/null; then log "--- Restarting PM2 process ---" pm2 restart "$PM2_APP" --update-env pm2 save else log "PM2 daemon is not running — leaving the start to pm2-root.service." fi log "=== Update Complete ===" log "Deployed version: $(node -p "require('$APP_DIR/package.json').version")" } main "$@"