From 8bb7fe97eb621bffdb4a71fe91d4ac4d7f5e9473 Mon Sep 17 00:00:00 2001 From: Zuev Date: Wed, 29 Jul 2026 23:13:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B1=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0?= =?UTF-8?q?=D1=81=D0=BD=D1=8B=D0=B9=20production=20rollout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/docker-build.yaml | 6 +- BUG_FIX_PROGRESS.md | 38 ++- STARTUP_GUIDE.md | 98 +++++--- .../main/resources/db/migration/V1__init.sql | 23 +- ...lign_academic_calendar_weeks_to_monday.sql | 23 -- ...ndarWeekNumberBaselineIntegrationTest.java | 70 ++++++ ...darWeekNumberMigrationIntegrationTest.java | 218 ------------------ docs/DATABASE.md | 14 +- docs/INFRASTRUCTURE.md | 14 +- graphify-out/.graphify_learning.json | 209 +++++++++++++++-- ...Œ_напиши_мне_инструкцию_как_это_всё_выыкатить.md | 26 +++ ..._схема_на_проде_на_удалённом_сервере_создана.md | 24 ++ ...01238_сам_можешь_выполнить_эти_действия.md | 25 ++ graphify-out/reflections/LESSONS.md | 17 +- scripts/check-artifact-pinning.sh | 4 + scripts/test-artifact-pinning.sh | 9 + 16 files changed, 494 insertions(+), 324 deletions(-) delete mode 100644 backend/src/main/resources/db/migration/V2__align_academic_calendar_weeks_to_monday.sql create mode 100644 backend/src/test/java/com/magistr/app/migration/AcademicCalendarWeekNumberBaselineIntegrationTest.java delete mode 100644 backend/src/test/java/com/magistr/app/migration/AcademicCalendarWeekNumberMigrationIntegrationTest.java create mode 100644 graphify-out/memory/query_20260729_192644_теперь_напиши_мне_инструкцию_как_это_всё_выыкатить.md create mode 100644 graphify-out/memory/query_20260729_195904_новая_схема_на_проде_на_удалённом_сервере_создана.md create mode 100644 graphify-out/memory/query_20260729_201238_сам_можешь_выполнить_эти_действия.md diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index 1923ce4..e1f9c06 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -6,10 +6,7 @@ on: - main tags: - 'v*' - -concurrency: - group: magistr-production - cancel-in-progress: false + workflow_dispatch: env: REGISTRY: gitea.zuev.company @@ -175,6 +172,7 @@ jobs: deploy-to-k8s: name: Развернуть проверенные digests needs: [build-and-push-backend, build-and-push-frontend, security-scan] + if: ${{ gitea.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest timeout-minutes: 15 environment: production diff --git a/BUG_FIX_PROGRESS.md b/BUG_FIX_PROGRESS.md index bc00ea1..46bd74f 100644 --- a/BUG_FIX_PROGRESS.md +++ b/BUG_FIX_PROGRESS.md @@ -1,6 +1,6 @@ # Журнал исправления проблем -Обновлено: 2026-07-19 (Europe/Moscow). +Обновлено: 2026-07-29 (Europe/Moscow). ## Область и неизменяемые данные @@ -21,7 +21,7 @@ | `V2__subgroups_active_unique_name.sql` | `68f5525a50ddba4f8800a63cab3cd0f84779e301c4a1f29a6e0399f97fbdb30b` | Текущая единая baseline-миграция: `V1__init.sql`, SHA-256 -`fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`. +`a984718777f1b4695868549e3c805c8b22edbb82fbe0b8731563adf5f11e1742`. Упоминания V2–V7 в исторических записях ниже описывают последовательность разработки до консолидации и не означают наличие этих файлов в текущем дереве. @@ -1047,21 +1047,41 @@ - Полностью закрыты и проверены 32 пункта. Для № 1 остаются только операторские ротация, provisioning, rollout и очистка истории, которые исходный prompt прямо запрещает выполнять без отдельных production-полномочий. -- Полный backend PostgreSQL/Testcontainers-прогон: 277 тестов, 0 failures, 0 errors, +- Полный backend PostgreSQL/Testcontainers-прогон: 300 тестов, 0 failures, 0 errors, 0 skipped. Четыре frontend test-suite, `npm run check`, Compose validation, Workflow YAML, artifact/deploy shell-тесты и `git diff --check` прошли. - `kubectl kustomize ../k8s`, production secret-scan, синтаксис deploy-скрипта и расширенный artifact-scan прошли. В production-манифестах отсутствуют mutable `latest` и `:main`. - Каталог Flyway содержит ровно `V1__init.sql`; V2–V4 и другие дополнительные миграции отсутствуют. Финальный SHA-256 V1: - `fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`. + `a984718777f1b4695868549e3c805c8b22edbb82fbe0b8731563adf5f11e1742`. +- Исправление нумерации недель календарного графика также перенесено из промежуточной V2 + непосредственно в V1. Чистое применение baseline и формула недель + `понедельник–воскресенье` подтверждены + `AcademicCalendarWeekNumberBaselineIntegrationTest`. ## Точка продолжения -Текущий этап: **реализация и проверка всех разрешённых изменений завершены**. +Текущий этап: **репозиторная реализация завершена, production подготовлен к первому +контролируемому rollout**. -Оставшееся операторское действие владельца: +29 июля 2026 года по отдельному разрешению владельца выполнена production-подготовка: -1. при готовности production-полномочий выполнить `docs/SECURITY_RUNBOOK.md`: ротацию, - provisioning, rollout и очистку истории для № 1. Эти действия не выполнялись - автоматически и не требуются для завершения репозиторной реализации. +- старый backend остановлен на `0` реплик; +- обе tenant-БД (`magistr`, `n8n`) доступны, их схемы `public` пусты, у application role + есть права `CREATE` на схему и БД; +- в `app-secret` без вывода значения добавлен новый случайный `JWT_SECRET`; +- из действующей конфигурации без вывода значений созданы переходные `tenants-secret` и + `otel-postgres-secret`; +- push в `main` больше не запускает production deploy: job разрешён только для ручного + `workflow_dispatch`; +- server-side dry-run выявил immutable `roleRef` старого RoleBinding; в + `../k8s/rbac.yaml` новый binding переименован в `backend-tenant-secret-binding`, после + чего `kubectl apply --dry-run=server -k ../k8s` прошёл. + +Оставшиеся операторские действия: + +1. Закоммитить и отправить изменения в `main`, дождаться checks, сборки и Trivy scan. +2. Выполнить первый rollout по digest через `../k8s/deploy.sh apply`. +3. После проверки удалить старые ConfigMap/RBAC-объекты, ротировать перенесённые DB/OTel + credentials и выполнить согласованную очистку истории по `docs/SECURITY_RUNBOOK.md`. diff --git a/STARTUP_GUIDE.md b/STARTUP_GUIDE.md index ff28b33..5a7a85d 100644 --- a/STARTUP_GUIDE.md +++ b/STARTUP_GUIDE.md @@ -58,53 +58,95 @@ Полный локальный сброс БД выполняется командой `docker compose down -v`, но она удалит все локальные данные. Обычная остановка без удаления данных: `docker compose down`. -## 2. Первый запуск в production +## 2. Обновление существующего production -1. Подготовьте K3s/Kubernetes-кластер и убедитесь, что `kubectl get nodes` работает. - PostgreSQL в текущие манифесты не входит: заранее создайте доступную из кластера БД для - каждого университета. Пользователь БД должен иметь права на создание и изменение схемы — - Flyway применит `V1__init.sql` автоматически. +Простой push в `main` для первого обновления **недостаточен**. Он выполняет проверки, +собирает и сканирует образы, но job `deploy-to-k8s` запускается только вручную через +`workflow_dispatch`. Новые файлы `../k8s` CI не применяет. Gitea 1.25 игнорирует +`environment` и `concurrency`, поэтому они не используются как production-защита. -2. Настройте DNS для `magistr.zuev.company` и остальных tenant-доменов на публичный - reverse proxy/Ingress. В текущем `../k8s/ingress.yaml` нет секции TLS: для HTTPS нужно - настроить внешний Caddy с сертификатом либо добавить cert-manager/TLS в кластер. +1. Подготовьте секреты по [`docs/SECURITY_RUNBOOK.md`](docs/SECURITY_RUNBOOK.md): -3. Создайте namespace и четыре обязательных Secret по - [`docs/SECURITY_RUNBOOK.md`](docs/SECURITY_RUNBOOK.md): + - `app-secret`: новый случайный `JWT_SECRET` не короче 32 байт, `POSTGRES_USER`, + `POSTGRES_PASSWORD`; + - `tenants-secret`: новый `tenants.json` с актуальными JDBC URL и ротированными паролями; + - `otel-postgres-secret`: восемь обязательных параметров подключений Collector; + - `gitea-registry`: доступ K3s к registry. - - `app-secret`: `JWT_SECRET`, `POSTGRES_USER`, `POSTGRES_PASSWORD`; - - `tenants-secret`: файл `tenants.json`; - - `otel-postgres-secret`: подключения Collector к БД; - - `gitea-registry`: доступ к приватным Docker-образам. + Для `magistr.zuev.company` нужен tenant с `"domain": "magistr"`, для + `n8n.zuev.company` — `"domain": "n8n"`. Секреты не сохраняйте в Git или CI-логах. - Для `magistr.zuev.company` запись в `tenants.json` должна иметь `"domain": "magistr"` - и реальный JDBC URL. Не сохраняйте значения Secret в Git или логах. +2. Выполните локальные проверки, затем отправьте изменения в `main`: -4. В Gitea Actions добавьте секреты `ZUEV_TOKEN` (доступ к registry) и - `KUBECONFIG_DATA` (kubeconfig в Base64). Push в `main` соберёт, проверит и опубликует - образы. При самом первом запуске автоматический deploy может ещё не найти Deployment — - возьмите два опубликованных digest из CI/registry и выполните bootstrap вручную: + ```bash + K8S_DIR=../k8s bash scripts/check-production-secrets.sh + K8S_DIR=../k8s bash scripts/check-artifact-pinning.sh + bash scripts/test-deploy-images.sh + kubectl kustomize ../k8s >/dev/null + ``` + + Дождитесь успешных checks, сборки и Trivy scan. Скопируйте точные backend/frontend + ссылки вида `image@sha256:...` из CI/registry. + +3. Объявите техническое окно и временно закройте публичный доступ во внешнем Caddy. Затем + остановите backend: + + ```bash + kubectl scale deployment/backend -n magistr --replicas=0 + ``` + + Полностью пересоздайте **каждую** PostgreSQL tenant-БД, указанную в новом + `tenants.json`. Недостаточно удалить `flyway_schema_history` или отдельные таблицы: + миграции V2–V7 объединены в новую `V1__init.sql`, поэтому старая БД получит checksum + error, а непустая БД без истории может быть ошибочно принята Flyway за baseline. + Пользователь БД должен уметь создавать схему, функции, триггеры и расширения + `pgcrypto`/`btree_gist`. + +4. С рабочей станции с актуальным каталогом `../k8s` выполните первый rollout вручную: ```bash export BACKEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-backend@sha256:' export FRONTEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-frontend@sha256:' - K8S_DIR=../k8s bash scripts/check-production-secrets.sh - K8S_DIR=../k8s bash scripts/check-artifact-pinning.sh - kubectl kustomize ../k8s >/dev/null bash ../k8s/deploy.sh apply ``` -5. Проверьте результат: + Скрипт применит ConfigMap, RBAC, Secret mount, probes, OTel и Ingress, поднимет две + реплики backend и дождётся Flyway/readiness. CI deploy после push не ожидает + подтверждения: он будет пропущен. Следующие image-only обновления можно запускать + вручную кнопкой `Run workflow`. + +5. Перед открытием публичного доступа проверьте: ```bash - bash ../k8s/deploy.sh status kubectl rollout status deployment/backend -n magistr --timeout=300s + kubectl rollout status deployment/frontend -n magistr --timeout=120s + kubectl get pods -n magistr kubectl logs -n magistr -l app=backend --tail=200 - curl -fsS https://magistr.zuev.company/ + curl -fsSI https://magistr.zuev.company/ ``` - После первого входа немедленно смените/отключите все тестовые пароли из `V1__init.sql`. - Следующие push в `main` CI/CD сможет развёртывать автоматически. + Проверьте в браузере login → reload → refresh → logout для каждого tenant-домена. + Refresh-cookie должна иметь `HttpOnly`, `Secure`, `SameSite=Lax`, path `/api/auth`; + access JWT не должен находиться в Local/Session Storage. До снятия технического окна + смените или отключите все демонстрационные учётные записи из `V1__init.sql`. + +6. После проверки удалите старый `ConfigMap/tenants-config`, отзовите старые DB/OTel + пароли и верните обычный доступ через Caddy. В дальнейшем image-only изменения можно + разворачивать ручным запуском workflow, но любые изменения `../k8s` по-прежнему нужно + применять отдельно или переносить этот каталог в управляемый CI репозиторий. + +### Важные ограничения rollback + +- Смена `JWT_SECRET` и сброс БД завершат все старые пользовательские сессии. +- Readiness требует доступности и успешной миграции всех tenant-БД; одна старая или + недоступная БД остановит rollout. +- Автоматический rollback возвращает только два image reference. Он не откатывает БД, + Secret, ConfigMap или RBAC; после применения новой V1 безопаснее исправлять проблему + вперёд либо снова пересоздавать БД под старую версию. +- CI не запускает Testcontainers integration tests: полный PostgreSQL-прогон нужно + повторить вручную, если после зафиксированных 300 успешных тестов код ещё менялся. +- `TRUSTED_PROXY_CIDRS=10.42.0.0/16` соответствует текущей pod-сети K3s. При смене + cluster CIDR его нужно обновить, иначе rate limit увидит неверные клиентские IP. ## 3. Что могу выполнить я diff --git a/backend/src/main/resources/db/migration/V1__init.sql b/backend/src/main/resources/db/migration/V1__init.sql index d040527..50868e5 100755 --- a/backend/src/main/resources/db/migration/V1__init.sql +++ b/backend/src/main/resources/db/migration/V1__init.sql @@ -731,12 +731,31 @@ CREATE TABLE IF NOT EXISTS academic_calendar_days ( CREATE INDEX IF NOT EXISTS idx_calendar_days_lookup ON academic_calendar_days(calendar_id, course_number, date); +CREATE OR REPLACE FUNCTION calculate_academic_calendar_week_number( + academic_year_start DATE, + calendar_date DATE +) +RETURNS INTEGER +LANGUAGE SQL +IMMUTABLE +STRICT +AS $$ + SELECT ( + ( + calendar_date + - academic_year_start + + EXTRACT(ISODOW FROM academic_year_start)::INTEGER + - 1 + ) / 7 + ) + 1 +$$; + INSERT INTO academic_calendar_days (calendar_id, course_number, date, week_number, day_of_week, activity_type_id) SELECT calendar.id, course.course_number, days.date::DATE, - ((days.date::DATE - ay.start_date) / 7) + 1, + calculate_academic_calendar_week_number(ay.start_date, days.date::DATE), EXTRACT(ISODOW FROM days.date::DATE)::INT, activity.id FROM academic_calendars calendar @@ -1262,6 +1281,8 @@ COMMENT ON COLUMN academic_calendar_days.date IS 'Дата'; COMMENT ON COLUMN academic_calendar_days.week_number IS 'Номер недели в учебном году'; COMMENT ON COLUMN academic_calendar_days.day_of_week IS 'День недели ISO'; COMMENT ON COLUMN academic_calendar_days.activity_type_id IS 'ID кода активности'; +COMMENT ON FUNCTION calculate_academic_calendar_week_number(DATE, DATE) + IS 'Номер недели календарного учебного графика по периодам понедельник–воскресенье'; COMMENT ON COLUMN academic_calendar_subjects.calendar_id IS 'ID календарного учебного графика'; COMMENT ON COLUMN academic_calendar_subjects.semester_number IS 'Номер учебного семестра внутри графика: 1, 2, 3 ...'; diff --git a/backend/src/main/resources/db/migration/V2__align_academic_calendar_weeks_to_monday.sql b/backend/src/main/resources/db/migration/V2__align_academic_calendar_weeks_to_monday.sql deleted file mode 100644 index ecc153c..0000000 --- a/backend/src/main/resources/db/migration/V2__align_academic_calendar_weeks_to_monday.sql +++ /dev/null @@ -1,23 +0,0 @@ --- Недели календарного учебного графика выровнены по ISO-неделе: --- понедельник — первый день, воскресенье — последний. -UPDATE academic_calendar_days AS calendar_day -SET week_number = ( - ( - calendar_day.date - - academic_year.start_date - + EXTRACT(ISODOW FROM academic_year.start_date)::INTEGER - - 1 - ) / 7 -) + 1 -FROM academic_calendars AS calendar -JOIN academic_years AS academic_year - ON academic_year.id = calendar.academic_year_id -WHERE calendar_day.calendar_id = calendar.id - AND calendar_day.week_number IS DISTINCT FROM ( - ( - calendar_day.date - - academic_year.start_date - + EXTRACT(ISODOW FROM academic_year.start_date)::INTEGER - - 1 - ) / 7 - ) + 1; diff --git a/backend/src/test/java/com/magistr/app/migration/AcademicCalendarWeekNumberBaselineIntegrationTest.java b/backend/src/test/java/com/magistr/app/migration/AcademicCalendarWeekNumberBaselineIntegrationTest.java new file mode 100644 index 0000000..026294c --- /dev/null +++ b/backend/src/test/java/com/magistr/app/migration/AcademicCalendarWeekNumberBaselineIntegrationTest.java @@ -0,0 +1,70 @@ +package com.magistr.app.migration; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +@DisplayName("Baseline нумерации недель календарного графика") +class AcademicCalendarWeekNumberBaselineIntegrationTest { + + @Container + static final PostgreSQLContainer POSTGRES = + new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES); + + @Test + @DisplayName("V1 считает недели по периодам понедельник–воскресенье") + void baselineAlignsWeeksToMonday() throws SQLException { + Flyway flyway = Flyway.configure() + .dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword()) + .locations("classpath:db/migration") + .cleanDisabled(false) + .load(); + + flyway.clean(); + flyway.migrate(); + + assertThat(flyway.info().current()).isNotNull(); + assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("1"); + + try (Connection connection = POSTGRES.createConnection("")) { + assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 1))) + .isEqualTo(1); + assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 6))) + .isEqualTo(1); + assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 7))) + .isEqualTo(2); + assertThat(calculateWeek(connection, LocalDate.of(2027, 9, 1), LocalDate.of(2027, 9, 6))) + .isEqualTo(2); + assertThat(calculateWeek(connection, LocalDate.of(2025, 9, 1), LocalDate.of(2025, 9, 8))) + .isEqualTo(2); + } + } + + private int calculateWeek(Connection connection, + LocalDate academicYearStart, + LocalDate calendarDate) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement( + "SELECT calculate_academic_calendar_week_number(?, ?)" + )) { + statement.setDate(1, Date.valueOf(academicYearStart)); + statement.setDate(2, Date.valueOf(calendarDate)); + try (ResultSet resultSet = statement.executeQuery()) { + assertThat(resultSet.next()).isTrue(); + return resultSet.getInt(1); + } + } + } +} diff --git a/backend/src/test/java/com/magistr/app/migration/AcademicCalendarWeekNumberMigrationIntegrationTest.java b/backend/src/test/java/com/magistr/app/migration/AcademicCalendarWeekNumberMigrationIntegrationTest.java deleted file mode 100644 index c32a4fc..0000000 --- a/backend/src/test/java/com/magistr/app/migration/AcademicCalendarWeekNumberMigrationIntegrationTest.java +++ /dev/null @@ -1,218 +0,0 @@ -package com.magistr.app.migration; - -import org.flywaydb.core.Flyway; -import org.flywaydb.core.api.MigrationVersion; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.sql.Connection; -import java.sql.Date; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThat; - -@Testcontainers -@DisplayName("Миграция нумерации недель календарного графика") -class AcademicCalendarWeekNumberMigrationIntegrationTest { - - @Container - static final PostgreSQLContainer POSTGRES = - new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES); - - @Test - @DisplayName("V2 переносит первый понедельник после неполной недели в неделю 2") - void migrationAlignsWeeksToMondayFor2026And2027() throws SQLException { - Flyway baseline = flyway("1"); - baseline.clean(); - baseline.migrate(); - - long calendar2026; - long calendar2027; - try (Connection connection = POSTGRES.createConnection("")) { - SeedReferences references = loadSeedReferences(connection); - - long year2026 = insertAcademicYear( - connection, - "2026-2027", - LocalDate.of(2026, 9, 1), - LocalDate.of(2027, 6, 30) - ); - calendar2026 = insertCalendar(connection, year2026, references, "График 2026-2027"); - insertCalendarDay(connection, calendar2026, references.activityTypeId(), - LocalDate.of(2026, 9, 1), 1, 2); - insertCalendarDay(connection, calendar2026, references.activityTypeId(), - LocalDate.of(2026, 9, 7), 1, 1); - - long year2027 = insertAcademicYear( - connection, - "2027-2028", - LocalDate.of(2027, 9, 1), - LocalDate.of(2028, 6, 30) - ); - calendar2027 = insertCalendar(connection, year2027, references, "График 2027-2028"); - insertCalendarDay(connection, calendar2027, references.activityTypeId(), - LocalDate.of(2027, 9, 1), 1, 3); - insertCalendarDay(connection, calendar2027, references.activityTypeId(), - LocalDate.of(2027, 9, 6), 1, 1); - } - - Flyway latest = latestFlyway(); - latest.migrate(); - - assertThat(latest.info().current()).isNotNull(); - assertThat(latest.info().current().getVersion().getVersion()).isEqualTo("2"); - try (Connection connection = POSTGRES.createConnection("")) { - assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 1))).isEqualTo(1); - assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 7))).isEqualTo(2); - assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 1))).isEqualTo(1); - assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 6))).isEqualTo(2); - } - } - - private Flyway flyway(String targetVersion) { - return Flyway.configure() - .dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword()) - .locations("classpath:db/migration") - .target(MigrationVersion.fromVersion(targetVersion)) - .cleanDisabled(false) - .load(); - } - - private Flyway latestFlyway() { - return Flyway.configure() - .dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword()) - .locations("classpath:db/migration") - .cleanDisabled(false) - .load(); - } - - private SeedReferences loadSeedReferences(Connection connection) throws SQLException { - long specialtyId = queryLong(connection, "SELECT id FROM specialties ORDER BY id LIMIT 1"); - long profileId = queryLong( - connection, - "SELECT id FROM specialty_profiles WHERE specialty_id = ? ORDER BY id LIMIT 1", - specialtyId - ); - long studyFormId = queryLong(connection, "SELECT id FROM education_forms ORDER BY id LIMIT 1"); - long activityTypeId = queryLong( - connection, - "SELECT id FROM academic_calendar_activity_types WHERE code = 'Т'" - ); - return new SeedReferences(specialtyId, profileId, studyFormId, activityTypeId); - } - - private long insertAcademicYear(Connection connection, - String title, - LocalDate startDate, - LocalDate endDate) throws SQLException { - try (PreparedStatement statement = connection.prepareStatement(""" - INSERT INTO academic_years (title, start_date, end_date) - VALUES (?, ?, ?) - RETURNING id - """)) { - statement.setString(1, title); - statement.setDate(2, Date.valueOf(startDate)); - statement.setDate(3, Date.valueOf(endDate)); - return requiredLong(statement); - } - } - - private long insertCalendar(Connection connection, - long academicYearId, - SeedReferences references, - String title) throws SQLException { - try (PreparedStatement statement = connection.prepareStatement(""" - INSERT INTO academic_calendars ( - title, - academic_year_id, - specialty_id, - specialty_profile_id, - study_form_id, - course_count - ) - VALUES (?, ?, ?, ?, ?, 1) - RETURNING id - """)) { - statement.setString(1, title); - statement.setLong(2, academicYearId); - statement.setLong(3, references.specialtyId()); - statement.setLong(4, references.profileId()); - statement.setLong(5, references.studyFormId()); - return requiredLong(statement); - } - } - - private void insertCalendarDay(Connection connection, - long calendarId, - long activityTypeId, - LocalDate date, - int weekNumber, - int dayOfWeek) throws SQLException { - try (PreparedStatement statement = connection.prepareStatement(""" - INSERT INTO academic_calendar_days ( - calendar_id, - course_number, - date, - week_number, - day_of_week, - activity_type_id - ) - VALUES (?, 1, ?, ?, ?, ?) - """)) { - statement.setLong(1, calendarId); - statement.setDate(2, Date.valueOf(date)); - statement.setInt(3, weekNumber); - statement.setInt(4, dayOfWeek); - statement.setLong(5, activityTypeId); - statement.executeUpdate(); - } - } - - private int findWeekNumber(Connection connection, - long calendarId, - LocalDate date) throws SQLException { - try (PreparedStatement statement = connection.prepareStatement(""" - SELECT week_number - FROM academic_calendar_days - WHERE calendar_id = ? - AND date = ? - """)) { - statement.setLong(1, calendarId); - statement.setDate(2, Date.valueOf(date)); - try (ResultSet resultSet = statement.executeQuery()) { - assertThat(resultSet.next()).isTrue(); - return resultSet.getInt(1); - } - } - } - - private long queryLong(Connection connection, String sql, Object... arguments) throws SQLException { - try (PreparedStatement statement = connection.prepareStatement(sql)) { - for (int index = 0; index < arguments.length; index++) { - statement.setObject(index + 1, arguments[index]); - } - return requiredLong(statement); - } - } - - private long requiredLong(PreparedStatement statement) throws SQLException { - try (ResultSet resultSet = statement.executeQuery()) { - assertThat(resultSet.next()).isTrue(); - return resultSet.getLong(1); - } - } - - private record SeedReferences( - long specialtyId, - long profileId, - long studyFormId, - long activityTypeId - ) { - } -} diff --git a/docs/DATABASE.md b/docs/DATABASE.md index d267987..a179a95 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -913,21 +913,21 @@ lifecycle ресурсов, эффективная сетка целевого 2. Формат имени: `V{номер}__{описание}.sql` (напр. `V1__init.sql`, `V2__add_departments.sql`) 3. **ЗАПРЕЩЕНО** изменять уже закоммиченные файлы миграций — это сломает контрольные суммы Flyway. Исключение допускается только по прямой просьбе пользователя и при полном сбросе tenant-БД. 4. Flyway запускается **программно** при первом обращении к БД тенанта (`TenantConfigWatcher.initDatabaseForTenant()`) -5. Настройка `baselineOnMigrate=true` — если в БД уже есть данные, Flyway начнёт с baseline +5. Настройка `baselineOnMigrate=true` — непустая БД без истории будет помечена baseline и + `V1` не выполнится; поэтому текущую консолидированную V1 применяют только к полностью + пустой tenant-схеме ### Текущие миграции | Файл | Описание | |------|----------| -| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии | -| `V2__align_academic_calendar_weeks_to_monday.sql` | Перенумерация сохранённых дней календарного графика по периодам `понедельник–воскресенье`, чтобы неполная первая неделя не заполнялась датами следующей недели | +| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики с нумерацией недель `понедельник–воскресенье`, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии | ### Этап разработки -По прямому решению владельца проекта прежние разработческие миграции V2–V7 были объединены -в baseline `V1`. Новая миграция `V2__align_academic_calendar_weeks_to_monday.sql` создана -после фиксации baseline и накатывается поверх существующих tenant-БД без изменения -контрольной суммы V1. +По прямому решению владельца проекта разработческие миграции V2–V7 объединены в baseline +`V1`. Правильная нумерация недель календарного графика также входит непосредственно в V1. +Перед применением этой редакции требуется полностью пустая tenant-схема. ### Полный сброс БД (локально) diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md index e716460..c7e0a4a 100644 --- a/docs/INFRASTRUCTURE.md +++ b/docs/INFRASTRUCTURE.md @@ -325,8 +325,8 @@ kubectl auth can-i update secret/tenants-secret \ --as="system:serviceaccount:magistr:${BACKEND_SERVICE_ACCOUNT}" -n magistr ``` -Production rollout и проверка реальных pod являются внешними операциями и без отдельного -разрешения из этой рабочей копии не выполнялись. +Production rollout и проверка реальных pod являются внешними операциями и выполняются из +этой рабочей копии только по отдельному разрешению владельца. ### Ручное обновление backend и frontend @@ -368,12 +368,16 @@ Deployment-файлов запрещено. jobs также возвращают registry digest каждого образа. 4. Отдельный обязательный job сканирует опубликованные digests закреплённым Trivy `0.63.0` и блокирует deploy при исправимых `HIGH`/`CRITICAL` уязвимостях. -5. Deploy job устанавливает фиксированный `kubectl v1.33.12` только после SHA-256 проверки. - Java Agent также имеет точную версию и checksum. +5. Push в `main` выполняет проверки, сборку, публикацию и scan, но не меняет production. + Deploy job запускается только вручную через `workflow_dispatch` и устанавливает + фиксированный `kubectl v1.33.12` после SHA-256 проверки. Java Agent также имеет точную + версию и checksum. 6. `scripts/deploy-images.sh` принимает только `image@sha256:...`, сохраняет предыдущие ссылки, применяет оба digest и ждёт rollout. При отказе автоматически возвращает оба предыдущих образа и повторно проверяет их готовность. -7. Workflow-wide concurrency lock не допускает одновременные production deployment. +7. Gitea 1.25 игнорирует `environment` и `concurrency`, поэтому workflow не полагается на + них как на защиту production. Оператор не должен запускать второй ручной deploy, пока + первый не завершён. `scripts/check-artifact-pinning.sh` проверяет Dockerfile, Compose, Actions, checksum и CI gates. При передаче `K8S_DIR=../k8s` он дополнительно требует digest у каждого production diff --git a/graphify-out/.graphify_learning.json b/graphify-out/.graphify_learning.json index 96c0d00..298e703 100644 --- a/graphify-out/.graphify_learning.json +++ b/graphify-out/.graphify_learning.json @@ -1,6 +1,27 @@ { - "generated_at": "2026-07-22T21:09:28.978295+00:00", + "generated_at": "2026-07-29T20:02:59.230356+00:00", "nodes": { + "_gitea_workflows_docker_build_build_and_push_docker_images": { + "code_fingerprint": "80373e004c326a786715725be21ddb9214b6ff382a02cb829acad1bb9c9098f8", + "label": "Workflow сборки и публикации Docker-образов", + "last": "2026-07-29T19:59:04.551881+00:00", + "provenance": [ + { + "date": "2026-07-29T19:59:04.551881+00:00", + "outcome": "useful", + "q": "новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше" + }, + { + "date": "2026-07-29T19:26:44.188240+00:00", + "outcome": "useful", + "q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?" + } + ], + "score": 1.999355769, + "source_file": ".gitea/workflows/docker-build.yaml", + "status": "preferred", + "uses": 2 + }, "backend_src_main_java_com_magistr_app_config_auth_authorizationinterceptor_authorizationinterceptor": { "code_fingerprint": "70b01b3fa22e058ee997250c52cc0affee54ab37aa5f8cc7eb74411487f1bbbf", "label": "AuthorizationInterceptor", @@ -22,7 +43,7 @@ "q": "Почему RequireRoles связывает множество контроллеров, моделей и сервисов как cross-community bridge?" } ], - "score": 2.185520319, + "score": 1.861135007, "source_file": "backend/src/main/java/com/magistr/app/config/auth/AuthorizationInterceptor.java", "status": "preferred", "uses": 3 @@ -38,7 +59,7 @@ "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" } ], - "score": 0.776480146, + "score": 0.661231273, "source_file": "backend/src/main/java/com/magistr/app/config/auth/JwtTokenService.java", "status": "tentative", "uses": 1 @@ -54,7 +75,7 @@ "q": "Почему RequireRoles связывает множество контроллеров, моделей и сервисов как cross-community bridge?" } ], - "score": 0.653312094, + "score": 0.556344408, "source_file": "backend/src/main/java/com/magistr/app/config/auth/RequireRoles.java", "status": "tentative", "uses": 1 @@ -70,7 +91,7 @@ "q": "Как безопасно обновлять tenant DataSource без потери рабочего подключения?" } ], - "score": 0.794603611, + "score": 0.676664767, "source_file": "backend/src/main/java/com/magistr/app/config/DataInitializer.java", "status": "tentative", "uses": 1 @@ -86,7 +107,7 @@ "q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'" } ], - "score": 0.75572808, + "score": 0.643559327, "source_file": "backend/src/main/java/com/magistr/app/config/tenant/ConfigMapUpdater.java", "status": "tentative", "uses": 1 @@ -102,7 +123,7 @@ "q": "Как безопасно заменить tenant DataSource без потери рабочего подключения?" } ], - "score": 0.794468884, + "score": 0.676550037, "source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantConfig.java", "status": "tentative", "uses": 1 @@ -128,7 +149,7 @@ "q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'" } ], - "score": 2.344800575, + "score": 1.99677413, "source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantConfigWatcher.java", "status": "preferred", "uses": 3 @@ -154,7 +175,7 @@ "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" } ], - "score": 2.365552641, + "score": 2.014446076, "source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java", "status": "preferred", "uses": 3 @@ -170,7 +191,7 @@ "q": "Почему в выборе аудитории показаны корпус и этаж?" } ], - "score": 0.976851438, + "score": 0.831862505, "source_file": "backend/src/main/java/com/magistr/app/controller/ClassroomController.java", "status": "tentative", "uses": 1 @@ -186,7 +207,7 @@ "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" } ], - "score": 0.776480146, + "score": 0.661231273, "source_file": "backend/src/main/java/com/magistr/app/controller/DatabaseController.java", "status": "tentative", "uses": 1 @@ -207,7 +228,7 @@ "q": "Как безопасно заменить tenant DataSource без потери рабочего подключения?" } ], - "score": 1.589072495, + "score": 1.353214804, "source_file": "backend/src/main/java/com/magistr/app/controller/DatabaseController.java", "status": "preferred", "uses": 2 @@ -223,7 +244,7 @@ "q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'" } ], - "score": 0.75572808, + "score": 0.643559327, "source_file": "backend/src/main/java/com/magistr/app/controller/GlobalExceptionHandler.java", "status": "tentative", "uses": 1 @@ -244,7 +265,7 @@ "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" } ], - "score": 1.752429602, + "score": 1.492325672, "source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleController.java", "status": "preferred", "uses": 2 @@ -260,7 +281,7 @@ "q": "Как безопасно валидировать и сохранять точечные изменения расписания?" } ], - "score": 0.78890947, + "score": 0.671815777, "source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleOverrideController.java", "status": "tentative", "uses": 1 @@ -281,11 +302,27 @@ "q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'" } ], - "score": 1.549927354, + "score": 1.319879771, "source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleRuleAdminController.java", "status": "preferred", "uses": 2 }, + "backend_src_main_java_com_magistr_app_model_academiccalendar_academiccalendar": { + "code_fingerprint": "5bdd7da771fe97443661a5bd06bed94f453ea036759c4de8c927ed68eb2b723d", + "label": "AcademicCalendar", + "last": "2026-07-22T21:09:57.015227+00:00", + "provenance": [ + { + "date": "2026-07-22T21:09:57.015227+00:00", + "outcome": "useful", + "q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу" + } + ], + "score": 0.851581632, + "source_file": "backend/src/main/java/com/magistr/app/model/AcademicCalendar.java", + "status": "tentative", + "uses": 1 + }, "backend_src_main_java_com_magistr_app_model_classroom_classroom": { "code_fingerprint": "5614d83b8b7e20fd2b76271b28a8ef11ffc137808ec2d7ed7cd23e42e0ab93ec", "label": "Classroom", @@ -297,11 +334,27 @@ "q": "Почему в выборе аудитории показаны корпус и этаж?" } ], - "score": 0.976851438, + "score": 0.831862505, "source_file": "backend/src/main/java/com/magistr/app/model/Classroom.java", "status": "tentative", "uses": 1 }, + "backend_src_main_java_com_magistr_app_model_department_department": { + "code_fingerprint": "2948555ebc816c44c9f745ce199b0a908930bf6c82cb4a0bd46c23ffac42ea6a", + "label": "Department", + "last": "2026-07-22T21:09:57.015227+00:00", + "provenance": [ + { + "date": "2026-07-22T21:09:57.015227+00:00", + "outcome": "useful", + "q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу" + } + ], + "score": 0.851581632, + "source_file": "backend/src/main/java/com/magistr/app/model/Department.java", + "status": "tentative", + "uses": 1 + }, "backend_src_main_java_com_magistr_app_model_schedulerule_schedulerule": { "code_fingerprint": "692d61c1bfd5b46f5865174dde8cf2d962d2ccd124b5e4320e2ba8535bb29605", "label": "ScheduleRule", @@ -318,7 +371,7 @@ "q": "Корректны ли inferred-связи вокруг ScheduleRule?" } ], - "score": 1.448240567, + "score": 1.23328582, "source_file": "backend/src/main/java/com/magistr/app/model/ScheduleRule.java", "status": "preferred", "uses": 2 @@ -334,7 +387,7 @@ "q": "Какие связи затрагивает полная валидация правил расписания в исправлении №7?" } ], - "score": 0.794199275, + "score": 0.676320444, "source_file": "backend/src/main/java/com/magistr/app/model/ScheduleRuleSlot.java", "status": "tentative", "uses": 1 @@ -342,16 +395,37 @@ "backend_src_main_java_com_magistr_app_model_semester_semester": { "code_fingerprint": "52546b552fff16ab841a63ea80fb806f6cc32edc0615ec53b3c002543d692f24", "label": "Semester", - "last": "2026-07-21T19:52:13.631700+00:00", + "last": "2026-07-22T21:09:57.015227+00:00", "provenance": [ + { + "date": "2026-07-22T21:09:57.015227+00:00", + "outcome": "useful", + "q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу" + }, { "date": "2026-07-21T19:52:13.631700+00:00", "outcome": "useful", "q": "в дополнительные фильры просмотра расписаний нужно добавить выбор семестра" } ], - "score": 0.975949456, + "score": 1.682676032, "source_file": "backend/src/main/java/com/magistr/app/model/Semester.java", + "status": "preferred", + "uses": 2 + }, + "backend_src_main_java_com_magistr_app_model_studentgroup_studentgroup": { + "code_fingerprint": "c4a022d9dd493adcfcf8bce3abd264e78d5539b45316d9e0aaef72f1b15091ac", + "label": "StudentGroup", + "last": "2026-07-22T21:09:57.015227+00:00", + "provenance": [ + { + "date": "2026-07-22T21:09:57.015227+00:00", + "outcome": "useful", + "q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу" + } + ], + "score": 0.851581632, + "source_file": "backend/src/main/java/com/magistr/app/model/StudentGroup.java", "status": "tentative", "uses": 1 }, @@ -366,7 +440,7 @@ "q": "Какие связи затрагивает полная валидация правил расписания в исправлении №7?" } ], - "score": 0.794199275, + "score": 0.676320444, "source_file": "backend/src/main/java/com/magistr/app/repository/SemesterRepository.java", "status": "tentative", "uses": 1 @@ -387,7 +461,7 @@ "q": "Как безопасно валидировать и сохранять точечные изменения расписания?" } ], - "score": 1.764858926, + "score": 1.502910177, "source_file": "backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java", "status": "preferred", "uses": 2 @@ -403,7 +477,7 @@ "q": "Корректны ли inferred-связи вокруг ScheduleRule?" } ], - "score": 0.654041293, + "score": 0.556965376, "source_file": "backend/src/test/java/com/magistr/app/model/ScheduleRuleTest.java", "status": "tentative", "uses": 1 @@ -419,10 +493,95 @@ "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" } ], - "score": 0.776480146, + "score": 0.661231273, "source_file": "compose.yaml", "status": "tentative", "uses": 1 + }, + "compose_db_service": { + "code_fingerprint": "944ed05aa3bb66491e0622baa9659fc2db3115c79076325ebe243aeb84ca09f9", + "label": "PostgreSQL-сервис Docker Compose", + "last": "2026-07-29T19:59:04.551881+00:00", + "provenance": [ + { + "date": "2026-07-29T19:59:04.551881+00:00", + "outcome": "useful", + "q": "новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше" + } + ], + "score": 0.999937245, + "source_file": "compose.yaml", + "status": "tentative", + "uses": 1 + }, + "docs_database_flyway_migrations": { + "code_fingerprint": "2f5d4ce61ba0f5d8a9141f4ac33375dfc26a68e3bdccdc4269cf2a4f45a50176", + "label": "Flyway-миграции", + "last": "2026-07-29T19:26:44.188240+00:00", + "provenance": [ + { + "date": "2026-07-29T19:26:44.188240+00:00", + "outcome": "useful", + "q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?" + } + ], + "score": 0.999418524, + "source_file": "docs/DATABASE.md", + "status": "tentative", + "uses": 1 + }, + "docs_infrastructure_jwt_secret_policy": { + "code_fingerprint": "fbce586f3b64dba203d336f5744d9895924e86e122cdde1939c0dec1600e1af1", + "label": "Политика JWT-секрета", + "last": "2026-07-29T19:26:44.188240+00:00", + "provenance": [ + { + "date": "2026-07-29T19:26:44.188240+00:00", + "outcome": "useful", + "q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?" + } + ], + "score": 0.999418524, + "source_file": "docs/INFRASTRUCTURE.md", + "status": "tentative", + "uses": 1 + }, + "docs_infrastructure_kubernetes_production": { + "code_fingerprint": "fbce586f3b64dba203d336f5744d9895924e86e122cdde1939c0dec1600e1af1", + "label": "Production-инфраструктура Kubernetes", + "last": "2026-07-29T19:59:04.551881+00:00", + "provenance": [ + { + "date": "2026-07-29T19:59:04.551881+00:00", + "outcome": "useful", + "q": "новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше" + }, + { + "date": "2026-07-29T19:26:44.188240+00:00", + "outcome": "useful", + "q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?" + } + ], + "score": 1.999355769, + "source_file": "docs/INFRASTRUCTURE.md", + "status": "preferred", + "uses": 2 + }, + "docs_infrastructure_tenants_configmap": { + "code_fingerprint": "fbce586f3b64dba203d336f5744d9895924e86e122cdde1939c0dec1600e1af1", + "label": "Tenants ConfigMap", + "last": "2026-07-29T19:26:44.188240+00:00", + "provenance": [ + { + "date": "2026-07-29T19:26:44.188240+00:00", + "outcome": "useful", + "q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?" + } + ], + "score": 0.999418524, + "source_file": "docs/INFRASTRUCTURE.md", + "status": "tentative", + "uses": 1 } }, "version": 1 diff --git a/graphify-out/memory/query_20260729_192644_теперь_напиши_мне_инструкцию_как_это_всё_выыкатить.md b/graphify-out/memory/query_20260729_192644_теперь_напиши_мне_инструкцию_как_это_всё_выыкатить.md new file mode 100644 index 0000000..d4f99c0 --- /dev/null +++ b/graphify-out/memory/query_20260729_192644_теперь_напиши_мне_инструкцию_как_это_всё_выыкатить.md @@ -0,0 +1,26 @@ +--- +type: "query" +date: "2026-07-29T19:26:44.188240+00:00" +question: "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?" +contributor: "graphify" +outcome: "useful" +source_nodes: ["Workflow сборки и публикации Docker-образов", "Production-инфраструктура Kubernetes", "Flyway-миграции", "Политика JWT-секрета", "Tenants ConfigMap"] +--- + +# Q: теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли? + +## Answer + +Expanded from original query via vocab: [auth, cookie, database, docker, flyway, jwt, kubernetes, production, refresh, secret, secure, workflow]. Прямой push в main для первого rollout небезопасен: workflow меняет только image digests, а актуальные ../k8s манифесты находятся вне Git-корня и не применяются. В действующем кластере отсутствуют tenants-secret и otel-postgres-secret, а app-secret не содержит JWT_SECRET. Для консолидированной V1 нужно полностью пересоздать все tenant-БД, затем выполнить первый rollout вручную по digest через ../k8s/deploy.sh. Инструкция и ограничения rollback обновлены в STARTUP_GUIDE.md. + +## Outcome + +- Signal: useful + +## Source Nodes + +- Workflow сборки и публикации Docker-образов +- Production-инфраструктура Kubernetes +- Flyway-миграции +- Политика JWT-секрета +- Tenants ConfigMap \ No newline at end of file diff --git a/graphify-out/memory/query_20260729_195904_новая_схема_на_проде_на_удалённом_сервере_создана.md b/graphify-out/memory/query_20260729_195904_новая_схема_на_проде_на_удалённом_сервере_создана.md new file mode 100644 index 0000000..bde1145 --- /dev/null +++ b/graphify-out/memory/query_20260729_195904_новая_схема_на_проде_на_удалённом_сервере_создана.md @@ -0,0 +1,24 @@ +--- +type: "query" +date: "2026-07-29T19:59:04.551881+00:00" +question: "новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше" +contributor: "graphify" +outcome: "useful" +source_nodes: ["Workflow сборки и публикации Docker-образов", "Production-инфраструктура Kubernetes", "PostgreSQL-сервис Docker Compose"] +--- + +# Q: новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше + +## Answer + +Expanded from original query via vocab: [auth, database, flyway, jwt, kubernetes, postgres, production, secret, workflow]. Граф связал workflow сборки immutable Docker-образов, production-инфраструктуру Kubernetes и tenant PostgreSQL. Проверка исходников и кластера подтвердила: консолидированная V1 проходит на чистом PostgreSQL, push в main запускает image-only rollout, а новые манифесты ../k8s нужно применить вручную. До push необходимо создать JWT_SECRET в app-secret, tenants-secret и otel-postgres-secret; значения секретов не читались и не изменялись. + +## Outcome + +- Signal: useful + +## Source Nodes + +- Workflow сборки и публикации Docker-образов +- Production-инфраструктура Kubernetes +- PostgreSQL-сервис Docker Compose \ No newline at end of file diff --git a/graphify-out/memory/query_20260729_201238_сам_можешь_выполнить_эти_действия.md b/graphify-out/memory/query_20260729_201238_сам_можешь_выполнить_эти_действия.md new file mode 100644 index 0000000..3e42d6b --- /dev/null +++ b/graphify-out/memory/query_20260729_201238_сам_можешь_выполнить_эти_действия.md @@ -0,0 +1,25 @@ +--- +type: "query" +date: "2026-07-29T20:12:38.880629+00:00" +question: "сам можешь выполнить эти действия?" +contributor: "graphify" +outcome: "useful" +source_nodes: ["Workflow сборки и публикации Docker-образов", "Production-инфраструктура Kubernetes", "Job деплоя в Kubernetes", "Политика JWT-секрета"] +--- + +# Q: сам можешь выполнить эти действия? + +## Answer + +Expanded from original query via vocab: [workflow, production, kubernetes, secret, jwt, tenant, postgres, otel, gitea, main, registry, flyway]. Первый traversal по перегруженному main был нерелевантен; повторный запрос без main связал Workflow сборки и публикации Docker-образов, Production-инфраструктуру Kubernetes, Job деплоя в Kubernetes и Политику JWT-секрета. По отдельному разрешению старый backend масштабирован до 0, обе tenant-БД read-only проверены как пустые и имеющие CREATE, в app-secret добавлен новый JWT_SECRET без вывода значения, созданы переходные tenants-secret и otel-postgres-secret. Workflow изменён так, что push выполняет build/scan, а deploy разрешён только ручному workflow_dispatch. Server dry-run выявил immutable roleRef; внешний rbac.yaml исправлен новым именем backend-tenant-secret-binding, повторный dry-run прошёл. + +## Outcome + +- Signal: useful + +## Source Nodes + +- Workflow сборки и публикации Docker-образов +- Production-инфраструктура Kubernetes +- Job деплоя в Kubernetes +- Политика JWT-секрета \ No newline at end of file diff --git a/graphify-out/reflections/LESSONS.md b/graphify-out/reflections/LESSONS.md index c9f756e..cddd6ca 100644 --- a/graphify-out/reflections/LESSONS.md +++ b/graphify-out/reflections/LESSONS.md @@ -1,30 +1,39 @@ # Lessons -_Auto-generated by `graphify reflect` from 11 session memories in graphify-out/memory/. Deterministic; no LLM. Use for orientation — verify before relying, and revisit dead ends if the code has changed since._ +_Auto-generated by `graphify reflect` from 14 session memories in graphify-out/memory/. Deterministic; no LLM. Use for orientation — verify before relying, and revisit dead ends if the code has changed since._ ## Summary -- 10 useful · 1 dead ends · 0 corrected · 0 unmarked +- 13 useful · 1 dead ends · 0 corrected · 0 unmarked ## Lessons **Preferred sources** — corroborated by ≥2 useful results; start here. +- `ScheduleGeneratorService` (3× useful) - `TenantRoutingDataSource` (3× useful) +- `Production-инфраструктура Kubernetes` (2× useful) +- `Workflow сборки и публикации Docker-образов` (2× useful) - `TenantConfigWatcher` (3× useful) - `AuthorizationInterceptor` (3× useful) +- `Semester` (2× useful) - `ScheduleQueryService` (2× useful) - `ScheduleController.java` (2× useful) - `DatabaseController` (2× useful) -- `ScheduleGeneratorService` (2× useful) - `ScheduleRuleAdminController` (2× useful) - `ScheduleRule` (2× useful) **Tentative** — useful in fewer than 2 results; verify before relying. +- `PostgreSQL-сервис Docker Compose` (1× useful) +- `Flyway-миграции` (1× useful) +- `Tenants ConfigMap` (1× useful) +- `Политика JWT-секрета` (1× useful) +- `AcademicCalendar` (1× useful) +- `Department` (1× useful) +- `StudentGroup` (1× useful) - `Classroom` (1× useful) - `ClassroomController.java` (1× useful) -- `Semester` (1× useful) - `DataInitializer` (1× useful) - `TenantConfig` (1× useful) - `ScheduleRuleSlot` (1× useful) diff --git a/scripts/check-artifact-pinning.sh b/scripts/check-artifact-pinning.sh index 4c603bd..83e024e 100755 --- a/scripts/check-artifact-pinning.sh +++ b/scripts/check-artifact-pinning.sh @@ -85,6 +85,10 @@ check_workflow_gates() { if ! grep -Eq '^ needs: \[build-and-push-backend, build-and-push-frontend, security-scan\]$' "$WORKFLOW"; then fail "deploy обязан зависеть от успешного security-scan" fi + if ! grep -Eq '^[[:space:]]{2}workflow_dispatch:[[:space:]]*$' "$WORKFLOW" \ + || ! grep -Fq "if: \${{ gitea.event_name == 'workflow_dispatch' }}" "$WORKFLOW"; then + fail "production deploy должен запускаться только через ручной workflow_dispatch" + fi if ! grep -Eq '^ KUBECTL_VERSION: v[0-9]+\.[0-9]+\.[0-9]+$' "$WORKFLOW" \ || ! grep -Eq '^ KUBECTL_SHA256: [0-9a-f]{64}$' "$WORKFLOW" \ diff --git a/scripts/test-artifact-pinning.sh b/scripts/test-artifact-pinning.sh index 9abd5e7..0200a88 100755 --- a/scripts/test-artifact-pinning.sh +++ b/scripts/test-artifact-pinning.sh @@ -29,4 +29,13 @@ if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then exit 1 fi +cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \ + "$FIXTURE_DIR/.gitea/workflows/docker-build.yaml" +sed -i "/gitea.event_name == 'workflow_dispatch'/d" \ + "$FIXTURE_DIR/.gitea/workflows/docker-build.yaml" +if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then + printf 'Ошибка теста: автоматический production deploy на push не был отклонён\n' >&2 + exit 1 +fi + printf 'Проверка закрепления артефактов завершена успешно\n'