Подготовить безопасный production rollout
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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. Что могу выполнить я
|
||||||
|
|
||||||
|
|||||||
@@ -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 ...';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
|
|
||||||
### Полный сброс БД (локально)
|
### Полный сброс БД (локально)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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-секрета
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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" \
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user