ci: смоук периметра МЕРЫ запускается сразу после деплоя
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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 1m14s
CI / openapi-codegen-check (pull_request) Successful in 2m8s
CI / backend-tests (pull_request) Successful in 16m22s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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 1m14s
CI / openapi-codegen-check (pull_request) Successful in 2m8s
CI / backend-tests (pull_request) Successful in 16m22s
scripts/smoke-mera-perimeter.sh — единственная проверка, которая видит публичный периметр целиком (короткие адреса, 301 с длинных, публичный API, закрытость B2B-путей на публичном домене). Запускался он только ночным cron'ом 06:17 UTC, поэтому регресс жил до суток, и находил его либо тот же cron, либо владелец — ровно тот сценарий, против которого проверки и писались. Добавлен job `perimeter-smoke` в ОБА пайплайна: конфиг прокси (deploy.yml) и фронт МЕРЫ (deploy-tradein.yml) едут раздельно, сломать периметр может каждый. Отдельный job, а не шаг внутри deploy: «выкатили» и «периметр цел» — два разных вердикта, красный смоук не должен читаться как неудавшийся деплой. Идёт только при deploy.result == 'success' — поверх несостоявшейся выкатки проверять нечего. Перед смоуком — ожидание готовности по ДВУМ признакам (лэндинг 200 и API 401): `up -d --force-recreate` отдаёт управление раньше, чем бэкенд начинает отвечать, и без ожидания смоук ловил бы гонку, а не регресс. По истечении 150 с ожидание не падает, а печатает warning и пускает смоук — иначе «не успел подняться» и «периметр сломан» слились бы в один красный шаг. Дублирование 20 строк в двух пайплайнах осознанное: `workflow_call` под act_runner не гарантирован, а зависимость, которая может молча не сработать, здесь хуже повтора. Заодно: - ci.yml: гейт `bash -n` на все shell-скрипты (14 файлов) — shellcheck'а в репозитории нет, а опечатка в смоуке обнаружилась бы следующим утром и выглядела бы как регресс периметра. Гейт падает, если не нашёл ни одного файла: пустая маска дала бы зелёный шаг, который ничего не проверяет. - perimeter-smoke.yml: push-триггер на сам скрипт — правка проверяется сразу. Проверено: смоук против прода сейчас зелёный целиком, 27 из 27 проверок, то есть в пайплайн въезжает работающий гейт, а не заведомо красный. Опасение из задачи про внешний геокодер проверено по коду и оказалось у́же: `geocoder.suggest` — цепочка «кадастр → DaData → Nominatim → []», каждый внешний тир под `except Exception`, отказ и квота дают 200 с пустым списком. Покраснеть проверка может только на зависании дольше 15 с. Записал это в самом скрипте, ожидание не ослаблял. Closes #2917
This commit is contained in:
parent
b1106916a9
commit
db03e93781
5 changed files with 158 additions and 0 deletions
|
|
@ -113,6 +113,36 @@ jobs:
|
||||||
python3 scripts/check-migration-lock-timeout.py --selftest
|
python3 scripts/check-migration-lock-timeout.py --selftest
|
||||||
python3 scripts/check-migration-lock-timeout.py
|
python3 scripts/check-migration-lock-timeout.py
|
||||||
|
|
||||||
|
- name: "Guard: shell-скрипты синтаксически валидны (#2917)"
|
||||||
|
# Соседям по этому job'у (caddy validate, lock_timeout) — тот же довод:
|
||||||
|
# дёшево, на каждом PR, ловит опечатку до прода.
|
||||||
|
#
|
||||||
|
# ЗАЧЕМ ИМЕННО ЭТО. scripts/smoke-mera-perimeter.sh — единственная
|
||||||
|
# проверка, которая видит публичный периметр МЕРЫ целиком, и до этого
|
||||||
|
# PR она запускалась только ночным cron'ом. Опечатка в ней обнаружилась
|
||||||
|
# бы следующим утром — и выглядела бы как регресс периметра, а не как
|
||||||
|
# сломанный скрипт. Ни один линтер шелла в репозитории не стоит
|
||||||
|
# (shellcheck нет), поэтому берём то, что есть в каждом образе: `bash -n`
|
||||||
|
# разбирает файл, не исполняя его.
|
||||||
|
#
|
||||||
|
# ГРАНИЦА: `bash -n` ловит СИНТАКСИС, а не смысл — неверный URL или
|
||||||
|
# перепутанный ожидаемый код он не увидит. Это не замена прогона,
|
||||||
|
# а защита от того, что скрипт вообще не запустится.
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
found=0
|
||||||
|
for f in $(git ls-files 'scripts/*.sh' 'ops/*.sh' 'ops/**/*.sh'); do
|
||||||
|
found=$((found + 1))
|
||||||
|
bash -n "$f" || { echo "::error file=$f::синтаксическая ошибка в shell-скрипте"; exit 1; }
|
||||||
|
done
|
||||||
|
# Ноль файлов означал бы, что гейт молча ничего не проверяет —
|
||||||
|
# ровно тот случай, когда зелёный шаг не значит ничего (#2871).
|
||||||
|
if [ "$found" -eq 0 ]; then
|
||||||
|
echo "::error::не найдено ни одного .sh — гейт бы прошёл впустую, проверь маску"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ синтаксис проверен у $found shell-скриптов"
|
||||||
|
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -1121,6 +1121,61 @@ jobs:
|
||||||
# неважно, пропущен он (test/build упали) или упал сам (SSH/миграция/
|
# неважно, пропущен он (test/build упали) или упал сам (SSH/миграция/
|
||||||
# health-check/сверка образов #2679). Красная точка встаёт именно там, где
|
# health-check/сверка образов #2679). Красная точка встаёт именно там, где
|
||||||
# решение реально принято, а не там, где она случайно оказалась по цепочке if.
|
# решение реально принято, а не там, где она случайно оказалась по цепочке if.
|
||||||
|
# ── Смоук публичного периметра МЕРЫ после выкатки (#2917) ──────────────────
|
||||||
|
#
|
||||||
|
# ЗАЧЕМ ЗДЕСЬ. scripts/smoke-mera-perimeter.sh — единственная проверка, которая
|
||||||
|
# видит периметр целиком (короткие адреса, 301 с длинных, публичный API,
|
||||||
|
# закрытость B2B-путей на публичном домене). До этого PR он запускался только
|
||||||
|
# по cron'у 06:17 UTC, то есть регресс жил до суток и находил его либо ночной
|
||||||
|
# прогон, либо владелец. Для правки, чья логика живёт в конфиге прокси, это
|
||||||
|
# единственный настоящий гейт — и он был асинхронным.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ ОТДЕЛЬНЫЙ JOB, А НЕ ШАГ В deploy. Вердикты разные: «выкатили» и
|
||||||
|
# «периметр цел» — два разных факта, и красный смоук не должен читаться как
|
||||||
|
# неудавшийся деплой. Деплой к этому моменту уже прошёл; смоук говорит, что
|
||||||
|
# именно получилось.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ ДУБЛИРУЕТСЯ В ДВУХ ПАЙПЛАЙНАХ. Конфиг прокси (deploy.yml) и фронт
|
||||||
|
# МЕРЫ (deploy-tradein.yml) едут раздельно, и сломать периметр может каждый.
|
||||||
|
# `workflow_call` под act_runner не гарантирован, поэтому 20 строк повторены
|
||||||
|
# осознанно вместо зависимости, которая может молча не сработать.
|
||||||
|
perimeter-smoke:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: deploy
|
||||||
|
# Только после РЕАЛЬНОЙ выкатки: при skipped/failed проверять нечего, а
|
||||||
|
# красный смоук поверх несостоявшегося деплоя увёл бы разбор не туда.
|
||||||
|
if: always() && needs.deploy.result == 'success'
|
||||||
|
timeout-minutes: 6
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Дождаться, пока периметр отвечает после пересоздания контейнеров
|
||||||
|
# `up -d --force-recreate` возвращает управление раньше, чем бэкенд
|
||||||
|
# начинает отвечать. Без ожидания смоук ловил бы не регресс, а гонку.
|
||||||
|
# Ждём ДВА признака: лэндинг (Caddy + фронт) и API (бэкенд поднялся) —
|
||||||
|
# одного мало, Caddy отвечает раньше апстрима.
|
||||||
|
run: |
|
||||||
|
set -uo pipefail
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
page=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://meraocenka.ru/ || true)
|
||||||
|
api=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://gendsgn.ru/trade-in/api/v1/me || true)
|
||||||
|
if [ "$page" = "200" ] && [ "$api" = "401" ]; then
|
||||||
|
echo "периметр отвечает (попытка $i): лэндинг $page, API $api"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "ждём готовности, попытка $i/30: лэндинг '${page:-нет ответа}', API '${api:-нет ответа}'"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
# НЕ падаем здесь: вердикт должен вынести смоук, а не таймаут ожидания.
|
||||||
|
# Иначе «не успел подняться» и «периметр сломан» слились бы в один
|
||||||
|
# красный шаг без разбора.
|
||||||
|
echo "::warning::за 150 с периметр так и не ответил ожидаемо — запускаем смоук, его вывод и будет диагнозом"
|
||||||
|
|
||||||
|
- name: Смоук периметра
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/smoke-mera-perimeter.sh
|
||||||
|
./scripts/smoke-mera-perimeter.sh
|
||||||
|
|
||||||
deploy-status:
|
deploy-status:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [test, build-backend, build-frontend, build-browser, deploy]
|
needs: [test, build-backend, build-frontend, build-browser, deploy]
|
||||||
|
|
|
||||||
|
|
@ -749,6 +749,61 @@ jobs:
|
||||||
# если deploy не завершился success — неважно, пропущен он (build упал) или
|
# если deploy не завершился success — неважно, пропущен он (build упал) или
|
||||||
# упал сам (SSH/миграция/health-check). Красная точка встаёт именно там, где
|
# упал сам (SSH/миграция/health-check). Красная точка встаёт именно там, где
|
||||||
# решение реально принято, а не там, где она случайно оказалась по цепочке if.
|
# решение реально принято, а не там, где она случайно оказалась по цепочке if.
|
||||||
|
# ── Смоук публичного периметра МЕРЫ после выкатки (#2917) ──────────────────
|
||||||
|
#
|
||||||
|
# ЗАЧЕМ ЗДЕСЬ. scripts/smoke-mera-perimeter.sh — единственная проверка, которая
|
||||||
|
# видит периметр целиком (короткие адреса, 301 с длинных, публичный API,
|
||||||
|
# закрытость B2B-путей на публичном домене). До этого PR он запускался только
|
||||||
|
# по cron'у 06:17 UTC, то есть регресс жил до суток и находил его либо ночной
|
||||||
|
# прогон, либо владелец. Для правки, чья логика живёт в конфиге прокси, это
|
||||||
|
# единственный настоящий гейт — и он был асинхронным.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ ОТДЕЛЬНЫЙ JOB, А НЕ ШАГ В deploy. Вердикты разные: «выкатили» и
|
||||||
|
# «периметр цел» — два разных факта, и красный смоук не должен читаться как
|
||||||
|
# неудавшийся деплой. Деплой к этому моменту уже прошёл; смоук говорит, что
|
||||||
|
# именно получилось.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ ДУБЛИРУЕТСЯ В ДВУХ ПАЙПЛАЙНАХ. Конфиг прокси (deploy.yml) и фронт
|
||||||
|
# МЕРЫ (deploy-tradein.yml) едут раздельно, и сломать периметр может каждый.
|
||||||
|
# `workflow_call` под act_runner не гарантирован, поэтому 20 строк повторены
|
||||||
|
# осознанно вместо зависимости, которая может молча не сработать.
|
||||||
|
perimeter-smoke:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: deploy
|
||||||
|
# Только после РЕАЛЬНОЙ выкатки: при skipped/failed проверять нечего, а
|
||||||
|
# красный смоук поверх несостоявшегося деплоя увёл бы разбор не туда.
|
||||||
|
if: always() && needs.deploy.result == 'success'
|
||||||
|
timeout-minutes: 6
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Дождаться, пока периметр отвечает после пересоздания контейнеров
|
||||||
|
# `up -d --force-recreate` возвращает управление раньше, чем бэкенд
|
||||||
|
# начинает отвечать. Без ожидания смоук ловил бы не регресс, а гонку.
|
||||||
|
# Ждём ДВА признака: лэндинг (Caddy + фронт) и API (бэкенд поднялся) —
|
||||||
|
# одного мало, Caddy отвечает раньше апстрима.
|
||||||
|
run: |
|
||||||
|
set -uo pipefail
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
page=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://meraocenka.ru/ || true)
|
||||||
|
api=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://gendsgn.ru/trade-in/api/v1/me || true)
|
||||||
|
if [ "$page" = "200" ] && [ "$api" = "401" ]; then
|
||||||
|
echo "периметр отвечает (попытка $i): лэндинг $page, API $api"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "ждём готовности, попытка $i/30: лэндинг '${page:-нет ответа}', API '${api:-нет ответа}'"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
# НЕ падаем здесь: вердикт должен вынести смоук, а не таймаут ожидания.
|
||||||
|
# Иначе «не успел подняться» и «периметр сломан» слились бы в один
|
||||||
|
# красный шаг без разбора.
|
||||||
|
echo "::warning::за 150 с периметр так и не ответил ожидаемо — запускаем смоук, его вывод и будет диагнозом"
|
||||||
|
|
||||||
|
- name: Смоук периметра
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/smoke-mera-perimeter.sh
|
||||||
|
./scripts/smoke-mera-perimeter.sh
|
||||||
|
|
||||||
deploy-status:
|
deploy-status:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build-backend, build-worker, build-frontend, deploy]
|
needs: [build-backend, build-worker, build-frontend, deploy]
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,15 @@ on:
|
||||||
schedule:
|
schedule:
|
||||||
# Раз в сутки, 06:17 UTC — вне пиков, время произвольное.
|
# Раз в сутки, 06:17 UTC — вне пиков, время произвольное.
|
||||||
- cron: '17 6 * * *'
|
- cron: '17 6 * * *'
|
||||||
|
# #2917: правка самого смоука должна проверяться сразу, а не следующим утром.
|
||||||
|
# Проверки read-only (curl по публичным адресам), поэтому прогонять их на
|
||||||
|
# push в main безопасно и дёшево. Синтаксис скрипта отдельно гейтится в
|
||||||
|
# ci.yml на каждом PR — здесь проверяется уже поведение против прода.
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'scripts/smoke-mera-perimeter.sh'
|
||||||
|
- '.forgejo/workflows/perimeter-smoke.yml'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: perimeter-smoke-mera
|
group: perimeter-smoke-mera
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,15 @@ check "meraocenka.ru/_next/image — must 404 (не открываем опти
|
||||||
# `/trade-in/api/*`. Зелёная только первая = API открыт целиком и тест это
|
# `/trade-in/api/*`. Зелёная только первая = API открыт целиком и тест это
|
||||||
# пропустил (ровно та ошибка, ради которой в Caddyfile выбран отдельный
|
# пропустил (ровно та ошибка, ради которой в Caddyfile выбран отдельный
|
||||||
# префикс, а не поимённый проброс v1-путей).
|
# префикс, а не поимённый проброс v1-путей).
|
||||||
|
# ПРО ВНЕШНЮЮ ЗАВИСИМОСТЬ (#2917). Опасение «упадёт DaData — покраснеет
|
||||||
|
# смоук без всякого регресса» проверено по коду и оказалось у́же, чем
|
||||||
|
# звучит: `geocoder.suggest` — это цепочка «кадастровый тир → DaData →
|
||||||
|
# Nominatim → []», и КАЖДЫЙ внешний тир обёрнут в `except Exception`
|
||||||
|
# (services/geocoder.py). Отказ, квота и 5xx провайдера дают пустой список
|
||||||
|
# и HTTP 200 — проверка остаётся зелёной. Покраснеть она может только если
|
||||||
|
# провайдер ВИСНЕТ дольше 15 с (--max-time у curl), то есть на зависании,
|
||||||
|
# а не на отказе. Ослаблять ожидание не стали: 200 здесь проверяет
|
||||||
|
# открытость пути анониму, ради которой проверка и написана.
|
||||||
check_post "meraocenka.ru public suggest — 200 anonymous" "$BASE_MERA/trade-in/api/public/mera/suggest" '{"q":"Малышева"}' 200
|
check_post "meraocenka.ru public suggest — 200 anonymous" "$BASE_MERA/trade-in/api/public/mera/suggest" '{"q":"Малышева"}' 200
|
||||||
check_post "meraocenka.ru public coverage — 200 anonymous" "$BASE_MERA/trade-in/api/public/mera/coverage" '{"lat":56.838,"lon":60.597,"rooms":2,"area_m2":54}' 200
|
check_post "meraocenka.ru public coverage — 200 anonymous" "$BASE_MERA/trade-in/api/public/mera/coverage" '{"lat":56.838,"lon":60.597,"rooms":2,"area_m2":54}' 200
|
||||||
check "meraocenka.ru v1 geocode — must stay 404" "$BASE_MERA/trade-in/api/v1/geocode/suggest?q=test" 404
|
check "meraocenka.ru v1 geocode — must stay 404" "$BASE_MERA/trade-in/api/v1/geocode/suggest?q=test" 404
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue