fix(tradein): гейт номеров миграций берёт эталон из git, ручной манифест удалён #2786

Merged
lekss361 merged 6 commits from fix/2683-manifest-drift into main 2026-08-15 20:04:17 +00:00
Collaborator

Ждёт владельца — НЕ мержить самому

Правка гейта = управляющий контур. Самомерж по такому 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

имён в манифесте: 216 · файлов *.sql: 232 · не дописано: 16
$ pytest tests/test_migrations_manifest.py      # СТАРЫЙ гейт, на самом main
4 passed

Отставание росло 13 → 15 (07.08) → 16 (10.08). Забытые имена: 217, 218, 219, 220, 221, 223,
224, 227, 228, 229, 230, 231, 232, 238, 239, 253. Ручной контракт дрейфует ровно так, как описано
в #2683.

Порядок для владельца

  1. Мержить этот PR раньше любого PR с новой миграцией. Иначе номер проедет с зелёной галкой,
    как 234 седьмого августа. Окно сейчас чистое: из открытых PR (#2546, #2661, #2751, #2794
    и этот) ни один не трогает tradein-mvp/backend/data/sql и ни один не трогает манифест —
    конфликт modify/delete после мержа не достанется никому.

  2. Зелёная галка на 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.

  3. Прод не трогается, ручных шагов нет. Манифест не *.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.

  4. 16 недостающих имён намеренно не дописаны. Допишешь руками — через сутки повторится
    (так и вышло: #2692 догнал список 06.08, к 07.08 отставание было уже 15, сегодня 16).

  5. Остались две живые ссылки на удалённый файл, обе — комментарии, обе оставлены осознанно:
    tradein-mvp/backend/data/sql/233_payments.sql (строки 4 и 11) — историческая заметка о
    переименованиях 228 → 232 → 233 внутри УЖЕ ПРИМЕНЁННОЙ миграции. Править применённый файл ради
    комментария — та самая привычка, от которой заведён инвариант 1, поэтому не тронуто. Заметка
    датирована и читается как история, а не как формулировка контракта.

Пробы гейта на текущем репозитории (2026-08-10)

Гоняется на голове этой ветки (77dfd072) и на реальных чужих деревьях, не на выдуманных.

# проба ожидание факт
1 новый 253_probe_final.sql, 253 занят в origin/main RED RED коллизия номеров миграций: 253_probe_final.sql — номер 253 уже занят: 253_scrape_proxy_domclick_affinity_release.sql
2a 001_trade_in_estimates.sql переименован в _v2 RED RED миграции пропали ...: ['001_trade_in_estimates.sql'] + вдогонку коллизия 001
2b 233_payments.sql удалён RED RED миграции пропали ...: ['233_payments.sql']
3 живая ветка #2546 (ответвилась 26.07), main ушёл на +48 миграций, своих правок ноль GREEN GREEN 3 passed
3b она же берёт «следующий свободный» 192 — свободный локально, занятый в main RED RED 192_probe_stale_branch.sql — номер 192 уже занят: 192_tradein_users_auth.sql
4 забытое имя на СТАРОМ гейте: 216/232, 16 не дописано (симптом) GREEN 4 passed — дрейф не краснеет никогда
4c та же ветка #2546 с тем же 192_probe, СТАРЫЙ гейт (для сравнения) GREEN 4 passed ← вот она, слепая зона
5 _SQL_PATHSPEC разъехался с раскладкой репо RED, не вакуумный зелёный RED эталон пуст ... была бы вакуумно-зелёной
6 git-эталона нет, CI=true RED RED Failed: нет git-эталона миграций
6 то же локально пропуск, называющий себя SKIPPED с той же причиной, оба nodeid в 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):

files on disk: 228 · names in manifest: 213 · не дописано: 15
$ pytest tests/test_migrations_manifest.py -v
4 passed

Дрейф вырос с 13 до 15 за сутки, сьют зелёный.

Что охраняется на самом деле → манифест не нужен

Он служил двум инвариантам, и оба выводимы:

