fix(deploy): migrate behind maintenance mode, resume a failed update, install deps
Codex was right on all three, and the first was a design error on my part: the checkout is bind-mounted into every container, so 'git merge' swaps the running code instantly — 'migrations before traffic' was not achievable by ordering. The updater now goes down → merge → dependencies → migrate → assets → restart → up, trading a short deliberate outage for never serving code whose schema does not exist yet. If a step fails the site STAYS down: coming back up with new code on an old schema is worse than staying dark until someone looks. It also records the last successfully deployed commit. A run that died halfway left the checkout ahead, so the next run said 'already up to date' and skipped the rest forever. And it installs dependencies when a lockfile moved: vendor/ and node_modules/ live in the bind mount and shadow the image, so rebuilding the image never updated them — migrations could run against stale packages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
834abcec40
commit
773ef4bd5f
|
|
@ -199,6 +199,11 @@ log "Creating your Owner account"
|
|||
docker compose exec -T app php artisan clupilot:create-admin \
|
||||
--email="$ADMIN_EMAIL" --name="$ADMIN_NAME" --password="$ADMIN_PASSWORD"
|
||||
|
||||
# Record what has been deployed: update.sh compares against this to tell a
|
||||
# finished deployment from one that died halfway.
|
||||
mkdir -p storage/app
|
||||
git rev-parse HEAD > storage/app/deployed-commit
|
||||
|
||||
# ── 8. Firewall ──────────────────────────────────────────────────────────────
|
||||
# Only what has to be reachable: SSH, the web ports, and the tunnel. The console
|
||||
# is additionally restricted to ADMIN_HOSTS inside the application.
|
||||
|
|
|
|||
|
|
@ -2,44 +2,92 @@
|
|||
#
|
||||
# CluPilot — pull the latest code and apply it.
|
||||
#
|
||||
# Order matters: migrations run BEFORE the new containers take traffic, because
|
||||
# code that expects a column the database does not have yet answers with 500s.
|
||||
# The queue workers are restarted at the end — they are long-running processes
|
||||
# and keep the old classes in memory otherwise.
|
||||
# The checkout is bind-mounted into every container, so a `git merge` swaps the
|
||||
# running code instantly. There is no atomic release directory to switch to;
|
||||
# what there is, is maintenance mode. So the order is:
|
||||
#
|
||||
# down → merge → dependencies → migrate → assets → restart → up
|
||||
#
|
||||
# That trades a short, deliberate outage for never serving code whose schema
|
||||
# does not exist yet. If any step fails the site STAYS down: coming back up with
|
||||
# new code on an old schema is worse than staying dark until someone looks.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
BRANCH="${BRANCH:-main}"
|
||||
STATE_FILE="storage/app/deployed-commit"
|
||||
|
||||
log() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; }
|
||||
|
||||
in_app() { docker compose exec -T app "$@"; }
|
||||
|
||||
down=0
|
||||
finish() {
|
||||
local code=$?
|
||||
if [[ $code -ne 0 && $down -eq 1 ]]; then
|
||||
warn "Update failed. The site is STILL in maintenance mode on purpose."
|
||||
warn "Fix the problem and re-run this script, or force it back up with:"
|
||||
warn " docker compose exec app php artisan up"
|
||||
fi
|
||||
exit $code
|
||||
}
|
||||
trap finish EXIT
|
||||
|
||||
log "Fetching $BRANCH"
|
||||
git fetch --quiet origin "$BRANCH"
|
||||
|
||||
before="$(git rev-parse HEAD)"
|
||||
git merge --quiet --ff-only "origin/$BRANCH"
|
||||
after="$(git rev-parse HEAD)"
|
||||
target="$(git rev-parse "origin/$BRANCH")"
|
||||
# What was last deployed successfully — not what is merely checked out. A run
|
||||
# that died halfway leaves these different, and the next run finishes the job
|
||||
# instead of declaring "already up to date".
|
||||
deployed="$(cat "$STATE_FILE" 2>/dev/null || echo '')"
|
||||
|
||||
if [[ "$before" == "$after" ]]; then
|
||||
if [[ "$before" == "$target" && "$deployed" == "$target" ]]; then
|
||||
log "Already up to date ($(git rev-parse --short HEAD))"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Updating $(git rev-parse --short "$before") → $(git rev-parse --short "$after")"
|
||||
git --no-pager log --oneline "$before..$after" | sed 's/^/ /'
|
||||
if [[ "$before" != "$target" ]]; then
|
||||
log "Updating $(git rev-parse --short "$before") → $(git rev-parse --short "$target")"
|
||||
git --no-pager log --oneline "$before..$target" | sed 's/^/ /'
|
||||
else
|
||||
warn "Code is current but the last update did not finish — repeating the steps."
|
||||
fi
|
||||
|
||||
# Rebuilt only when the image definition actually changed — a rebuild is
|
||||
# minutes, a no-op check is seconds.
|
||||
if ! git diff --quiet "$before" "$after" -- docker/ composer.json composer.lock package.json package-lock.json; then
|
||||
log "Enabling maintenance mode"
|
||||
in_app php artisan down --retry=60 >/dev/null || warn "Could not enable maintenance mode (continuing)"
|
||||
down=1
|
||||
|
||||
log "Checking out $BRANCH"
|
||||
git merge --quiet --ff-only "origin/$BRANCH"
|
||||
after="$(git rev-parse HEAD)"
|
||||
|
||||
# The image is only rebuilt when its definition changed — minutes versus seconds.
|
||||
if ! git diff --quiet "$before" "$after" -- docker/ 2>/dev/null; then
|
||||
log "Rebuilding the image"
|
||||
docker compose build --quiet app
|
||||
fi
|
||||
|
||||
# vendor/ and node_modules/ live in the bind mount, so they shadow whatever the
|
||||
# image contains: rebuilding the image does NOT update them. Install explicitly
|
||||
# whenever a lockfile moved, or the migration below runs against stale packages.
|
||||
if ! git diff --quiet "$before" "$after" -- composer.json composer.lock 2>/dev/null || [[ ! -d vendor ]]; then
|
||||
log "Installing PHP dependencies"
|
||||
in_app composer install --no-interaction --no-dev --prefer-dist --no-progress --optimize-autoloader
|
||||
fi
|
||||
|
||||
if ! git diff --quiet "$before" "$after" -- package.json package-lock.json 2>/dev/null || [[ ! -d node_modules ]]; then
|
||||
log "Installing JS dependencies"
|
||||
in_app npm ci --no-fund --no-audit
|
||||
fi
|
||||
|
||||
log "Applying migrations"
|
||||
docker compose exec -T app php artisan migrate --force
|
||||
in_app php artisan migrate --force
|
||||
|
||||
log "Rebuilding assets"
|
||||
docker compose exec -T app npm run build
|
||||
in_app npm run build
|
||||
|
||||
log "Restarting services"
|
||||
docker compose up -d
|
||||
|
|
@ -47,7 +95,12 @@ docker compose up -d
|
|||
# keep running the code from before the update.
|
||||
docker compose restart queue queue-provisioning scheduler reverb
|
||||
|
||||
docker compose exec -T app php artisan config:clear >/dev/null
|
||||
docker compose exec -T app php artisan view:clear >/dev/null
|
||||
in_app php artisan config:clear >/dev/null
|
||||
in_app php artisan view:clear >/dev/null
|
||||
|
||||
log "Leaving maintenance mode"
|
||||
in_app php artisan up >/dev/null
|
||||
down=0
|
||||
|
||||
printf '%s' "$after" > "$STATE_FILE"
|
||||
log "Done — now on $(git rev-parse --short HEAD)"
|
||||
|
|
|
|||
Loading…
Reference in New Issue