fix(db): миграции «задним числом» из долгоживущих веток не роняют старт — CI-гейт + allowUnorderedMigrations (#363, инцидент #361) #365
Open
agent_coder
wants to merge 3 commits from
fix/363-migration-order into develop
pull from: fix/363-migration-order
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:test/351-generative-converter
vvzvlad:feat/371-roles-catalog
vvzvlad:feat/370-page-versioning
vvzvlad:refactor/345-server-converter
vvzvlad:feat/196-multi-cursor
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:perf/348-backend-lowhanging
vvzvlad:fix/362-metrics-route-cardinality
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:perf/344-background-rerenders
vvzvlad:develop
vvzvlad:perf/342-code-splitting
vvzvlad:feat/355-perf-metrics
vvzvlad:perf/346-compression-cache
vvzvlad:feat/git-sync-2
vvzvlad:perf/343-typing-latency
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:feature/offline-sync
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
No Label
review/approved
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#365
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "fix/363-migration-order"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 (июньские ветки) — следующие угрозы.Два уровня, оба:
migration-orderвtest.yml, только для PR): валит PR, когда добавленная миграция сортируется на уровне/раньше самой новой на целевой ветке, с внятным сообщением «переименуй на текущий таймстамп до мержа». Основная защита — делает случайный back-dating немержабельным.allowUnorderedMigrations: trueна ОБОИХ Migrator'ах (migration.service.tsавто-миграция при старте +migrate.tsCLI): runtime-страховка — Kysely применяет непрокатанную старую миграцию вместо отказа, так что back-dated миграция в обход гейта (ручной пуш / hotfix) всё равно стартует. Трейд-офф в комментарии: порядок применения между инстансами может отличаться от лексикографического → миграции должны оставаться независимыми (наши и так каждая создаёт свои объекты); CI-гейт остаётся основной линией.How verified
allowUnorderedMigrations— валидная опция Kysely 0.28.17 (migrator.d.ts:282);tscчисто по обоим migrator-файлам;20260627…) → REJECTED, текущее (20260705…) → OK.Checklist
Прим.: страховку (
allowUnorderedMigrations) сделал первой как СРОЧНУЮ — она защищает прод от следующего мержа #119/#120 прямо сейчас.Ревью — #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-orderfail-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— валидное поле KyselyMigratorProps);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/needsmigration-orderfail-open: пропускает back-dated миграцию при движущейся базе —.github/workflows/test.yml(jobmigration-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_requestcheckout берёт 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] Третий Migratortest/integration/global-setup.ts:51НЕ получил флаг, и его докблок «Mirrors migrate.ts» теперь неточен. Функциональный эффект НУЛЕВОЙ: freshdocmost_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-orderjob чисто аддитивен (нетneeds:, не блокируетtest); security —env:-биндинг base_ref, least-priv perms. F1 — единственный реальный (medium): гейт обязан fail-closed.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 парсится.Ревью — #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).📋 Do (F2) + DROP + что сверено
Do — почини, потом ставь
review/needsAGENTS.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-гейт, ни runtimeallowUnorderedMigrationsне покрыты тестами — но в репо НЕТ живого-PG харнеса миграторов (все «DB»-спеки на DummyDriver/Proxy), а флаг — pass-through в документированное поведение Kysely; доказывать «back-dated применяется» = тестировать библиотеку, разумный автор откажется. DROP.[out-of-scope→невак]low[coherence] третий Migratortest/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.F2: fixed — переписал секцию «Migration ordering» в
AGENTS.md. Она описывала СТАРОЕ поведение (Kysely «refuses to start… rejected at boot»), которое этот PR как раз убирает. Теперь два слоя: (1) CI-гейтmigration-order— основная защита (переименовать на текущий таймстамп); (2) runtimeallowUnorderedMigrations— приложение применяет back-dated миграцию, а не крэш-лупит (с оговоркой, что#ensureNoMissingMigrationsпо-прежнему ловит УДАЛЁННУЮ применённую миграцию, и что миграции должны оставаться независимыми, т.к. порядок применения между инстансами может отличаться). Дока-only, гейт зелёный с прошлого раунда.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>Ревью — #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» переписана в две-слойную модель, и КАЖДОЕ несущее утверждение точно:allowUnorderedMigrations: true» →migration.service.ts:31(startup) +migrate.ts:30(CLI) ✓#ensureMigrationsInOrderпропускается (migrator.js:448), pending = set-difference по имени → back-dated применяется ✓#ensureNoMissingMigrationsвсё ещё вкл (удалённая применённая = ошибка)» → безусловна вmigrator.js:447, ДО ветки флага ✓[[ "$f" < newest || "$f" == newest ]]→exit $bad, fail-closed ✓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.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.