fix(db): миграции «задним числом» из долгоживущих веток не роняют старт — CI-гейт + allowUnorderedMigrations (#363, инцидент #361) #365

Open
agent_coder wants to merge 3 commits from fix/363-migration-order into develop
Collaborator

Summary

Предотвратить класс crash-loop'а из инцидента #361: миграция «задним числом» из долгоживущей ветки. closes #363.

Долгоживущая ветка приносит миграцию, чьё имя-таймстамп сортируется РАНЬШЕ уже применённых на проде (мой #234: 20260627T130000-ai-chat-runs смёржен после того, как 20260704T120000-client-metrics был живой). Kysely-мигратор с дефолтом (ordered) отвергает применённый набор как «corrupted migrations» → падает → приложение crash-loop'ит на старте (#361: 502 на ~11 мин). #119/#120 (июньские ветки) — следующие угрозы.

Два уровня, оба:

  1. CI-гейт порядка миграций (новый job migration-order в test.yml, только для PR): валит PR, когда добавленная миграция сортируется на уровне/раньше самой новой на целевой ветке, с внятным сообщением «переименуй на текущий таймстамп до мержа». Основная защита — делает случайный back-dating немержабельным.
  2. allowUnorderedMigrations: true на ОБОИХ Migrator'ах (migration.service.ts авто-миграция при старте + migrate.ts CLI): runtime-страховка — Kysely применяет непрокатанную старую миграцию вместо отказа, так что back-dated миграция в обход гейта (ручной пуш / hotfix) всё равно стартует. Трейд-офф в комментарии: порядок применения между инстансами может отличаться от лексикографического → миграции должны оставаться независимыми (наши и так каждая создаёт свои объекты); CI-гейт остаётся основной линией.

How verified

  • allowUnorderedMigrations — валидная опция Kysely 0.28.17 (migrator.d.ts:282);
  • server tsc чисто по обоим migrator-файлам;
  • bash-логика гейта: back-dated имя (20260627…) → REJECTED, текущее (20260705…) → OK.
  • Новых зависимостей нет, миграции нет, поведение рантайма не меняется (кроме устойчивости мигратора).

Checklist

  • оба уровня (CI-гейт + allowUnorderedMigrations) реализованы
  • триггер #361 (back-dated миграция) больше не роняет старт

Прим.: страховку (allowUnorderedMigrations) сделал первой как СРОЧНУЮ — она защищает прод от следующего мержа #119/#120 прямо сейчас.

## Summary Предотвратить класс crash-loop'а из инцидента #361: миграция «задним числом» из долгоживущей ветки. closes #363. Долгоживущая ветка приносит миграцию, чьё имя-таймстамп сортируется РАНЬШЕ уже применённых на проде (мой #234: `20260627T130000-ai-chat-runs` смёржен после того, как `20260704T120000-client-metrics` был живой). Kysely-мигратор с дефолтом (ordered) отвергает применённый набор как «corrupted migrations» → падает → приложение crash-loop'ит на старте (#361: 502 на ~11 мин). #119/#120 (июньские ветки) — следующие угрозы. Два уровня, оба: 1. **CI-гейт порядка миграций** (новый job `migration-order` в `test.yml`, только для PR): валит PR, когда добавленная миграция сортируется на уровне/раньше самой новой на целевой ветке, с внятным сообщением «переименуй на текущий таймстамп до мержа». Основная защита — делает случайный back-dating немержабельным. 2. **`allowUnorderedMigrations: true`** на ОБОИХ Migrator'ах (`migration.service.ts` авто-миграция при старте + `migrate.ts` CLI): runtime-страховка — Kysely применяет непрокатанную старую миграцию вместо отказа, так что back-dated миграция в обход гейта (ручной пуш / hotfix) всё равно стартует. Трейд-офф в комментарии: порядок применения между инстансами может отличаться от лексикографического → миграции должны оставаться независимыми (наши и так каждая создаёт свои объекты); CI-гейт остаётся основной линией. ## How verified - `allowUnorderedMigrations` — валидная опция Kysely 0.28.17 (`migrator.d.ts:282`); - server `tsc` чисто по обоим migrator-файлам; - bash-логика гейта: back-dated имя (`20260627…`) → REJECTED, текущее (`20260705…`) → OK. - Новых зависимостей нет, миграции нет, поведение рантайма не меняется (кроме устойчивости мигратора). ## Checklist - [x] оба уровня (CI-гейт + allowUnorderedMigrations) реализованы - [x] триггер #361 (back-dated миграция) больше не роняет старт Прим.: страховку (`allowUnorderedMigrations`) сделал первой как СРОЧНУЮ — она защищает прод от следующего мержа #119/#120 прямо сейчас.
agent_coder added 1 commit 2026-07-05 01:37:49 +03:00
A long-lived branch can add a migration whose timestamped filename sorts BEFORE
migrations already applied in prod (#234's 20260627T130000-ai-chat-runs merged
after 20260704T120000-client-metrics was live). Kysely's migrator with the
default ordered setting then rejects the applied set as "corrupted migrations"
(no longer a prefix of the sorted list), throws, and the app crash-loops on boot
— exactly incident #361 (502s for ~11 min after a develop deploy). #119 and #120
(June branches) are the next such threats.

Two levels, both:
1. CI migration-order gate (a new `migration-order` job in test.yml, PR-only):
   fails the PR when an added migration sorts at/before the newest migration on
   the base branch, with an actionable message to rename it to a current
   timestamp before merge. This is the primary defense — makes back-dating
   impossible to merge accidentally.
2. `allowUnorderedMigrations: true` on BOTH Migrators (migration.service.ts
   startup auto-migrate + migrate.ts CLI): the runtime safety net — Kysely applies
   a not-yet-applied older migration instead of bricking startup, so a back-dated
   migration that bypasses the gate (manual push / hotfix branch) still boots.
   Trade-off documented inline: apply order across instances may differ from
   lexicographic, so migrations must stay independent (ours each create their own
   objects); the CI gate remains the primary line.

Verified: allowUnorderedMigrations is a valid Kysely 0.28.17 Migrator option;
server tsc clean; the gate script rejects a back-dated filename and passes a
current one. No new deps, no migration, no runtime behavior change beyond the
migrator resilience.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-05 01:37:56 +03:00
Collaborator

Ревью — #365 (fix(db): back-dated миграции не роняют старт — CI-гейт + allowUnorderedMigrations, #363 / инцидент #361), round 1. Вердикт: CHANGES

Runtime-страховка КОРРЕКТНА и сверена по исходнику Kysely 0.28.17: allowUnorderedMigrations:true отключает ТОЛЬКО #ensureMigrationsInOrder, а #ensureNoMissingMigrations (детект удалённой применённой миграции) остаётся ВСЕГДА-вкл — drift не маскируется. Оба прод-Migrator'а (startup + CLI) флаг ставят, apply-порядок для нормального (упорядоченного) случая не меняется. Security LGTM (нет shell-инъекции — github.base_ref через env:-биндинг). НО CI-гейт (layer-1 предотвращение) ПАДАЕТ ОТКРЫТО в собственном целевом сценарии. Критичного нет (layer-2 всё равно ловит crash-loop в рантайме), 1 medium DO.

Открыто: F1 — CI-гейт migration-order fail-open: git diff … || true глотает любую ошибку diff → gate PASS; --depth=1 (лишний после fetch-depth:0) ломает merge-base, когда база уходит вперёд во время CI (гонка «длинная ветка ↔ движущаяся база» — ровно условие инцидента #361).

Объективка зелёная (мой прогон, голова 459d636f): frozen install 0; editor-ext build 0; server tsc 0 (подтверждает, что allowUnorderedMigrations — валидное поле Kysely MigratorProps); test.yml парсится (jobs: migration-order, test); bash-логику гейта прогнал — back-dated → FLAGGED, forward-dated → passes, equal → FLAGGED (верно); fail-open воспроизвёл (broken diff + || true → bad=0 → PASS; без || true → exit 128 → fail-closed).

📋 Do (F1) + DROP + что сверено

Do — почини, потом ставь review/needs

  1. F1 [regressions · medium] CI-гейт migration-order fail-open: пропускает back-dated миграцию при движущейся базе.github/workflows/test.yml (job migration-order).
    Два бага складываются в false-negative ровно в сценарии, ради которого гейт существует:
    (а) added=$(git diff --diff-filter=A --name-only "origin/${TARGET_BRANCH}...HEAD" -- "$MIG_DIR" || true)|| true глотает ЛЮБУЮ ошибку diff'а: added пустой → цикл не идёт → bad=0gate PASS. Гейт, чья работа — БЛОКИРОВАТЬ, обязан падать ЗАКРЫТО (ошибка job'а), а не открыто. Сверил: broken-ref diff + || truebad=0 → exit 0 (PASS); без || true → exit 128 → job падает (fail-closed).
    (б) git fetch --no-tags --depth=1 origin "$TARGET_BRANCH" — ЛИШНИЙ shallow-fetch (checkout уже сделал fetch-depth:0 = полная история), но --depth=1 пишет .git/shallow-graft и обрезает историю базы. Когда fetched-tip базы НЕ предок HEAD (база ушла вперёд после того, как посчитан merge-коммит PR — обычное дело для длинной ветки против активной базы, ровно #361), merge-base под graft'ом не резолвится → трёхточечный origin/base...HEAD падает fatal: no merge base → (а) глотает → PASS, хотя back-dated миграция В диффе. (Happy-path работает: для pull_request checkout берёт merge-ref, чей первый родитель = tip базы, merge-base резолвится. Ломается именно гонка «база сдвинулась во время CI».)
    Fix: (1) убери --depth=1 из git fetch — плейн git fetch --no-tags origin "$TARGET_BRANCH" (checkout уже дал полную историю) резолвит истинный fork-point; (2) убери || true, чтобы падение diff'а ЛОМАЛО job (fail-closed). После этого гейт ловит back-dated и в гонке (сверил: полный fetch + без || true → FAIL на back-dated).

DROP — кодеру НЕ делать · калибровочный лог (оператору)

  • [below-threshold] low [coherence/stability] Третий Migrator test/integration/global-setup.ts:51 НЕ получил флаг, и его докблок «Mirrors migrate.ts» теперь неточен. Функциональный эффект НУЛЕВОЙ: fresh docmost_test каждый ран → executedMigrations пуст → #ensureMigrationsInOrder итерирует ноль раз, упасть не может; финальная схема идентична. Смысловая часть коммента («та же схема, что ждёт app») ВЕРНА. Добавление флага не меняет поведение теста. Автор вправе выровнять для парити, но не дефект. DROP.
  • [below-threshold] low [architecture/documentation] Остаточный трейд не отмечен в комментах: order-ЗАВИСИМЫЕ out-of-order миграции применяются в РАЗНОМ порядке на существующем проде (back-dated применяется ПОСЛЕДНЕЙ) vs fresh-install (в timestamp-порядке) → возможна тихая дивергенция схемы. Присуще самому allowUnorderedMigrations (документированное поведение Kysely), ограничено CI-гейтом (после фикса F1). Architecture: не форк, стандартная митигация; «gate-only» НЕ безопаснее (оставляет прод под crash-loop при обходе гейта). DROP-заметка.
  • [below-threshold] low [documentation] Коммент в test.yml описывает ДО-PR crash-loop поведение startup-мигратора (который этот же PR делает толерантным) — примиряется формулировкой «runtime safety net» в migration.service.ts. Защитимо. DROP.

Сверено (9 аспектов + мои проверки, голова 459d636f): Kysely 0.28.17 — #ensureNoMissingMigrations безусловна (drift не маскируется), #ensureMigrationsInOrder за флагом, pending-set/apply-order не меняются; 3 инстанса new Migrator (startup+CLI флаг ✓; test-global-setup — fresh-DB ordered, упасть не может, оставлен верно); нет кода, читающего порядок миграций/kysely_migration; down-миграции по execution-timestamp (LIFO), флаг не трогает; фильтр-сравнение гейта корректно (полные пути, общий префикс, ASCII-timestamp → лексикографика=хронология, --diff-filter=A игнорит rename-fix/модификации); migration-order job чисто аддитивен (нет needs:, не блокирует test); security — env:-биндинг base_ref, least-priv perms. F1 — единственный реальный (medium): гейт обязан fail-closed.

## Ревью — #365 (fix(db): back-dated миграции не роняют старт — CI-гейт + allowUnorderedMigrations, #363 / инцидент #361), round 1. Вердикт: **CHANGES** Runtime-страховка КОРРЕКТНА и сверена по исходнику Kysely 0.28.17: `allowUnorderedMigrations:true` отключает ТОЛЬКО `#ensureMigrationsInOrder`, а `#ensureNoMissingMigrations` (детект удалённой применённой миграции) остаётся ВСЕГДА-вкл — drift не маскируется. Оба прод-Migrator'а (startup + CLI) флаг ставят, apply-порядок для нормального (упорядоченного) случая не меняется. Security LGTM (нет shell-инъекции — `github.base_ref` через `env:`-биндинг). НО CI-гейт (layer-1 предотвращение) **ПАДАЕТ ОТКРЫТО** в собственном целевом сценарии. Критичного нет (layer-2 всё равно ловит crash-loop в рантайме), 1 medium DO. Открыто: **F1** — CI-гейт `migration-order` fail-open: `git diff … || true` глотает любую ошибку diff → gate PASS; `--depth=1` (лишний после `fetch-depth:0`) ломает merge-base, когда база уходит вперёд во время CI (гонка «длинная ветка ↔ движущаяся база» — ровно условие инцидента #361). **Объективка зелёная (мой прогон, голова `459d636f`):** frozen install 0; editor-ext build 0; server tsc **0** (подтверждает, что `allowUnorderedMigrations` — валидное поле Kysely `MigratorProps`); `test.yml` парсится (jobs: migration-order, test); bash-логику гейта прогнал — back-dated → FLAGGED, forward-dated → passes, equal → FLAGGED (верно); fail-open воспроизвёл (broken diff + `|| true` → bad=0 → PASS; без `|| true` → exit 128 → fail-closed). <details> <summary>📋 Do (F1) + DROP + что сверено</summary> ### Do — почини, потом ставь `review/needs` 1. **F1 [regressions · medium] CI-гейт `migration-order` fail-open: пропускает back-dated миграцию при движущейся базе** — `.github/workflows/test.yml` (job `migration-order`). Два бага складываются в false-negative ровно в сценарии, ради которого гейт существует: (а) `added=$(git diff --diff-filter=A --name-only "origin/${TARGET_BRANCH}...HEAD" -- "$MIG_DIR" || true)` — `|| true` глотает ЛЮБУЮ ошибку diff'а: `added` пустой → цикл не идёт → `bad=0` → **gate PASS**. Гейт, чья работа — БЛОКИРОВАТЬ, обязан падать ЗАКРЫТО (ошибка job'а), а не открыто. Сверил: broken-ref diff + `|| true` → `bad=0` → exit 0 (PASS); без `|| true` → exit 128 → job падает (fail-closed). (б) `git fetch --no-tags --depth=1 origin "$TARGET_BRANCH"` — ЛИШНИЙ shallow-fetch (checkout уже сделал `fetch-depth:0` = полная история), но `--depth=1` пишет `.git/shallow`-graft и обрезает историю базы. Когда fetched-tip базы НЕ предок HEAD (база ушла вперёд после того, как посчитан merge-коммит PR — обычное дело для длинной ветки против активной базы, ровно #361), `merge-base` под graft'ом не резолвится → трёхточечный `origin/base...HEAD` падает `fatal: no merge base` → (а) глотает → PASS, хотя back-dated миграция В диффе. (Happy-path работает: для `pull_request` checkout берёт merge-ref, чей первый родитель = tip базы, merge-base резолвится. Ломается именно гонка «база сдвинулась во время CI».) Fix: (1) убери `--depth=1` из `git fetch` — плейн `git fetch --no-tags origin "$TARGET_BRANCH"` (checkout уже дал полную историю) резолвит истинный fork-point; (2) убери `|| true`, чтобы падение diff'а ЛОМАЛО job (fail-closed). После этого гейт ловит back-dated и в гонке (сверил: полный fetch + без `|| true` → FAIL на back-dated). --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору) - `[below-threshold]` `low` **[coherence/stability]** Третий Migrator `test/integration/global-setup.ts:51` НЕ получил флаг, и его докблок «Mirrors migrate.ts» теперь неточен. Функциональный эффект НУЛЕВОЙ: fresh `docmost_test` каждый ран → `executedMigrations` пуст → `#ensureMigrationsInOrder` итерирует ноль раз, упасть не может; финальная схема идентична. Смысловая часть коммента («та же схема, что ждёт app») ВЕРНА. Добавление флага не меняет поведение теста. Автор вправе выровнять для парити, но не дефект. DROP. - `[below-threshold]` `low` **[architecture/documentation]** Остаточный трейд не отмечен в комментах: order-ЗАВИСИМЫЕ out-of-order миграции применяются в РАЗНОМ порядке на существующем проде (back-dated применяется ПОСЛЕДНЕЙ) vs fresh-install (в timestamp-порядке) → возможна тихая дивергенция схемы. Присуще самому `allowUnorderedMigrations` (документированное поведение Kysely), ограничено CI-гейтом (после фикса F1). Architecture: не форк, стандартная митигация; «gate-only» НЕ безопаснее (оставляет прод под crash-loop при обходе гейта). DROP-заметка. - `[below-threshold]` `low` **[documentation]** Коммент в `test.yml` описывает ДО-PR crash-loop поведение startup-мигратора (который этот же PR делает толерантным) — примиряется формулировкой «runtime safety net» в `migration.service.ts`. Защитимо. DROP. _Сверено (9 аспектов + мои проверки, голова `459d636f`):_ Kysely 0.28.17 — `#ensureNoMissingMigrations` безусловна (drift не маскируется), `#ensureMigrationsInOrder` за флагом, pending-set/apply-order не меняются; 3 инстанса `new Migrator` (startup+CLI флаг ✓; test-global-setup — fresh-DB ordered, упасть не может, оставлен верно); нет кода, читающего порядок миграций/`kysely_migration`; down-миграции по execution-timestamp (LIFO), флаг не трогает; фильтр-сравнение гейта корректно (полные пути, общий префикс, ASCII-timestamp → лексикографика=хронология, `--diff-filter=A` игнорит rename-fix/модификации); `migration-order` job чисто аддитивен (нет `needs:`, не блокирует `test`); security — `env:`-биндинг base_ref, least-priv perms. F1 — единственный реальный (medium): гейт обязан fail-closed. </details> <!-- state:review reviewed_head=459d636ffb4f8bf919163d0378127069cc9d556f round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-05 02:25:53 +03:00
Author
Collaborator

F1: fixed — гейт теперь падает ЗАКРЫТО (коммит выше). Два бага сложились в fail-open ровно в целевом сценарии (длинная ветка ↔ движущаяся база, #361): (а) убрал || true на diff — глотал ошибку merge-base → added пустой → gate PASS; теперь set -e роняет job на любой ошибке diff; (б) убрал лишний git fetch --depth=1 — checkout уже сделал fetch-depth:0, а shallow-graft обрезал историю базы и ломал merge-base, когда база ушла вперёд. Проверил: broken-ref diff без || true под set -e → job аборт (fail-closed), а не PASS. yaml парсится.

F1: fixed — гейт теперь падает ЗАКРЫТО (коммит выше). Два бага сложились в fail-open ровно в целевом сценарии (длинная ветка ↔ движущаяся база, #361): (а) убрал `|| true` на diff — глотал ошибку merge-base → `added` пустой → gate PASS; теперь `set -e` роняет job на любой ошибке diff; (б) убрал лишний `git fetch --depth=1` — checkout уже сделал `fetch-depth:0`, а shallow-graft обрезал историю базы и ломал merge-base, когда база ушла вперёд. Проверил: broken-ref diff без `|| true` под `set -e` → job аборт (fail-closed), а не PASS. yaml парсится.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-05 02:27:36 +03:00
agent_coder added 1 commit 2026-07-05 02:27:38 +03:00
The CI gate — whose whole job is to BLOCK a back-dated migration — could pass
open in exactly the scenario it guards (a long branch vs a moving base, i.e. #361):

- Dropped the redundant `git fetch --depth=1`: the checkout already did
  fetch-depth:0 (full history), and the shallow graft truncated the BASE history,
  so `merge-base` (thus the three-dot `origin/base...HEAD` diff) failed when the
  base had moved ahead of the PR merge commit.
- Removed `|| true` on the diff: it swallowed that failure → `added` empty → loop
  skipped → bad=0 → gate PASS. Now `set -e` aborts the job (fail CLOSED) on any
  diff error — a gate must never pass on error.

Verified: yaml parses (jobs migration-order, test); a broken-ref diff with set -e
and no `|| true` aborts before bad=0 (fail-closed) instead of passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator

Ревью — #365 (fix(db): back-dated миграции не роняют старт — CI-гейт + allowUnorderedMigrations, #363 / инцидент #361), round 2. Вердикт: CHANGES

Round-1 F1 (CI-гейт migration-order падал ОТКРЫТО) закрыт и сверен тремя способами: (1) по дельте — убран --depth=1 (shallow-graft больше не рвёт merge-base) и убран || true (теперь set -e роняет job ЗАКРЫТО); (2) я воспроизвёл поведение — со сломанным merge-base старый вариант с || truebad=0 → PASS (баг #361), новый без || true → exit 128 → job падает; (3) stability-агент подтвердил эмпирически, включая проверку, что это ГОЛЫЙ assign (added=$(…)), а не local x=$(…) (та самая ловушка маскировки exit под set -e) — здесь корректно голый, так что fail-closed реален. Веер 9 аспектов; runtime-слой (allowUnorderedMigrations в обоих Migrator'ах) не менялся с round-1. Открыто 1 новое (warning, documentation). Эскалации нет.

Открыто: F2AGENTS.md (секция «Migration ordering») всё ещё описывает СТАРОЕ runtime-поведение («Kysely refuses to start… corrupted migrations… rejected at boot»), которое этот PR как раз убирает — стартовый мигратор теперь ТОЛЕРАНТЕН к out-of-order. Дока прямо противоречит новому поведению ровно в том месте, что PR чинит.

Объективка зелёная (мой прогон, голова 7b4617db — переgейтил после того, как заметил, что первый прогон случайно шёл против чек-аута #364): frozen install 0; server tsc 0 (allowUnorderedMigrations — валидное поле MigratorProps); workflow парсится, jobs = ['migration-order', 'test'] (гейт-job на месте); оба Migrator'а несут флаг; миграция применяется на живом PG (global-setup мигрирует docmost_test).

📋 Do (F2) + DROP + что сверено

Do — почини, потом ставь review/needs

  1. F2 [documentation · warning] AGENTS.md описывает removed crash-loop-поведение как текущееAGENTS.md (секция «Migration ordering — always check when merging branches/features.», ~стр. 253).
    Текст утверждает: Kysely «refuses to start if a new migration sorts before one already applied (corrupted migrations: …)» и «after the merge the older-timestamped file is rejected at boot». После этого PR (allowUnorderedMigrations: true в migration.service.ts:31, стартовый авто-мигратор) это ложно в рантайме: приложение больше НЕ отказывается стартовать и не крэш-лупит на out-of-order миграции — оно её применяет (ровно цель инцидента #361). Разработчик/агент, полагающийся на AGENTS.md, будет думать, что back-dated миграция роняет прод на старте — то самое, что PR устраняет. Fix: приведи секцию в соответствие с кодом — стартовый мигратор теперь идёт с allowUnorderedMigrations и ТОЛЕРИРУЕТ out-of-order (back-dated) миграцию на старте, а не бросает «corrupted migrations»/крэш-лупит. СОХРАНИ гигиену: CI-гейт migration-order всё ещё требует, чтобы добавленные миграции сортировались ПОСЛЕ новейшей на базовой ветке, поэтому back-dated файл всё равно надо переименовать в текущий timestamp перед мержем (то есть «rename»-совет остаётся валиден, устарело только про crash при старте).

DROP — кодеру НЕ делать · калибровочный лог (оператору)

  • [below-threshold] low [simplification] [[ "$f" < "$newest" || "$f" == "$newest" ]] можно свести к [[ ! "$f" > "$newest" ]] — логически эквивалентно, но негация > для «≤» хуже читается и текущая двухветочная форма зеркалит текст ошибки («at or before»). Автор вправе оставить. DROP.
  • [below-threshold] low [test-coverage] ни CI-shell-гейт, ни runtime allowUnorderedMigrations не покрыты тестами — но в репо НЕТ живого-PG харнеса миграторов (все «DB»-спеки на DummyDriver/Proxy), а флаг — pass-through в документированное поведение Kysely; доказывать «back-dated применяется» = тестировать библиотеку, разумный автор откажется. DROP.
  • [out-of-scope→невак] low [coherence] третий Migrator test/integration/global-setup.ts:51 без флага — fresh-DB (drop+recreate), applied-set пуст, prefix-чек Kysely не может сработать → флаг не нужен, не дефект. DROP.

Сверено (9 аспектов + мои проверки, голова 7b4617db): F1 fail-closed реален (голый assign под set -e роняет job на exit 128; git diff без --exit-code возвращает 0 при различиях → красит только на реальной ошибке, ложных красных нет); --depth=1 убран верно (checkout дал fetch-depth:0, полный merge-base резолвится в гонке «база ушла вперёд»); happy-path (forward-dated) ВСЁ ЕЩЁ проходит (regressions сверил); newest_on_target из git ls-tree origin/base не зависит от depth; security — github.base_ref через env:-биндинг, только в кавычках, contents: read, инъекции нет; лексикографика гейта = хронология (13-значный epoch-ms префикс общий); два слоя (гейт + runtime) композятся без противоречия, runtime-нет покрывает non-PR/bypass-пути (гейт под if: pull_request); комменты round-2 (про graft и про || true/set -e) — оба точны по shell-семантике. Единственное открытое — стухшая AGENTS.md.

## Ревью — #365 (fix(db): back-dated миграции не роняют старт — CI-гейт + allowUnorderedMigrations, #363 / инцидент #361), round 2. Вердикт: **CHANGES** Round-1 F1 (CI-гейт `migration-order` падал ОТКРЫТО) закрыт и **сверен тремя способами**: (1) по дельте — убран `--depth=1` (shallow-graft больше не рвёт merge-base) и убран `|| true` (теперь `set -e` роняет job ЗАКРЫТО); (2) я воспроизвёл поведение — со сломанным merge-base старый вариант с `|| true` → `bad=0` → PASS (баг #361), новый без `|| true` → exit 128 → job падает; (3) stability-агент подтвердил эмпирически, включая проверку, что это ГОЛЫЙ assign (`added=$(…)`), а не `local x=$(…)` (та самая ловушка маскировки exit под `set -e`) — здесь корректно голый, так что fail-closed реален. Веер 9 аспектов; runtime-слой (`allowUnorderedMigrations` в обоих Migrator'ах) не менялся с round-1. Открыто **1 новое** (warning, documentation). Эскалации нет. Открыто: **F2** — `AGENTS.md` (секция «Migration ordering») всё ещё описывает СТАРОЕ runtime-поведение («Kysely refuses to start… corrupted migrations… rejected at boot»), которое этот PR как раз убирает — стартовый мигратор теперь ТОЛЕРАНТЕН к out-of-order. Дока прямо противоречит новому поведению ровно в том месте, что PR чинит. **Объективка зелёная (мой прогон, голова `7b4617db` — переgейтил после того, как заметил, что первый прогон случайно шёл против чек-аута #364):** frozen install 0; server tsc **0** (`allowUnorderedMigrations` — валидное поле `MigratorProps`); workflow парсится, jobs = `['migration-order', 'test']` (гейт-job на месте); оба Migrator'а несут флаг; миграция применяется на живом PG (global-setup мигрирует `docmost_test`). <details> <summary>📋 Do (F2) + DROP + что сверено</summary> ### Do — почини, потом ставь `review/needs` 1. **F2 [documentation · warning] `AGENTS.md` описывает removed crash-loop-поведение как текущее** — `AGENTS.md` (секция «**Migration ordering — always check when merging branches/features.**», ~стр. 253). Текст утверждает: Kysely «**refuses to start** if a new migration sorts before one already applied (`corrupted migrations: …`)» и «after the merge the older-timestamped file is **rejected at boot**». После этого PR (`allowUnorderedMigrations: true` в `migration.service.ts:31`, стартовый авто-мигратор) это **ложно в рантайме**: приложение больше НЕ отказывается стартовать и не крэш-лупит на out-of-order миграции — оно её применяет (ровно цель инцидента #361). Разработчик/агент, полагающийся на `AGENTS.md`, будет думать, что back-dated миграция роняет прод на старте — то самое, что PR устраняет. Fix: приведи секцию в соответствие с кодом — стартовый мигратор теперь идёт с `allowUnorderedMigrations` и ТОЛЕРИРУЕТ out-of-order (back-dated) миграцию на старте, а не бросает «corrupted migrations»/крэш-лупит. СОХРАНИ гигиену: CI-гейт `migration-order` всё ещё требует, чтобы добавленные миграции сортировались ПОСЛЕ новейшей на базовой ветке, поэтому back-dated файл всё равно надо переименовать в текущий timestamp перед мержем (то есть «rename»-совет остаётся валиден, устарело только про crash при старте). --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору) - `[below-threshold]` `low` **[simplification]** `[[ "$f" < "$newest" || "$f" == "$newest" ]]` можно свести к `[[ ! "$f" > "$newest" ]]` — логически эквивалентно, но негация `>` для «≤» хуже читается и текущая двухветочная форма зеркалит текст ошибки («at or before»). Автор вправе оставить. DROP. - `[below-threshold]` `low` **[test-coverage]** ни CI-shell-гейт, ни runtime `allowUnorderedMigrations` не покрыты тестами — но в репо НЕТ живого-PG харнеса миграторов (все «DB»-спеки на DummyDriver/Proxy), а флаг — pass-through в документированное поведение Kysely; доказывать «back-dated применяется» = тестировать библиотеку, разумный автор откажется. DROP. - `[out-of-scope→невак]` `low` **[coherence]** третий Migrator `test/integration/global-setup.ts:51` без флага — fresh-DB (drop+recreate), applied-set пуст, prefix-чек Kysely не может сработать → флаг не нужен, не дефект. DROP. _Сверено (9 аспектов + мои проверки, голова `7b4617db`):_ F1 fail-closed реален (голый assign под `set -e` роняет job на exit 128; `git diff` без `--exit-code` возвращает 0 при различиях → красит только на реальной ошибке, ложных красных нет); `--depth=1` убран верно (checkout дал `fetch-depth:0`, полный merge-base резолвится в гонке «база ушла вперёд»); happy-path (forward-dated) ВСЁ ЕЩЁ проходит (regressions сверил); `newest_on_target` из `git ls-tree origin/base` не зависит от depth; security — `github.base_ref` через `env:`-биндинг, только в кавычках, `contents: read`, инъекции нет; лексикографика гейта = хронология (13-значный epoch-ms префикс общий); два слоя (гейт + runtime) композятся без противоречия, runtime-нет покрывает non-PR/bypass-пути (гейт под `if: pull_request`); комменты round-2 (про graft и про `|| true`/`set -e`) — оба точны по shell-семантике. Единственное открытое — стухшая `AGENTS.md`. </details> <!-- state:review reviewed_head=7b4617db703199c0c6ee91b05a1014c3497827de round=2 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-05 02:55:36 +03:00
Author
Collaborator

F2: fixed — переписал секцию «Migration ordering» в AGENTS.md. Она описывала СТАРОЕ поведение (Kysely «refuses to start… rejected at boot»), которое этот PR как раз убирает. Теперь два слоя: (1) CI-гейт migration-order — основная защита (переименовать на текущий таймстамп); (2) runtime allowUnorderedMigrations — приложение применяет back-dated миграцию, а не крэш-лупит (с оговоркой, что #ensureNoMissingMigrations по-прежнему ловит УДАЛЁННУЮ применённую миграцию, и что миграции должны оставаться независимыми, т.к. порядок применения между инстансами может отличаться). Дока-only, гейт зелёный с прошлого раунда.

F2: fixed — переписал секцию «Migration ordering» в `AGENTS.md`. Она описывала СТАРОЕ поведение (Kysely «refuses to start… rejected at boot»), которое этот PR как раз убирает. Теперь два слоя: (1) CI-гейт `migration-order` — основная защита (переименовать на текущий таймстамп); (2) runtime `allowUnorderedMigrations` — приложение применяет back-dated миграцию, а не крэш-лупит (с оговоркой, что `#ensureNoMissingMigrations` по-прежнему ловит УДАЛЁННУЮ применённую миграцию, и что миграции должны оставаться независимыми, т.к. порядок применения между инстансами может отличаться). Дока-only, гейт зелёный с прошлого раунда.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-05 02:57:29 +03:00
agent_coder added 1 commit 2026-07-05 02:57:30 +03:00
The "Migration ordering" section still described the OLD crash-loop-at-boot
behavior this PR removes ("Kysely refuses to start … rejected at boot"). Rewrote
it to the new two-layer model: the CI migration-order gate is the primary defense
(rename to a current timestamp), and the runtime now sets allowUnorderedMigrations
so the app applies a back-dated migration instead of crash-looping (with the note
that #ensureNoMissingMigrations still guards a removed applied migration, and that
migrations must stay independent since apply order can differ across instances).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator

Ревью — #365 (fix(db): back-dated миграции не роняют старт — CI-гейт + allowUnorderedMigrations, #363 / инцидент #361), round 3. Вердикт: PASS

Round-2 F2 (стухшая AGENTS.md, описывавшая removed crash-loop как текущее поведение) закрыт — и сверен по коду + исходнику Kysely 0.28.17. Секция «Migration ordering» переписана в две-слойную модель, и КАЖДОЕ несущее утверждение точно:

  • «оба Migrator'а ставят allowUnorderedMigrations: true» → migration.service.ts:31 (startup) + migrate.ts:30 (CLI) ✓
  • «приложение НЕ отказывается стартовать — применяет пропущенную старшую» → Kysely: при флаге #ensureMigrationsInOrder пропускается (migrator.js:448), pending = set-difference по имени → back-dated применяется ✓
  • «#ensureNoMissingMigrations всё ещё вкл (удалённая применённая = ошибка)» → безусловна в migrator.js:447, ДО ветки флага ✓
  • «CI-гейт валит PR с миграцией at/before новейшей на базе» → [[ "$f" < newest || "$f" == newest ]]exit $bad, fail-closed ✓
  • rename-гигиена (переименуй в timestamp после последней; ls | sort | tail) — сохранена ✓

Бонус: кодер сам задокументировал остаточный трейд, который я в round-1 отметил как below-threshold DROP — «apply-порядок может расходиться с лексикографикой между инстансами → миграции обязаны быть независимыми», и корректно сузил runtime-слой до «покрывает только обход гейта (manual push / hotfix)». Documentation + coherence — оба LGTM.

Объективка: round-3-дельта — ТОЛЬКО AGENTS.md (ноль кода), поэтому зелёный гейт round-2 в силе (server tsc 0; workflow jobs [migration-order, test]; оба Migrator'а несут флаг; миграция применяется на живом PG).

Замечаний нет. review/approved.

## Ревью — #365 (fix(db): back-dated миграции не роняют старт — CI-гейт + allowUnorderedMigrations, #363 / инцидент #361), round 3. Вердикт: **PASS** ✅ Round-2 F2 (стухшая `AGENTS.md`, описывавшая removed crash-loop как текущее поведение) закрыт — и сверен по коду + исходнику Kysely 0.28.17. Секция «Migration ordering» переписана в две-слойную модель, и КАЖДОЕ несущее утверждение точно: - «оба Migrator'а ставят `allowUnorderedMigrations: true`» → `migration.service.ts:31` (startup) + `migrate.ts:30` (CLI) ✓ - «приложение НЕ отказывается стартовать — применяет пропущенную старшую» → Kysely: при флаге `#ensureMigrationsInOrder` пропускается (`migrator.js:448`), pending = set-difference по имени → back-dated применяется ✓ - «`#ensureNoMissingMigrations` всё ещё вкл (удалённая применённая = ошибка)» → безусловна в `migrator.js:447`, ДО ветки флага ✓ - «CI-гейт валит PR с миграцией at/before новейшей на базе» → `[[ "$f" < newest || "$f" == newest ]]` → `exit $bad`, fail-closed ✓ - rename-гигиена (переименуй в timestamp после последней; `ls | sort | tail`) — сохранена ✓ Бонус: кодер сам задокументировал остаточный трейд, который я в round-1 отметил как below-threshold DROP — «apply-порядок может расходиться с лексикографикой между инстансами → миграции обязаны быть независимыми», и корректно сузил runtime-слой до «покрывает только обход гейта (manual push / hotfix)». Documentation + coherence — оба LGTM. **Объективка:** round-3-дельта — ТОЛЬКО `AGENTS.md` (ноль кода), поэтому зелёный гейт round-2 в силе (server tsc 0; workflow jobs `[migration-order, test]`; оба Migrator'а несут флаг; миграция применяется на живом PG). Замечаний нет. `review/approved`. <!-- state:review reviewed_head=0050ad7e... round=3 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-05 03:18:24 +03:00
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/363-migration-order:fix/363-migration-order
git checkout fix/363-migration-order
Sign in to join this conversation.