инвариант чем заменён
не переименовывать/удалять применённую миграцию файлы в точке ветвления (merge-base с origin/main)
новый файл не переиспользует NN номера в полном origin/main ∪ рабочем дереве

«Применено на проде» = «доехало до 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, поэтому
недельная ветка не краснеет за чужие миграции.

Красный прогон на намеренно сломанном входе

проба ожидание факт
PR #2754 as is: ветка несёт 234_trade_in_estimates_retain_until.sql, main — 234_scrape_runs_ban_kind_unknown.sql RED RED: коллизия номеров миграций: 234_trade_in_estimates_retain_until.sql — номер 234 уже занят: 234_scrape_runs_ban_kind_unknown.sql
тот же PR, старый гейт (для сравнения) GREEN 4 passed
ветка от 2026-07-30, +43 миграции в main, своих правок нет GREEN (п.4) GREEN 3 passed
она же берёт номер 238, занятый в main после ветвления RED RED 238_probe_collision.sql — номер 238 уже занят: 238_listings_ceiling_height_unify.sql
та же ветка, старый гейт на том же дереве (для сравнения) GREEN 4 passed
переименование применённой 001_trade_in_estimates.sql RED RED миграции пропали из tradein-mvp/backend/data/sql/
два новых файла с одним NN в одной ветке RED RED, названы оба
_SQL_PATHSPEC разъехался → эталон пуст RED, не вакуумный зелёный RED эталон пуст ... была бы вакуумно-зелёной
git-эталона нет, CI=true RED RED Failed: нет git-эталона миграций
то же локально пропуск, называющий себя SKIPPED с причиной, оба nodeid внесены в 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 смержен 07.08; коллизию 234 поймал человек и починил руками (e6591a45, 234 → 240), гейт при этом молчал.

  1. Смержить этот PR раньше #2754. После мержа #2754 станет красным на коллизии 234 — это и есть
    цель. Автору #2754 переименовать файл в свободный номер.
  2. #2754 получит конфликт modify/delete по _manifest_applied.txt — хунк с манифестом просто снять,
    файла больше нет.
  3. 15 «недостающих» имён намеренно не дописаны: дописывать больше некуда и незачем.
  4. Прод не трогается: _manifest_applied.txt не *.sql, ни deploy-цикл, ни bootstrap схемы в CI его
    не читают. Откат — git revert, миграции не затрагиваются.
  5. Прод-БД я не опрашивал (боевой SSH без разрешения). Равенство «main == применено» взято из замера
    в комментарии 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: 0 checkout идёт 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, а не «зелёная, потому что не бежала».

