fix(tradein): гейт номеров миграций берёт эталон из git, ручной манифест удалён #2786
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2786
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2683-manifest-drift"
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?
Ждёт владельца — НЕ мержить самому
Правка гейта = управляющий контур. Самомерж по такому PR запрещён (
.claude/rules/git-pr.md,self-extending guard). CI довожу до зелёного, решение — за владельцем.
ОБНОВЛЕНО 2026-08-10 — синхронизация с main, конфликт снят
PR пролежал трое суток и оброс конфликтом
modify/deleteпоtradein-mvp/backend/data/sql/_manifest_applied.txt: он этот файл удаляет, а чужие PRпродолжали в него дописывать. Разрешено удалением — удаление и есть предмет PR.
Проверено, что это не «принято на веру». За трое суток main дописал в манифест ровно три имени
(
240_trade_in_estimates_retain_until.sql,250_drop_duplicate_expires_at_index.sql,251_listings_drop_ceiling_height.sql) и ни одной строки, которая манифест читает:git log -S"_manifest_applied" 306fd828..origin/mainвне самого файла — пусто. Новыйscripts/check-migration-lock-timeout.py, приехавший в main за это время, тоже ходитglob("*.sql"). Потребителя не появилось — опровержения нет.Синхронизация сделана merge-коммитом, а не rebase: rebase потребовал бы
--forceв общуюветку. Побочный плюс — голова ветки физически СОДЕРЖИТ слияние с
27e199e3, то есть её зелёнаягалка говорит именно о результате слияния (почему это важно — п.2 ниже). Дерево merge-варианта
побайтно совпало с деревом rebase-варианта (
git diff <rebase> <merge>пуст).Свежий замер дрейфа на
27e199e3Отставание росло 13 → 15 (07.08) → 16 (10.08). Забытые имена: 217, 218, 219, 220, 221, 223,
224, 227, 228, 229, 230, 231, 232, 238, 239, 253. Ручной контракт дрейфует ровно так, как описано
в #2683.
Порядок для владельца
Мержить этот PR раньше любого PR с новой миграцией. Иначе номер проедет с зелёной галкой,
как 234 седьмого августа. Окно сейчас чистое: из открытых PR (#2546, #2661, #2751, #2794
и этот) ни один не трогает
tradein-mvp/backend/data/sqlи ни один не трогает манифест —конфликт
modify/deleteпосле мержа не достанется никому.Зелёная галка на PR в этом Forgejo — про голову ветки, а не про слияние.
refs/pull/N/mergeздесь не публикуется (
ls-remote: 1620*/head, ноль*/merge), в логе раннера прямымтекстом
::debug::ref = 'refs/pull/2786/head'. Значит галка, отзеленевшая не сегодня, ничегоне говорит о слиянии с текущим main. Ровно так 07.08 проехал номер 234: чек #2754 позеленел
заранее, потом его номер занял чужой мерж, и чек об этом не узнал. Коллизию в итоге поймал
человек и починил руками (
e6591a45, 234 → 240) — гейт молчал. У ЭТОГО PR голова содержитmerge с
27e199e3, поэтому его галка про слияние честная — ровно до следующего мержа в main.Прод не трогается, ручных шагов нет. Манифест не
*.sql. Цикл миграций вdeploy-tradein.ymlберётls -1 backend/data/sql/*.sql; bootstrap схемы вci-tradein.yml—ls -1 tradein-mvp/backend/data/sql/*.sql;scripts/check-migration-lock-timeout.py—Path(dir).glob("*.sql"). Никаких миграций PR не добавляет и не переименовывает.Откат —
git revert.16 недостающих имён намеренно не дописаны. Допишешь руками — через сутки повторится
(так и вышло: #2692 догнал список 06.08, к 07.08 отставание было уже 15, сегодня 16).
Остались две живые ссылки на удалённый файл, обе — комментарии, обе оставлены осознанно:
tradein-mvp/backend/data/sql/233_payments.sql(строки 4 и 11) — историческая заметка опереименованиях 228 → 232 → 233 внутри УЖЕ ПРИМЕНЁННОЙ миграции. Править применённый файл ради
комментария — та самая привычка, от которой заведён инвариант 1, поэтому не тронуто. Заметка
датирована и читается как история, а не как формулировка контракта.
Пробы гейта на текущем репозитории (2026-08-10)
Гоняется на голове этой ветки (
77dfd072) и на реальных чужих деревьях, не на выдуманных.253_probe_final.sql, 253 занят в origin/mainколлизия номеров миграций: 253_probe_final.sql — номер 253 уже занят: 253_scrape_proxy_domclick_affinity_release.sql001_trade_in_estimates.sqlпереименован в_v2миграции пропали ...: ['001_trade_in_estimates.sql']+ вдогонку коллизия 001233_payments.sqlудалёнмиграции пропали ...: ['233_payments.sql']3 passed192_probe_stale_branch.sql — номер 192 уже занят: 192_tradein_users_auth.sql4 passed— дрейф не краснеет никогда192_probe, СТАРЫЙ гейт4 passed← вот она, слепая зона_SQL_PATHSPECразъехался с раскладкой репоэталон пуст ... была бы вакуумно-зелёнойCI=trueFailed: нет git-эталона миграцийskip_allowlist.txtПроба 3 — не «зелёная, потому что ничего не проверяет»: то же самое дерево в пробе 3b краснеет.
Проба 4c — прямое сравнение старого и нового гейта на одном дереве.
Опровергнутые предпосылки (2026-08-10)
«Забытое в манифесте имя прячет любую коллизию по своему номеру» — НЕТ. Прогнал: на main
добавил
238_probe_dup.sqlпри забытом238_listings_ceiling_height_unify.sql— старый гейтпокраснел (
префикс 238 уже у нового 238_listings_ceiling_height_unify.sql). Оба файла длянего «новые», и ветка
seen_new_prefixих ловит. Слепая зона старого гейта уже, чем япредполагал, и она ровно одна: файл, которого в дереве ветки НЕТ вовсе (проба 4c) — то есть
кросс-ветковый случай 212/#2682 и 234/#2754. Формулировка «дрейф прячет коллизии» в разборе
07.08 была шире факта; сам вывод (список не может покраснеть от собственного отставания) стоит.
«Ребейз» — сделан merge-коммит. Rebase потребовал бы force-push в общую ветку.
Комментарий в
skip_allowlist.txtутверждал, что воркфлоу «тянут main отдельным шагом». Этот шагсняли ещё во втором коммите PR (из job-контейнера
git.gendsgn.ruнедостижим, run 6977), эталондаёт сам
checkoutсfetch-depth: 0. Комментарий поправлен отдельным коммитом — иначе ончитался бы дальше как факт.
Ниже — исходный разбор от 2026-08-07, он не менялся. Числа там соответствуют состоянию на тот день (228/213, дрейф 15).
Почему гейт пропускал дрейф (не гипотеза, а прогон)
Гипотеза «тест смотрит только на файлы из диффа PR» не подтвердилась: диффа он не видит вовсе,
_actual_sql_files()— этоglob("*.sql")по всему каталогу.Настоящая причина другая и хуже. «Новым» тест считал файл, которого нет в манифесте, а новые
файлы от манифеста освобождены — прямым текстом в докстринге
test_manifest_covers_all_but_new_files: «Этот тест НЕ требует, чтобы новый файл уже был вmanifest». Забытое имя и новая миграция этого PR для гейта неразличимы. Дрейф был не пропуском
проверки, а её штатным исключением, поэтому покраснеть от дрейфа гейт не мог никогда — ни в какой
ветке.
Прогон на
main(2026-08-07, через сутки после ручной догонки в #2692):Дрейф вырос с 13 до 15 за сутки, сьют зелёный.
Что охраняется на самом деле → манифест не нужен
Он служил двум инвариантам, и оба выводимы:
«Применено на проде» = «доехало до main»:
deploy-tradein.ymlгоняет КАЖДЫЙdata/sql/*.sqlизmain под
ON_ERROR_STOPи трекает по bare filename в_schema_migrations. Отдельного знанияманифест не хранил — он был ручной копией
git ls-tree origin/main, всегда отстающей. Удалён.Дрейфовать больше нечему: списка, который можно забыть обновить, нет.
Кросс-ветковая коллизия закрыта
Номер нового файла сверяется с полным
origin/main, а не с рабочим деревом. Проверено на живомоткрытом PR #2754 (см. ниже).
Ограничение из п.4 учтено: удаления сверяются с точкой ветвления, а не с origin/main, поэтому
недельная ветка не краснеет за чужие миграции.
Красный прогон на намеренно сломанном входе
234_trade_in_estimates_retain_until.sql, main —234_scrape_runs_ban_kind_unknown.sqlколлизия номеров миграций: 234_trade_in_estimates_retain_until.sql — номер 234 уже занят: 234_scrape_runs_ban_kind_unknown.sql4 passed3 passed238_probe_collision.sql — номер 238 уже занят: 238_listings_ceiling_height_unify.sql4 passed001_trade_in_estimates.sqlмиграции пропали из tradein-mvp/backend/data/sql/_SQL_PATHSPECразъехался → эталон пустэталон пуст ... была бы вакуумно-зелёнойCI=trueFailed: нет git-эталона миграцийskip_allowlist.txtПлюс
test_collision_rule_flags_a_taken_number— постоянная проверка самого правила на литеральныхвходах (случай #2754 зашит как ожидание). Тавтологии нет: ожидания не выводятся из содержимого
data/sql.CI
ci-tradein.yml(backend-tests) иdeploy-tradein.yml(test):fetch-depth: 0+ отдельныйgit fetch origin main. Причина в комментариях: этот Forgejo не публикуетrefs/pull/N/merge(
ls-remote: 1620*/head, ноль*/merge), то есть CI тестирует голову ветки, а наdepth=1нетни
origin/main, ни общего предка. Без эталона гейт красный, а не тихо пропущенный.Одна формулировка контракта (п.7)
Было четыре: шапка манифеста, правило 3 там же, хвост манифеста,
.claude/rules/tradein.md.Осталась одна — докстринг
tests/test_migration_numbering.py. Остальные удалены вместе с файломлибо заменены ссылкой на неё. Заодно исправлен сам рецепт:
git ls-treeбез-rпечатаеткаталог одной строкой, а не файлы — в таком виде он ходил по issue и по правилам и никогда не работал.
Что делать владельцу (07.08 — УСТАРЕЛО)
цель. Автору #2754 переименовать файл в свободный номер.
_manifest_applied.txt— хунк с манифестом просто снять,файла больше нет.
_manifest_applied.txtне*.sql, ни deploy-цикл, ни bootstrap схемы в CI егоне читают. Откат —
git revert, миграции не затрагиваются.в комментарии issue (226/226) и из строгого
ON_ERROR_STOP-цикла деплоя. Направление ошибкибезопасное: лишний файл в эталоне запрещает переименование, которое, возможно, и было бы можно.
Refs #2683
Правка по факту прогона (коммит 2)
Первая редакция добавляла отдельный шаг
git fetch origin main— он уронилCI Trade-In / backend-tests(run 6977): из job-контейнераgit.gendsgn.ru:443недостижим, connection refused за5 мс; сеть есть только у самого checkout. Шаг снят как ненужный: в логе того же прогона видно, что
при
fetch-depth: 0checkout идёт refspec'ом+refs/heads/*:refs/remotes/origin/*—origin/mainпоявляется сам. Осталось изменение в одну строку на job.
Тот же лог заодно подтверждает диагноз прямой цитатой раннера:
::debug::ref = 'refs/pull/2786/head'— CI тестирует голову ветки, не слияние с main.
Прогон на этом PR (
0b14e64b) — 8/8 зелёныхCI Trade-In / backend-tests: 4018 passed, ни одного пропуска гейта, ни одного неучтённогопропуска (хук
skip_allowlist). То есть проверка в CI действительно исполнилась противorigin/main, а не «зелёная, потому что не бежала».27e199e3) в fix/2683-manifest-drift 9e5bcd3fa9Конфликт снят повторно (12.08) — PR снова сливается чисто
Не мержу — правка гейта это управляющий контур, решение за владельцем. Только вернул PR в мержабельное состояние.
За двое суток main дописал в манифест ещё три имени (#2818, #2840, #2843), и
modify/deleteвернулся. Влилorigin/mainв ветку (обычный мерж-коммит, без перезаписи истории). Два конфликта:_manifest_applied.txt— разрешён удалением: удаление и есть предмет PR.skip_allowlist.txt— содержательный, и здесь важно, что сохранены оба набора: пропуски гейта номеров (ветка) и пропуски живых тестов повтора домов из #2844 (main). Проверено: 5 строк обоих видов на месте, чужие объявления не потеряны.Проверка слияния — через
git merge-tree --write-tree origin/main <ветка>, а не по полюmergeableв API: оно пересчитывается асинхронно и сразу после пуша ещё показывалоFalse.Поправка к обоснованию из тела PR
Выше написано «ни одной строки, которая манифест читает». Это верно для строк, добавленных после точки отсчёта, но звучит шире, чем есть: читатель у манифеста есть —
tradein-mvp/backend/tests/test_migrations_manifest.py:30(_MANIFEST = _SQL_DIR / "_manifest_applied.txt").Вывод от этого не страдает: этот же PR удаляет и сам тест (138 строк), то есть читатель уходит вместе с читаемым. Остальные упоминания — комментарии (
deploy-tradein.yml:447,233_payments.sql), не чтение.Формулировку стоит поправить именно потому, что в нынешнем виде она приглашает не перепроверять.
Цена ожидания — она растёт, а не стоит
Каждая новая миграция заново ломает этот PR: за неделю он конфликтовал дважды, оба раза по одной и той же причине. Разрешение механическое, но требует ручного прохода, и чем дольше PR открыт, тем чаще.
Обратная сторона важнее: пока манифест жив, он остаётся вторым источником правды о номерах миграций — а именно расхождение этих двух источников PR и чинит.
CI зелёный на восьми задачах.
Живой пример цены ожидания — сегодня, дважды за час
Два независимых PR (#2850 и #2851) взяли один и тот же номер миграции 259. Git коллизию не увидел и не мог: имена файлов разные, содержимое разное, конфликта нет.
Оба прошли CI зелёными. Оба показывали
mergeable: true. После мержа первого второй по-прежнему сливался чисто — и приехал бы на прод с дублирующимся номером, где применение миграций идёт по порядку имён.Разошлось только потому, что я посмотрел глазами на список файлов перед мержем. Перенумеровал вручную: 259 → 260, плюс ссылка в
tests/test_dead_code_sweep_2674.pyи запись в манифесте.Это ровно тот случай, ради которого написан этот PR. Он лежит с 10.08; за это время в main приехало пять миграций (255-259), и дважды его пришлось расконфликтовывать вручную.
Отдельно стоит отметить, чего гейт стоил бы здесь по времени: коллизия была видна за одну команду
ls, но увидеть её должен был не человек перед мержем, а проверка — потому что человек смотрит не всегда, а сегодня совпало, что смотрел.