Production PocketBase takes its schema from the production branch, never from the dev server's checkout
pb-prod (the public API) ran the migrations in the pulled checkout, which follows the table (development), so a restart moved production's schema to unpublished work. It now reads /data/pb-prod-migrations/current: a snapshot of PRODUCTION_BRANCH's migrations taken at start (the ref build-cdn.sh uses) and refreshed from the very snapshot a publish builds. Only a change restarts pb-prod, and the restart is local: pb-prod runs in a loop, so the container stays up. It fails closed: an error keeps the last good snapshot, and with none it is an empty folder, never the development checkout. --automigrate=false, so a change made in prod's dashboard writes no migration. /admin/status shows prodSchema next to published.
This commit is contained in:
parent
8425d215fb
commit
101928ebbf
4 changed files with 85 additions and 6 deletions
|
|
@ -9,8 +9,8 @@
|
|||
// /admin/pb-signin-check a page that signs in to the team PocketBase and shows who it thinks you are
|
||||
// Groups come from oauth2-proxy's X-Forwarded-Groups, which it sets on signed-in routes only.
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync, readFileSync, statSync } from 'node:fs'
|
||||
import { join, normalize } from 'node:path'
|
||||
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
||||
import { basename, join, normalize } from 'node:path'
|
||||
import { serveTreeFile } from './cdn-cache.mjs'
|
||||
|
||||
const env = process.env
|
||||
|
|
@ -41,6 +41,8 @@ function startBuild(by) {
|
|||
})
|
||||
}
|
||||
const publishedCommit = () => (existsSync('/data/cdn/COMMIT') ? readFileSync('/data/cdn/COMMIT', 'utf8').trim() : null)
|
||||
// The production PocketBase's schema: the snapshot its migrations came from (pb-prod-migrations.sh), or 'empty'.
|
||||
const prodSchema = () => { try { return basename(realpathSync('/data/pb-prod-migrations/current')) } catch { return null } }
|
||||
|
||||
// AUTO-BUILD: the public build follows PRODUCTION_BRANCH by itself. main is the shared sandbox; production
|
||||
// moves only when someone promotes (git push origin main:production), so rebuilding on every move is
|
||||
|
|
@ -140,7 +142,7 @@ Bun.serve({
|
|||
}
|
||||
// A one-click check of the team PocketBase sign-in (behind the site sign-in like every /admin/ path).
|
||||
if (p === '/admin/pb-signin-check') return new Response(Bun.file('/app/repo/deployment/cloud/pb-signin-check.html'), { headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' } })
|
||||
if (p === '/admin/status') return Response.json({ ...last, production: { branch: PROD, autoBuildMinutes: AUTO_MIN, head: watch.seen, published: publishedCommit(), error: watch.error } })
|
||||
if (p === '/admin/status') return Response.json({ ...last, production: { branch: PROD, autoBuildMinutes: AUTO_MIN, head: watch.seen, published: publishedCommit(), prodSchema: prodSchema(), error: watch.error } })
|
||||
if (p === '/admin/build') {
|
||||
if (req.method !== 'POST') return text('POST only', 405)
|
||||
if (!isAdmin(req)) return text('builds are for team admins', 403)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ rm -rf "$NEW"; mkdir -p /data/cdn; mv "$SNAP/public" "$NEW"
|
|||
bash "$EXDIR/apply-exclusions.sh" --check "$NEW" || { echo "REFUSED: excluded content in the build; not published"; rm -rf "$NEW" "$SNAP" "$EXDIR"; exit 1; }
|
||||
ln -sfn "$NEW" /data/cdn/current.tmp && mv -T /data/cdn/current.tmp /data/cdn/current
|
||||
echo "$SHA" > /data/cdn/COMMIT
|
||||
# The production PocketBase follows the SAME snapshot: its migrations, then a restart of pb-prod only (exit 0 = the
|
||||
# migrations changed). A failure keeps pb-prod on its last good schema; the publish itself stands.
|
||||
bash /app/repo/deployment/cloud/pb-prod-migrations.sh from "$SNAP/backend/pocketbase/pb_migrations" "$SHA"
|
||||
if [ $? -eq 0 ] && [ -f /tmp/pb-prod.pid ]; then
|
||||
kill "$(cat /tmp/pb-prod.pid)" 2>/dev/null && echo "$(date -u +%FT%TZ) pb-prod restarting on schema $SHA"
|
||||
fi
|
||||
find /data/cdn -maxdepth 1 -name 'build-*' ! -path "$NEW" -exec rm -rf {} +
|
||||
rm -rf "$SNAP" "$EXDIR"
|
||||
echo "$(date -u +%FT%TZ) published $SHA ($PB) at /cdn/dx-maps/"
|
||||
|
|
|
|||
|
|
@ -93,9 +93,13 @@ for g in "${G[@]}"; do g="${g// /}"; [ -n "$g" ] && ALLOW+=(--allowed-group="$g"
|
|||
export OAUTH2_PROXY_CLIENT_SECRET="$OIDC_CLIENT_SECRET"
|
||||
export OAUTH2_PROXY_COOKIE_SECRET="${OIDC_COOKIE_SECRET:-$(head -c 32 /dev/urandom | base64 | tr -d '=+/' | head -c 32)}"
|
||||
|
||||
# Backend: PocketBase DEV (/pb/, signed-in) and PROD (/api-prod/, public API only). Schema = the
|
||||
# migrations in the repo; data = the /data volume. The superuser comes from env, never printed.
|
||||
# Backend: PocketBase DEV (/pb/, signed-in) and PROD (/api-prod/, public API only); data = the /data volume.
|
||||
# The superuser comes from env, never printed. SCHEMA: dev runs the migrations in this checkout (BRANCH, the
|
||||
# table); prod runs ONLY the production branch's (PRODUCTION_BRANCH), from its own snapshot under /data
|
||||
# (pb-prod-migrations.sh), refreshed by build-cdn.sh when a publish lands. Never from this checkout.
|
||||
MIG=/app/repo/backend/pocketbase/pb_migrations; mkdir -p "$MIG" /data/pb-dev /data/pb-prod /data/cdn
|
||||
bash /app/repo/deployment/cloud/pb-prod-migrations.sh fetch
|
||||
PROD_MIG=/data/pb-prod-migrations/current
|
||||
for inst in dev prod; do
|
||||
if [ -n "${PB_SUPERUSER_EMAIL:-}" ] && [ -n "${PB_SUPERUSER_PASSWORD:-}" ]; then
|
||||
pocketbase superuser upsert "$PB_SUPERUSER_EMAIL" "$PB_SUPERUSER_PASSWORD" --dir "/data/pb-$inst" >/dev/null 2>&1 \
|
||||
|
|
@ -109,7 +113,16 @@ NOHOOKS=/var/empty/pb_hooks_disabled
|
|||
GATE=/app/pb_hooks_gate
|
||||
DEV_HOOKS=$NOHOOKS; [ -f "$GATE/team-gate.pb.js" ] && DEV_HOOKS=$GATE
|
||||
pocketbase serve --http 127.0.0.1:8090 --dir /data/pb-dev --migrationsDir "$MIG" --hooksDir "$DEV_HOOKS" &
|
||||
pocketbase serve --http 127.0.0.1:8091 --dir /data/pb-prod --migrationsDir "$MIG" --hooksDir "$NOHOOKS" &
|
||||
# pb-prod runs in a restart loop: after a publish that changes its schema, build-cdn.sh stops it (PID in
|
||||
# /tmp/pb-prod.pid) and it comes straight back on the new migrations, which PocketBase applies at start. The loop
|
||||
# never ends, so a pb-prod exit restarts pb-prod, not the container. --automigrate=false: a change made in its
|
||||
# dashboard writes no migration file anywhere.
|
||||
( while :; do
|
||||
pocketbase serve --http 127.0.0.1:8091 --dir /data/pb-prod --migrationsDir "$PROD_MIG" --automigrate=false \
|
||||
--hooksDir "$NOHOOKS" &
|
||||
echo $! > /tmp/pb-prod.pid; wait $!
|
||||
echo "$(date -u +%FT%TZ) [pb-prod] exited ($?); starting again on $(basename "$(readlink "$PROD_MIG")")"; sleep 1
|
||||
done ) &
|
||||
bun /app/repo/deployment/cloud/admin.mjs &
|
||||
|
||||
# TEAM PB: laptops reach pb-dev's API WITHOUT the site sign-in (DX_PB_URL=https://<host>/pb), and PocketBase
|
||||
|
|
|
|||
58
deployment/cloud/pb-prod-migrations.sh
Executable file
58
deployment/cloud/pb-prod-migrations.sh
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
#!/bin/bash
|
||||
# pb-prod-migrations.sh — the PRODUCTION PocketBase (pb-prod, the public API) takes its schema from the production
|
||||
# branch, never from the checkout the dev server follows. The dev server's checkout is BRANCH (the table, e.g.
|
||||
# development); production is PRODUCTION_BRANCH (e.g. main). PocketBase migrations are code with full access to
|
||||
# the database, so production runs only what was published.
|
||||
#
|
||||
# Keeps /data/pb-prod-migrations/<sha>/, a `current` symlink that pb-prod reads, and `previous` for rollback.
|
||||
# pb-prod-migrations.sh fetch at container start (entry.sh): fetch PRODUCTION_BRANCH into
|
||||
# refs/cdn/<branch>, the ref build-cdn.sh uses, and snapshot its migrations
|
||||
# pb-prod-migrations.sh from <dir> <sha> after a publish (build-cdn.sh): the snapshot that was just published
|
||||
# Exit: 0 = current now holds different migrations (restart pb-prod to apply them); 3 = unchanged; 1 = failed, and
|
||||
# the last good `current` is kept. FAIL-CLOSED: with no good snapshot yet, `current` is an EMPTY folder (no app
|
||||
# schema). It never points at the development checkout.
|
||||
set -u
|
||||
ROOT=/data/pb-prod-migrations
|
||||
REL=backend/pocketbase/pb_migrations
|
||||
log() { echo "$(date -u +%FT%TZ) [pb-prod schema] $*"; }
|
||||
mkdir -p "$ROOT/empty"
|
||||
[ -L "$ROOT/current" ] || ln -sfn "$ROOT/empty" "$ROOT/current"
|
||||
|
||||
# install <source dir, may be absent = no migrations on that branch> <sha>
|
||||
install() {
|
||||
local src=$1 sha=$2 new="$ROOT/$2" old
|
||||
old=$(readlink "$ROOT/current")
|
||||
rm -rf "$new.tmp" && mkdir -p "$new.tmp" || return 1
|
||||
if [ -d "$src" ]; then cp -a "$src/." "$new.tmp/" || return 1; fi
|
||||
if [ -d "$old" ] && diff -rq "$old" "$new.tmp" >/dev/null 2>&1; then
|
||||
rm -rf "$new.tmp"; log "unchanged at $sha ($(ls "$old" | wc -l) files, from $(basename "$old"))"; return 3
|
||||
fi
|
||||
rm -rf "$new" && mv "$new.tmp" "$new" || return 1
|
||||
ln -sfn "$new" "$ROOT/current.tmp" && mv -T "$ROOT/current.tmp" "$ROOT/current" || return 1
|
||||
[ -d "$old" ] && [ "$old" != "$new" ] && ln -sfn "$old" "$ROOT/previous.tmp" && mv -T "$ROOT/previous.tmp" "$ROOT/previous"
|
||||
# keep empty/, current and previous; drop older snapshots
|
||||
for d in "$ROOT"/*/; do d=${d%/}
|
||||
case "$d" in "$ROOT/empty"|"$(readlink "$ROOT/current")"|"$(readlink "$ROOT/previous" 2>/dev/null)") ;; *) rm -rf "$d" ;; esac
|
||||
done
|
||||
log "now $sha: $(ls "$new" | wc -l) migration files (was $(basename "$old"))"; return 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
fetch)
|
||||
PB="${PRODUCTION_BRANCH:-production}"
|
||||
cd /app/repo || exit 1
|
||||
# Its own ref and no FETCH_HEAD, exactly as build-cdn.sh: the dev server's pull loop uses FETCH_HEAD.
|
||||
if ! git fetch -q --no-write-fetch-head --depth 1 origin "+refs/heads/$PB:refs/cdn/$PB"; then
|
||||
log "cannot fetch '$PB': keeping $(basename "$(readlink "$ROOT/current")")"; exit 1
|
||||
fi
|
||||
SHA=$(git rev-parse --short "refs/cdn/$PB")
|
||||
T=$(mktemp -d)
|
||||
if git ls-tree -d "refs/cdn/$PB" "$REL" | grep -q .; then
|
||||
git archive "refs/cdn/$PB" "$REL" | tar -x -C "$T" || { log "snapshot of $PB failed: keeping the last good"; rm -rf "$T"; exit 1; }
|
||||
fi
|
||||
install "$T/$REL" "$SHA"; rc=$?; rm -rf "$T"; exit $rc ;;
|
||||
from)
|
||||
[ $# -eq 3 ] || { echo "usage: $0 from <migrations dir> <sha>"; exit 1; }
|
||||
install "$2" "$3"; exit $? ;;
|
||||
*) echo "usage: $0 fetch | from <migrations dir> <sha>"; exit 1 ;;
|
||||
esac
|
||||
Loading…
Reference in a new issue