## Ждёт владельца — НЕ мержить самому Правка гейта = управляющий контур. Самомерж по такому 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` ``` имён в манифесте: 216 · файлов *.sql: 232 · не дописано: 16 $ pytest tests/test_migrations_manifest.py # СТАРЫЙ гейт, на самом main 4 passed ``` Отставание росло 13 → 15 (07.08) → **16** (10.08). Забытые имена: 217, 218, 219, 220, 221, 223, 224, 227, 228, 229, 230, 231, 232, 238, 239, 253. Ручной контракт дрейфует ровно так, как описано в #2683. ## Порядок для владельца 1. **Мержить этот PR раньше любого PR с новой миграцией.** Иначе номер проедет с зелёной галкой, как 234 седьмого августа. **Окно сейчас чистое**: из открытых PR (#2546, #2661, #2751, #2794 и этот) ни один не трогает `tradein-mvp/backend/data/sql` и ни один не трогает манифест — конфликт `modify/delete` после мержа не достанется никому. 2. **Зелёная галка на 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. 3. **Прод не трогается, ручных шагов нет.** Манифест не `*.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`. 4. **16 недостающих имён намеренно не дописаны.** Допишешь руками — через сутки повторится (так и вышло: #2692 догнал список 06.08, к 07.08 отставание было уже 15, сегодня 16). 5. Остались две **живые** ссылки на удалённый файл, обе — комментарии, обе оставлены осознанно: `tradein-mvp/backend/data/sql/233_payments.sql` (строки 4 и 11) — историческая заметка о переименованиях 228 → 232 → 233 внутри УЖЕ ПРИМЕНЁННОЙ миграции. Править применённый файл ради комментария — та самая привычка, от которой заведён инвариант 1, поэтому не тронуто. Заметка датирована и читается как история, а не как формулировка контракта. ## Пробы гейта на текущем репозитории (2026-08-10) Гоняется на голове этой ветки (`77dfd072`) и на реальных чужих деревьях, не на выдуманных. | # | проба | ожидание | факт | |---|---|---|---| | 1 | новый `253_probe_final.sql`, 253 занят в origin/main | RED | **RED** `коллизия номеров миграций: 253_probe_final.sql — номер 253 уже занят: 253_scrape_proxy_domclick_affinity_release.sql` | | 2a | `001_trade_in_estimates.sql` переименован в `_v2` | RED | **RED** `миграции пропали ...: ['001_trade_in_estimates.sql']` + вдогонку коллизия 001 | | 2b | `233_payments.sql` удалён | RED | **RED** `миграции пропали ...: ['233_payments.sql']` | | 3 | **живая** ветка #2546 (ответвилась 26.07), main ушёл на **+48** миграций, своих правок ноль | GREEN | **GREEN** `3 passed` | | 3b | она же берёт «следующий свободный» 192 — свободный локально, занятый в main | RED | **RED** `192_probe_stale_branch.sql — номер 192 уже занят: 192_tradein_users_auth.sql` | | 4 | забытое имя на СТАРОМ гейте: 216/232, 16 не дописано | (симптом) | **GREEN** `4 passed` — дрейф не краснеет никогда | | 4c | та же ветка #2546 с тем же `192_probe`, СТАРЫЙ гейт | (для сравнения) | **GREEN** `4 passed` ← вот она, слепая зона | | 5 | `_SQL_PATHSPEC` разъехался с раскладкой репо | RED, не вакуумный зелёный | **RED** `эталон пуст ... была бы вакуумно-зелёной` | | 6 | git-эталона нет, `CI=true` | RED | **RED** `Failed: нет git-эталона миграций` | | 6 | то же локально | пропуск, называющий себя | **SKIPPED** с той же причиной, оба nodeid в `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): ``` files on disk: 228 · names in manifest: 213 · не дописано: 15 $ pytest tests/test_migrations_manifest.py -v 4 passed ``` Дрейф вырос с 13 до 15 за сутки, сьют зелёный. ## Что охраняется на самом деле → манифест не нужен Он служил двум инвариантам, и оба выводимы: | инвариант | чем заменён | |---|---| | не переименовывать/удалять применённую миграцию | файлы в **точке ветвления** (merge-base с origin/main) | | новый файл не переиспользует NN | номера в **полном origin/main** ∪ рабочем дереве | «Применено на проде» = «доехало до 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, поэтому недельная ветка не краснеет за чужие миграции. ## Красный прогон на намеренно сломанном входе | проба | ожидание | факт | |---|---|---| | PR #2754 as is: ветка несёт `234_trade_in_estimates_retain_until.sql`, main — `234_scrape_runs_ban_kind_unknown.sql` | RED | **RED**: `коллизия номеров миграций: 234_trade_in_estimates_retain_until.sql — номер 234 уже занят: 234_scrape_runs_ban_kind_unknown.sql` | | тот же PR, **старый** гейт | (для сравнения) | GREEN `4 passed` | | ветка от 2026-07-30, +43 миграции в main, своих правок нет | GREEN (п.4) | **GREEN** `3 passed` | | она же берёт номер 238, занятый в main после ветвления | RED | **RED** `238_probe_collision.sql — номер 238 уже занят: 238_listings_ceiling_height_unify.sql` | | та же ветка, старый гейт на том же дереве | (для сравнения) | GREEN `4 passed` | | переименование применённой `001_trade_in_estimates.sql` | RED | **RED** `миграции пропали из tradein-mvp/backend/data/sql/` | | два новых файла с одним NN в одной ветке | RED | **RED**, названы оба | | `_SQL_PATHSPEC` разъехался → эталон пуст | RED, не вакуумный зелёный | **RED** `эталон пуст ... была бы вакуумно-зелёной` | | git-эталона нет, `CI=true` | RED | **RED** `Failed: нет git-эталона миграций` | | то же локально | пропуск, называющий себя | **SKIPPED** с причиной, оба nodeid внесены в `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 смержен 07.08; коллизию 234 поймал человек и починил руками (`e6591a45`, 234 → 240), гейт при этом молчал. 1. **Смержить этот PR раньше #2754.** После мержа #2754 станет красным на коллизии 234 — это и есть цель. Автору #2754 переименовать файл в свободный номер. 2. #2754 получит конфликт modify/delete по `_manifest_applied.txt` — хунк с манифестом просто снять, файла больше нет. 3. 15 «недостающих» имён **намеренно не дописаны**: дописывать больше некуда и незачем. 4. Прод не трогается: `_manifest_applied.txt` не `*.sql`, ни deploy-цикл, ни bootstrap схемы в CI его не читают. Откат — `git revert`, миграции не затрагиваются. 5. Прод-БД я не опрашивал (боевой SSH без разрешения). Равенство «main == применено» взято из замера в комментарии 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: 0` checkout идёт 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`, а не «зелёная, потому что не бежала».
bot-backend added 1 commit 2026-08-07 09:36:26 +00:00
fix(tradein): гейт номеров миграций берёт эталон из git, ручной манифест удалён
Some checks failed
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / backend-tests (pull_request) Failing after 21s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 55s
CI Trade-In / frontend-checks (pull_request) Successful in 1m26s
CI / openapi-codegen-check (pull_request) Successful in 2m7s
CI / backend-tests (pull_request) Successful in 16m53s
2e20b6307b
_manifest_applied.txt по построению не мог покраснеть. Тест считал «новым»
любой файл, которого нет в списке, а новые файлы от списка освобождены
(докстринг test_manifest_covers_all_but_new_files: «НЕ требует, чтобы новый
файл уже был в manifest»). Забытое имя и новая миграция PR для гейта — одно и
то же, поэтому дрейф был не пропуском проверки, а её штатным исключением.
Замер на main 2026-08-07: 15 имён не дописано, все четыре теста зелёные —
через сутки после того, как #2692 догнал список руками.

