#!/bin/bash # # Boot Update Script: pull latest main, install, build, and prepare for PM2. # Runs on LXC container boot BEFORE PM2 starts (my-weekly-todo-update.service). # # 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 here: every command used to be piped into `tee`, so the shell saw # tee's exit status (always 0). A failed npm install or build did not stop the script # and did not fail the unit — it just logged "finished" and left a half-built tree. set -euo pipefail local APP_DIR="/root/My-Weekly-ToDo-List" local LOG_FILE="/var/log/app-boot-update.log" # Send everything to the log once, instead of piping each command through tee. exec > >(tee -a "$LOG_FILE") 2>&1 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; } trap 'log "!!! Boot update FAILED at line $LINENO (exit $?) — not restarting the app"' 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 log "=== Boot update started ===" cd "$APP_DIR" log "Ensuring main branch..." git checkout main log "Fetching from origin..." git fetch origin main local LOCAL REMOTE NEXT_DIR LOCAL=$(git rev-parse HEAD) REMOTE=$(git rev-parse origin/main) NEXT_DIR="$APP_DIR/.next" if [ "$LOCAL" = "$REMOTE" ] && [ -d "$NEXT_DIR" ]; then log "Already up-to-date and .next exists. No rebuild needed." log "=== Boot update finished (no changes) ===" return 0 fi if [ "$LOCAL" != "$REMOTE" ]; then log "Changes detected. Updating from $LOCAL to $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 also clears node_modules # itself; if that ever fails with ENOTEMPTY, delete the directory by hand and rerun. log "Installing dependencies..." npm ci fi if [ ! -d "$NEXT_DIR" ] || [ "$LOCAL" != "$REMOTE" ]; then 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 log "Building application..." 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 "Loading session..." while the API still answers 200. log "Preparing standalone directory (static assets and env)..." 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 fi log "Deployed version: $(node -p "require('$APP_DIR/package.json').version")" log "=== Boot update finished (verified/rebuilt) ===" } main "$@"