#!/usr/bin/env bash # val-switch.sh — 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 }" 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 -- ` 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)"