Список при этом был лишь копией того, что git и так знает: deploy-tradein.yml
применяет КАЖДЫЙ data/sql/*.sql из main под ON_ERROR_STOP, то есть «файл
доехал до main» и есть «имя закреплено на проде». Ведём эталон в git — и
дрейфовать становится нечему.

Кросс-ветковая дыра закрыта тем же ходом: номер нового файла сверяется с
ПОЛНЫМ origin/main, а не с рабочим деревом, поэтому коллизия с миграцией,
смерженной после ветвления, находится. Проверено на живом PR #2754
(234_trade_in_estimates_retain_until против 234_scrape_runs_ban_kind_unknown
из main): старый гейт зелёный, новый красный.

Удаление/переименование применённой миграции сверяется с ТОЧКОЙ ВЕТВЛЕНИЯ, а
не с origin/main: иначе ветка недельной давности краснела бы за чужие
миграции. Проверено — ветка от 2026-07-30 при +43 миграциях в main зелёная.

CI: checkout переведён на fetch-depth 0 + отдельный fetch main. Этот Forgejo
не публикует refs/pull/N/merge (1620 */head, ноль */merge), а на depth=1 нет
ни origin/main, ни общего предка — без этого гейту не с чем сверять, и он
намеренно красный, а не тихо пропущенный.

Контракт сведён к одной формулировке — докстринг test_migration_numbering.py;
шапка манифеста, правило 3, хвост манифеста и рецепт из .claude/rules
удалены или заменены ссылкой. Заодно исправлен сам рецепт: `git ls-tree` без
`-r` печатает каталог, а не файлы.

