Гейт #3467 больше не слеп к || true и к маунту без :ro (ревью PR #3475)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 15s
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 2m51s
CI / backend-tests (pull_request) Successful in 18m17s

Мутации ревьюера проходили гейт насквозь:

- к шагу `POST /-/reload` дописано `|| true` — все 7 тестов зелёные. Regex
  заканчивался на URL и хвост строки не смотрел; сегодня спасает `set -e`, но
  именно так («зелёная сводка ≠ зелёный выход») в этом репозитории уже ломали
  гейты. Добавлен отрицательный lookahead на `||` в той же строке;
- новый одиночный файловый маунт БЕЗ суффикса `:ro` — тест зелёный, потому что
  распознавание маунта требовало `:ro`. Права к типу маунта отношения не имеют:
  инод он держит одинаково. Требование снято.

Проверка promtool переехала ВЫШЕ `docker compose up -d`: прежнее место (между
подъёмом и перезагрузкой) не спасало от битого конфига, потому что `up -d`
пересоздаёт контейнер при смене образа. Новый тест сторожит и это.

Якоря границ серверной джобы и блока триггеров читаются через find с внятным
assert: при переименовании шага гейт скажет, что сломалось, а не даст ValueError.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
bot-backend 2026-09-12 16:18:45 +05:00
parent c31aff68a9
commit 3aef5c3631
2 changed files with 73 additions and 43 deletions

View file

@ -287,6 +287,43 @@ jobs:
# обязан быть безопасен даже при полном доступе к дашбордам.
bash scripts/setup-metrics-grafana-role.sh
# ── Prometheus: проверить конфигурацию ДО ЛЮБОГО применения ─────
# Тот же порядок, что у Caddy ниже: проверка — здесь, применение —
# только после неё. Стоит ВЫШЕ `up -d` намеренно: `up -d`
# пересоздаёт контейнер при смене образа, и тогда битый конфиг
# положил бы Prometheus раньше, чем деплой успел бы покраснеть.
# Сама перезагрузка — в конце, после приёмки: дёргать ручку
# раньше, чем процесс отвечает, нечем.
#
# ЗАЧЕМ ЭТО ВООБЩЕ. Правила подключены КАТАЛОГОМ, поэтому новый
# текст контейнер видит сразу — но Prometheus читает правила только
# при старте либо по явной перезагрузке, а деплой не делал ни того,
# ни другого. 12.09: контейнер работал с 27.08, на диске (и внутри
# контейнера) лежали правила из #3464, а /api/v1/rules отдавал
# прежние. Зелёный деплой, новый файл на диске, старое поведение —
# отказ без единого отрицательного признака. Значит и все прежние
# правки ops/metrics/prometheus/** вступали в силу лишь тогда, когда
# контейнер пересоздавался по какой-то посторонней причине.
#
# Проверяем ОДНОРАЗОВЫМ контейнером того же образа по файлам С
# ДИСКА, а не `docker exec` в работающий: prometheus.yml подключён
# бинд-маунтом ОДНОГО файла, и работающий контейнер после `git
# reset` читает СТАРЫЙ инод — exec проверял бы не тот текст,
# который применится.
#
# Нужны ОБЕ команды: при пустом (или не смонтированном) каталоге
# правил `check config` печатает «0 rule files found» и проходит, а
# `check rules` по явному шаблону в этом случае падает — проверено
# на стенде prom/prometheus:v3.1.0.
PROM_IMAGE=$(grep -oE 'prom/prometheus:[^ ]+' docker-compose.metrics.yml | head -1)
if ! docker run --rm -v "$PWD/ops/metrics/prometheus:/etc/prometheus:ro" \
--entrypoint sh "$PROM_IMAGE" -c \
'promtool check config /etc/prometheus/prometheus.yml && promtool check rules /etc/prometheus/rules/*.yml'; then
echo "ОШИБКА: конфиг или правила Prometheus не проходят проверку — ничего не применяем."
echo "Работающий Prometheus не тронут и остаётся на прежней конфигурации. Чинить и повторять."
exit 1
fi
COMPOSE_PROFILES="$PROFILES" \
docker compose -p gendesign-metrics -f docker-compose.metrics.yml pull --quiet
COMPOSE_PROFILES="$PROFILES" \
@ -317,42 +354,6 @@ jobs:
echo "Alertmanager: контейнер пересоздан — иначе читал бы конфиг по старому иноду."
fi
# ── Prometheus: СНАЧАЛА проверить конфигурацию ─────────────────
# Тот же порядок, что у Caddy ниже: проверка — здесь, применение —
# только после неё (сама перезагрузка стоит в конце, после приёмки:
# дёргать ручку раньше, чем процесс отвечает, нечем).
#
# ЗАЧЕМ ЭТО ВООБЩЕ. Правила подключены КАТАЛОГОМ, поэтому новый
# текст контейнер видит сразу — но Prometheus читает правила только
# при старте либо по явной перезагрузке, а деплой не делал ни того,
# ни другого. 12.09: контейнер работал с 27.08, на диске (и внутри
# контейнера) лежали правила из #3464, а /api/v1/rules отдавал
# прежние. Зелёный деплой, новый файл на диске, старое поведение —
# отказ без единого отрицательного признака. Значит и все прежние
# правки ops/metrics/prometheus/** вступали в силу лишь тогда, когда
# контейнер пересоздавался по какой-то посторонней причине.
#
# Проверяем ОДНОРАЗОВЫМ контейнером того же образа по файлам С
# ДИСКА, а не `docker exec` в работающий, по двум причинам:
# 1) prometheus.yml подключён бинд-маунтом ОДНОГО файла, и
# работающий контейнер после `git reset` читает СТАРЫЙ инод —
# exec проверял бы не тот текст, который применится;
# 2) битый конфиг обязан остановить деплой ДО пересоздания
# контейнера, иначе Prometheus не поднимется вовсе.
#
# Нужны ОБЕ команды: при пустом (или не смонтированном) каталоге
# правил `check config` печатает «0 rule files found» и проходит, а
# `check rules` по явному шаблону в этом случае падает — проверено
# на стенде prom/prometheus:v3.1.0.
PROM_IMAGE=$(grep -oE 'prom/prometheus:[^ ]+' docker-compose.metrics.yml | head -1)
if ! docker run --rm -v "$PWD/ops/metrics/prometheus:/etc/prometheus:ro" \
--entrypoint sh "$PROM_IMAGE" -c \
'promtool check config /etc/prometheus/prometheus.yml && promtool check rules /etc/prometheus/rules/*.yml'; then
echo "ОШИБКА: конфиг или правила Prometheus не проходят проверку — перезагрузка НЕ выполнена."
echo "Работающий Prometheus не тронут и остаётся на прежней конфигурации. Чинить и повторять."
exit 1
fi
# ── Бинд-маунт ОДНОГО файла держит инод ────────────────────────
# Та же ловушка, что у Alertmanager выше и у Alloy в джобах агентов:
# `git reset --hard` пишет изменившийся файл НОВЫМ инодом, а

