Подготовить безопасный production rollout

This commit is contained in:
Zuev
2026-07-29 23:13:17 +03:00
parent a78a93f2f0
commit 8bb7fe97eb
16 changed files with 494 additions and 324 deletions

View File

@@ -6,10 +6,7 @@ on:
- main - main
tags: tags:
- 'v*' - 'v*'
workflow_dispatch:
concurrency:
group: magistr-production
cancel-in-progress: false
env: env:
REGISTRY: gitea.zuev.company REGISTRY: gitea.zuev.company
@@ -175,6 +172,7 @@ jobs:
deploy-to-k8s: deploy-to-k8s:
name: Развернуть проверенные digests name: Развернуть проверенные digests
needs: [build-and-push-backend, build-and-push-frontend, security-scan] needs: [build-and-push-backend, build-and-push-frontend, security-scan]
if: ${{ gitea.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
environment: production environment: production

View File

@@ -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` | | `V2__subgroups_active_unique_name.sql` | `68f5525a50ddba4f8800a63cab3cd0f84779e301c4a1f29a6e0399f97fbdb30b` |
Текущая единая baseline-миграция: `V1__init.sql`, SHA-256 Текущая единая baseline-миграция: `V1__init.sql`, SHA-256
`fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`. `a984718777f1b4695868549e3c805c8b22edbb82fbe0b8731563adf5f11e1742`.
Упоминания V2–V7 в исторических записях ниже описывают последовательность разработки до Упоминания V2–V7 в исторических записях ниже описывают последовательность разработки до
консолидации и не означают наличие этих файлов в текущем дереве. консолидации и не означают наличие этих файлов в текущем дереве.
@@ -1047,21 +1047,41 @@
- Полностью закрыты и проверены 32 пункта. Для № 1 остаются только операторские ротация, - Полностью закрыты и проверены 32 пункта. Для № 1 остаются только операторские ротация,
provisioning, rollout и очистка истории, которые исходный prompt прямо запрещает provisioning, rollout и очистка истории, которые исходный prompt прямо запрещает
выполнять без отдельных production-полномочий. выполнять без отдельных 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, 0 skipped. Четыре frontend test-suite, `npm run check`, Compose validation,
Workflow YAML, artifact/deploy shell-тесты и `git diff --check` прошли. Workflow YAML, artifact/deploy shell-тесты и `git diff --check` прошли.
- `kubectl kustomize ../k8s`, production secret-scan, синтаксис deploy-скрипта и расширенный - `kubectl kustomize ../k8s`, production secret-scan, синтаксис deploy-скрипта и расширенный
artifact-scan прошли. В production-манифестах отсутствуют mutable `latest` и `:main`. artifact-scan прошли. В production-манифестах отсутствуют mutable `latest` и `:main`.
- Каталог Flyway содержит ровно `V1__init.sql`; V2–V4 и другие дополнительные миграции - Каталог Flyway содержит ровно `V1__init.sql`; V2–V4 и другие дополнительные миграции
отсутствуют. Финальный SHA-256 V1: отсутствуют. Финальный SHA-256 V1:
`fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`. `a984718777f1b4695868549e3c805c8b22edbb82fbe0b8731563adf5f11e1742`.
- Исправление нумерации недель календарного графика также перенесено из промежуточной V2
непосредственно в V1. Чистое применение baseline и формула недель
`понедельник–воскресенье` подтверждены
`AcademicCalendarWeekNumberBaselineIntegrationTest`.
## Точка продолжения ## Точка продолжения
Текущий этап: **реализация и проверка всех разрешённых изменений завершены**. Текущий этап: **репозиторная реализация завершена, production подготовлен к первому
контролируемому rollout**.
Оставшееся операторское действие владельца: 29 июля 2026 года по отдельному разрешению владельца выполнена production-подготовка:
1. при готовности production-полномочий выполнить `docs/SECURITY_RUNBOOK.md`: ротацию, - старый backend остановлен на `0` реплик;
provisioning, rollout и очистку истории для № 1. Эти действия не выполнялись - обе 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`.

View File

@@ -58,53 +58,95 @@
Полный локальный сброс БД выполняется командой `docker compose down -v`, но она удалит Полный локальный сброс БД выполняется командой `docker compose down -v`, но она удалит
все локальные данные. Обычная остановка без удаления данных: `docker compose down`. все локальные данные. Обычная остановка без удаления данных: `docker compose down`.
## 2. Первый запуск в production ## 2. Обновление существующего production
1. Подготовьте K3s/Kubernetes-кластер и убедитесь, что `kubectl get nodes` работает. Простой push в `main` для первого обновления **недостаточен**. Он выполняет проверки,
PostgreSQL в текущие манифесты не входит: заранее создайте доступную из кластера БД для собирает и сканирует образы, но job `deploy-to-k8s` запускается только вручную через
каждого университета. Пользователь БД должен иметь права на создание и изменение схемы — `workflow_dispatch`. Новые файлы `../k8s` CI не применяет. Gitea 1.25 игнорирует
Flyway применит `V1__init.sql` автоматически. `environment` и `concurrency`, поэтому они не используются как production-защита.
2. Настройте DNS для `magistr.zuev.company` и остальных tenant-доменов на публичный 1. Подготовьте секреты по [`docs/SECURITY_RUNBOOK.md`](docs/SECURITY_RUNBOOK.md):
reverse proxy/Ingress. В текущем `../k8s/ingress.yaml` нет секции TLS: для HTTPS нужно
настроить внешний Caddy с сертификатом либо добавить cert-manager/TLS в кластер.
3. Создайте namespace и четыре обязательных Secret по - `app-secret`: новый случайный `JWT_SECRET` не короче 32 байт, `POSTGRES_USER`,
[`docs/SECURITY_RUNBOOK.md`](docs/SECURITY_RUNBOOK.md): `POSTGRES_PASSWORD`;
- `tenants-secret`: новый `tenants.json` с актуальными JDBC URL и ротированными паролями;
- `otel-postgres-secret`: восемь обязательных параметров подключений Collector;
- `gitea-registry`: доступ K3s к registry.
- `app-secret`: `JWT_SECRET`, `POSTGRES_USER`, `POSTGRES_PASSWORD`; Для `magistr.zuev.company` нужен tenant с `"domain": "magistr"`, для
- `tenants-secret`: файл `tenants.json`; `n8n.zuev.company` — `"domain": "n8n"`. Секреты не сохраняйте в Git или CI-логах.
- `otel-postgres-secret`: подключения Collector к БД;
- `gitea-registry`: доступ к приватным Docker-образам.
Для `magistr.zuev.company` запись в `tenants.json` должна иметь `"domain": "magistr"` 2. Выполните локальные проверки, затем отправьте изменения в `main`:
и реальный JDBC URL. Не сохраняйте значения Secret в Git или логах.
4. В Gitea Actions добавьте секреты `ZUEV_TOKEN` (доступ к registry) и ```bash
`KUBECONFIG_DATA` (kubeconfig в Base64). Push в `main` соберёт, проверит и опубликует K8S_DIR=../k8s bash scripts/check-production-secrets.sh
образы. При самом первом запуске автоматический deploy может ещё не найти Deployment — K8S_DIR=../k8s bash scripts/check-artifact-pinning.sh
возьмите два опубликованных digest из CI/registry и выполните bootstrap вручную: 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 ```bash
export BACKEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-backend@sha256:<digest>' export BACKEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-backend@sha256:<digest>'
export FRONTEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-frontend@sha256:<digest>' export FRONTEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-frontend@sha256:<digest>'
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 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
bash ../k8s/deploy.sh status
kubectl rollout status deployment/backend -n magistr --timeout=300s 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 kubectl logs -n magistr -l app=backend --tail=200
curl -fsS https://magistr.zuev.company/ curl -fsSI https://magistr.zuev.company/
``` ```
После первого входа немедленно смените/отключите все тестовые пароли из `V1__init.sql`. Проверьте в браузере login → reload → refresh → logout для каждого tenant-домена.
Следующие push в `main` CI/CD сможет развёртывать автоматически. 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. Что могу выполнить я ## 3. Что могу выполнить я

View File

@@ -731,12 +731,31 @@ CREATE TABLE IF NOT EXISTS academic_calendar_days (
CREATE INDEX IF NOT EXISTS idx_calendar_days_lookup CREATE INDEX IF NOT EXISTS idx_calendar_days_lookup
ON academic_calendar_days(calendar_id, course_number, date); 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) INSERT INTO academic_calendar_days (calendar_id, course_number, date, week_number, day_of_week, activity_type_id)
SELECT SELECT
calendar.id, calendar.id,
course.course_number, course.course_number,
days.date::DATE, 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, EXTRACT(ISODOW FROM days.date::DATE)::INT,
activity.id activity.id
FROM academic_calendars calendar 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.week_number IS 'Номер недели в учебном году';
COMMENT ON COLUMN academic_calendar_days.day_of_week IS 'День недели ISO'; COMMENT ON COLUMN academic_calendar_days.day_of_week IS 'День недели ISO';
COMMENT ON COLUMN academic_calendar_days.activity_type_id IS 'ID кода активности'; 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.calendar_id IS 'ID календарного учебного графика';
COMMENT ON COLUMN academic_calendar_subjects.semester_number IS 'Номер учебного семестра внутри графика: 1, 2, 3 ...'; COMMENT ON COLUMN academic_calendar_subjects.semester_number IS 'Номер учебного семестра внутри графика: 1, 2, 3 ...';

View File

@@ -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;

View File

@@ -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);
}
}
}
}

View File

@@ -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
) {
}
}

View File

@@ -913,21 +913,21 @@ lifecycle ресурсов, эффективная сетка целевого
2. Формат имени: `V{номер}__{описание}.sql` (напр. `V1__init.sql`, `V2__add_departments.sql`) 2. Формат имени: `V{номер}__{описание}.sql` (напр. `V1__init.sql`, `V2__add_departments.sql`)
3. **ЗАПРЕЩЕНО** изменять уже закоммиченные файлы миграций — это сломает контрольные суммы Flyway. Исключение допускается только по прямой просьбе пользователя и при полном сбросе tenant-БД. 3. **ЗАПРЕЩЕНО** изменять уже закоммиченные файлы миграций — это сломает контрольные суммы Flyway. Исключение допускается только по прямой просьбе пользователя и при полном сбросе tenant-БД.
4. Flyway запускается **программно** при первом обращении к БД тенанта (`TenantConfigWatcher.initDatabaseForTenant()`) 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-ограничения, конкурентно безопасные триггеры и комментарии | | `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики с нумерацией недель `понедельник–воскресенье`, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии |
| `V2__align_academic_calendar_weeks_to_monday.sql` | Перенумерация сохранённых дней календарного графика по периодам `понедельник–воскресенье`, чтобы неполная первая неделя не заполнялась датами следующей недели |
### Этап разработки ### Этап разработки
По прямому решению владельца проекта прежние разработческие миграции V2–V7 были объединены По прямому решению владельца проекта разработческие миграции V2–V7 объединены в baseline
в baseline `V1`. Новая миграция `V2__align_academic_calendar_weeks_to_monday.sql` создана `V1`. Правильная нумерация недель календарного графика также входит непосредственно в V1.
после фиксации baseline и накатывается поверх существующих tenant-БД без изменения Перед применением этой редакции требуется полностью пустая tenant-схема.
контрольной суммы V1.
### Полный сброс БД (локально) ### Полный сброс БД (локально)

View File

@@ -325,8 +325,8 @@ kubectl auth can-i update secret/tenants-secret \
--as="system:serviceaccount:magistr:${BACKEND_SERVICE_ACCOUNT}" -n magistr --as="system:serviceaccount:magistr:${BACKEND_SERVICE_ACCOUNT}" -n magistr
``` ```
Production rollout и проверка реальных pod являются внешними операциями и без отдельного Production rollout и проверка реальных pod являются внешними операциями и выполняются из
разрешения из этой рабочей копии не выполнялись. этой рабочей копии только по отдельному разрешению владельца.
### Ручное обновление backend и frontend ### Ручное обновление backend и frontend
@@ -368,12 +368,16 @@ Deployment-файлов запрещено.
jobs также возвращают registry digest каждого образа. jobs также возвращают registry digest каждого образа.
4. Отдельный обязательный job сканирует опубликованные digests закреплённым Trivy `0.63.0` и 4. Отдельный обязательный job сканирует опубликованные digests закреплённым Trivy `0.63.0` и
блокирует deploy при исправимых `HIGH`/`CRITICAL` уязвимостях. блокирует deploy при исправимых `HIGH`/`CRITICAL` уязвимостях.
5. Deploy job устанавливает фиксированный `kubectl v1.33.12` только после SHA-256 проверки. 5. Push в `main` выполняет проверки, сборку, публикацию и scan, но не меняет production.
Java Agent также имеет точную версию и checksum. Deploy job запускается только вручную через `workflow_dispatch` и устанавливает
фиксированный `kubectl v1.33.12` после SHA-256 проверки. Java Agent также имеет точную
версию и checksum.
6. `scripts/deploy-images.sh` принимает только `image@sha256:...`, сохраняет предыдущие 6. `scripts/deploy-images.sh` принимает только `image@sha256:...`, сохраняет предыдущие
ссылки, применяет оба digest и ждёт rollout. При отказе автоматически возвращает оба ссылки, применяет оба 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 `scripts/check-artifact-pinning.sh` проверяет Dockerfile, Compose, Actions, checksum и CI
gates. При передаче `K8S_DIR=../k8s` он дополнительно требует digest у каждого production gates. При передаче `K8S_DIR=../k8s` он дополнительно требует digest у каждого production

View File

@@ -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": { "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": { "backend_src_main_java_com_magistr_app_config_auth_authorizationinterceptor_authorizationinterceptor": {
"code_fingerprint": "70b01b3fa22e058ee997250c52cc0affee54ab37aa5f8cc7eb74411487f1bbbf", "code_fingerprint": "70b01b3fa22e058ee997250c52cc0affee54ab37aa5f8cc7eb74411487f1bbbf",
"label": "AuthorizationInterceptor", "label": "AuthorizationInterceptor",
@@ -22,7 +43,7 @@
"q": "Почему RequireRoles связывает множество контроллеров, моделей и сервисов как cross-community bridge?" "q": "Почему RequireRoles связывает множество контроллеров, моделей и сервисов как cross-community bridge?"
} }
], ],
"score": 2.185520319, "score": 1.861135007,
"source_file": "backend/src/main/java/com/magistr/app/config/auth/AuthorizationInterceptor.java", "source_file": "backend/src/main/java/com/magistr/app/config/auth/AuthorizationInterceptor.java",
"status": "preferred", "status": "preferred",
"uses": 3 "uses": 3
@@ -38,7 +59,7 @@
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
} }
], ],
"score": 0.776480146, "score": 0.661231273,
"source_file": "backend/src/main/java/com/magistr/app/config/auth/JwtTokenService.java", "source_file": "backend/src/main/java/com/magistr/app/config/auth/JwtTokenService.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -54,7 +75,7 @@
"q": "Почему RequireRoles связывает множество контроллеров, моделей и сервисов как cross-community bridge?" "q": "Почему RequireRoles связывает множество контроллеров, моделей и сервисов как cross-community bridge?"
} }
], ],
"score": 0.653312094, "score": 0.556344408,
"source_file": "backend/src/main/java/com/magistr/app/config/auth/RequireRoles.java", "source_file": "backend/src/main/java/com/magistr/app/config/auth/RequireRoles.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -70,7 +91,7 @@
"q": "Как безопасно обновлять tenant DataSource без потери рабочего подключения?" "q": "Как безопасно обновлять tenant DataSource без потери рабочего подключения?"
} }
], ],
"score": 0.794603611, "score": 0.676664767,
"source_file": "backend/src/main/java/com/magistr/app/config/DataInitializer.java", "source_file": "backend/src/main/java/com/magistr/app/config/DataInitializer.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -86,7 +107,7 @@
"q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'" "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", "source_file": "backend/src/main/java/com/magistr/app/config/tenant/ConfigMapUpdater.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -102,7 +123,7 @@
"q": "Как безопасно заменить tenant DataSource без потери рабочего подключения?" "q": "Как безопасно заменить tenant DataSource без потери рабочего подключения?"
} }
], ],
"score": 0.794468884, "score": 0.676550037,
"source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantConfig.java", "source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantConfig.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -128,7 +149,7 @@
"q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'" "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", "source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantConfigWatcher.java",
"status": "preferred", "status": "preferred",
"uses": 3 "uses": 3
@@ -154,7 +175,7 @@
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
} }
], ],
"score": 2.365552641, "score": 2.014446076,
"source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java", "source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java",
"status": "preferred", "status": "preferred",
"uses": 3 "uses": 3
@@ -170,7 +191,7 @@
"q": "Почему в выборе аудитории показаны корпус и этаж?" "q": "Почему в выборе аудитории показаны корпус и этаж?"
} }
], ],
"score": 0.976851438, "score": 0.831862505,
"source_file": "backend/src/main/java/com/magistr/app/controller/ClassroomController.java", "source_file": "backend/src/main/java/com/magistr/app/controller/ClassroomController.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -186,7 +207,7 @@
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
} }
], ],
"score": 0.776480146, "score": 0.661231273,
"source_file": "backend/src/main/java/com/magistr/app/controller/DatabaseController.java", "source_file": "backend/src/main/java/com/magistr/app/controller/DatabaseController.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -207,7 +228,7 @@
"q": "Как безопасно заменить tenant DataSource без потери рабочего подключения?" "q": "Как безопасно заменить tenant DataSource без потери рабочего подключения?"
} }
], ],
"score": 1.589072495, "score": 1.353214804,
"source_file": "backend/src/main/java/com/magistr/app/controller/DatabaseController.java", "source_file": "backend/src/main/java/com/magistr/app/controller/DatabaseController.java",
"status": "preferred", "status": "preferred",
"uses": 2 "uses": 2
@@ -223,7 +244,7 @@
"q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'" "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", "source_file": "backend/src/main/java/com/magistr/app/controller/GlobalExceptionHandler.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -244,7 +265,7 @@
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
} }
], ],
"score": 1.752429602, "score": 1.492325672,
"source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleController.java", "source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleController.java",
"status": "preferred", "status": "preferred",
"uses": 2 "uses": 2
@@ -260,7 +281,7 @@
"q": "Как безопасно валидировать и сохранять точечные изменения расписания?" "q": "Как безопасно валидировать и сохранять точечные изменения расписания?"
} }
], ],
"score": 0.78890947, "score": 0.671815777,
"source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleOverrideController.java", "source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleOverrideController.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -281,11 +302,27 @@
"q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'" "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", "source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleRuleAdminController.java",
"status": "preferred", "status": "preferred",
"uses": 2 "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": { "backend_src_main_java_com_magistr_app_model_classroom_classroom": {
"code_fingerprint": "5614d83b8b7e20fd2b76271b28a8ef11ffc137808ec2d7ed7cd23e42e0ab93ec", "code_fingerprint": "5614d83b8b7e20fd2b76271b28a8ef11ffc137808ec2d7ed7cd23e42e0ab93ec",
"label": "Classroom", "label": "Classroom",
@@ -297,11 +334,27 @@
"q": "Почему в выборе аудитории показаны корпус и этаж?" "q": "Почему в выборе аудитории показаны корпус и этаж?"
} }
], ],
"score": 0.976851438, "score": 0.831862505,
"source_file": "backend/src/main/java/com/magistr/app/model/Classroom.java", "source_file": "backend/src/main/java/com/magistr/app/model/Classroom.java",
"status": "tentative", "status": "tentative",
"uses": 1 "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": { "backend_src_main_java_com_magistr_app_model_schedulerule_schedulerule": {
"code_fingerprint": "692d61c1bfd5b46f5865174dde8cf2d962d2ccd124b5e4320e2ba8535bb29605", "code_fingerprint": "692d61c1bfd5b46f5865174dde8cf2d962d2ccd124b5e4320e2ba8535bb29605",
"label": "ScheduleRule", "label": "ScheduleRule",
@@ -318,7 +371,7 @@
"q": "Корректны ли inferred-связи вокруг ScheduleRule?" "q": "Корректны ли inferred-связи вокруг ScheduleRule?"
} }
], ],
"score": 1.448240567, "score": 1.23328582,
"source_file": "backend/src/main/java/com/magistr/app/model/ScheduleRule.java", "source_file": "backend/src/main/java/com/magistr/app/model/ScheduleRule.java",
"status": "preferred", "status": "preferred",
"uses": 2 "uses": 2
@@ -334,7 +387,7 @@
"q": "Какие связи затрагивает полная валидация правил расписания в исправлении №7?" "q": "Какие связи затрагивает полная валидация правил расписания в исправлении №7?"
} }
], ],
"score": 0.794199275, "score": 0.676320444,
"source_file": "backend/src/main/java/com/magistr/app/model/ScheduleRuleSlot.java", "source_file": "backend/src/main/java/com/magistr/app/model/ScheduleRuleSlot.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -342,16 +395,37 @@
"backend_src_main_java_com_magistr_app_model_semester_semester": { "backend_src_main_java_com_magistr_app_model_semester_semester": {
"code_fingerprint": "52546b552fff16ab841a63ea80fb806f6cc32edc0615ec53b3c002543d692f24", "code_fingerprint": "52546b552fff16ab841a63ea80fb806f6cc32edc0615ec53b3c002543d692f24",
"label": "Semester", "label": "Semester",
"last": "2026-07-21T19:52:13.631700+00:00", "last": "2026-07-22T21:09:57.015227+00:00",
"provenance": [ "provenance": [
{
"date": "2026-07-22T21:09:57.015227+00:00",
"outcome": "useful",
"q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу"
},
{ {
"date": "2026-07-21T19:52:13.631700+00:00", "date": "2026-07-21T19:52:13.631700+00:00",
"outcome": "useful", "outcome": "useful",
"q": "в дополнительные фильры просмотра расписаний нужно добавить выбор семестра" "q": "в дополнительные фильры просмотра расписаний нужно добавить выбор семестра"
} }
], ],
"score": 0.975949456, "score": 1.682676032,
"source_file": "backend/src/main/java/com/magistr/app/model/Semester.java", "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", "status": "tentative",
"uses": 1 "uses": 1
}, },
@@ -366,7 +440,7 @@
"q": "Какие связи затрагивает полная валидация правил расписания в исправлении №7?" "q": "Какие связи затрагивает полная валидация правил расписания в исправлении №7?"
} }
], ],
"score": 0.794199275, "score": 0.676320444,
"source_file": "backend/src/main/java/com/magistr/app/repository/SemesterRepository.java", "source_file": "backend/src/main/java/com/magistr/app/repository/SemesterRepository.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -387,7 +461,7 @@
"q": "Как безопасно валидировать и сохранять точечные изменения расписания?" "q": "Как безопасно валидировать и сохранять точечные изменения расписания?"
} }
], ],
"score": 1.764858926, "score": 1.502910177,
"source_file": "backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java", "source_file": "backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java",
"status": "preferred", "status": "preferred",
"uses": 2 "uses": 2
@@ -403,7 +477,7 @@
"q": "Корректны ли inferred-связи вокруг ScheduleRule?" "q": "Корректны ли inferred-связи вокруг ScheduleRule?"
} }
], ],
"score": 0.654041293, "score": 0.556965376,
"source_file": "backend/src/test/java/com/magistr/app/model/ScheduleRuleTest.java", "source_file": "backend/src/test/java/com/magistr/app/model/ScheduleRuleTest.java",
"status": "tentative", "status": "tentative",
"uses": 1 "uses": 1
@@ -419,10 +493,95 @@
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?" "q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
} }
], ],
"score": 0.776480146, "score": 0.661231273,
"source_file": "compose.yaml", "source_file": "compose.yaml",
"status": "tentative", "status": "tentative",
"uses": 1 "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 "version": 1

View File

@@ -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

View File

@@ -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

View File

@@ -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-секрета

View File

@@ -1,30 +1,39 @@
# Lessons # 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 ## Summary
- 10 useful · 1 dead ends · 0 corrected · 0 unmarked - 13 useful · 1 dead ends · 0 corrected · 0 unmarked
## Lessons ## Lessons
**Preferred sources** — corroborated by ≥2 useful results; start here. **Preferred sources** — corroborated by ≥2 useful results; start here.
- `ScheduleGeneratorService` (3× useful)
- `TenantRoutingDataSource` (3× useful) - `TenantRoutingDataSource` (3× useful)
- `Production-инфраструктура Kubernetes` (2× useful)
- `Workflow сборки и публикации Docker-образов` (2× useful)
- `TenantConfigWatcher` (3× useful) - `TenantConfigWatcher` (3× useful)
- `AuthorizationInterceptor` (3× useful) - `AuthorizationInterceptor` (3× useful)
- `Semester` (2× useful)
- `ScheduleQueryService` (2× useful) - `ScheduleQueryService` (2× useful)
- `ScheduleController.java` (2× useful) - `ScheduleController.java` (2× useful)
- `DatabaseController` (2× useful) - `DatabaseController` (2× useful)
- `ScheduleGeneratorService` (2× useful)
- `ScheduleRuleAdminController` (2× useful) - `ScheduleRuleAdminController` (2× useful)
- `ScheduleRule` (2× useful) - `ScheduleRule` (2× useful)
**Tentative** — useful in fewer than 2 results; verify before relying. **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) - `Classroom` (1× useful)
- `ClassroomController.java` (1× useful) - `ClassroomController.java` (1× useful)
- `Semester` (1× useful)
- `DataInitializer` (1× useful) - `DataInitializer` (1× useful)
- `TenantConfig` (1× useful) - `TenantConfig` (1× useful)
- `ScheduleRuleSlot` (1× useful) - `ScheduleRuleSlot` (1× useful)

View File

@@ -85,6 +85,10 @@ check_workflow_gates() {
if ! grep -Eq '^ needs: \[build-and-push-backend, build-and-push-frontend, security-scan\]$' "$WORKFLOW"; then if ! grep -Eq '^ needs: \[build-and-push-backend, build-and-push-frontend, security-scan\]$' "$WORKFLOW"; then
fail "deploy обязан зависеть от успешного security-scan" fail "deploy обязан зависеть от успешного security-scan"
fi 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" \ if ! grep -Eq '^ KUBECTL_VERSION: v[0-9]+\.[0-9]+\.[0-9]+$' "$WORKFLOW" \
|| ! grep -Eq '^ KUBECTL_SHA256: [0-9a-f]{64}$' "$WORKFLOW" \ || ! grep -Eq '^ KUBECTL_SHA256: [0-9a-f]{64}$' "$WORKFLOW" \

View File

@@ -29,4 +29,13 @@ if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then
exit 1 exit 1
fi 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' printf 'Проверка закрепления артефактов завершена успешно\n'