Files
LMS/scripts/ldd-migration-upgrade.sh
Vladislav Kopnin 82394cc688 feat: закрыть остаточные P1/P2 по профилю, когорте и ведомости
- профиль приведён к docs/09: variant/level, updatedBy/name, лимит попыток
- V14: NOT NULL и полная уникальность когорты, preflight legacy-групп
- возврат группы из архива и история членства на странице группы
- пагинация групп и плоского списка материалов
- пакетные сдачи ведомости одним запросом
- XLSX-preflight импорта и audit-тесты ростера
- LDD: 66 claims, 398 тестов, release PASS
2026-10-29 20:51:27 +03:00

227 lines
11 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"