feat: add return to service workflow
This commit is contained in:
1 parent
72ad19a071
commit
f2b736b08e
19 files changed
+2883
-46
No files matched your search
@@ -57,7 +57,7 @@ echo "Upgrading the previous head to Begin Repair..."
|
||||
run_alembic upgrade 0009_inventory_asset_id_history
|
||||
previous_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${previous_revision}" == *"0009_inventory_asset_id_history"* ]]
|
||||
run_alembic upgrade head
|
||||
run_alembic upgrade 0010_begin_repair
|
||||
current_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${current_revision}" == *"0010_begin_repair"* ]]
|
||||
|
||||
@@ -534,17 +534,17 @@ SELECT
|
||||
SQL
|
||||
)"
|
||||
[[ "${downgrade_state}" == "t" ]]
|
||||
run_alembic upgrade head
|
||||
run_alembic upgrade 0010_begin_repair
|
||||
reupgraded_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${reupgraded_revision}" == *"0010_begin_repair"* ]]
|
||||
|
||||
echo "Migrating an empty database from base through head..."
|
||||
echo "Migrating an empty database from base through Begin Repair..."
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d postgres \
|
||||
-c "CREATE DATABASE minerops_repair_empty"' >/dev/null
|
||||
"${compose[@]}" run --rm --no-deps \
|
||||
-e "POSTGRES_DB=${empty_database}" \
|
||||
backend alembic upgrade head
|
||||
backend alembic upgrade 0010_begin_repair
|
||||
|
||||
empty_revision="$(
|
||||
"${compose[@]}" run --rm --no-deps \
|
||||
|
||||
@@ -88,7 +88,7 @@ health_json="$(cat "${health_file}")"
|
||||
|
||||
echo "Checking migrations and required schema..."
|
||||
current_revision="$("${compose[@]}" exec -T backend alembic current 2>/dev/null)"
|
||||
[[ "${current_revision}" == *"0011_repair_timeline"* ]]
|
||||
[[ "${current_revision}" == *"0012_return_to_service"* ]]
|
||||
|
||||
missing_tables="$(
|
||||
"${compose[@]}" exec -T postgres sh -c \
|
||||
|
||||
@@ -57,7 +57,7 @@ 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 head
|
||||
run_alembic upgrade 0011_repair_timeline
|
||||
current_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${current_revision}" == *"0011_repair_timeline"* ]]
|
||||
|
||||
@@ -149,17 +149,17 @@ SQL
|
||||
downgraded_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${downgraded_revision}" == *"0010_begin_repair"* ]]
|
||||
|
||||
run_alembic upgrade head
|
||||
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 head..."
|
||||
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 head
|
||||
backend alembic upgrade 0011_repair_timeline
|
||||
|
||||
empty_revision="$(
|
||||
"${compose[@]}" run --rm --no-deps \
|
||||
|
||||
Executable
+295
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
project_name="${MINEROPS_RETURN_MIGRATION_PROJECT:-minerops-return-migration-$$}"
|
||||
empty_database="minerops_return_empty"
|
||||
compose=(
|
||||
docker compose
|
||||
--project-name "${project_name}"
|
||||
--env-file .env.dev
|
||||
-f compose.dev.yaml
|
||||
)
|
||||
|
||||
cleanup() {
|
||||
status=$?
|
||||
if (( status != 0 )); then
|
||||
echo
|
||||
echo "Return to Service migration test failed. Recent PostgreSQL logs:"
|
||||
"${compose[@]}" logs --no-color --tail 160 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 "Preparing previous head and truthful legacy completion data..."
|
||||
run_alembic upgrade 0011_repair_timeline
|
||||
previous_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${previous_revision}" == *"0011_repair_timeline"* ]]
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL'
|
||||
INSERT INTO users (
|
||||
id, username, display_name, password_hash, role_id
|
||||
)
|
||||
SELECT
|
||||
'00000000-0000-0000-0000-000000001301',
|
||||
'return-migration-test',
|
||||
'Return Migration Test',
|
||||
'not-used',
|
||||
id
|
||||
FROM roles
|
||||
WHERE name = 'Admin';
|
||||
|
||||
INSERT INTO miners (
|
||||
id, asset_tag, status, lifecycle_status, first_seen_at, last_seen_at
|
||||
) VALUES (
|
||||
'00000000-0000-0000-0000-000000001302',
|
||||
'RETURN-MIGRATION-LEGACY',
|
||||
'unknown',
|
||||
'active',
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
|
||||
INSERT INTO repairs (
|
||||
id, miner_id, status, reason, opened_by, completed_at, completed_by
|
||||
) VALUES (
|
||||
'00000000-0000-0000-0000-000000001303',
|
||||
'00000000-0000-0000-0000-000000001302',
|
||||
'COMPLETED',
|
||||
'OTHER',
|
||||
'00000000-0000-0000-0000-000000001301',
|
||||
now(),
|
||||
'00000000-0000-0000-0000-000000001301'
|
||||
);
|
||||
SQL
|
||||
|
||||
echo "Upgrading previous head to Return to Service..."
|
||||
run_alembic upgrade head
|
||||
current_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${current_revision}" == *"0012_return_to_service"* ]]
|
||||
|
||||
echo "Checking schema, permission matrix, constraints, and legacy preservation..."
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL'
|
||||
DO $$
|
||||
DECLARE
|
||||
v_roles TEXT[];
|
||||
v_constraint_count INTEGER;
|
||||
BEGIN
|
||||
IF (
|
||||
SELECT count(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'repairs'
|
||||
AND column_name IN ('completion_summary', 'resulting_lifecycle')
|
||||
) <> 2 THEN
|
||||
RAISE EXCEPTION 'Return to Service columns are missing';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM repairs
|
||||
WHERE id = '00000000-0000-0000-0000-000000001303'
|
||||
AND (
|
||||
completion_summary IS NOT NULL
|
||||
OR resulting_lifecycle IS NOT NULL
|
||||
)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Legacy completion metadata was fabricated';
|
||||
END IF;
|
||||
|
||||
IF (SELECT count(*) FROM permissions WHERE key = 'repairs.complete') <> 1 THEN
|
||||
RAISE EXCEPTION 'repairs.complete permission was not registered once';
|
||||
END IF;
|
||||
|
||||
SELECT array_agg(roles.name ORDER BY roles.name)
|
||||
INTO v_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.complete';
|
||||
IF v_roles <> ARRAY['Admin', 'Manager', 'Technician'] THEN
|
||||
RAISE EXCEPTION 'Unexpected repairs.complete role matrix: %', v_roles;
|
||||
END IF;
|
||||
|
||||
SELECT count(*)
|
||||
INTO v_constraint_count
|
||||
FROM pg_constraint
|
||||
WHERE conrelid IN ('repairs'::regclass, 'repair_events'::regclass)
|
||||
AND conname IN (
|
||||
'ck_repairs_resulting_lifecycle',
|
||||
'ck_repairs_completion_summary',
|
||||
'ck_repairs_completion_metadata_state',
|
||||
'ck_repair_events_type',
|
||||
'ck_repair_events_payload_shape'
|
||||
);
|
||||
IF v_constraint_count <> 5 THEN
|
||||
RAISE EXCEPTION 'Expected Return to Service constraints are missing';
|
||||
END IF;
|
||||
|
||||
IF (
|
||||
SELECT convalidated
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'repairs'::regclass
|
||||
AND conname = 'ck_repairs_completion_metadata_state'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Legacy completion compatibility constraint was unexpectedly validated';
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
INSERT INTO repairs (
|
||||
miner_id, status, reason, opened_by, completed_at, completed_by
|
||||
) VALUES (
|
||||
'00000000-0000-0000-0000-000000001302',
|
||||
'COMPLETED',
|
||||
'OTHER',
|
||||
'00000000-0000-0000-0000-000000001301',
|
||||
now(),
|
||||
'00000000-0000-0000-0000-000000001301'
|
||||
);
|
||||
RAISE EXCEPTION 'New completed repair without resulting lifecycle was accepted';
|
||||
EXCEPTION
|
||||
WHEN check_violation THEN NULL;
|
||||
END;
|
||||
END
|
||||
$$;
|
||||
|
||||
INSERT INTO permissions (key, description)
|
||||
VALUES ('repairs.complete', 'Return repaired miners to service')
|
||||
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.complete'
|
||||
WHERE roles.name IN ('Admin', 'Manager', 'Technician')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF (SELECT count(*) FROM permissions WHERE key = 'repairs.complete') <> 1
|
||||
OR (
|
||||
SELECT count(*)
|
||||
FROM role_permissions
|
||||
JOIN permissions ON permissions.id = role_permissions.permission_id
|
||||
WHERE permissions.key = 'repairs.complete'
|
||||
) <> 3 THEN
|
||||
RAISE EXCEPTION 'repairs.complete seeding is not idempotent';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
SQL
|
||||
|
||||
echo "Downgrading and re-upgrading without new completion history..."
|
||||
run_alembic downgrade 0011_repair_timeline
|
||||
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.complete'
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM permissions WHERE key = 'repairs.read'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'repairs'
|
||||
AND column_name IN ('completion_summary', 'resulting_lifecycle')
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM repairs
|
||||
WHERE id = '00000000-0000-0000-0000-000000001303'
|
||||
AND status = 'COMPLETED'
|
||||
AND completed_at IS NOT NULL
|
||||
AND completed_by IS NOT NULL
|
||||
);
|
||||
SQL
|
||||
)"
|
||||
[[ "${downgrade_state}" == "t" ]]
|
||||
downgraded_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${downgraded_revision}" == *"0011_repair_timeline"* ]]
|
||||
run_alembic upgrade head
|
||||
reupgraded_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${reupgraded_revision}" == *"0012_return_to_service"* ]]
|
||||
|
||||
echo "Checking downgrade refusal after authoritative completion history..."
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL'
|
||||
UPDATE repairs
|
||||
SET completion_summary = 'Migration completion',
|
||||
resulting_lifecycle = 'active'
|
||||
WHERE id = '00000000-0000-0000-0000-000000001303';
|
||||
|
||||
INSERT INTO repair_events (
|
||||
repair_id, sequence, event_type, created_by, payload
|
||||
) VALUES (
|
||||
'00000000-0000-0000-0000-000000001303',
|
||||
1,
|
||||
'REPAIR_COMPLETED',
|
||||
'00000000-0000-0000-0000-000000001301',
|
||||
'{
|
||||
"previous_miner_lifecycle":"in_repair",
|
||||
"resulting_miner_lifecycle":"active",
|
||||
"completion_summary":"Migration completion"
|
||||
}'::jsonb
|
||||
);
|
||||
SQL
|
||||
if downgrade_error="$(run_alembic downgrade 0011_repair_timeline 2>&1)"; then
|
||||
echo "Downgrade discarded Return to Service history." >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ "${downgrade_error}" == *"would lose authoritative history"* ]]
|
||||
failed_revision="$(run_alembic current 2>/dev/null)"
|
||||
[[ "${failed_revision}" == *"0012_return_to_service"* ]]
|
||||
|
||||
echo "Migrating an empty database from base through head..."
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d postgres \
|
||||
-c "CREATE DATABASE minerops_return_empty"' >/dev/null
|
||||
"${compose[@]}" run --rm --no-deps \
|
||||
-e "POSTGRES_DB=${empty_database}" \
|
||||
backend alembic upgrade head
|
||||
empty_revision="$(
|
||||
"${compose[@]}" run --rm --no-deps \
|
||||
-e "POSTGRES_DB=${empty_database}" \
|
||||
backend alembic current 2>/dev/null
|
||||
)"
|
||||
[[ "${empty_revision}" == *"0012_return_to_service"* ]]
|
||||
|
||||
echo "Return to Service migration test passed."
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
project_name="${MINEROPS_RETURN_SERVICE_TEST_PROJECT:-minerops-return-service-$$}"
|
||||
compose=(
|
||||
docker compose
|
||||
--project-name "${project_name}"
|
||||
--env-file .env.dev
|
||||
-f compose.dev.yaml
|
||||
)
|
||||
|
||||
cleanup() {
|
||||
status=$?
|
||||
if (( status != 0 )); then
|
||||
echo
|
||||
echo "Return to Service tests 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
|
||||
|
||||
echo "Starting disposable Return to Service database..."
|
||||
"${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 Return to Service test image..."
|
||||
"${compose[@]}" build backend
|
||||
|
||||
echo "Migrating disposable database..."
|
||||
"${compose[@]}" run --rm --no-deps backend alembic upgrade head
|
||||
|
||||
echo "Running Return to Service repository, service, and router tests..."
|
||||
"${compose[@]}" run --rm --no-deps \
|
||||
-v "${repo_root}/backend/tests:/app/tests:ro" \
|
||||
backend python -m unittest -v tests.test_return_to_service
|
||||
|
||||
echo "Return to Service tests passed."
|
||||
Reference in new issue
Block a user