Files
geointel/scripts/verify_release_upgrade_smoke.sh
T
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

137 lines
4.4 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
CONTAINER="geointel"
BACKUP_DIR=""
OUTPUT=""
CONFIRM="false"
RESTORE_RESULT=""
TARGET_DB=""
usage() {
cat <<'EOF'
Usage: bash scripts/verify_release_upgrade_smoke.sh \
--backup-dir PATH --confirm-isolated-upgrade [options]
Restores a verified release backup into a generated temporary database, runs
the currently deployed image's Alembic upgrade against that database, verifies
PostGIS and the single expected head, then removes the temporary database.
Options:
--container NAME
--output PATH
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--backup-dir) BACKUP_DIR="$2"; shift 2 ;;
--container) CONTAINER="$2"; shift 2 ;;
--output) OUTPUT="$2"; shift 2 ;;
--confirm-isolated-upgrade) CONFIRM="true"; shift ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
if [ "$CONFIRM" != "true" ] || [ -z "$BACKUP_DIR" ]; then
echo "Explicit --confirm-isolated-upgrade and --backup-dir are required." >&2
exit 2
fi
for required in docker python3; do
command -v "$required" >/dev/null 2>&1 || {
echo "Missing required command: $required" >&2
exit 2
}
done
BACKUP_DIR="$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).expanduser().resolve())' "$BACKUP_DIR")"
RESTORE_RESULT="$(mktemp "${TMPDIR:-/tmp}/geointel-upgrade-restore.XXXXXX.json")"
cleanup() {
if [ -n "$TARGET_DB" ] && [[ "$TARGET_DB" =~ ^geointel_restore_verify_[0-9_]+$ ]]; then
db_user="$(docker exec "$CONTAINER" sh -c 'printf %s "${POSTGRES_USER:-${GEOINTEL_POSTGRES_USER:-geointel}}"' 2>/dev/null || true)"
if [ -n "$db_user" ]; then
docker exec "$CONTAINER" dropdb --if-exists -U "$db_user" "$TARGET_DB" >/dev/null 2>&1 || true
fi
fi
rm -f -- "$RESTORE_RESULT"
}
trap cleanup EXIT
bash "$(dirname "$0")/restore_release_backup_smoke.sh" \
--backup-dir "$BACKUP_DIR" \
--container "$CONTAINER" \
--confirm-isolated-restore \
--keep-database \
--output "$RESTORE_RESULT"
TARGET_DB="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["temporary_database"])' "$RESTORE_RESULT")"
if ! [[ "$TARGET_DB" =~ ^geointel_restore_verify_[0-9_]+$ ]]; then
echo "Unsafe temporary database returned by restore smoke: ${TARGET_DB}" >&2
exit 3
fi
docker exec -e TARGET_DB="$TARGET_DB" "$CONTAINER" sh -c '
target_url="$(python -c '"'"'
import os
from sqlalchemy import URL
print(
URL.create(
"postgresql+psycopg",
username=os.environ["GEOINTEL_POSTGRES_USER"],
password=os.environ["GEOINTEL_POSTGRES_PASSWORD"],
host="127.0.0.1",
port=5432,
database=os.environ["TARGET_DB"],
)
.render_as_string(hide_password=False)
)
'"'"')"
export DATABASE_URL="$target_url"
python -m alembic upgrade head
'
DB_USER="$(docker exec "$CONTAINER" sh -c 'printf %s "${POSTGRES_USER:-${GEOINTEL_POSTGRES_USER:-geointel}}"' )"
ALEMBIC_HEAD="$(docker exec "$CONTAINER" psql -X -v ON_ERROR_STOP=1 -U "$DB_USER" -d "$TARGET_DB" -Atqc \
"SELECT version_num FROM alembic_version;")"
EXPECTED_HEAD="$(docker exec "$CONTAINER" sh -c 'python -m alembic heads | awk "{print \$1}"')"
POSTGIS_VERSION="$(docker exec "$CONTAINER" psql -X -v ON_ERROR_STOP=1 -U "$DB_USER" -d "$TARGET_DB" -Atqc \
"SELECT postgis_version();")"
RELEASE_REVISION="$(docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$CONTAINER")"
if [ "$ALEMBIC_HEAD" != "$EXPECTED_HEAD" ]; then
echo "Upgraded temporary database head '${ALEMBIC_HEAD}' differs from '${EXPECTED_HEAD}'." >&2
exit 4
fi
if [ -z "$OUTPUT" ]; then
OUTPUT="${BACKUP_DIR%/}-upgrade-smoke.json"
fi
python3 - "$OUTPUT" <<PY
import json
import pathlib
payload = {
"schema_version": 1,
"verified_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"backup_dir": ${BACKUP_DIR@Q},
"release_revision": ${RELEASE_REVISION@Q},
"temporary_database": ${TARGET_DB@Q},
"production_database_untouched": True,
"temporary_database_retained": False,
"alembic_head": ${ALEMBIC_HEAD@Q},
"postgis_version": ${POSTGIS_VERSION@Q},
"upgrade_status": "passed",
}
path = pathlib.Path(__import__("sys").argv[1]).expanduser().resolve()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
PY
cleanup
trap - EXIT
echo "Isolated release upgrade passed at ${ALEMBIC_HEAD}; temporary database removed."