#!/usr/bin/env bash # LDD migration evidence: master-схема + данные -> миграции ветки. # Одноразовая БД, рабочие данные не затрагиваются. set -euo pipefail cd "$(dirname "$0")/.." DB="lms_ldd_mig_$(date -u +%H%M%S)_$$" EVIDENCE="build/evidence/migration.json" POSTGRES_WAS_RUNNING=0 FAILED=0 log() { echo ">> $*"; } cleanup() { local code=$? docker exec -i lms-postgres psql -U lms -d postgres -v ON_ERROR_STOP=1 \ -c "DROP DATABASE IF EXISTS $DB;" >/dev/null 2>&1 || true if ((POSTGRES_WAS_RUNNING == 0)); then docker compose stop postgres >/dev/null 2>&1 || true fi exit $code } trap cleanup EXIT INT TERM command -v docker >/dev/null 2>&1 || { echo "Нужен docker" >&2; exit 1; } git rev-parse --verify master >/dev/null 2>&1 || { echo "Нужна ветка master в git" >&2; exit 1; } if docker compose ps --status running --services 2>/dev/null | grep -qx postgres; then POSTGRES_WAS_RUNNING=1 fi docker compose up -d postgres >/dev/null for _ in $(seq 1 40); do docker exec lms-postgres pg_isready -U lms -d postgres >/dev/null 2>&1 && break sleep 1 done docker exec lms-postgres pg_isready -U lms -d postgres >/dev/null docker exec -i lms-postgres psql -U lms -d postgres -v ON_ERROR_STOP=1 \ -c "CREATE DATABASE $DB;" >/dev/null mkdir -p build/evidence apply() { docker exec -i lms-postgres psql -U lms -d "$DB" -v ON_ERROR_STOP=1 >/tmp/lms-ldd-migration.log } log "Разворачиваю master-схему и синтетические данные..." { git show master:services/identity-service/src/main/resources/db/migration/V1__init.sql echo 'SET search_path TO identity;' git show master:services/identity-service/src/main/resources/db/migration/V2__accounts.sql git show master:services/identity-service/src/main/resources/db/migration/V3__login_attempts.sql cat <<'SQL' INSERT INTO accounts (id, login, password_hash, role, must_change_password) VALUES ('00000000-0000-7000-8000-000000001001','admin','x','ADMIN',false), ('00000000-0000-7000-8000-000000001002','s1','x','STUDENT',false); INSERT INTO students (account_id, student_no, last_name, first_name) VALUES ('00000000-0000-7000-8000-000000001002','S1','Иванов','Иван'); INSERT INTO groups (id, name) VALUES ('00000000-0000-7000-8000-000000002001','ПИ-11БО'); INSERT INTO enrollments (group_id, student_id) VALUES ('00000000-0000-7000-8000-000000002001','00000000-0000-7000-8000-000000001002'); SQL echo 'SET search_path TO academic;' git show master:services/academic-service/src/main/resources/db/migration/V1__init.sql git show master:services/academic-service/src/main/resources/db/migration/V2__schema.sql git show master:services/academic-service/src/main/resources/db/migration/V3__seed.sql git show master:services/academic-service/src/main/resources/db/migration/V4__content.sql git show master:services/academic-service/src/main/resources/db/migration/V5__content_published.sql cat <<'SQL' INSERT INTO labs (id, number, title, semester_id) VALUES ('00000000-0000-7000-8000-000000003001',1,'ЛР 1','00000000-0000-7000-8000-000000000001'); INSERT INTO chapters (id, title, slug) VALUES ('00000000-0000-7000-8000-000000003002','Глава','osnovy-c'); SQL echo 'SET search_path TO progress;' git show master:services/progress-service/src/main/resources/db/migration/V1__init.sql git show master:services/progress-service/src/main/resources/db/migration/V2__schema.sql git show master:services/progress-service/src/main/resources/db/migration/V3__grades.sql git show master:services/progress-service/src/main/resources/db/migration/V4__submissions.sql git show master:services/progress-service/src/main/resources/db/migration/V5__defense_sessions.sql git show master:services/progress-service/src/main/resources/db/migration/V6__grade_comments.sql git show master:services/progress-service/src/main/resources/db/migration/V7__grade_publications.sql git show master:services/progress-service/src/main/resources/db/migration/V8__grade_publication_students.sql git show master:services/progress-service/src/main/resources/db/migration/V9__grade_general_comment.sql git show master:services/progress-service/src/main/resources/db/migration/V11__attestations.sql git show master:services/progress-service/src/main/resources/db/migration/V12__attestation_semester_and_publication.sql cat <<'SQL' INSERT INTO lab_grades (student_id, lab_id, score) VALUES ('00000000-0000-7000-8000-000000001002','00000000-0000-7000-8000-000000003001',4.5); INSERT INTO grade_comments (student_id, lab_id, text) VALUES ('00000000-0000-7000-8000-000000001002','00000000-0000-7000-8000-000000003001','ok'); SQL } | apply log "Применяю миграции ветки..." { echo 'SET search_path TO identity;' cat services/identity-service/src/main/resources/db/migration/V4__roles.sql cat services/identity-service/src/main/resources/db/migration/V5__groups_cohorts.sql cat services/identity-service/src/main/resources/db/migration/V6__student_statuses.sql cat services/identity-service/src/main/resources/db/migration/V7__promotion.sql cat services/identity-service/src/main/resources/db/migration/V8__membership_history.sql cat services/identity-service/src/main/resources/db/migration/V9__account_statuses.sql cat services/identity-service/src/main/resources/db/migration/V10__audit_events.sql cat services/identity-service/src/main/resources/db/migration/V11__refresh_tokens.sql cat services/identity-service/src/main/resources/db/migration/V12__refresh_absolute.sql cat services/identity-service/src/main/resources/db/migration/V13__audit_keyset_indexes.sql cat services/identity-service/src/main/resources/db/migration/V14__cohort_constraints.sql echo 'SET search_path TO academic;' cat services/academic-service/src/main/resources/db/migration/V6__courses.sql cat services/academic-service/src/main/resources/db/migration/V7__labs_course.sql cat services/academic-service/src/main/resources/db/migration/V8__chapters_course.sql cat services/academic-service/src/main/resources/db/migration/V9__article_assets_indexes.sql cat services/academic-service/src/main/resources/db/migration/V10__course_title_unique.sql cat services/academic-service/src/main/resources/db/migration/V11__global_audience_all.sql cat services/academic-service/src/main/resources/db/migration/V12__asset_links_backfill.sql echo 'SET search_path TO progress;' cat services/progress-service/src/main/resources/db/migration/V13__profile_authors.sql } | apply log "Проверяю postflight..." CHECK_SQL=$(cat <<'SQL' SET search_path TO identity; SELECT 'audit_events=' || count(*) FROM information_schema.tables WHERE table_schema='identity' AND table_name='audit_events'; SELECT 'refresh_tokens=' || count(*) FROM information_schema.tables WHERE table_schema='identity' AND table_name='refresh_tokens'; SELECT 'refresh_absolute=' || count(*) FROM information_schema.columns WHERE table_schema='identity' AND table_name='refresh_tokens' AND column_name='absolute_expires_at'; SELECT 'audit_keyset_idx=' || count(*) FROM pg_indexes WHERE schemaname='identity' AND indexname='audit_events_occurred_id_idx'; SELECT 'students=' || count(*) FROM students; SELECT 'groups=' || count(*) FROM groups; SELECT 'cohort_not_null=' || count(*) FROM information_schema.columns WHERE table_schema='identity' AND table_name='groups' AND column_name IN ('stream','degree','course','number','enrollment_year') AND is_nullable='NO'; SELECT 'cohort_unique_full=' || count(*) FROM pg_indexes WHERE schemaname='identity' AND indexname='groups_cohort_unique' AND indexdef NOT LIKE '%WHERE%'; SELECT 'history=' || count(*) FROM membership_history; SET search_path TO academic; SELECT 'labs_course_id=' || count(*) FROM information_schema.columns WHERE table_schema='academic' AND table_name='labs' AND column_name='course_id'; SELECT 'chapters_course_id=' || count(*) FROM information_schema.columns WHERE table_schema='academic' AND table_name='chapters' AND column_name='course_id'; SELECT 'article_assets=' || count(*) FROM information_schema.tables WHERE table_schema='academic' AND table_name='article_assets'; SELECT 'labs=' || count(*) FROM labs; SELECT 'chapters=' || count(*) FROM chapters; SET search_path TO progress; SELECT 'grade_author=' || count(*) FROM information_schema.columns WHERE table_schema='progress' AND table_name='grade_comments' AND column_name='author_account_id'; SELECT 'grade_updated_by=' || count(*) FROM information_schema.columns WHERE table_schema='progress' AND table_name='lab_grades' AND column_name='updated_by_account_id'; SELECT 'grades=' || count(*) FROM lab_grades; SELECT 'comments=' || count(*) FROM grade_comments; SQL ) RESULT=$(docker exec -i lms-postgres psql -U lms -d "$DB" -At -v ON_ERROR_STOP=1 -c "$CHECK_SQL") # V13/V14 повторно: идемпотентность. if { echo 'SET search_path TO progress;'; cat services/progress-service/src/main/resources/db/migration/V13__profile_authors.sql; } | apply; then V13_TWICE="passed" else V13_TWICE="failed" FAILED=1 fi if { echo 'SET search_path TO identity;'; cat services/identity-service/src/main/resources/db/migration/V14__cohort_constraints.sql; } | apply; then V14_TWICE="passed" else V14_TWICE="failed" FAILED=1 fi printf '%s\n' "$RESULT" | sed 's/^/ /' python3 - "$EVIDENCE" "$RESULT" "$V13_TWICE" "$V14_TWICE" <<'PY' import json import sys out, result, v13, v14 = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] required = { "audit_events=1", "refresh_tokens=1", "refresh_absolute=1", "audit_keyset_idx=1", "students=1", "groups=1", "cohort_not_null=5", "cohort_unique_full=1", "history=1", "labs_course_id=1", "chapters_course_id=1", "article_assets=1", "labs=1", "chapters=1", "grade_author=1", "grade_updated_by=1", "grades=1", "comments=1", } lines = {line.strip() for line in result.splitlines() if line.strip()} missing = sorted(required - lines) failed = bool(missing) or v13 != "passed" or v14 != "passed" if missing: status = "failed" detail = "postflight не подтвердил: " + ", ".join(missing) elif v13 != "passed": status = "failed" detail = "V13 не применилась повторно" elif v14 != "passed": status = "failed" detail = "V14 не применилась повторно" else: status = "passed" detail = "master->branch: postflight, повторные V13 и V14 успешны" checks = [{"id": "migration.master_upgrade", "status": status, "detail": detail}] with open(out, "w", encoding="utf-8") as fh: now = __import__("datetime").datetime.now(__import__("datetime").UTC).isoformat() json.dump({"generatedAt": now, "checks": checks}, fh, ensure_ascii=False, indent=2) print(("PASS " if not failed else "FAIL ") + "migration.master_upgrade: " + detail) sys.exit(1 if failed else 0) PY log "Migration evidence: $EVIDENCE"