feat: add return to service workflow

This commit is contained in:
lussierj committed 2026-07-29 19:02:23 +00:00
1 parent 72ad19a071
commit f2b736b08e
19 files changed
+2883 -46

No files matched your search

+4 -4
View File
@@ -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 \
+1 -1
View File
@@ -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 \
+4 -4
View File
@@ -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 \
+295
View File
@@ -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."
+59
View File
@@ -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."