mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-02 16:26:56 +00:00
Migrations are forward-only and the appview applies whatever is missing on boot, so moving to the next batch does not need a reset at all — Air rebuilds into the new code and the live DB migrates in place. Verified moving onto val/04-oauth: 0031 appeared in schema_migrations on its own, on top of a level-27 database, with the appview healthy afterwards. That matters more than it sounds. ui.db holds the OAuth sessions and the appview's signing keys, so the old wipe-on-every-switch cost an interactive `docker-credential-atcr login` per batch, which is most of what made the stack awkward to hand to an agent. Validating in stack order is all forward motion, so in the normal case there is now no login at all. A reset is still done when the live DB carries migrations the branch's code has never heard of, which is what going backward means, and the per-set snapshot is still banked so that case can restore rather than start empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
143 lines
6.6 KiB
Bash
Executable File
143 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# val-switch.sh <branch> — move the dev stack to a validation branch.
|
|
#
|
|
# Why each step exists (learned the hard way on batch 00):
|
|
#
|
|
# * The appview DB is migrated FORWARD ONLY. The live volume sits at whatever
|
|
# migration main last applied (34 today). Older batch code then hits columns
|
|
# that no longer exist (batch 00 selects tags.id, dropped by 0032) and
|
|
# columns it never learned to write (manifests.manifest_key NOT NULL, added
|
|
# by 0033/0034 — backfill dies on every insert). So the appview volume gets
|
|
# destroyed on every switch and re-migrated from scratch to the branch level.
|
|
#
|
|
# * The HOLD volume must survive. It holds the hold's did:web signing key and
|
|
# the CAR store (captain, crew, layer, stats, scan records). Nuking it gives
|
|
# the hold a new identity and drops every pushed layer.
|
|
#
|
|
# * docker-compose.yml is pinned to main. a7c7db6 (batch 01) is what makes the
|
|
# appview share the hold's netns so did:web:localhost%3A8080 resolves; every
|
|
# compose-based batch needs it, including batch 00, which lands before it.
|
|
# The file is dev-only (nothing in deploy/ or CI reads it), so pinning it is
|
|
# a test-fixture decision, not a change to what is being validated.
|
|
# NEVER `git add docker-compose.yml` while validating.
|
|
#
|
|
# Usage: ./val-switch.sh val/00-mixed
|
|
set -euo pipefail
|
|
|
|
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"
|
|
|
|
echo "==> restoring pinned dev compose before checkout"
|
|
git checkout -- docker-compose.yml 2>/dev/null || true
|
|
|
|
echo "==> checking out ${BRANCH}"
|
|
git checkout "$BRANCH"
|
|
|
|
# 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
|
|
|
|
# Resetting the DB matters because ui.db holds the OAuth sessions AND the
|
|
# appview's oauth_p256/jwt_rsa keys, so wiping it logs out the registry
|
|
# credential helper and costs an interactive `docker-credential-atcr login`.
|
|
#
|
|
# Going FORWARD (the normal case — batches are validated in stack order) needs
|
|
# no reset at all: migrations are forward-only and the appview applies whatever
|
|
# is missing on boot, so Air rebuilding into the next branch migrates the live
|
|
# DB in place and the session survives. Only going BACKWARD needs a reset, since
|
|
# the DB then carries migrations the branch's code has never heard of.
|
|
#
|
|
# Key the snapshot on the FULL SET of migrations present, not the highest one.
|
|
# Batching reorders migrations: val/04 carries e75b2e2, whose migration is 0031,
|
|
# while 0028-0030 arrive later in batches 09, 12 and 14. So val/04 holds
|
|
# {..0027, 0031} and val/09 holds {..0027, 0028, 0031} — both have a max of
|
|
# 0031, and keying on the max would restore val/04's snapshot onto val/09, which
|
|
# needs 0028's schema. The fingerprint distinguishes them.
|
|
MIGS=$(ls pkg/appview/db/migrations/*.yaml 2>/dev/null \
|
|
| sed -E 's|.*/([0-9]{4})_.*|\1|' | sort -n)
|
|
LEVEL=$(printf '%s\n' "$MIGS" | tail -1)
|
|
LEVEL=${LEVEL:-0000}
|
|
FINGERPRINT=$(printf '%s\n' "$MIGS" | md5sum | cut -c1-8)
|
|
SNAP="$SNAPDIR/ui.level-${LEVEL}-${FINGERPRINT}.db"
|
|
echo "==> branch migrations: max ${LEVEL}, set ${FINGERPRINT} ($(printf '%s\n' "$MIGS" | wc -l) files)"
|
|
|
|
# Which migrations does the live DB already carry that this branch does not know?
|
|
APPLIED=$(docker exec atcr-appview sh -c \
|
|
'sqlite3 /var/lib/atcr/ui.db "select version from schema_migrations"' 2>/dev/null || true)
|
|
EXTRA=$(python3 -c '
|
|
import sys
|
|
applied = {v.strip().lstrip("0") or "0" for v in sys.argv[1].split() if v.strip()}
|
|
branch = {v.strip().lstrip("0") or "0" for v in sys.argv[2].split() if v.strip()}
|
|
print(" ".join(sorted(applied - branch, key=int)))
|
|
' "$APPLIED" "$MIGS" 2>/dev/null || echo "")
|
|
|
|
if [ -z "$APPLIED" ]; then
|
|
echo "==> no live DB; starting fresh"
|
|
docker compose rm -sf atcr-appview
|
|
docker volume rm "$UI_VOLUME" 2>/dev/null || true
|
|
docker compose up -d atcr-appview
|
|
elif [ -z "$EXTRA" ]; then
|
|
echo "==> moving forward; leaving the DB alone (Air migrates it in place, session survives)"
|
|
docker compose up -d atcr-appview >/dev/null 2>&1 || true
|
|
else
|
|
echo "==> moving BACKWARD; the DB carries migrations this branch lacks: ${EXTRA}"
|
|
docker compose rm -sf atcr-appview
|
|
if [ -f "$SNAP" ]; then
|
|
echo " restoring snapshot ${LEVEL}-${FINGERPRINT} (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 this migration set; starting from an empty DB (expect a re-login)"
|
|
docker volume rm "$UI_VOLUME" 2>/dev/null || true
|
|
fi
|
|
docker compose up -d atcr-appview
|
|
fi
|
|
|
|
echo "==> waiting for appview to answer on 127.0.0.1:5000"
|
|
for i in $(seq 1 90); do
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5000/v2/ || true)
|
|
if [ "$code" != "000" ]; then
|
|
echo " up after ${i}s (HTTP ${code})"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
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 "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 (login again if the hold rebuilt)"
|
|
echo "registry: docker-credential-atcr login (needed after an empty-DB start)"
|