Refs #2683
Light1YT added 1 commit 2026-08-07 09:49:07 +00:00
fix(ci): убрать лишний fetch main — depth=0 уже тянет refs/heads/*
All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Successful in 56s
CI Trade-In / frontend-checks (pull_request) Successful in 1m40s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m32s
CI Trade-In / backend-tests (pull_request) Successful in 5m8s
CI / backend-tests (pull_request) Successful in 16m12s
0b14e64b58
Отдельный шаг `git fetch origin main` в backend-тестовых job'ах ронял прогон:
из job-контейнера git.gendsgn.ru:443 недостижим (run 6977, connection refused
за 5 мс), сеть есть только у самого checkout. Шаг и не был нужен — в логе того
же прогона видно, что при fetch-depth: 0 checkout идёт refspec'ом
`+refs/heads/*:refs/remotes/origin/*`, то есть origin/main появляется сам.

Refs #2683
Light1YT added 2 commits 2026-08-09 20:28:40 +00:00
Конфликт modify/delete по tradein-mvp/backend/data/sql/_manifest_applied.txt
разрешён удалением — удаление и есть предмет PR. За трое суток в main дописали
три имени (240/250/251); дописывать их некуда: файла больше нет, а гейт
tests/test_migration_numbering.py берёт эталон применённого из origin/main.

Проверено, что удаление ничего не оставляет без потребителя: манифест не *.sql,
цикл миграций в deploy-tradein.yml и bootstrap схемы в ci-tradein.yml берут
glob '*.sql', новый scripts/check-migration-lock-timeout.py — тоже.

Замер дрейфа на 27e199e3: 216 имён в манифесте против 232 файлов, отставание
16 (было 15 на 07.08). Старый гейт на этом дереве: 4 passed.

# Conflicts:
#	tradein-mvp/backend/data/sql/_manifest_applied.txt
chore(tradein): комментарий в skip_allowlist описывал снятый шаг git fetch
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 52s
CI Trade-In / frontend-checks (pull_request) Successful in 1m25s
CI / openapi-codegen-check (pull_request) Successful in 2m23s
CI Trade-In / backend-tests (pull_request) Successful in 4m17s
CI / backend-tests (pull_request) Successful in 15m39s
77dfd072a7
Во втором коммите PR отдельный `git fetch origin main` был снят (из
job-контейнера git.gendsgn.ru недостижим, run 6977), эталон даёт сам checkout с
fetch-depth: 0. Комментарий про «тянут main отдельным шагом» остался и читался
бы дальше как факт.
Light1YT added 1 commit 2026-08-12 18:06:53 +00:00
Merge remote-tracking branch 'origin/main' into fix/2683-manifest-drift
Some checks failed
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 9s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 54s
CI / openapi-codegen-check (pull_request) Successful in 2m54s
CI / backend-tests (pull_request) Failing after 1m14s
CI Trade-In / frontend-checks (pull_request) Successful in 1m19s
CI Trade-In / backend-tests (pull_request) Successful in 4m44s
2c8dc46205
# Conflicts:
#	tradein-mvp/backend/data/sql/_manifest_applied.txt
#	tradein-mvp/backend/tests/skip_allowlist.txt
Author
Collaborator

Конфликт снят повторно (12.08) — PR снова сливается чисто

Не мержу — правка гейта это управляющий контур, решение за владельцем. Только вернул PR в мержабельное состояние.

За двое суток main дописал в манифест ещё три имени (#2818, #2840, #2843), и modify/delete вернулся. Влил origin/main в ветку (обычный мерж-коммит, без перезаписи истории). Два конфликта:

  1. _manifest_applied.txt — разрешён удалением: удаление и есть предмет PR.
  2. 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 зелёный на восьми задачах.

## Конфликт снят повторно (12.08) — PR снова сливается чисто **Не мержу** — правка гейта это управляющий контур, решение за владельцем. Только вернул PR в мержабельное состояние. За двое суток main дописал в манифест ещё три имени (#2818, #2840, #2843), и `modify/delete` вернулся. Влил `origin/main` в ветку (обычный мерж-коммит, без перезаписи истории). Два конфликта: 1. **`_manifest_applied.txt`** — разрешён удалением: удаление и есть предмет PR. 2. **`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 зелёный на восьми задачах.
Author
Collaborator

Живой пример цены ожидания — сегодня, дважды за час

Два независимых PR (#2850 и #2851) взяли один и тот же номер миграции 259. Git коллизию не увидел и не мог: имена файлов разные, содержимое разное, конфликта нет.

на main занято до 258 (258_houses_imv_transient_attempts.sql, #2843)
#2850 → 259_data_quality_drop_pct_cadastr.sql
#2851 → 259_houses_drop_has_panorama.sql

Оба прошли CI зелёными. Оба показывали mergeable: true. После мержа первого второй по-прежнему сливался чисто — и приехал бы на прод с дублирующимся номером, где применение миграций идёт по порядку имён.

Разошлось только потому, что я посмотрел глазами на список файлов перед мержем. Перенумеровал вручную: 259 → 260, плюс ссылка в tests/test_dead_code_sweep_2674.py и запись в манифесте.

Это ровно тот случай, ради которого написан этот PR. Он лежит с 10.08; за это время в main приехало пять миграций (255-259), и дважды его пришлось расконфликтовывать вручную.

Отдельно стоит отметить, чего гейт стоил бы здесь по времени: коллизия была видна за одну команду ls, но увидеть её должен был не человек перед мержем, а проверка — потому что человек смотрит не всегда, а сегодня совпало, что смотрел.

## Живой пример цены ожидания — сегодня, дважды за час Два независимых PR (#2850 и #2851) взяли **один и тот же номер миграции 259**. Git коллизию не увидел и не мог: имена файлов разные, содержимое разное, конфликта нет. ``` на main занято до 258 (258_houses_imv_transient_attempts.sql, #2843) #2850 → 259_data_quality_drop_pct_cadastr.sql #2851 → 259_houses_drop_has_panorama.sql ``` Оба прошли CI зелёными. Оба показывали `mergeable: true`. После мержа первого второй по-прежнему сливался **чисто** — и приехал бы на прод с дублирующимся номером, где применение миграций идёт по порядку имён. Разошлось только потому, что я посмотрел глазами на список файлов перед мержем. Перенумеровал вручную: 259 → 260, плюс ссылка в `tests/test_dead_code_sweep_2674.py` и запись в манифесте. **Это ровно тот случай, ради которого написан этот PR.** Он лежит с 10.08; за это время в main приехало пять миграций (255-259), и дважды его пришлось расконфликтовывать вручную. Отдельно стоит отметить, чего гейт стоил бы здесь по времени: коллизия была видна за одну команду `ls`, но увидеть её должен был не человек перед мержем, а проверка — потому что человек смотрит не всегда, а сегодня совпало, что смотрел.
bot-backend added 1 commit 2026-08-15 19:45:02 +00:00
Слияние main в fix/2683-manifest-drift
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Successful in 1m0s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m41s
CI / openapi-codegen-check (pull_request) Successful in 2m41s
CI Trade-In / backend-tests (pull_request) Successful in 5m31s
CI / backend-tests (pull_request) Successful in 17m12s
90133c4c2e
main принёс PR #2751 (ruff-шаг в ci-tradein.yml/backend-tests) параллельно с
fetch-depth: 0 из этой ветки в том же job'е — не противоречат друг другу,
слились автоматически.

Единственное ручное разрешение — _manifest_applied.txt (modify/delete):
main дописал файл, ветка его удаляет. Разрешение — удаление, это и есть
предмет PR: гейт номеров миграций берёт эталон из git (origin/main), а не
из ручного манифеста, который отставал и по построению не мог покраснеть
(#2683, живой инцидент 15.08 — коллизия 264_ между двумя независимыми ветками).
lekss361 merged commit a617ec7939 into main 2026-08-15 20:04:17 +00:00
lekss361 deleted branch fix/2683-manifest-drift 2026-08-15 20:04:18 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2786
No description provided.