Быстрый путь «правка только прокси» наконец включается: считаем изменённые файлы сами (#3448) #3465

Merged
bot-backend merged 4 commits from fix/3448-caddy-only-detection into main 2026-09-12 11:43:39 +00:00
Collaborator

Closes #3448

Причина установлена — и она НЕ та, что в гипотезе

Гипотеза issue (пустой github.event.before у мерж-коммита) опровергнута. Улика — лог джобы changes того самого прогона (run 10881, задача 29244, мерж 84920e6c), снятый с диска Forgejo (/data/gitea/actions_log/lekss361/gendesign/3c/29244.log.zst; через API логи недоступны — все actions/* эндпоинты на Forgejo 10.0.3 отдают 404):

Changes will be detected between 204e2e09de940f44bb6bbeb45e38b2b44d710168 and main
[command]/usr/bin/git diff --no-renames --name-status -z 204e2e09..refs/remotes/origin/main
M caddy/sites/apps.caddy
Detected 1 changed files

before валиден, коммит дотянут git fetch --depth=1, дифф верный — ровно один файл. Настоящая причина в следующих строках того же лога:

##[group]Filter caddy = true
Matching files:
caddy/sites/apps.caddy [modified]
##[group]Filter non_caddy = true
Matching files:
caddy/sites/apps.caddy [modified]

Исключённый файл сам себя и «исключил»: dorny/paths-filter склеивает шаблоны ОДНОГО фильтра через some, то есть ИЛИ (src/filter.ts: patterns.some(aPredicate), параметр predicate-quantifier по умолчанию some — проверено в исходнике действия на раннере). Поэтому

non_caddy:
  - '**'
  - '!Caddyfile'
  - '!caddy/**'

значит «подходит под ** ИЛИ не Caddyfile ИЛИ не caddy/**», а ** матчит всё. non_caddy был true всегдаcaddy_only = false всегда → deploy-caddy пропускался с первого дня #2916. В логе самой джобы это видно дословно: Skipping job 'deploy-caddy' due to 'github.event_name == 'push' && needs.changes.outputs.caddy_only == 'true'' — и Forgejo показал этот пропуск зелёным.

Прод подтверждает (только чтение): gendesign-caddy-1 Created 2026-09-11T20:40:14, в логе ровно одно serving initial configuration и ни одного reload.

Что изменено

.forgejo/workflows/deploy.yml — job changes считает список файлов сам: git diff --no-renames --name-only <before> HEAD по явным границам, флаги backend/frontend/infra/caddy_only выводятся из этого списка теми же наборами путей, что были в фильтрах. dorny/paths-filter из этого воркфлоу убран.

Почему не заплатка к фильтрам: predicate-quantifier: every действует на ВЕСЬ блок и сломал бы backend/frontend/infra — фикс снова висел бы на незаметном умолчании чужого действия.

Два требования #3448 закрыты в этой же правке:

  • Решение видно в логе. Шаг печатает базу, список файлов и строку Флаги: backend=… frontend=… infra=… caddy_only=…. Без этого «сработало» и «просто не совпало» выглядят одинаково, а единственным свидетелем остаётся метка Created контейнера на проде.
  • Fail-safe вместо тихого нуля. База не разрешилась (ручной запуск, пустой/нулевой before, коммита нет в клоне) → изменённым считается весь репозиторий, деплой полный. Фолбэка на HEAD^..HEAD намеренно нет: у мерж-коммита он дал бы верный ответ, а у push'а из нескольких коммитов молча урезал бы список и включил быстрый путь там, где приехал бэкенд.

Проверка, которая краснеет при регрессе

backend/tests/ops/test_3448_caddy_only_detection.py — не текстовый гейт: он ИЗВЛЕКАЕТ шаг из deploy.yml и ИСПОЛНЯЕТ его на временном git-репозитории с настоящим мерж-коммитом (случай #3448 дословно), сверяя значения флагов:

  • только caddy/sites/apps.caddycaddy_only=true;
  • caddy + backend → caddy_only=false, backend=true;
  • before пустой / нулевой / ручной запуск → полный деплой, а не пустой список;
  • список файлов и флаги напечатаны.

Плюс отдельная проверка по ВСЕМ воркфлоу репозитория: ни один paths-filter не вычитает пути шаблонами !… без predicate-quantifier: every — это класс бага, а не единственный его случай.

Фальсификация (дефект возвращён руками, git stash не использовался):

5 failed, 9 passed
FAILED test_merge_with_only_caddy_file_takes_the_fast_path
FAILED test_caddy_plus_backend_is_a_full_deploy
FAILED test_missing_base_falls_back_to_full_deploy
FAILED test_decision_is_visible_in_the_log
FAILED test_no_paths_filter_relies_on_exclusion_patterns[deploy.yml]

E  AssertionError: deploy.yml: job changes, фильтр 'non_caddy' вычитает пути шаблонами
   ['!Caddyfile', '!caddy/**'] — при `some` (умолчание) они склеиваются через ИЛИ
   и фильтр становится true ВСЕГДА. Так #2916 не сработал ни разу (#3448).

На исправленном коде: 14 passed.

Критерий приёмки (из #3448)

Следующий мерж с единственным файлом под caddy/** не должен менять docker inspect gendesign-caddy-1 --format '{{.Created}}' (сейчас 2026-09-11T20:40:14.789645422Z), а в логе Caddy должен появиться reload, а не новое serving initial configuration. В логе джобы changes при этом будет видно Флаги: … caddy_only=true.

Раунд 2 — по итогам deep-ревью (коммиты 9a8aaa2d, 2e928c71)

БЛОКЕР: оживший быстрый путь снимал гард свежести :latest

Пока caddy_only был мёртв, любой push шёл полным деплоем, и гард #2950 (job deploy) прикрывал прод по умолчанию. При caddy_only=true джоба deploy пропускается целиком — вместе с гардом. Сценарий: push A правит бэкенд, билды ~6 мин, deploy в очереди; через 2 мин push B правит только caddy/; на Forgejo 10.0.3 ещё не стартовавшая deploy прогона A отменяется даже при cancel-in-progress: false (наблюдение 21.08.2026 10:35:13, шапка scripts/check-latest-image-revision.sh). Дифф A..B — один caddy-файл → быстрый путь → compose pull + up -d не делает никто: прод на старом образе, голова main зелёная.

Закрыто сторожем в ssh-скрипте deploy-caddy до git reset --hard: если прод отстаёт от origin/main не только по Caddyfile/caddy/** — шаг падает и называет файлы. До reset намеренно: при отказе прод-HEAD остаётся честным для следующего прогона.

Чего сторож не ловит (прямо): deploy прогона A, упавшую после git reset --hard (например на миграции). Тогда прод-HEAD уже равен A, контейнеры старые, и следующий caddy-only push пройдёт быстрым путём. Это закрывается только настоящим маркером «что задеплоено» — у Trade-In он есть (/opt/gendesign/.tradein-deployed-sha), у ПТИЦЫ нет; заводить его в этом PR не стал.

Остальное

  • Квотирование путей. git diff --name-only / git ls-files при core.quotePath=true (умолчание) отдают не-ASCII пути закавыченными — backend/<кириллица>.py давал backend=false. Старый paths-filter брал --name-status -z, где квотирования нет: единственное место, где переход на свой diff менял поведение. Добавлен -c core.quotePath=false в обе команды и в сторож.
  • Лок. deploy-caddy делает git reset --hard в /opt/gendesign — теперь берёт тот же flock /var/lock/gendesign-docker-deploy.lock, что и полный деплой (#2950).
  • ci.yml — фильтр backend расширен на три файла (правка списка идёт ТОЛЬКО этой веткой, чтобы параллельные PR не подрались за один список):
    • .forgejo/workflows/ci-tradein.yml — там тоже живёт paths-filter, без этой строки правка с !-шаблоном не запустила бы backend-tests, то есть гейт этого PR не побежал бы на той правке, от которой стережёт;
    • .forgejo/workflows/deploy-metrics.yml и docker-compose.metrics.yml — их читает гейт backend/tests/ops/test_3467_prometheus_reload.py из параллельного PR #3475; без них правка, трогающая только deploy-metrics.yml (скажем, дописывающая || true к шагу перезагрузки Prometheus), даёт backend=false, backend-tests пропускается и регрессия уезжает в main зелёной.
  • Докстринг фикстуры с мержем утверждал защиту, которой у неё нет («дифф последнего коммита на мерж-коммите пуст» — верно для git show, но не для git diff HEAD^ HEAD). Переписан; подмену базы стережёт отдельная фикстура.

Мутационный прогон (8 мутаций, все красные)

M7  CADDY_RE → ^(Caddyfile|caddy)     2 failed  test_paths_that_merely_start_with_caddy_are_not_the_fast_path[caddy-extra/x.txt, Caddyfile.bak]
M1  снять проверку N -gt 0            1 failed  test_empty_diff_is_not_the_fast_path
M5  выкинуть data/sql из backend      1 failed  test_data_sql_counts_as_backend
M9  база → HEAD^ вместо before        1 failed  test_multi_commit_push_is_not_truncated
Q2  убрать core.quotePath=false       1 failed  test_non_ascii_path_is_classified
B1  убрать гард отставания прода      2 failed  test_fast_path_{allowed_when_prod_lags_only_by_proxy_config,refuses_when_prod_lags_by_code}
B2  гард ПОСЛЕ git reset              2 failed  (те же — сторож обязан стоять до reset)
B3  убрать flock в deploy-caddy       1 failed  test_fast_path_takes_the_same_host_lock

Чистое состояние — 18 passed. Сторож не текстовая проверка: его кусок извлекается из ssh-скрипта и исполняется на временном репозитории, где прод-дерево отстаёт от origin/main.

Проследить первый caddy-only push после мержа

Это будет первое в истории срабатывание deploy-caddy. По логу джобы changes теперь видно решение (база, список файлов, строка Флаги: … caddy_only=true), то есть «сработало» отличимо от «просто не совпало» ещё до того, как смотреть на прод.

🤖 Generated with Claude Code

Closes #3448 ## Причина установлена — и она НЕ та, что в гипотезе Гипотеза issue (пустой `github.event.before` у мерж-коммита) **опровергнута**. Улика — лог джобы `changes` того самого прогона (run 10881, задача 29244, мерж `84920e6c`), снятый с диска Forgejo (`/data/gitea/actions_log/lekss361/gendesign/3c/29244.log.zst`; через API логи недоступны — все `actions/*` эндпоинты на Forgejo 10.0.3 отдают 404): ``` Changes will be detected between 204e2e09de940f44bb6bbeb45e38b2b44d710168 and main [command]/usr/bin/git diff --no-renames --name-status -z 204e2e09..refs/remotes/origin/main M caddy/sites/apps.caddy Detected 1 changed files ``` `before` валиден, коммит дотянут `git fetch --depth=1`, дифф верный — ровно один файл. Настоящая причина в следующих строках того же лога: ``` ##[group]Filter caddy = true Matching files: caddy/sites/apps.caddy [modified] ##[group]Filter non_caddy = true Matching files: caddy/sites/apps.caddy [modified] ``` Исключённый файл сам себя и «исключил»: `dorny/paths-filter` склеивает шаблоны ОДНОГО фильтра через `some`, то есть ИЛИ (`src/filter.ts`: `patterns.some(aPredicate)`, параметр `predicate-quantifier` по умолчанию `some` — проверено в исходнике действия на раннере). Поэтому ```yaml non_caddy: - '**' - '!Caddyfile' - '!caddy/**' ``` значит «подходит под `**` ИЛИ не Caddyfile ИЛИ не caddy/\*\*», а `**` матчит всё. `non_caddy` был **true всегда** → `caddy_only` = false всегда → `deploy-caddy` пропускался с первого дня #2916. В логе самой джобы это видно дословно: `Skipping job 'deploy-caddy' due to 'github.event_name == 'push' && needs.changes.outputs.caddy_only == 'true''` — и Forgejo показал этот пропуск зелёным. Прод подтверждает (только чтение): `gendesign-caddy-1` Created `2026-09-11T20:40:14`, в логе ровно одно `serving initial configuration` и ни одного reload. ## Что изменено `.forgejo/workflows/deploy.yml` — job `changes` считает список файлов сам: `git diff --no-renames --name-only <before> HEAD` по явным границам, флаги `backend`/`frontend`/`infra`/`caddy_only` выводятся из этого списка теми же наборами путей, что были в фильтрах. `dorny/paths-filter` из этого воркфлоу убран. Почему не заплатка к фильтрам: `predicate-quantifier: every` действует на ВЕСЬ блок и сломал бы `backend`/`frontend`/`infra` — фикс снова висел бы на незаметном умолчании чужого действия. Два требования #3448 закрыты в этой же правке: * **Решение видно в логе.** Шаг печатает базу, список файлов и строку `Флаги: backend=… frontend=… infra=… caddy_only=…`. Без этого «сработало» и «просто не совпало» выглядят одинаково, а единственным свидетелем остаётся метка `Created` контейнера на проде. * **Fail-safe вместо тихого нуля.** База не разрешилась (ручной запуск, пустой/нулевой `before`, коммита нет в клоне) → изменённым считается весь репозиторий, деплой полный. Фолбэка на `HEAD^..HEAD` намеренно нет: у мерж-коммита он дал бы верный ответ, а у push'а из нескольких коммитов молча урезал бы список и включил быстрый путь там, где приехал бэкенд. ## Проверка, которая краснеет при регрессе `backend/tests/ops/test_3448_caddy_only_detection.py` — не текстовый гейт: он ИЗВЛЕКАЕТ шаг из `deploy.yml` и ИСПОЛНЯЕТ его на временном git-репозитории с настоящим **мерж-коммитом** (случай #3448 дословно), сверяя значения флагов: * только `caddy/sites/apps.caddy` → `caddy_only=true`; * caddy + backend → `caddy_only=false`, `backend=true`; * `before` пустой / нулевой / ручной запуск → полный деплой, а не пустой список; * список файлов и флаги напечатаны. Плюс отдельная проверка по ВСЕМ воркфлоу репозитория: ни один `paths-filter` не вычитает пути шаблонами `!…` без `predicate-quantifier: every` — это класс бага, а не единственный его случай. **Фальсификация** (дефект возвращён руками, `git stash` не использовался): ``` 5 failed, 9 passed FAILED test_merge_with_only_caddy_file_takes_the_fast_path FAILED test_caddy_plus_backend_is_a_full_deploy FAILED test_missing_base_falls_back_to_full_deploy FAILED test_decision_is_visible_in_the_log FAILED test_no_paths_filter_relies_on_exclusion_patterns[deploy.yml] E AssertionError: deploy.yml: job changes, фильтр 'non_caddy' вычитает пути шаблонами ['!Caddyfile', '!caddy/**'] — при `some` (умолчание) они склеиваются через ИЛИ и фильтр становится true ВСЕГДА. Так #2916 не сработал ни разу (#3448). ``` На исправленном коде: `14 passed`. ## Критерий приёмки (из #3448) Следующий мерж с единственным файлом под `caddy/**` **не должен менять** `docker inspect gendesign-caddy-1 --format '{{.Created}}'` (сейчас `2026-09-11T20:40:14.789645422Z`), а в логе Caddy должен появиться reload, а не новое `serving initial configuration`. В логе джобы `changes` при этом будет видно `Флаги: … caddy_only=true`. --- ## Раунд 2 — по итогам deep-ревью (коммиты `9a8aaa2d`, `2e928c71`) ### БЛОКЕР: оживший быстрый путь снимал гард свежести `:latest` Пока `caddy_only` был мёртв, любой push шёл полным деплоем, и гард #2950 (job `deploy`) прикрывал прод по умолчанию. При `caddy_only=true` джоба `deploy` пропускается целиком — вместе с гардом. Сценарий: push A правит бэкенд, билды ~6 мин, `deploy` в очереди; через 2 мин push B правит только `caddy/`; на Forgejo 10.0.3 ещё не стартовавшая `deploy` прогона A отменяется **даже при `cancel-in-progress: false`** (наблюдение 21.08.2026 10:35:13, шапка `scripts/check-latest-image-revision.sh`). Дифф A..B — один caddy-файл → быстрый путь → `compose pull` + `up -d` не делает никто: прод на старом образе, голова main зелёная. Закрыто сторожем в ssh-скрипте `deploy-caddy` **до** `git reset --hard`: если прод отстаёт от `origin/main` не только по `Caddyfile`/`caddy/**` — шаг падает и называет файлы. До reset намеренно: при отказе прод-HEAD остаётся честным для следующего прогона. **Чего сторож не ловит (прямо):** `deploy` прогона A, упавшую *после* `git reset --hard` (например на миграции). Тогда прод-HEAD уже равен A, контейнеры старые, и следующий caddy-only push пройдёт быстрым путём. Это закрывается только настоящим маркером «что задеплоено» — у Trade-In он есть (`/opt/gendesign/.tradein-deployed-sha`), у ПТИЦЫ нет; заводить его в этом PR не стал. ### Остальное * **Квотирование путей.** `git diff --name-only` / `git ls-files` при `core.quotePath=true` (умолчание) отдают не-ASCII пути закавыченными — `backend/<кириллица>.py` давал `backend=false`. Старый paths-filter брал `--name-status -z`, где квотирования нет: единственное место, где переход на свой diff менял поведение. Добавлен `-c core.quotePath=false` в обе команды и в сторож. * **Лок.** `deploy-caddy` делает `git reset --hard` в `/opt/gendesign` — теперь берёт тот же `flock /var/lock/gendesign-docker-deploy.lock`, что и полный деплой (#2950). * **ci.yml — фильтр `backend` расширен на три файла** (правка списка идёт ТОЛЬКО этой веткой, чтобы параллельные PR не подрались за один список): * `.forgejo/workflows/ci-tradein.yml` — там тоже живёт paths-filter, без этой строки правка с `!`-шаблоном не запустила бы `backend-tests`, то есть гейт этого PR не побежал бы на той правке, от которой стережёт; * `.forgejo/workflows/deploy-metrics.yml` и `docker-compose.metrics.yml` — их читает гейт `backend/tests/ops/test_3467_prometheus_reload.py` из параллельного PR #3475; без них правка, трогающая только `deploy-metrics.yml` (скажем, дописывающая `|| true` к шагу перезагрузки Prometheus), даёт `backend=false`, `backend-tests` пропускается и регрессия уезжает в main зелёной. * **Докстринг фикстуры с мержем** утверждал защиту, которой у неё нет («дифф последнего коммита на мерж-коммите пуст» — верно для `git show`, но не для `git diff HEAD^ HEAD`). Переписан; подмену базы стережёт отдельная фикстура. ### Мутационный прогон (8 мутаций, все красные) ``` M7 CADDY_RE → ^(Caddyfile|caddy) 2 failed test_paths_that_merely_start_with_caddy_are_not_the_fast_path[caddy-extra/x.txt, Caddyfile.bak] M1 снять проверку N -gt 0 1 failed test_empty_diff_is_not_the_fast_path M5 выкинуть data/sql из backend 1 failed test_data_sql_counts_as_backend M9 база → HEAD^ вместо before 1 failed test_multi_commit_push_is_not_truncated Q2 убрать core.quotePath=false 1 failed test_non_ascii_path_is_classified B1 убрать гард отставания прода 2 failed test_fast_path_{allowed_when_prod_lags_only_by_proxy_config,refuses_when_prod_lags_by_code} B2 гард ПОСЛЕ git reset 2 failed (те же — сторож обязан стоять до reset) B3 убрать flock в deploy-caddy 1 failed test_fast_path_takes_the_same_host_lock ``` Чистое состояние — `18 passed`. Сторож не текстовая проверка: его кусок извлекается из ssh-скрипта и исполняется на временном репозитории, где прод-дерево отстаёт от `origin/main`. ### Проследить первый caddy-only push после мержа Это будет **первое в истории** срабатывание `deploy-caddy`. По логу джобы `changes` теперь видно решение (база, список файлов, строка `Флаги: … caddy_only=true`), то есть «сработало» отличимо от «просто не совпало» ещё до того, как смотреть на прод. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-09-12 09:27:47 +00:00
Быстрый путь caddy_only: считаем изменённые файлы сами, без исключающих шаблонов (#3448)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 13s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m34s
CI / backend-tests (pull_request) Successful in 17m41s
091137a0c5
Быстрый путь «правка ТОЛЬКО прокси» (#2916) не отработал ни разу: мерж
84920e6c, где в диффе один файл caddy/sites/apps.caddy, пересоздал весь стек
ПТИЦЫ (gendesign-caddy-1 Created=2026-09-11T20:40:14, в логе Caddy
"serving initial configuration" — холодный старт, а не reload).

ПРИЧИНА НЕ ТА, ЧТО В ГИПОТЕЗЕ. Гипотеза #3448 — пустой github.event.before у
мерж-коммита — опровергнута логом задачи 29244 (run 10881):

    Changes will be detected between 204e2e09de and main
    git diff --no-renames --name-status -z 204e2e09..refs/remotes/origin/main
    M caddy/sites/apps.caddy
    Detected 1 changed files

before валиден, коммит дотянут, дифф верный. Дальше в том же логе:

    ##[group]Filter non_caddy = true
    Matching files:
    caddy/sites/apps.caddy [modified]

Исключённый файл сам себя и «исключил». dorny/paths-filter склеивает шаблоны
одного фильтра через some, то есть ИЛИ (src/filter.ts: patterns.some(aPredicate),
predicate-quantifier по умолчанию some), поэтому

    non_caddy: ['**', '!Caddyfile', '!caddy/**']

читается как «подходит под ** ИЛИ не Caddyfile ИЛИ не caddy/**» — а ** матчит
всё. non_caddy был true ВСЕГДА, caddy_only — false всегда, deploy-caddy
пропускался. Сигнала не было ни одного: пропущенную джобу Forgejo рисует
зелёной, и «зелёный deploy-caddy» неотличим от невыполненного.

ЧТО СДЕЛАНО. Job changes считает список файлов сам: git diff по явным границам
(before → HEAD), флаги backend/frontend/infra/caddy_only выводятся из этого
списка. Заплатки к фильтрам не годятся: predicate-quantifier: every действует
на ВЕСЬ блок и сломал бы backend/frontend/infra, то есть фикс снова висел бы на
незаметном умолчании.

Шаг ПЕЧАТАЕТ и список файлов, и итоговые флаги — у правки должен быть
наблюдаемый признак, иначе «сработало» и «просто не совпало» выглядят одинаково.
База не разрешилась (ручной запуск, пустой/нулевой before, коммита нет в клоне)
→ изменённым считается весь репозиторий: лишний полный деплой безопаснее
пропущенного. Фолбэка на HEAD^..HEAD намеренно нет — у push'а из нескольких
коммитов он молча урезал бы список и включил быстрый путь там, где приехал бэкенд.

Гейт backend/tests/ops/test_3448_caddy_only_detection.py ИСПОЛНЯЕТ этот шаг на
временном репозитории с настоящим мерж-коммитом и проверяет значения флагов:
только caddy → caddy_only=true; caddy+backend → false; база не разрешилась →
полный деплой; решение видно в логе. Отдельная проверка ловит класс бага во всех
воркфлоу — исключающие шаблоны '!' в любом paths-filter без predicate-quantifier: every.

Приёмка на проде: следующий мерж с единственным файлом под caddy/** не меняет
docker inspect gendesign-caddy-1 --format '{{.Created}}', а в логе Caddy —
reload, а не "serving initial configuration".

Closes #3448

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Light1YT added 2 commits 2026-09-12 10:57:22 +00:00
Оживший быстрый путь снимает гард свежести :latest. Пока caddy_only был мёртв,
любой push шёл полным деплоем, и гард #2950 прикрывал прод по умолчанию. Теперь
при caddy_only=true джоба `deploy` пропускается целиком — вместе с гардом.

Сценарий отказа. Push A правит бэкенд, билды ~6 мин, `deploy` в очереди. Через
2 мин push B правит только caddy/. На Forgejo 10.0.3 ещё не стартовавшая
`deploy` предыдущего прогона отменяется ДАЖЕ при cancel-in-progress: false
(наблюдение 21.08.2026 10:35:13, шапка scripts/check-latest-image-revision.sh;
workflow-level concurrency там не исполняется, см. deploy.yml). Дифф A..B — один
caddy-файл, быстрый путь включается, `compose pull` + `up -d` не делает никто:
прод крутит старый образ, голова main зелёная, сигнала нет.

Сторож в ssh-скрипте deploy-caddy: если прод отстаёт от origin/main не только
по Caddyfile/caddy/**, быстрый путь запрещён, шаг падает и называет файлы.
Стоит ДО `git reset --hard` намеренно — при отказе прод-HEAD остаётся честным
для следующего прогона. У Trade-In для того же заведён отдельный маркер
(/opt/gendesign/.tradein-deployed-sha, deploy-tradein.yml), у ПТИЦЫ маркера нет,
и `git reset` делает прод-HEAD его эквивалентом.

ЧЕГО СТОРОЖ НЕ ЛОВИТ: `deploy`, упавшую ПОСЛЕ `git reset --hard` (например на
миграции). Тогда прод-HEAD уже равен новому коммиту, а контейнеры старые —
это остаётся за настоящим маркером «что задеплоено».

Тот же лок, что и у полного деплоя. deploy-caddy делает `git reset --hard` в
/opt/gendesign, то есть правит прод-дерево — ровно то, что job `deploy`
сериализует через flock /var/lock/gendesign-docker-deploy.lock. Пока путь был
мёртв, сталкиваться было нечему; теперь это первая джоба, трогающая прод-дерево
в обход сериализации.

Квотирование путей. `git diff --name-only` и `git ls-files` при core.quotePath
(умолчание true) отдают не-ASCII пути закавыченными с \NNN-экранированием —
`^backend/` такую строку не матчит, и файл backend/<кириллица>.py дал бы
backend=false. Старый paths-filter брал `--name-status -z`, где квотирования
нет: это единственное место, где переход на свой diff менял поведение. В дереве
такие пути уже живут (docs/Бизнес-план…). Добавлен `-c core.quotePath=false` в
обе команды и в сторож выше.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Гейт #3448: закрыть зелёные мутации, добавить признак непустоты, запускать на ci-tradein.yml
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 16s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m42s
CI / openapi-codegen-check (pull_request) Successful in 2m37s
CI / backend-tests (pull_request) Successful in 19m21s
2e928c715b
Мутационный прогон нашёл пять зелёных мутаций — то есть мест, где логику можно
сломать, а гейт этого не заметит. Закрыты фикстурами, каждая краснеет ровно на
своей мутации:

* CADDY_RE → `^(Caddyfile|caddy)`: тогда `caddy-extra/**` и `Caddyfile.bak`
  дают caddy_only=true — тихий пропуск полного деплоя, против которого весь PR;
* снятие проверки «файлов больше нуля»: пустой дифф формально удовлетворяет
  «ни один файл не лежит вне caddy» и отключает сборку;
* выпадение `data/sql/**` из backend: миграции едут в backend-образе;
* подмена базы на `HEAD^..HEAD`: на ОДНОМ мерж-коммите даёт верный ответ и
  выглядит рабочей, а на push'е из нескольких коммитов теряет первый — фикстура
  «бэкенд-коммит + caddy-коммит» это ловит;
* потеря `core.quotePath=false`: кириллический путь под backend/ выпадает из
  классификации.

Плюс прод-сторож из deploy-caddy: его кусок (от PROD_HEAD до `git reset --hard`)
извлекается из ssh-скрипта и ИСПОЛНЯЕТСЯ на временном репозитории, где прод-дерево
отстаёт от origin/main — отдельно законный случай (отстал только конфиг прокси)
и отказной (отстал бэкенд). Проверяется и порядок: сторож обязан стоять ДО
`git reset`. Команда ищется регуляркой по началу строки, а не подстрокой:
`git reset --hard` упоминается выше в комментариях, и поиск по тексту находил
объяснение вместо кода.

Признак непустоты у проверки исключающих `!`-шаблонов: раньше она бы прошла при
нулевом охвате (переименуют действие, заведут .yaml) — теперь отдельно
утверждается, что хотя бы один шаг paths-filter найден, как это сделано в ci.yml
для shell-гейта. Маска расширена до *.y*ml, параметризация — по найденным шагам.

ci.yml: в фильтр `backend` добавлен `.forgejo/workflows/ci-tradein.yml` — там
тоже живёт paths-filter, и без этой строки правка с `!`-шаблоном не запустила бы
backend-tests, то есть гейт не побежал бы ровно на той правке, от которой стережёт.

Докстринг фикстуры с мержем переписан: он утверждал, что «дифф последнего
коммита» на мерж-коммите даёт пустой список (это верно для `git show`, а не для
`git diff HEAD^ HEAD`) — то есть обещал защиту, которой у этой фикстуры нет.
Теперь там сказано, что подмену базы стережёт отдельная проверка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Light1YT added 1 commit 2026-09-12 11:20:32 +00:00
ci.yml: запускать backend-tests на правках метрик (#3467/#3475)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 19s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m36s
CI / openapi-codegen-check (pull_request) Successful in 2m48s
CI / backend-tests (pull_request) Successful in 18m27s
05959464ae
Гейт backend/tests/ops/test_3467_prometheus_reload.py (едет в PR #3475) читает
.forgejo/workflows/deploy-metrics.yml и docker-compose.metrics.yml. Пока этих
путей нет в фильтре `backend`, правка, трогающая ТОЛЬКО deploy-metrics.yml —
например дописывающая `|| true` к шагу перезагрузки Prometheus, — даёт
backend=false: джоба backend-tests пропускается, гейт не исполняется, регрессия
уезжает в main зелёной. Это ровно тот класс, который осуждает комментарий
двумя абзацами выше в этом же файле: гейт, который не запускается на той самой
правке, от которой стережёт, — украшение.

Список правится одной веткой намеренно: параллельный PR #3475 его не трогает,
иначе две ветки подрались бы за один фильтр.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 8f10ded2a0 into main 2026-09-12 11:43:39 +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#3465
No description provided.