fix: make update-app.sh the single update path and run it at boot
Some checks are pending
Playwright Tests / test (push) Waiting to run
Some checks are pending
Playwright Tests / test (push) Waiting to run
There were two near-identical update scripts and they drifted: the npm ci fix from 1.114.1 only ever landed in update-app.sh, so the boot path kept running npm install. my-weekly-todo-update.service now runs update-app.sh, and boot-update.sh is a shim kept only so a not-yet-recopied unit still works. To be usable at boot the script needed three things it did not have: - Skip the rebuild when HEAD already matches origin/main and a standalone build exists, so a reboot is not a full rebuild every time (--force overrides). - Do not restart PM2 when its daemon is not up yet. At boot this unit runs before pm2-root.service, and probing with `pm2 describe` would spawn a stray daemon that races the real service, so it checks the pid file. - A flock single-instance lock, so a boot run and a manual run cannot install into the same tree at once. It also picks up prisma migrate deploy / generate and the .env copy that only the boot script had. The log moved to /var/log/my-weekly-todo-update.log. v1.115.2 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9d4e40e4e3
commit
6f4fe219dc
28
CHANGELOG.md
28
CHANGELOG.md
@ -2,6 +2,34 @@
|
|||||||
|
|
||||||
All notable changes to this project are documented in this file.
|
All notable changes to this project are documented in this file.
|
||||||
|
|
||||||
|
## [1.115.2] - 2026-09-03
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `scripts/update-app.sh` is now the single update path, used both for a manual deploy and
|
||||||
|
for the boot run; `my-weekly-todo-update.service` points at it. There were two
|
||||||
|
near-identical scripts and they drifted — the `npm ci` fix from 1.114.1 only ever landed
|
||||||
|
in update-app.sh, so the boot path kept running `npm install`. `scripts/boot-update.sh`
|
||||||
|
is now a shim that calls it, kept only so a not-yet-recopied unit file still works.
|
||||||
|
- The update log moved from `/var/log/app-boot-update.log` to
|
||||||
|
`/var/log/my-weekly-todo-update.log`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `update-app.sh` skips the rebuild when `HEAD` already matches `origin/main` and
|
||||||
|
`.next/standalone/server.js` exists, so a reboot no longer costs a full rebuild. Pass
|
||||||
|
`--force` to rebuild anyway.
|
||||||
|
- `update-app.sh` takes a `flock` single-instance lock, so a boot run and a manual run can
|
||||||
|
no longer install into the same tree at once — which is how node_modules ended up in a
|
||||||
|
state npm could not clean up.
|
||||||
|
- `update-app.sh` runs `prisma migrate deploy` and `prisma generate` (previously only the
|
||||||
|
boot script did), and copies `.env` into the standalone output.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `update-app.sh` no longer fails at boot trying to restart a PM2 process that does not
|
||||||
|
exist yet. It restarts only when the PM2 daemon is actually running, and otherwise leaves
|
||||||
|
the start to `pm2-root.service`, which is ordered after this unit. It deliberately does
|
||||||
|
not probe with `pm2 describe`, since any pm2 command spawns a daemon that would race the
|
||||||
|
real service.
|
||||||
|
|
||||||
## [1.115.1] - 2026-09-03
|
## [1.115.1] - 2026-09-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.115.1",
|
"version": "1.115.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.115.1",
|
"version": "1.115.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.115.1",
|
"version": "1.115.2",
|
||||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,92 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# Boot Update Script: pull latest main, install, build, and prepare for PM2.
|
# Deprecated: kept only so a stale /etc/systemd/system/my-weekly-todo-update.service that
|
||||||
# Runs on LXC container boot BEFORE PM2 starts (my-weekly-todo-update.service).
|
# still points here keeps working. update-app.sh is the single update path — it handles both
|
||||||
#
|
# the manual deploy and the boot run. Once the unit has been recopied from
|
||||||
# The whole body is wrapped in a function so bash parses it in one go: this script lives
|
# scripts/my-weekly-todo-update.service, this file can be deleted.
|
||||||
# inside the very checkout it updates, and `git pull` rewriting it mid-run would otherwise
|
|
||||||
# make bash resume at a stale file offset.
|
|
||||||
|
|
||||||
main() {
|
exec /bin/bash /root/My-Weekly-ToDo-List/scripts/update-app.sh "$@"
|
||||||
# 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 "$@"
|
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=My Weekly ToDo List - Boot Update (git pull + build)
|
Description=My Weekly ToDo List - Boot Update (git pull + build)
|
||||||
|
# Runs the same script as a manual deploy; it skips the rebuild when nothing changed and
|
||||||
|
# leaves starting the app to pm2-root.service, which is ordered after this unit.
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
Before=pm2-root.service
|
Before=pm2-root.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/bin/bash /root/My-Weekly-ToDo-List/scripts/boot-update.sh
|
ExecStart=/bin/bash /root/My-Weekly-ToDo-List/scripts/update-app.sh
|
||||||
WorkingDirectory=/root/My-Weekly-ToDo-List
|
WorkingDirectory=/root/My-Weekly-ToDo-List
|
||||||
Environment=HOME=/root
|
Environment=HOME=/root
|
||||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|||||||
@ -6,34 +6,96 @@
|
|||||||
# It is NOT run under Docker — deploy.sh and docker-compose.yml in this repo describe an
|
# 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.
|
# older Portainer setup that is not what serves todo.martin-bierschenk.de.
|
||||||
#
|
#
|
||||||
# Usage on the host: /root/My-Weekly-ToDo-List/scripts/update-app.sh
|
# 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
|
# 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
|
# inside the very checkout it updates, and `git pull` rewriting it mid-run would otherwise
|
||||||
# make bash resume at a stale file offset.
|
# make bash resume at a stale file offset.
|
||||||
|
|
||||||
main() {
|
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
|
set -euo pipefail
|
||||||
|
|
||||||
# Fail fast instead of hanging on a credential prompt when run without a terminal
|
|
||||||
# (e.g. from systemd). The host authenticates via a read-only SSH deploy key.
|
|
||||||
export GIT_TERMINAL_PROMPT=0
|
|
||||||
|
|
||||||
local APP_DIR="/root/My-Weekly-ToDo-List"
|
local APP_DIR="/root/My-Weekly-ToDo-List"
|
||||||
local PM2_APP="my-weekly-todo"
|
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"
|
cd "$APP_DIR"
|
||||||
|
|
||||||
echo "--- Pulling latest changes from git ---"
|
log "=== Update started ==="
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
# npm ci, not npm install: it installs exactly what package-lock.json specifies and
|
log "--- Ensuring main branch ---"
|
||||||
# never rewrites it. npm install rewrites the lockfile, which leaves the working tree
|
git checkout main
|
||||||
# dirty and makes the *next* git pull abort.
|
|
||||||
echo "--- Installing dependencies ---"
|
|
||||||
npm ci
|
|
||||||
|
|
||||||
echo "--- Building application ---"
|
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
|
||||||
|
|
||||||
|
log "--- Building application ---"
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# next.config.js sets output: 'standalone', and Next.js does NOT copy public/ or
|
# next.config.js sets output: 'standalone', and Next.js does NOT copy public/ or
|
||||||
@ -41,16 +103,27 @@ main() {
|
|||||||
# ships new server code whose HTML references new chunk hashes while the old chunks sit
|
# 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
|
# 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.
|
# server-rendered shell ("Loading session...") while the API still answers 200.
|
||||||
echo "--- Copying static assets into standalone output ---"
|
log "--- Copying static assets into standalone output ---"
|
||||||
cp -r public .next/standalone/
|
cp -r public .next/standalone/
|
||||||
|
mkdir -p .next/standalone/.next/static
|
||||||
cp -r .next/static .next/standalone/.next/
|
cp -r .next/static .next/standalone/.next/
|
||||||
|
cp .env .next/standalone/ 2>/dev/null || true
|
||||||
|
|
||||||
echo "--- Restarting PM2 process ---"
|
# At boot this script runs before pm2-root.service, so the PM2 daemon is not up yet and
|
||||||
pm2 restart "$PM2_APP" --update-env
|
# there is nothing to restart — pm2-root.service resurrects the app right after. Probing
|
||||||
pm2 save
|
# 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
|
||||||
|
|
||||||
echo "--- Update Complete ---"
|
log "=== Update Complete ==="
|
||||||
echo "Deployed version: $(node -p "require('$APP_DIR/package.json').version")"
|
log "Deployed version: $(node -p "require('$APP_DIR/package.json').version")"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user