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
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
_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
This commit is contained in:
parent
306fd82836
commit
2e20b6307b
8 changed files with 270 additions and 392 deletions
|
|
@ -41,9 +41,21 @@ In-app scheduler (`scrape_schedules`, tick 60s, `python -m app.scheduler_main`,
|
||||||
|
|
||||||
`tradein-mvp/backend/data/sql/NN_*.sql` применяется автоматически на деплое через `_schema_migrations`
|
`tradein-mvp/backend/data/sql/NN_*.sql` применяется автоматически на деплое через `_schema_migrations`
|
||||||
в `.forgejo/workflows/deploy-tradein.yml` (НЕ init-only, strict exit-1). Idempotency критична —
|
в `.forgejo/workflows/deploy-tradein.yml` (НЕ init-only, strict exit-1). Idempotency критична —
|
||||||
деструктивный DDL хитит прод на деплое. NN-нумерация уже 3-значная и ИМЕЕТ коллизии (`108_*` ×2,
|
деструктивный DDL хитит прод на деплое.
|
||||||
`084_*` ×2) → перед новым файлом `ls tradein-mvp/backend/data/sql | grep '^NN'` на дубль basename,
|
|
||||||
не доверяй `tail`.
|
**Номер новой миграции сверяй с `origin/main`, не с локальным `ls`** — локальное дерево не видит
|
||||||
|
миграций, смерженных после ветвления (так разъехались 212 в #2682 и 234 в #2754):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin main
|
||||||
|
git ls-tree -r --name-only origin/main -- tradein-mvp/backend/data/sql | tail
|
||||||
|
```
|
||||||
|
|
||||||
|
`-r` обязателен — без него `ls-tree` печатает сам каталог одной строкой, а не файлы.
|
||||||
|
|
||||||
|
Правило целиком — в докстринге `tradein-mvp/backend/tests/test_migration_numbering.py` (единственная
|
||||||
|
формулировка контракта, #2683); он же гейтит его в CI. Дописывать имя в какой-либо список НЕ надо:
|
||||||
|
`_manifest_applied.txt` удалён — он отставал и по построению не мог покраснеть.
|
||||||
|
|
||||||
## Rapid-merge trap
|
## Rapid-merge trap
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,21 @@ jobs:
|
||||||
CI_PG: ci-pg-tradein-${{ github.run_id }}
|
CI_PG: ci-pg-tradein-${{ github.run_id }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# ПОЛНАЯ история, а не дефолтный depth=1 (#2683).
|
||||||
|
# tests/test_migration_numbering.py сверяет номер новой миграции с
|
||||||
|
# origin/main и точкой ветвления — на shallow-клоне нет ни того, ни
|
||||||
|
# другого, и гейт красится «нет git-эталона». Это намеренно: молча
|
||||||
|
# пропущенная проверка и есть тот зелёный, который ничего не проверяет.
|
||||||
|
# Пак репозитория ~33 MiB, полный fetch дешевле разбора коллизии на проде.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Дотянуть origin/main (эталон номеров миграций)
|
||||||
|
# checkout выкачивает ТОЛЬКО ref события: на pull_request это
|
||||||
|
# refs/pull/N/head (merge-ref этот Forgejo не публикует — ls-remote видит
|
||||||
|
# 1620 */head и ноль */merge), так что ветки main в клоне нет вовсе.
|
||||||
|
# Без этого шага сверять номера не с чем.
|
||||||
|
run: git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||||
|
|
||||||
- name: Поднять Postgres и собрать схему tradein
|
- name: Поднять Postgres и собрать схему tradein
|
||||||
working-directory: .
|
working-directory: .
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,13 @@ jobs:
|
||||||
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Как в ci-tradein.yml: tests/test_migration_numbering.py (#2683) требует
|
||||||
|
# origin/main и общего предка с HEAD. На depth=1 их нет, и гейт красный.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Дотянуть origin/main (эталон номеров миграций)
|
||||||
|
run: git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
# Официальный standalone-инсталлер: системный `pip install uv` на
|
# Официальный standalone-инсталлер: системный `pip install uv` на
|
||||||
|
|
@ -396,8 +403,9 @@ jobs:
|
||||||
# Tracking через _schema_migrations (порт паттерна из deploy.yml):
|
# Tracking через _schema_migrations (порт паттерна из deploy.yml):
|
||||||
# каждый .sql применяется РОВНО один раз, failed migration → exit 1
|
# каждый .sql применяется РОВНО один раз, failed migration → exit 1
|
||||||
# (никаких swallowed errors). cwd = /opt/gendesign/tradein-mvp.
|
# (никаких swallowed errors). cwd = /opt/gendesign/tradein-mvp.
|
||||||
# NB: цикл берёт только *.sql — data/sql/_manifest_applied.txt (инвариант
|
# ИМЕННО ЭТОТ цикл делает main эталоном применённого: всё, что доехало
|
||||||
# #2216) glob'ом не подхватывается.
|
# до main, здесь и применяется, а имя закрепляется в _schema_migrations.
|
||||||
|
# На этом стоит гейт номеров — tests/test_migration_numbering.py (#2683).
|
||||||
|
|
||||||
# Pre-existence detection ДО CREATE TABLE: если таблицы ещё нет, это
|
# Pre-existence detection ДО CREATE TABLE: если таблицы ещё нет, это
|
||||||
# первый deploy после внедрения tracking на уже-наполненной prod-БД
|
# первый deploy после внедрения tracking на уже-наполненной prod-БД
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"""Инварианты миграций БД `auth` (data/sql/auth/*.sql) + её bootstrap (ops/db-bootstrap/*.sql).
|
"""Инварианты миграций БД `auth` (data/sql/auth/*.sql) + её bootstrap (ops/db-bootstrap/*.sql).
|
||||||
|
|
||||||
Прецедента manifest-теста для КОРНЕВОГО data/sql в этом репозитории нет (он есть только
|
Снимка «уже применённого» здесь нет и не нужно: у соседнего стека такой файл-список был
|
||||||
в tradein: tradein-mvp/backend/tests/test_migrations_manifest.py по
|
(tradein data/sql/_manifest_applied.txt) и его удалили в #2683 — он отставал от каталога
|
||||||
tradein-mvp/backend/data/sql/_manifest_applied.txt). Заводить manifest на 154 legacy-файла
|
и по построению не мог покраснеть. Аналог гейта для tradein теперь берёт эталон из git:
|
||||||
корневого каталога — не задача этого PR, поэтому здесь проверяются инварианты, которые
|
tradein-mvp/backend/tests/test_migration_numbering.py. Здесь же проверяются инварианты,
|
||||||
можно проверить БЕЗ снимка «уже применённого»: они выполнимы на новом каталоге с первого
|
выполнимые БЕЗ всякого эталона: они верны на новом каталоге с первого дня и ловят
|
||||||
дня и ловят регрессии, которые иначе всплывают только на проде во время деплоя.
|
регрессии, которые иначе всплывают только на проде во время деплоя.
|
||||||
|
|
||||||
Тест не требует БД — только чтение файлов.
|
Тест не требует БД — только чтение файлов.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
# _manifest_applied.txt — CONTRACT (issue #2216)
|
|
||||||
#
|
|
||||||
# Отсортированный список ВСЕХ имён миграций (bare filename) в data/sql/,
|
|
||||||
# которые на момент коммита уже применены/забейслайнены на проде.
|
|
||||||
# Прод трекает миграции по bare-filename в public._schema_migrations —
|
|
||||||
# переименование или удаление применённого файла => повторный прогон на
|
|
||||||
# проде (новый filename считается неприменённым) => дубль-эффекты/ошибки.
|
|
||||||
#
|
|
||||||
# ПРАВИЛА (enforced tests/test_migrations_manifest.py):
|
|
||||||
# 1. Каждое имя здесь ОБЯЗАНО существовать в data/sql/ (нельзя rename/rm applied).
|
|
||||||
# 2. Новый .sql-файл => НЕ переиспользуй NN-префикс (кроме 6 grandfathered дублей).
|
|
||||||
# 3. Добавляя новую миграцию — допиши её имя сюда В ТОМ ЖЕ PR (список отсортирован).
|
|
||||||
#
|
|
||||||
# Комментарии (# ...) и пустые строки тест игнорирует.
|
|
||||||
001_trade_in_estimates.sql
|
|
||||||
002_core_tables.sql
|
|
||||||
003_seed_deals.sql
|
|
||||||
004_extend_trade_in_estimates.sql
|
|
||||||
005_geocode_tracking.sql
|
|
||||||
007_estimate_photos.sql
|
|
||||||
008_crm_fields.sql
|
|
||||||
009_houses.sql
|
|
||||||
010_houses_alter.sql
|
|
||||||
011_listings_alter.sql
|
|
||||||
012_sellers.sql
|
|
||||||
013_listings_alter_seller.sql
|
|
||||||
014_house_reviews.sql
|
|
||||||
015_scrape_runs.sql
|
|
||||||
016_listings_snapshots.sql
|
|
||||||
017_house_placement_history.sql
|
|
||||||
018_avito_imv_evaluations.sql
|
|
||||||
019_listings_alter_cian.sql
|
|
||||||
020_houses_alter_cian.sql
|
|
||||||
021_management_companies.sql
|
|
||||||
022_agents_table.sql
|
|
||||||
023_offer_price_history.sql
|
|
||||||
024_houses_price_dynamics.sql
|
|
||||||
025_house_reliability_checks.sql
|
|
||||||
026_external_valuations.sql
|
|
||||||
027_cian_session_cookies.sql
|
|
||||||
028_matching_tables.sql
|
|
||||||
029_extend_matching_valuation_dynamics.sql
|
|
||||||
030_avito_imv_cache_key_unique.sql
|
|
||||||
031_houses_alter_yandex.sql
|
|
||||||
032_yandex_history.sql
|
|
||||||
033_listings_alter_yandex.sql
|
|
||||||
034_trade_in_estimates_geom.sql
|
|
||||||
035_drop_duplicate_indexes.sql
|
|
||||||
040_houses_extend.sql
|
|
||||||
041_house_sources_noop.sql
|
|
||||||
042_listing_sources_price_divergence_idx.sql
|
|
||||||
043_house_reviews_extend.sql
|
|
||||||
044_external_valuations_link.sql
|
|
||||||
045_house_placement_history_extend.sql
|
|
||||||
046_views.sql
|
|
||||||
047_cian_history_sanitize.sql
|
|
||||||
050_search_optimization.sql
|
|
||||||
051_scrape_runs_extend.sql
|
|
||||||
052_scrape_schedules.sql
|
|
||||||
053_scraper_settings.sql
|
|
||||||
054_scraper_settings_global.sql
|
|
||||||
060_postgres_fdw_extension.sql
|
|
||||||
061_drop_legacy_cad_buildings.sql
|
|
||||||
062_clean_avito_addresses.sql
|
|
||||||
063_backfill_houses_and_link_listings.sql
|
|
||||||
064_house_imv_phase_c.sql
|
|
||||||
065_trade_in_estimates_floor_optional.sql
|
|
||||||
066_address_mismatch_audit.sql
|
|
||||||
067_v_street_sales_vs_listings.sql
|
|
||||||
068_drop_v_street_sales_vs_listings.sql
|
|
||||||
069_trade_in_estimates_dadata_fields.sql
|
|
||||||
070_houses_dadata_enrichment.sql
|
|
||||||
071_houses_cian_zhk_url.sql
|
|
||||||
072_scrape_schedules_seed_cian_rosreestr.sql
|
|
||||||
073_normalize_repair_state.sql
|
|
||||||
075_backfill_repair_state_from_description.sql
|
|
||||||
076_account_estimate_quota.sql
|
|
||||||
077_dedup_hash_plain_key_backfill.sql
|
|
||||||
078_scrape_schedules_seed_yandex_sweep.sql
|
|
||||||
079_listing_source_history.sql
|
|
||||||
080_asking_to_sold_ratios.sql
|
|
||||||
081_trade_in_estimates_expected_sold.sql
|
|
||||||
082_scrape_schedules_seed_ratio_refresh.sql
|
|
||||||
083_trade_in_estimates_created_by.sql
|
|
||||||
084_brand_praktika_fill.sql
|
|
||||||
084_scrape_schedules_seed_n1_sweep.sql
|
|
||||||
085_quarter_price_index_fdw.sql
|
|
||||||
086_deals_address_trgm_index.sql
|
|
||||||
087_fdw_server_options.sql
|
|
||||||
088_scrape_schedules_seed_search_matview_refresh.sql
|
|
||||||
089_listings_geo_precision.sql
|
|
||||||
090_scrape_schedules_seed_deactivate_stale_avito.sql
|
|
||||||
091_scrape_schedules_seed_yandex_address_backfill.sql
|
|
||||||
092_sber_price_index.sql
|
|
||||||
093_scrape_schedules_seed_sber_index_pull.sql
|
|
||||||
094_cadastral_unify.sql
|
|
||||||
095_dead_schema.sql
|
|
||||||
096_scrape_schedules_seed_rosreestr_quarter_poll.sql
|
|
||||||
097_index_hygiene.sql
|
|
||||||
098_asking_to_sold_ratios_tiered.sql
|
|
||||||
099_brand_praktika_logo_wordmark.sql
|
|
||||||
100_enable_deactivate_stale_avito.sql
|
|
||||||
101_gendesign_reader_role.sql
|
|
||||||
102_grant_listings_gendesign_reader.sql
|
|
||||||
103_scrape_schedules_seed_newbuilding_enrich.sql
|
|
||||||
104_index_hygiene_geom_dedup.sql
|
|
||||||
105_market_schema_yandex_enrichment.sql
|
|
||||||
106_scrape_schedules_seed_yandex_newbuilding_sweep.sql
|
|
||||||
107_scrape_schedules_seed_cian_city_sweep.sql
|
|
||||||
108_clean_avito_addresses_v2.sql
|
|
||||||
108_merge_duplicate_houses.sql
|
|
||||||
109_asking_to_sold_ratio_segment_filter.sql
|
|
||||||
110_scrape_schedules_seed_geocode_missing_listings.sql
|
|
||||||
111_listings_avito_detail_fields.sql
|
|
||||||
112_scrape_schedules_seed_avito_detail_backfill.sql
|
|
||||||
113_deactivate_ghost_duplicate_listings.sql
|
|
||||||
113_yandex_detail_backfill.sql
|
|
||||||
114_disable_n1_sweep.sql
|
|
||||||
115_scrape_schedules_seed_deactivate_stale_yandex_cian.sql
|
|
||||||
116_offer_price_history_change_trigger.sql
|
|
||||||
117_listings_last_seen_deactivate_index.sql
|
|
||||||
118_enable_cian_city_sweep.sql
|
|
||||||
119_yandex_city_sweep_center_combos.sql
|
|
||||||
120_restore_partial_active_indexes.sql
|
|
||||||
121_remove_brand_praktika.sql
|
|
||||||
121_yandex_rich_fields.sql
|
|
||||||
122_enable_domclick_city_sweep.sql
|
|
||||||
123_avito_newbuilding_sweep_schedule.sql
|
|
||||||
124_cad_buildings_local.sql
|
|
||||||
124_deglue_avito_addresses.sql
|
|
||||||
125_scrape_schedules_seed_cadastral_geo_match.sql
|
|
||||||
126_scrape_schedules_seed_cian_full_load.sql
|
|
||||||
127_scrape_schedules_seed_avito_full_load.sql
|
|
||||||
128_listings_card_hash.sql
|
|
||||||
129_avito_full_load_incremental_split.sql
|
|
||||||
130_backfill_listings_house_id_fk.sql
|
|
||||||
130_ekb_geoportal_buildings.sql
|
|
||||||
131_fix_diff_percent_overflow.sql
|
|
||||||
132_scrape_schedules_seed_house_imv.sql
|
|
||||||
133_listings_uq_source_source_id.sql
|
|
||||||
134_listings_geom_geography_gist.sql
|
|
||||||
135_scrape_schedules_seed_house_dedup_merge.sql
|
|
||||||
136_backfill_listings_house_id_fk_source_identity.sql
|
|
||||||
137_listings_addr_norm_trgm.sql
|
|
||||||
138_domclick_bff_rewrite_schedule.sql
|
|
||||||
139_premium_houses.sql
|
|
||||||
140_yandex_house_type_backfill.sql
|
|
||||||
141_cian_promote_house_type.sql
|
|
||||||
142_premium_buildings_curated.sql
|
|
||||||
143_building_sale_share_schema.sql
|
|
||||||
144_gar_canon_addr_match.sql
|
|
||||||
145_building_sale_share_plausible_denom.sql
|
|
||||||
146_sale_share_45d_and_zhkh_denom.sql
|
|
||||||
147_canon_strip_geo_prefixes.sql
|
|
||||||
148_dedup_apartments_in_sale_share.sql
|
|
||||||
149_zhkh_priority_denominator.sql
|
|
||||||
150_sale_share_listing_geo_filter.sql
|
|
||||||
151_clean_bare_street_aliases.sql
|
|
||||||
152_sale_share_floors_guard.sql
|
|
||||||
153_sale_share_listings_floors_plausibility.sql
|
|
||||||
154_market_contract_views.sql
|
|
||||||
155_reader_grants_to_contract_views.sql
|
|
||||||
156_revoke_raw_from_reader.sql
|
|
||||||
157_scrape_proxies.sql
|
|
||||||
158_seed_proxy_healthcheck_schedule.sql
|
|
||||||
159_houses_fias_idx.sql
|
|
||||||
160_seed_deactivate_stale_domklik_n1.sql
|
|
||||||
161_backfill_scraped_at_active_recent.sql
|
|
||||||
162_seed_deals_freshness_monitor.sql
|
|
||||||
163_disable_deactivate_stale_domklik.sql
|
|
||||||
164_yandex_url_canonicalize_active_dups.sql
|
|
||||||
165_remove_n1_source.sql
|
|
||||||
166_purge_listings_phones.sql
|
|
||||||
167_drop_client_pii.sql
|
|
||||||
168_fdw_osm_poi_ekb.sql
|
|
||||||
169_osm_poi_ekb_local.sql
|
|
||||||
170_scrape_schedules_seed_osm_poi_ekb_refresh.sql
|
|
||||||
171_scrape_schedules_seed_geoportal_coords_backfill.sql
|
|
||||||
172_trade_in_leads.sql
|
|
||||||
173_scrape_proxies_add_domclick_affinity.sql
|
|
||||||
174_domclick_session_cookies.sql
|
|
||||||
175_scrape_schedules_seed_domclick_detail_backfill.sql
|
|
||||||
176_domrf_kapremont.sql
|
|
||||||
177_deals_city_region.sql
|
|
||||||
178_deal_city_price_bands.sql
|
|
||||||
179_scrape_schedules_seed_oblast_city_sweeps.sql
|
|
||||||
180_seed_sber_freshness_monitor.sql
|
|
||||||
181_clamp_bad_listing_dates.sql
|
|
||||||
182_trade_in_leads_consent_proof.sql
|
|
||||||
183_reenable_deactivate_stale_domklik.sql
|
|
||||||
184_user_events.sql
|
|
||||||
185_account_quota_overrides.sql
|
|
||||||
186_tg_support.sql
|
|
||||||
187_web_support_chat.sql
|
|
||||||
188_tg_support_chat_id_scope.sql
|
|
||||||
189_account_estimate_usage_nonnegative.sql
|
|
||||||
190_sale_share_price_bucket_signature.sql
|
|
||||||
191_account_quota_unlimited_flag.sql
|
|
||||||
192_tradein_users_auth.sql
|
|
||||||
193_tradein_users_seed.sql
|
|
||||||
194_deal_city_price_bands_tiers.sql
|
|
||||||
195_scrape_schedules_seed_deal_city_price_bands_refresh.sql
|
|
||||||
196_listings_city.sql
|
|
||||||
197_backfill_listings_city_from_url.sql
|
|
||||||
198_scrape_proxy_rotations.sql
|
|
||||||
199_scrape_proxies_asocks_rotate_url.sql
|
|
||||||
200_region_code_foreign_cities.sql
|
|
||||||
201_purge_dead_mobileproxy_proxies.sql
|
|
||||||
202_listing_source_snapshot_budget_sec.sql
|
|
||||||
203_purge_geocode_cache_house_letter.sql
|
|
||||||
204_cian_oblast_sweeps_secondary.sql
|
|
||||||
205_sales_vs_listings_city_filter.sql
|
|
||||||
206_scrape_schedules_cut_wasteful_load.sql
|
|
||||||
207_backfill_yandex_cian_city_geo_cleanup.sql
|
|
||||||
208_reenable_domclick_detail_backfill.sql
|
|
||||||
209_scrape_proxies_disabled_reason.sql
|
|
||||||
210_scrape_proxy_source_bans.sql
|
|
||||||
211_sales_vs_listings_segment_guard.sql
|
|
||||||
212_sber_index_pull_weekly.sql
|
|
||||||
213_listings_snapshots_status_vocab.sql
|
|
||||||
214_drop_dead_run_metrics.sql
|
|
||||||
215_avito_full_load_window_matches_cadence.sql
|
|
||||||
216_dead_code_sweep.sql
|
|
||||||
#
|
|
||||||
# 2026-08-06: список догнан до факта прода. Проверка перед правкой —
|
|
||||||
# _schema_migrations на tradein-postgres: 209 применённых имён, здесь было
|
|
||||||
# 178; расхождение — 31 имя, все в одну сторону (применено, но не заморожено).
|
|
||||||
# Обратного расхождения нет: ни одной строки, которой не было бы на проде.
|
|
||||||
#
|
|
||||||
# Тем самым снято отложенное условие из прошлой редакции: 187/188 (веб-чат
|
|
||||||
# поддержки, #2532/#2533) откладывались до подтверждения, что они осели на
|
|
||||||
# проде в финальном виде. Они в _schema_migrations — условие выполнено.
|
|
||||||
#
|
|
||||||
# 217-232 сюда намеренно не дописаны этой миграцией (222/225): в момент
|
|
||||||
# правки они уже слиты в main и применены на проде (см. _schema_migrations),
|
|
||||||
# но их авторы не дописали имена в тот же PR — это чужой пробел, не наш;
|
|
||||||
# self-maintenance-контракт (см. докстринг test_migrations_manifest.py)
|
|
||||||
# требует дописывать только СВОЙ файл в СВОЁМ PR, что и сделано ниже для
|
|
||||||
# 222/225 по прецеденту 233_payments.sql.
|
|
||||||
222_db_audit_cleanup.sql
|
|
||||||
225_listing_source_snapshots_run_id_idx.sql
|
|
||||||
233_payments.sql
|
|
||||||
234_scrape_runs_ban_kind_unknown.sql
|
|
||||||
|
|
@ -52,3 +52,12 @@ tests/test_purge_expired_trade_in_data.py::test_real_purge_deletes_only_anonymou
|
||||||
# дефолт mark_banned ('unknown') проходит CHECK-констрейнт из миграции 234:
|
# дефолт mark_banned ('unknown') проходит CHECK-констрейнт из миграции 234:
|
||||||
# на мок-лэйне (deploy-tradein.yml, DSN-заглушка) констрейнта нет вовсе.
|
# на мок-лэйне (deploy-tradein.yml, DSN-заглушка) констрейнта нет вовсе.
|
||||||
tests/test_2764_ban_kind_no_default.py::test_real_default_ban_kind_survives_the_check_constraint
|
tests/test_2764_ban_kind_no_default.py::test_real_default_ban_kind_survives_the_check_constraint
|
||||||
|
|
||||||
|
# Гейт номеров миграций (#2683) сверяется с origin/main и точкой ветвления. Где
|
||||||
|
# git-эталона нет — прогон внутри prod-образа, экспорт исходников без .git —
|
||||||
|
# проверять не с чем, и тест это ГОВОРИТ вслух вместо тихого зелёного.
|
||||||
|
# В CI пропуска не бывает: при CI/GITHUB_ACTIONS та же ветка делает pytest.fail
|
||||||
|
# (отсутствие эталона в пайплайне — сломанный гейт, а не «нечего проверять»),
|
||||||
|
# а ci-tradein.yml/deploy-tradein.yml тянут main отдельным шагом.
|
||||||
|
tests/test_migration_numbering.py::test_applied_migration_is_not_renamed_or_deleted
|
||||||
|
tests/test_migration_numbering.py::test_new_migration_takes_a_free_number
|
||||||
|
|
|
||||||
215
tradein-mvp/backend/tests/test_migration_numbering.py
Normal file
215
tradein-mvp/backend/tests/test_migration_numbering.py
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
"""Инварианты нумерации миграций trade-in (issues #2216, #2683).
|
||||||
|
|
||||||
|
КОНТРАКТ. ОДНА ФОРМУЛИРОВКА, И ОНА ЗДЕСЬ — больше нигде её дублировать не надо.
|
||||||
|
|
||||||
|
Эталон «что уже закреплено на проде» — origin/main, а не файл-список.
|
||||||
|
deploy-tradein.yml прогоняет КАЖДЫЙ data/sql/*.sql из main под ON_ERROR_STOP
|
||||||
|
(падение миграции => красный деплой) и трекает применённое по bare filename в
|
||||||
|
public._schema_migrations. То есть «файл доехал до main» == «имя закреплено на
|
||||||
|
проде», и вести это знание отдельно от git незачем: git и есть журнал.
|
||||||
|
|
||||||
|
Отсюда ровно два инварианта, и ниже проверяются именно они.
|
||||||
|
|
||||||
|
1. Имя, существовавшее в точке ветвления, нельзя переименовать или удалить.
|
||||||
|
Прод помнит СТАРОЕ имя; новое считается неприменённым и прогоняется
|
||||||
|
повторно — дубль-INSERT / повторный DDL / PK violation под ON_ERROR_STOP,
|
||||||
|
то есть либо красный деплой, либо тихо задвоенные данные. Нужно изменить
|
||||||
|
уже применённую миграцию — заводи НОВЫЙ файл, старый оставь как есть.
|
||||||
|
|
||||||
|
2. НОВЫЙ файл обязан нести NN-префикс, свободный не только в рабочем дереве,
|
||||||
|
но и в origin/main. Порядок применения — `ls | sort`, два файла с одним NN
|
||||||
|
дают неопределённый порядок. Шесть исторических дублей (084/108/113/121/
|
||||||
|
124/130) не «новые» и не флагаются.
|
||||||
|
|
||||||
|
ДОПИСЫВАТЬ НИЧЕГО НЕ НАДО. Автор миграции кладёт файл со свободным номером — и
|
||||||
|
всё. Списка, который можно забыть обновить, здесь больше нет: до #2683 таким
|
||||||
|
списком был data/sql/_manifest_applied.txt, и он по построению не мог
|
||||||
|
покраснеть — «файл, которого нет в списке» и «новый файл этого PR» были для
|
||||||
|
теста одним и тем же, поэтому забытое имя навсегда оставалось зелёным
|
||||||
|
(замер 2026-08-07: 15 забытых имён, сьют зелёный).
|
||||||
|
|
||||||
|
ПОЧЕМУ ТОЧКА ВЕТВЛЕНИЯ, А НЕ САМ origin/main. Ветка, отведённая неделю назад,
|
||||||
|
не содержит миграций, смерженных после неё. Правило «origin/main ⊆ рабочее
|
||||||
|
дерево» красило бы каждую такую ветку без вины автора — и его отключили бы
|
||||||
|
через неделю. Сверка с merge-base ловит ровно то, что удалила или
|
||||||
|
переименовала ЭТА ветка, а номера при этом сверяются с ПОЛНЫМ origin/main,
|
||||||
|
чтобы коллизия с миграцией, смерженной после ветвления, всё-таки нашлась.
|
||||||
|
|
||||||
|
КАК УБЕДИТЬСЯ, ЧТО СТОРОЖ УМЕЕТ КРАСНЕТЬ (не на слово):
|
||||||
|
test_collision_rule_flags_a_taken_number ниже проверяет само правило на
|
||||||
|
литеральных входах, а сквозной прогон воспроизводится так —
|
||||||
|
git worktree add --detach /tmp/wt <sha до нужной миграции>
|
||||||
|
touch /tmp/wt/tradein-mvp/backend/data/sql/<занятый-NN>_probe.sql
|
||||||
|
(cd /tmp/wt/tradein-mvp/backend && pytest tests/test_migration_numbering.py)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_TESTS_DIR = Path(__file__).resolve().parent
|
||||||
|
_SQL_DIR = _TESTS_DIR.parent / "data" / "sql"
|
||||||
|
_REPO_ROOT = _TESTS_DIR.parents[2]
|
||||||
|
# Путь каталога ОТ КОРНЯ РЕПО — им адресуем дерево коммита через git ls-tree.
|
||||||
|
_SQL_PATHSPEC = "tradein-mvp/backend/data/sql"
|
||||||
|
|
||||||
|
_NN_PREFIX = re.compile(r"^(\d+)_")
|
||||||
|
# origin — штатный remote; forgejo/main оставлен как исторический алиас.
|
||||||
|
_MAIN_REFS = ("origin/main", "forgejo/main", "main")
|
||||||
|
|
||||||
|
|
||||||
|
def _git(*args: str) -> str | None:
|
||||||
|
"""stdout git-команды, либо None если git недоступен/команда упала."""
|
||||||
|
try:
|
||||||
|
done = subprocess.run(
|
||||||
|
# Фиксированный argv, без shell — args приходят только из этого модуля.
|
||||||
|
["git", "-C", str(_REPO_ROOT), *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
return done.stdout if done.returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _sql_names_at(rev: str) -> set[str]:
|
||||||
|
"""Bare-имена *.sql в data/sql на ревизии rev."""
|
||||||
|
out = _git("ls-tree", "-r", "-z", "--name-only", rev, "--", _SQL_PATHSPEC) or ""
|
||||||
|
return {PurePosixPath(p).name for p in out.split("\0") if p.endswith(".sql")}
|
||||||
|
|
||||||
|
|
||||||
|
def _sql_names_on_disk() -> set[str]:
|
||||||
|
"""Bare-имена *.sql в рабочем дереве — включая ещё не закоммиченные."""
|
||||||
|
return {p.name for p in _SQL_DIR.glob("*.sql")}
|
||||||
|
|
||||||
|
|
||||||
|
def _prefix(name: str) -> str | None:
|
||||||
|
m = _NN_PREFIX.match(name)
|
||||||
|
return m.group(1) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def _collisions(new_names: Iterable[str], universe: Iterable[str]) -> list[str]:
|
||||||
|
"""Для каждого НОВОГО имени — чужие имена с тем же NN-префиксом."""
|
||||||
|
by_prefix: dict[str, set[str]] = {}
|
||||||
|
for name in universe:
|
||||||
|
p = _prefix(name)
|
||||||
|
if p is not None:
|
||||||
|
by_prefix.setdefault(p, set()).add(name)
|
||||||
|
|
||||||
|
found: list[str] = []
|
||||||
|
for name in sorted(new_names):
|
||||||
|
p = _prefix(name)
|
||||||
|
if p is None:
|
||||||
|
continue
|
||||||
|
others = sorted(by_prefix.get(p, set()) - {name})
|
||||||
|
if others:
|
||||||
|
found.append(f"{name} — номер {p} уже занят: {', '.join(others)}")
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def _baseline() -> tuple[set[str], set[str]]:
|
||||||
|
"""(имена в точке ветвления, имена в main). Без git-эталона проверять нечего."""
|
||||||
|
main_ref = next(
|
||||||
|
(r for r in _MAIN_REFS if _git("rev-parse", "--verify", "--quiet", f"{r}^{{commit}}")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
merge_base = None
|
||||||
|
if main_ref is not None:
|
||||||
|
out = _git("merge-base", main_ref, "HEAD")
|
||||||
|
merge_base = out.strip() if out else None
|
||||||
|
|
||||||
|
if main_ref is None or merge_base is None:
|
||||||
|
why = (
|
||||||
|
f"нет git-эталона миграций: ни один из {_MAIN_REFS} не резолвится либо у него "
|
||||||
|
f"нет общего предка с HEAD (repo={_REPO_ROOT}). Лечится "
|
||||||
|
"`git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main` "
|
||||||
|
"и НЕ shallow-клоном (нужен общий предок)."
|
||||||
|
)
|
||||||
|
# В CI это не «нечего проверять», а сломанный гейт: пропуск здесь и есть
|
||||||
|
# тот зелёный, который ничего не проверяет. Поэтому красим.
|
||||||
|
if os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"):
|
||||||
|
pytest.fail(why)
|
||||||
|
pytest.skip(why)
|
||||||
|
|
||||||
|
base_names = _sql_names_at(merge_base)
|
||||||
|
main_names = _sql_names_at(main_ref)
|
||||||
|
# Анти-вакуум: пустой эталон сделал бы обе проверки зелёными всегда.
|
||||||
|
# Ровно так ломается сторож, если _SQL_PATHSPEC разъедется с раскладкой репо.
|
||||||
|
assert base_names, (
|
||||||
|
f"эталон пуст: git ls-tree {merge_base} -- {_SQL_PATHSPEC} не вернул ни одного "
|
||||||
|
"*.sql. Проверка номеров была бы вакуумно-зелёной — почини путь."
|
||||||
|
)
|
||||||
|
assert main_names, f"в {main_ref} не найдено *.sql по пути {_SQL_PATHSPEC} — то же самое."
|
||||||
|
return base_names, main_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_applied_migration_is_not_renamed_or_deleted() -> None:
|
||||||
|
"""Файл, существовавший в точке ветвления, обязан существовать и сейчас.
|
||||||
|
|
||||||
|
Red => эта ветка переименовала или удалила миграцию, которую прод уже
|
||||||
|
применил и помнит по СТАРОМУ имени. Верни исходное имя; нужно поправить
|
||||||
|
поведение — заводи новый файл с новым номером.
|
||||||
|
"""
|
||||||
|
on_disk = _sql_names_on_disk()
|
||||||
|
assert on_disk, f"не найдено *.sql в {_SQL_DIR}"
|
||||||
|
base_names, _ = _baseline()
|
||||||
|
|
||||||
|
gone = sorted(base_names - on_disk)
|
||||||
|
assert not gone, (
|
||||||
|
f"миграции пропали из {_SQL_PATHSPEC}/ (переименованы или удалены): {gone}. "
|
||||||
|
"Прод трекает их по bare-filename в _schema_migrations — под новым именем "
|
||||||
|
"миграция прогонится повторно. Верни имена как были."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_migration_takes_a_free_number() -> None:
|
||||||
|
"""Новый файл не переиспользует NN, занятый в origin/main или в этой ветке.
|
||||||
|
|
||||||
|
Red => номер уже занят. Возьми следующий свободный, сверяясь с origin/main:
|
||||||
|
|
||||||
|
git fetch origin main
|
||||||
|
git ls-tree -r --name-only origin/main -- tradein-mvp/backend/data/sql | tail
|
||||||
|
|
||||||
|
`-r` обязателен: без него ls-tree печатает сам каталог, а не файлы (в этом
|
||||||
|
виде рецепт и ходил по issue #2683 — и молча возвращал одну строку).
|
||||||
|
Локального `ls` недостаточно: он не видит миграций, смерженных после
|
||||||
|
ветвления — ровно так разъехались 212 в #2682 и 234 в #2754.
|
||||||
|
"""
|
||||||
|
on_disk = _sql_names_on_disk()
|
||||||
|
base_names, main_names = _baseline()
|
||||||
|
|
||||||
|
new_names = on_disk - base_names
|
||||||
|
problems = _collisions(new_names, main_names | on_disk)
|
||||||
|
assert not problems, "коллизия номеров миграций: " + "; ".join(problems)
|
||||||
|
|
||||||
|
|
||||||
|
def test_collision_rule_flags_a_taken_number() -> None:
|
||||||
|
"""Проверка самого правила — сторож обязан уметь краснеть (#2683 п.6).
|
||||||
|
|
||||||
|
Ожидания здесь — ЛИТЕРАЛЫ, а не производные от содержимого data/sql: тест,
|
||||||
|
который берёт ожидание из охраняемой настройки, зелен при любой настройке.
|
||||||
|
"""
|
||||||
|
# Реальный случай #2754: ветка отвелась до того, как в main приехал 234_scrape.
|
||||||
|
assert _collisions(
|
||||||
|
["234_trade_in_estimates_retain_until.sql"],
|
||||||
|
{
|
||||||
|
"233_payments.sql",
|
||||||
|
"234_scrape_runs_ban_kind_unknown.sql",
|
||||||
|
"234_trade_in_estimates_retain_until.sql",
|
||||||
|
},
|
||||||
|
) == [
|
||||||
|
"234_trade_in_estimates_retain_until.sql — номер 234 уже занят: "
|
||||||
|
"234_scrape_runs_ban_kind_unknown.sql"
|
||||||
|
]
|
||||||
|
# Два новых файла с одним номером внутри одной ветки — оба названы.
|
||||||
|
assert len(_collisions(["300_a.sql", "300_b.sql"], {"300_a.sql", "300_b.sql"})) == 2
|
||||||
|
# Свободный номер — тишина; исторические дубли не новые и не флагаются.
|
||||||
|
assert _collisions(["300_a.sql"], {"084_x.sql", "084_y.sql", "300_a.sql"}) == []
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
"""Invariants over data/sql migrations (issue #2216).
|
|
||||||
|
|
||||||
Прод применяет миграции по BARE FILENAME: deploy-tradein.yml трекает каждый
|
|
||||||
`data/sql/*.sql` в таблице `public._schema_migrations` (PRIMARY KEY = filename).
|
|
||||||
Из этого следуют два хрупких инварианта, которые этот тест защищает от регрессии:
|
|
||||||
|
|
||||||
1. Переименование / удаление УЖЕ ПРИМЕНЁННОЙ миграции ломает прод: новый
|
|
||||||
filename считается неприменённым и прогоняется повторно (дубль-INSERT,
|
|
||||||
повторный DDL, PK violation под ON_ERROR_STOP => деплой падает или, хуже,
|
|
||||||
молча дублирует данные). Manifest `data/sql/_manifest_applied.txt` — это
|
|
||||||
слепок применённых имён на момент коммита; любой из них ОБЯЗАН существовать.
|
|
||||||
|
|
||||||
2. Два разных файла с одинаковым NN-префиксом ("дубль-префикс") — источник
|
|
||||||
двусмысленного порядка применения (`ls | sort` детерминирован, но человек
|
|
||||||
легко создаёт коллизию). 6 исторических дублей grandfathered'ы (оба в
|
|
||||||
manifest). Любой НОВЫЙ файл обязан нести уникальный префикс.
|
|
||||||
|
|
||||||
Self-maintenance: добавляя новую миграцию, допиши её имя в manifest В ТОМ ЖЕ PR
|
|
||||||
(см. assert-сообщения ниже). Тест требует data/sql ⊇ manifest и уникальность
|
|
||||||
префикса у новых файлов; сам manifest дополняет автор миграции.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
_BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
_SQL_DIR = _BACKEND_ROOT / "data" / "sql"
|
|
||||||
_MANIFEST = _SQL_DIR / "_manifest_applied.txt"
|
|
||||||
|
|
||||||
_NN_PREFIX = re.compile(r"^(\d+)_")
|
|
||||||
|
|
||||||
|
|
||||||
def _read_manifest() -> list[str]:
|
|
||||||
"""Имена миграций из manifest; # comments и пустые строки игнорируются."""
|
|
||||||
names: list[str] = []
|
|
||||||
for raw in _MANIFEST.read_text(encoding="utf-8").splitlines():
|
|
||||||
line = raw.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
names.append(line)
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
def _actual_sql_files() -> set[str]:
|
|
||||||
return {p.name for p in _SQL_DIR.glob("*.sql")}
|
|
||||||
|
|
||||||
|
|
||||||
def _prefix(name: str) -> str | None:
|
|
||||||
m = _NN_PREFIX.match(name)
|
|
||||||
return m.group(1) if m else None
|
|
||||||
|
|
||||||
|
|
||||||
def test_manifest_entries_all_exist() -> None:
|
|
||||||
"""Каждый файл из manifest СУЩЕСТВУЕТ в data/sql/.
|
|
||||||
|
|
||||||
Red => применённая миграция переименована или удалена. Прод трекает по
|
|
||||||
bare-filename: старое имя остаётся в _schema_migrations, НОВОЕ имя считается
|
|
||||||
неприменённым и прогоняется повторно. Восстанови исходное имя файла (или,
|
|
||||||
если переименование намеренное — так делать НЕЛЬЗЯ для уже-применённых
|
|
||||||
миграций: заведи НОВЫЙ файл, а старый оставь как есть).
|
|
||||||
"""
|
|
||||||
actual = _actual_sql_files()
|
|
||||||
manifest = _read_manifest()
|
|
||||||
missing = sorted(n for n in manifest if n not in actual)
|
|
||||||
assert not missing, (
|
|
||||||
"Миграции из _manifest_applied.txt отсутствуют в data/sql/ "
|
|
||||||
f"(переименованы/удалены?): {missing}. Эти имена уже применены на проде "
|
|
||||||
"(tracking по bare-filename в _schema_migrations) — их нельзя "
|
|
||||||
"переименовывать/удалять. Верни исходные имена файлов."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_manifest_is_sorted_and_unique() -> None:
|
|
||||||
"""Manifest отсортирован (codepoint) и без дублей — детерминированный слепок."""
|
|
||||||
manifest = _read_manifest()
|
|
||||||
assert manifest == sorted(manifest), (
|
|
||||||
"_manifest_applied.txt не отсортирован. Пересортируй записи "
|
|
||||||
"(LC_ALL=C sort / Python sorted())."
|
|
||||||
)
|
|
||||||
dupes = sorted({n for n in manifest if manifest.count(n) > 1})
|
|
||||||
assert not dupes, f"Дублирующиеся строки в _manifest_applied.txt: {dupes}"
|
|
||||||
|
|
||||||
|
|
||||||
def test_new_files_do_not_reuse_prefix() -> None:
|
|
||||||
"""Новые (не в manifest) .sql-файлы НЕ переиспользуют существующий NN-префикс.
|
|
||||||
|
|
||||||
Grandfathered дубли (084/108/113/121/124/130) — оба файла в manifest, поэтому
|
|
||||||
не флагаются: считаются "существующими", а не "новыми".
|
|
||||||
|
|
||||||
Red => новый файл взял префикс уже присутствующей миграции. Присвой
|
|
||||||
следующий свободный NN и допиши имя в _manifest_applied.txt (тот же PR).
|
|
||||||
"""
|
|
||||||
actual = _actual_sql_files()
|
|
||||||
manifest = set(_read_manifest())
|
|
||||||
|
|
||||||
# Префиксы, «занятые» уже-применёнными (manifest) миграциями.
|
|
||||||
baseline_prefixes: set[str] = set()
|
|
||||||
for name in manifest:
|
|
||||||
p = _prefix(name)
|
|
||||||
if p is not None:
|
|
||||||
baseline_prefixes.add(p)
|
|
||||||
|
|
||||||
new_files = sorted(actual - manifest)
|
|
||||||
collisions: list[str] = []
|
|
||||||
# Внутри новых файлов префикс тоже обязан быть уникален (два новых с одним NN).
|
|
||||||
seen_new_prefix: dict[str, str] = {}
|
|
||||||
for name in new_files:
|
|
||||||
p = _prefix(name)
|
|
||||||
if p is None:
|
|
||||||
continue
|
|
||||||
if p in baseline_prefixes:
|
|
||||||
collisions.append(f"{name} (префикс {p} занят применённой миграцией)")
|
|
||||||
elif p in seen_new_prefix:
|
|
||||||
collisions.append(f"{name} (префикс {p} уже у нового {seen_new_prefix[p]})")
|
|
||||||
else:
|
|
||||||
seen_new_prefix[p] = name
|
|
||||||
|
|
||||||
assert not collisions, (
|
|
||||||
"Новые миграции переиспользуют NN-префикс: " + "; ".join(collisions) + ". "
|
|
||||||
"Присвой следующий свободный номер и добавь имя файла в "
|
|
||||||
"_manifest_applied.txt в ЭТОМ ЖЕ PR."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_manifest_covers_all_but_new_files() -> None:
|
|
||||||
"""data/sql ⊇ manifest, и каждый новый файл имеет уникальный префикс —
|
|
||||||
напоминание о self-maintenance: manifest дополняется вместе с миграцией.
|
|
||||||
|
|
||||||
Этот тест НЕ требует, чтобы новый файл уже был в manifest (иначе PR с новой
|
|
||||||
миграцией всегда красный). Он лишь гарантирует, что manifest не отстал от
|
|
||||||
реальности В ЧАСТИ применённых имён (см. test_manifest_entries_all_exist) и
|
|
||||||
что новые файлы не создают префикс-коллизий (см. предыдущий тест).
|
|
||||||
"""
|
|
||||||
# Sanity: manifest непустой и в data/sql есть файлы — защита от битых путей.
|
|
||||||
assert _actual_sql_files(), f"Не найдено *.sql в {_SQL_DIR}"
|
|
||||||
assert _read_manifest(), f"_manifest_applied.txt пуст: {_MANIFEST}"
|
|
||||||
Loading…
Add table
Reference in a new issue