Compare commits
No commits in common. "d6b43c61007d5cec71e805a9d80cb69d96ab8161" and "84920e6cbdce263822d2b2e95ae24db9da2b6589" have entirely different histories.
d6b43c6100
...
84920e6cbd
2 changed files with 0 additions and 205 deletions
|
|
@ -98,24 +98,6 @@ jobs:
|
||||||
python3 scripts/check-compose-ambiguous-hosts.py --selftest
|
python3 scripts/check-compose-ambiguous-hosts.py --selftest
|
||||||
python3 scripts/check-compose-ambiguous-hosts.py
|
python3 scripts/check-compose-ambiguous-hosts.py
|
||||||
|
|
||||||
- name: "Guard: подмена фронта МЕРЫ без окна недоступности (#3274)"
|
|
||||||
# Тем же шагом-соседом и по той же причине: секунды на PR, падение
|
|
||||||
# блокирует merge.
|
|
||||||
#
|
|
||||||
# ЗАЧЕМ. Публичный лендинг лежал 30–90 с на КАЖДОМ деплое МЕРЫ —
|
|
||||||
# не потому, что подмена контейнера медленная (0,5 с), а потому, что
|
|
||||||
# `up -d` со списком сервисов делает create всех (старые контейнеры
|
|
||||||
# УДАЛЯЮТСЯ) и только потом start, дождавшись зависимостей. Лечение —
|
|
||||||
# две половинки в разных файлах: `frontend` вынесен из общей пачки в
|
|
||||||
# deploy-tradein.yml + ретрай подключения в caddy/sites/apps.caddy.
|
|
||||||
# Обе обратимы молча и незаметно (дописать frontend обратно в SERVICES
|
|
||||||
# «за компанию»; скопировать новый публичный путь с блока без импорта),
|
|
||||||
# а отказ виден только непрерывной пробой во время деплоя — то есть
|
|
||||||
# никогда, если её никто не запустил.
|
|
||||||
run: |
|
|
||||||
python3 scripts/check-frontend-swap-window.py --selftest
|
|
||||||
python3 scripts/check-frontend-swap-window.py
|
|
||||||
|
|
||||||
- name: "Guard: Caddyfile синтаксически валиден"
|
- name: "Guard: Caddyfile синтаксически валиден"
|
||||||
# Тем же шагом-соседом и по той же причине, что два гейта рядом: бежит
|
# Тем же шагом-соседом и по той же причине, что два гейта рядом: бежит
|
||||||
# на КАЖДОМ PR, стоит секунды, падение блокирует merge.
|
# на КАЖДОМ PR, стоит секунды, падение блокирует merge.
|
||||||
|
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Гейт: подмена tradein-frontend не возвращает окно недоступности (#3274).
|
|
||||||
|
|
||||||
ПОЧЕМУ. Публичный лендинг meraocenka.ru лежал 30–90 с на КАЖДОМ деплое МЕРЫ.
|
|
||||||
Причина — НЕ медленная подмена контейнера (она стоит полсекунды), а то, что
|
|
||||||
`docker compose up -d` со СПИСКОМ сервисов работает в две фазы: сначала create
|
|
||||||
(старый контейнер каждого сервиса останавливается и удаляется — иначе занято
|
|
||||||
`container_name`), потом start, в порядке зависимостей и с ожиданием их
|
|
||||||
условий. Всё, что между фазами, фронт лежит.
|
|
||||||
|
|
||||||
Замер на проде 10.09 (docker inspect .Created/.StartedAt, два деплоя подряд):
|
|
||||||
|
|
||||||
пачка сервисов: tradein-backend создан 15:01:40 → запущен 15:02:10 (30 с)
|
|
||||||
один сервис: tradein-frontend создан 16:42:17.5 → запущен 16:42:18.0 (0,5 с)
|
|
||||||
|
|
||||||
В логе Caddy у первого деплоя три 503 на лендинге (15:01:46, 15:01:52,
|
|
||||||
15:02:06), у второго — ни одного.
|
|
||||||
|
|
||||||
ЧТО ДЕРЖИТ РЕШЕНИЕ, И ПОЧЕМУ ЭТО ГЕЙТ, А НЕ КОММЕНТАРИЙ. Половинки лежат в
|
|
||||||
разных файлах и обе невидимо обратимы:
|
|
||||||
|
|
||||||
1) deploy-tradein.yml — `frontend` ВЫНЕСЕН из общего `up -d $SERVICES` в
|
|
||||||
свою команду. Достаточно дописать его обратно в SERVICES «за компанию»,
|
|
||||||
и окно вернётся целиком, молча: деплой останется зелёным.
|
|
||||||
2) caddy/sites/apps.caddy — каждый `reverse_proxy tradein-frontend:3000`
|
|
||||||
импортирует (tradein_frontend_retry) (lb_try_duration), который добирает
|
|
||||||
оставшиеся ~0,5 с. Новый публичный путь копируют с соседнего блока — и
|
|
||||||
если копируют блок БЕЗ импорта, именно этот путь снова отдаёт 502.
|
|
||||||
|
|
||||||
Оба отказа не видны ни по статусу джобы, ни по глазам: их видно только
|
|
||||||
непрерывной пробой во время деплоя (scripts/probe-deploy-window.sh).
|
|
||||||
|
|
||||||
ЧЕГО НЕ ЛОВИТ. Это разбор текста, а не исполнение: гейт не проверяет, что
|
|
||||||
команда реально отработала и что Caddy реально ретраит (это проверяется
|
|
||||||
пробой на живом деплое). Не смотрит на сервисы, кроме frontend, — бэкенду
|
|
||||||
ретрай намеренно не дан (его старт длиннее, чем разумное ожидание клиента).
|
|
||||||
|
|
||||||
Запуск: python3 scripts/check-frontend-swap-window.py [--selftest]
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
|
||||||
WORKFLOW = REPO / ".forgejo" / "workflows" / "deploy-tradein.yml"
|
|
||||||
APPS_CADDY = REPO / "caddy" / "sites" / "apps.caddy"
|
|
||||||
|
|
||||||
SNIPPET_NAME = "tradein_frontend_retry"
|
|
||||||
FRONTEND_UPSTREAM = "reverse_proxy tradein-frontend:3000 {"
|
|
||||||
|
|
||||||
# `SERVICES="browser backend tgbot"` / `SERVICES="$SERVICES scraper"`.
|
|
||||||
_SERVICES_RE = re.compile(r'^\s*SERVICES=(["\']?)(.*?)\1\s*$', re.M)
|
|
||||||
# Одиночная команда подмены фронта.
|
|
||||||
_FRONTEND_UP_RE = re.compile(r"up -d --no-deps frontend\s*$", re.M)
|
|
||||||
|
|
||||||
|
|
||||||
def strip_comments(text: str) -> str:
|
|
||||||
"""Убирает строки-комментарии (shell/YAML/Caddy — везде `#`).
|
|
||||||
|
|
||||||
Обязательно: разбор дефекта живёт в комментарии рядом с правкой и содержит
|
|
||||||
его же формулировку. Без этого гейт спорил бы с собственным объяснением.
|
|
||||||
"""
|
|
||||||
return "\n".join(ln for ln in text.splitlines() if not ln.lstrip().startswith("#"))
|
|
||||||
|
|
||||||
|
|
||||||
def check_workflow(text: str) -> list[str]:
|
|
||||||
body = strip_comments(text)
|
|
||||||
errors: list[str] = []
|
|
||||||
|
|
||||||
for m in _SERVICES_RE.finditer(body):
|
|
||||||
value = m.group(2)
|
|
||||||
if re.search(r"(^|\s)frontend(\s|$)", value):
|
|
||||||
errors.append(
|
|
||||||
f'SERVICES={value!r} снова содержит `frontend`: он вернётся в общий '
|
|
||||||
f"`up -d` со списком сервисов, а это и есть окно 30–90 с (#3274). "
|
|
||||||
f"Фронт подменяется отдельной командой `up -d --no-deps frontend`."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not _FRONTEND_UP_RE.search(body):
|
|
||||||
errors.append(
|
|
||||||
"в deploy-tradein.yml нет отдельной команды `up -d --no-deps frontend` — "
|
|
||||||
"подмена фронта либо пропала, либо снова уехала в общую пачку (#3274)."
|
|
||||||
)
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def check_caddy(text: str) -> list[str]:
|
|
||||||
errors: list[str] = []
|
|
||||||
lines = text.splitlines()
|
|
||||||
body = strip_comments(text)
|
|
||||||
|
|
||||||
if f"({SNIPPET_NAME}) {{" not in body:
|
|
||||||
errors.append(
|
|
||||||
f"снипет ({SNIPPET_NAME}) не объявлен в apps.caddy — импортировать нечего."
|
|
||||||
)
|
|
||||||
elif "lb_try_duration" not in body:
|
|
||||||
errors.append(
|
|
||||||
f"снипет ({SNIPPET_NAME}) есть, но без `lb_try_duration` — он больше "
|
|
||||||
"ничего не добирает, оставшиеся ~0,5 с подмены снова видны как 502."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Блок апстрима фронта: от строки `reverse_proxy tradein-frontend:3000 {`
|
|
||||||
# до закрывающей скобки на её же отступе. Импорт должен быть внутри.
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.strip().startswith("#") or FRONTEND_UPSTREAM not in line:
|
|
||||||
continue
|
|
||||||
indent = len(line) - len(line.lstrip())
|
|
||||||
block: list[str] = []
|
|
||||||
for nxt in lines[i + 1 :]:
|
|
||||||
if nxt.strip() == "}" and (len(nxt) - len(nxt.lstrip())) == indent:
|
|
||||||
break
|
|
||||||
block.append(nxt)
|
|
||||||
if not any(f"import {SNIPPET_NAME}" in b for b in block if not b.strip().startswith("#")):
|
|
||||||
errors.append(
|
|
||||||
f"apps.caddy:{i + 1} — `reverse_proxy tradein-frontend:3000` без "
|
|
||||||
f"`import {SNIPPET_NAME}`: на этом пути подмена контейнера снова "
|
|
||||||
f"видна посетителю как 502 (#3274)."
|
|
||||||
)
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def selftest() -> None:
|
|
||||||
good_wf = 'SERVICES="browser backend tgbot"\n docker compose up -d --no-deps frontend\n'
|
|
||||||
assert check_workflow(good_wf) == [], check_workflow(good_wf)
|
|
||||||
|
|
||||||
bad_wf = 'SERVICES="browser backend frontend tgbot"\n docker compose up -d --no-deps frontend\n'
|
|
||||||
assert any("SERVICES" in e for e in check_workflow(bad_wf)), "не поймал frontend в SERVICES"
|
|
||||||
|
|
||||||
missing_wf = 'SERVICES="browser backend tgbot"\n docker compose up -d --no-deps $SERVICES\n'
|
|
||||||
assert any("отдельной команды" in e for e in check_workflow(missing_wf)), (
|
|
||||||
"не поймал пропажу отдельной команды"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Комментарий с той же формулировкой не должен ронять гейт.
|
|
||||||
commented = '# SERVICES="browser backend frontend tgbot" # так было до #3274\n' + good_wf
|
|
||||||
assert check_workflow(commented) == [], "гейт спорит с собственным комментарием"
|
|
||||||
|
|
||||||
good_caddy = (
|
|
||||||
"(tradein_frontend_retry) {\n lb_try_duration 2s\n}\n"
|
|
||||||
"handle {\n reverse_proxy tradein-frontend:3000 {\n"
|
|
||||||
" import tradein_frontend_retry\n header_up -X-Y\n }\n}\n"
|
|
||||||
)
|
|
||||||
assert check_caddy(good_caddy) == [], check_caddy(good_caddy)
|
|
||||||
|
|
||||||
bad_caddy = (
|
|
||||||
"(tradein_frontend_retry) {\n lb_try_duration 2s\n}\n"
|
|
||||||
"handle {\n reverse_proxy tradein-frontend:3000 {\n"
|
|
||||||
" header_up -X-Y\n }\n}\n"
|
|
||||||
)
|
|
||||||
assert any("без `import" in e for e in check_caddy(bad_caddy)), "не поймал блок без импорта"
|
|
||||||
|
|
||||||
no_snippet = (
|
|
||||||
"handle {\n reverse_proxy tradein-frontend:3000 {\n"
|
|
||||||
" import tradein_frontend_retry\n }\n}\n"
|
|
||||||
)
|
|
||||||
assert any("не объявлен" in e for e in check_caddy(no_snippet)), "не поймал пропажу снипета"
|
|
||||||
|
|
||||||
print("SELFTEST OK")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
if "--selftest" in sys.argv:
|
|
||||||
selftest()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
errors = check_workflow(WORKFLOW.read_text(encoding="utf-8"))
|
|
||||||
errors += check_caddy(APPS_CADDY.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
print("Гейт #3274 (окно подмены фронта) НЕ ПРОЙДЕН:\n")
|
|
||||||
for e in errors:
|
|
||||||
print(f" ✗ {e}")
|
|
||||||
print(
|
|
||||||
"\nЧем проверять эффект на живом деплое: scripts/probe-deploy-window.sh "
|
|
||||||
"(запускать НА хосте прода, см. шапку скрипта)."
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print("Гейт #3274: фронт подменяется отдельной командой, все 9 апстримов с ретраем — OK")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
Loading…
Add table
Reference in a new issue