test/e2e: snapshot the appview DB per migration level

Wiping ui.db on every switch also wipes the OAuth sessions and the appview's
oauth_p256/jwt_rsa keys, so each batch cost an interactive
`docker-credential-atcr login`. There are only five distinct migration levels
across the stack (27 for batches 00-08, 28 for 09-11, 29 for 12-13, 32 for 14,
34 for 15), so the DB is snapshotted per level and restored instead of
re-migrated. One login now serves every batch sharing a level.

Also skip the compose pin from val/01 onward: a7c7db6 lands there, so the
branch's own compose already has the shared netns and pinning would drag in
later batches' changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Evan Jarrett
2026-08-25 16:34:25 -05:00
co-authored by Claude Opus 5
parent 5aefa85048
commit 50e77ac7a4
+59 -15
View File
@@ -28,6 +28,8 @@ BRANCH="${1:?usage: val-switch.sh <branch>}"
REPO=/home/data/atcr.io
PROJECT=atcrio
UI_VOLUME=${PROJECT}_atcr-ui
SNAPDIR=${ATCR_VAL_SNAPDIR:-$HOME/.cache/atcr-val-snapshots}
mkdir -p "$SNAPDIR"
cd "$REPO"
@@ -37,18 +39,49 @@ git checkout -- docker-compose.yml 2>/dev/null || true
echo "==> checking out ${BRANCH}"
git checkout "$BRANCH"
echo "==> pinning docker-compose.yml to main (dev topology only, do not commit)"
git checkout main -- docker-compose.yml
# `git checkout <ref> -- <path>` STAGES the file. Left staged, the next commit
# on the batch branch silently swallows main's compose file. Unstage it so it
# shows as an ordinary working-tree modification.
git restore --staged docker-compose.yml
# Only batch 00 predates a7c7db6, which is what puts the appview in the hold's
# netns so did:web:localhost%3A8080 resolves. From val/01 onward the branch's
# own compose already has it, and pinning would drag in later batches' changes.
if grep -q 'network_mode: "service:atcr-hold"' docker-compose.yml; then
echo "==> branch compose already has the shared netns; no pin needed"
else
echo "==> pinning docker-compose.yml to main (dev topology only, do not commit)"
git checkout main -- docker-compose.yml
# `git checkout <ref> -- <path>` STAGES the file. Left staged, the next commit
# on the batch branch silently swallows main's compose file. Unstage it so it
# shows as an ordinary working-tree modification.
git restore --staged docker-compose.yml
fi
echo "==> tearing down appview + its DB volume"
# The appview DB only has to match the branch's MIGRATION LEVEL, not the
# branch. Across the whole stack there are five distinct levels (27 for batches
# 00-08, 28 for 09-11, 29 for 12-13, 32 for 14, 34 for 15), so a per-level
# snapshot is reused instead of re-migrating from empty every time.
#
# This matters because ui.db holds the OAuth sessions AND the appview's
# oauth_p256/jwt_rsa keys. Nuking it logs out the registry credential helper, so
# a naive wipe-per-switch costs an interactive `docker-credential-atcr login` on
# every batch. Restoring a snapshot keeps the session alive across every batch
# that shares a level.
LEVEL=$(ls pkg/appview/db/migrations/*.yaml 2>/dev/null \
| sed -E 's|.*/([0-9]{4})_.*|\1|' | sort -n | tail -1)
LEVEL=${LEVEL:-0000}
SNAP="$SNAPDIR/ui.level-${LEVEL}.db"
echo "==> branch migration level: ${LEVEL}"
echo "==> tearing down appview"
docker compose rm -sf atcr-appview
docker volume rm "$UI_VOLUME" 2>/dev/null || echo " (volume already gone)"
echo "==> starting appview on a fresh DB"
if [ -f "$SNAP" ]; then
echo "==> restoring DB snapshot for level ${LEVEL} (keeps OAuth sessions alive)"
docker run --rm -v "${UI_VOLUME}:/v" -v "$SNAPDIR:/s:ro" alpine sh -c \
'rm -f /v/ui.db /v/ui.db-wal /v/ui.db-shm && cp /s/'"$(basename "$SNAP")"' /v/ui.db'
else
echo "==> no snapshot for level ${LEVEL}; starting from an empty DB"
docker volume rm "$UI_VOLUME" 2>/dev/null || echo " (volume already gone)"
fi
echo "==> starting appview"
docker compose up -d atcr-appview
echo "==> waiting for appview to answer on 127.0.0.1:5000"
@@ -61,13 +94,24 @@ for i in $(seq 1 90); do
sleep 1
done
echo "==> migration level on the fresh DB"
docker exec atcr-appview sh -c \
'sqlite3 /var/lib/atcr/ui.db "select max(version) from schema_migrations"' 2>/dev/null \
|| echo " (db not created yet — check: docker logs atcr-appview)"
ACTUAL=$(docker exec atcr-appview sh -c \
'sqlite3 /var/lib/atcr/ui.db "select max(version) from schema_migrations"' 2>/dev/null || true)
echo "==> migration level on the running DB: ${ACTUAL:-unknown} (expected ${LEVEL#0})"
# Bank a snapshot for this level so the next batch at the same level restores
# instead of re-migrating — and keeps its OAuth sessions.
if [ ! -f "$SNAP" ] && [ -n "$ACTUAL" ]; then
echo "==> banking a snapshot for level ${LEVEL}"
docker exec atcr-appview sh -c \
'sqlite3 /var/lib/atcr/ui.db ".backup /var/lib/atcr/ui.snap.db"' \
&& docker run --rm -v "${UI_VOLUME}:/v" -v "$SNAPDIR:/s" alpine sh -c \
"mv /v/ui.snap.db /s/$(basename "$SNAP")" \
&& echo " saved $SNAP"
fi
echo
echo "branch: $(git log --oneline -1)"
echo "compose: pinned to main (expect it to show as modified; leave it unstaged)"
echo "migrations: level ${ACTUAL:-?}"
echo "appview: http://127.0.0.1:5000 (NOT localhost — that 307s)"
echo "hold admin: http://127.0.0.1:8080/admin (OAuth as did:plc:pddp4xt5lgnv2qsegbzzs4xg / evan.jarrett.net)"
echo "hold admin: http://127.0.0.1:8080/admin (login again if the hold rebuilt)"
echo "registry: docker-credential-atcr login (needed after an empty-DB start)"