#!/usr/bin/env bash set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${repo_root}" project_name="${MINEROPS_TIMELINE_MIGRATION_PROJECT:-minerops-timeline-migration-$$}" empty_database="minerops_timeline_empty" compose=( docker compose --project-name "${project_name}" --env-file .env.dev -f compose.dev.yaml ) cleanup() { status=$? if (( status != 0 )); then echo echo "Repair Timeline migration test failed. Recent PostgreSQL logs:" "${compose[@]}" logs --no-color --tail 120 postgres || true fi "${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true exit "${status}" } trap cleanup EXIT if [[ ! -f .env.dev ]]; then echo "Missing .env.dev. Run ./scripts/dev-env-init.sh first." >&2 exit 1 fi run_alembic() { "${compose[@]}" run --rm --no-deps backend alembic "$@" } echo "Starting disposable PostgreSQL..." "${compose[@]}" up --detach postgres for attempt in $(seq 1 60); do if "${compose[@]}" exec -T postgres sh -ec \ 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ >/dev/null 2>&1; then break fi if (( attempt == 60 )); then echo "PostgreSQL did not become ready." >&2 exit 1 fi sleep 1 done echo "Building migration image..." "${compose[@]}" build backend echo "Upgrading the previous head to Repair Timeline..." run_alembic upgrade 0010_begin_repair previous_revision="$(run_alembic current 2>/dev/null)" [[ "${previous_revision}" == *"0010_begin_repair"* ]] run_alembic upgrade 0011_repair_timeline current_revision="$(run_alembic current 2>/dev/null)" [[ "${current_revision}" == *"0011_repair_timeline"* ]] echo "Checking repairs.read permission and role assignments..." "${compose[@]}" exec -T postgres sh -ec \ 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ <<'SQL' DO $$ DECLARE v_read_roles TEXT[]; v_create_roles TEXT[]; BEGIN IF to_regclass('public.repairs') IS NULL OR to_regclass('public.repair_events') IS NULL THEN RAISE EXCEPTION 'Repair tables changed during timeline migration'; END IF; IF (SELECT count(*) FROM permissions WHERE key = 'repairs.read') <> 1 THEN RAISE EXCEPTION 'repairs.read permission was not registered once'; END IF; SELECT array_agg(roles.name ORDER BY roles.name) INTO v_read_roles FROM role_permissions JOIN roles ON roles.id = role_permissions.role_id JOIN permissions ON permissions.id = role_permissions.permission_id WHERE permissions.key = 'repairs.read'; IF v_read_roles <> ARRAY[ 'Admin', 'Manager', 'Read Only', 'Technician' ] THEN RAISE EXCEPTION 'Unexpected repairs.read role matrix: %', v_read_roles; END IF; SELECT array_agg(roles.name ORDER BY roles.name) INTO v_create_roles FROM role_permissions JOIN roles ON roles.id = role_permissions.role_id JOIN permissions ON permissions.id = role_permissions.permission_id WHERE permissions.key = 'repairs.create'; IF v_create_roles <> ARRAY['Admin', 'Manager', 'Technician'] THEN RAISE EXCEPTION 'repairs.create role matrix changed: %', v_create_roles; END IF; INSERT INTO permissions (key, description) VALUES ('repairs.read', 'View repair details and timeline events') ON CONFLICT (key) DO UPDATE SET description = EXCLUDED.description; INSERT INTO role_permissions (role_id, permission_id) SELECT roles.id, permissions.id FROM roles JOIN permissions ON permissions.key = 'repairs.read' WHERE roles.name IN ('Admin', 'Manager', 'Technician', 'Read Only') ON CONFLICT DO NOTHING; IF (SELECT count(*) FROM permissions WHERE key = 'repairs.read') <> 1 OR ( SELECT count(*) FROM role_permissions JOIN permissions ON permissions.id = role_permissions.permission_id WHERE permissions.key = 'repairs.read' ) <> 4 THEN RAISE EXCEPTION 'repairs.read seeding is not idempotent'; END IF; END $$; SQL echo "Downgrading and re-upgrading Repair Timeline..." run_alembic downgrade 0010_begin_repair downgrade_state="$( "${compose[@]}" exec -T postgres sh -ec \ 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At' \ <<'SQL' SELECT NOT EXISTS ( SELECT 1 FROM permissions WHERE key = 'repairs.read' ) AND EXISTS ( SELECT 1 FROM permissions WHERE key = 'repairs.create' ) AND to_regclass('public.repairs') IS NOT NULL AND to_regclass('public.repair_events') IS NOT NULL; SQL )" [[ "${downgrade_state}" == "t" ]] downgraded_revision="$(run_alembic current 2>/dev/null)" [[ "${downgraded_revision}" == *"0010_begin_repair"* ]] run_alembic upgrade 0011_repair_timeline reupgraded_revision="$(run_alembic current 2>/dev/null)" [[ "${reupgraded_revision}" == *"0011_repair_timeline"* ]] echo "Migrating an empty database from base through Repair Timeline..." "${compose[@]}" exec -T postgres sh -ec \ 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d postgres \ -c "CREATE DATABASE minerops_timeline_empty"' >/dev/null "${compose[@]}" run --rm --no-deps \ -e "POSTGRES_DB=${empty_database}" \ backend alembic upgrade 0011_repair_timeline empty_revision="$( "${compose[@]}" run --rm --no-deps \ -e "POSTGRES_DB=${empty_database}" \ backend alembic current 2>/dev/null )" [[ "${empty_revision}" == *"0011_repair_timeline"* ]] echo "Repair Timeline migration test passed."