View file

@ -49,9 +49,14 @@ WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml"
COMPOSE = REPO_ROOT / "docker-compose.metrics.yml"
# Перезагрузка Prometheus: POST на ручку lifecycle из работающего контейнера.
#
# Хвост строки смотрим НАРОЧНО: `|| true` после команды глушит её отказ, и шаг
# остаётся зелёным с неперечитанной конфигурацией — то есть ровно тот разрыв,
# который тут и чинится, только теперь ещё и с «зелёной сводкой». Ревьюер
# #3475 этой мутацией прошёл гейт насквозь.
RELOAD = re.compile(
r"docker exec gendesign-prometheus wget[^\n]*--post-data[^\n]*"
r"http://localhost:9090/-/reload"
r"http://localhost:9090/-/reload(?![^\n]*\|\|)"
)
# Проверка конфигурации: обе команды promtool по файлам с диска.
CHECK = re.compile(
@ -60,8 +65,10 @@ CHECK = re.compile(
)
# Ветка «проверка не прошла» — от `if ! ... promtool ...` до её `fi`.
CHECK_FAILED_BRANCH = re.compile(r"if ! docker run[^\n]*promtool[^\n]*; then(.+?)\n\s*fi\n", re.S)
# Одиночный файловый маунт в серверном стеке: ./ops/metrics/... : /путь/в/контейнере
MOUNT = re.compile(r"-\s+\./(ops/metrics/\S+?):(/\S+?):ro")
# Одиночный файловый маунт в серверном стеке: ./ops/metrics/... : /путь/в/контейнере.
# Суффикс прав (`:ro`) НЕ требуется: инод маунт держит одинаково что на чтение,
# что на запись, а требование `:ro` делало гейт слепым к маунту без него.
MOUNT = re.compile(r"-\s+\./(ops/metrics/[^\s:]+):(/[^\s:]+)")
def _server_script() -> str:
@ -71,8 +78,13 @@ def _server_script() -> str:
«команда целиком» ловил бы только первую строку.
"""
text = WORKFLOW.read_text(encoding="utf-8")
start = text.index("Поднять серверный стек")
end = text.index("# ═══ АГЕНТЫ")
start = text.find("Поднять серверный стек")
end = text.find("# ═══ АГЕНТЫ")
assert start != -1 and end != -1, (
"в deploy-metrics.yml не найдены границы серверной джобы: шаг «Поднять "
"серверный стек» или заголовок «АГЕНТЫ» переименовали — гейт смотрел бы "
"не туда, поправь якоря здесь"
)
return re.sub(r"\s*\\\n\s*", " ", text[start:end])
@ -83,8 +95,9 @@ def test_rules_are_delivered_by_this_workflow() -> None:
запускает: «объявлено» и «исполняется» разошлись бы уже на триггере.
"""
text = WORKFLOW.read_text(encoding="utf-8")
paths = text[text.index("paths:") : text.index("workflow_dispatch")]
assert '"ops/metrics/**"' in paths, (
start, end = text.find("paths:"), text.find("workflow_dispatch")
assert start != -1 and end != -1, "в deploy-metrics.yml не найден блок триггеров paths:"
assert '"ops/metrics/**"' in text[start:end], (
"деплой не запускается на правку ops/metrics/** — правила меняются мимо него"
)
@ -118,6 +131,22 @@ def test_reload_comes_after_config_check() -> None:
)
def test_config_check_precedes_any_application() -> None:
"""Проверка стоит ВЫШЕ `up -d`, а не между подъёмом и перезагрузкой.
`up -d` пересоздаёт контейнер при смене образа: проверка ниже него не
успевает спасти битый конфиг уже применён стартом нового контейнера, и
Prometheus не поднимется вовсе.
"""
script = _server_script()
check = CHECK.search(script)
assert check, "в деплое нет проверки promtool (check config + check rules)"
assert check.start() < script.index("up -d --remove-orphans"), (
"проверка конфигурации стоит после `up -d` — контейнер успеет "
"пересоздаться с битым конфигом раньше, чем деплой покраснеет"
)
def test_failed_check_stops_deploy_without_reload() -> None:
"""Провалившаяся проверка обрывает деплой и НЕ перезагружает Prometheus."""
branch = CHECK_FAILED_BRANCH.search(_server_script())