Быстрый путь 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
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
Быстрый путь «правка ТОЛЬКО прокси» (#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 between204e2e09deand 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>
This commit is contained in:
parent
9fa01e7ebe
commit
091137a0c5
2 changed files with 329 additions and 28 deletions
|
|
@ -121,37 +121,98 @@ jobs:
|
||||||
infra: ${{ steps.filter.outputs.infra }}
|
infra: ${{ steps.filter.outputs.infra }}
|
||||||
# #2916: правка ТОЛЬКО конфига прокси. `infra` для этого не годится — он
|
# #2916: правка ТОЛЬКО конфига прокси. `infra` для этого не годится — он
|
||||||
# включает и compose, и сам workflow, где полный деплой обязателен.
|
# включает и compose, и сам workflow, где полный деплой обязателен.
|
||||||
# `github.event_name == 'push'` первым множителем НАМЕРЕННО: на
|
caddy_only: ${{ steps.filter.outputs.caddy_only }}
|
||||||
# workflow_dispatch у paths-filter нет диффа, и любой его ответ не должен
|
|
||||||
# уметь отключить сборку — ручной прогон обязан оставаться полным.
|
|
||||||
caddy_only: ${{ github.event_name == 'push' && steps.filter.outputs.caddy == 'true' && steps.filter.outputs.non_caddy == 'false' }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dorny/paths-filter@v3
|
|
||||||
|
# ── #3448: список изменённых файлов считаем САМИ ─────────────────────────
|
||||||
|
#
|
||||||
|
# ЧТО БЫЛО. Быстрый путь «правка только прокси» (#2916) не отработал НИ
|
||||||
|
# РАЗУ. Причина — НЕ пустой `event.before`: эта гипотеза опровергнута
|
||||||
|
# логом задачи 29244 (run 10881, мерж 84920e6c) — `before` там валиден,
|
||||||
|
# 204e2e09…, и `git diff` вернул ровно один файл. Причина в семантике
|
||||||
|
# самого фильтра: dorny/paths-filter склеивает шаблоны ОДНОГО фильтра
|
||||||
|
# через `some`, то есть ИЛИ (src/filter.ts: `patterns.some(aPredicate)`,
|
||||||
|
# predicate-quantifier по умолчанию `some`). Список
|
||||||
|
# non_caddy: ['**', '!Caddyfile', '!caddy/**']
|
||||||
|
# читается не как «всё, КРОМЕ caddy», а как «подходит под `**` ИЛИ не
|
||||||
|
# Caddyfile ИЛИ не caddy/**». `**` матчит всё, поэтому non_caddy был true
|
||||||
|
# ВСЕГДА и caddy_only — false всегда. В логе это видно дословно:
|
||||||
|
# ##[group]Filter non_caddy = true
|
||||||
|
# Matching files:
|
||||||
|
# caddy/sites/apps.caddy [modified]
|
||||||
|
# Исключённый файл сам себя и «исключил». deploy-caddy при этом
|
||||||
|
# пропускался, а Forgejo рисует пропущенную джобу зелёной — сигнала не
|
||||||
|
# было ни одного.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ ШЕЛЛ, А НЕ ЗАПЛАТКА К ФИЛЬТРАМ. Разность множеств тут нужна одна
|
||||||
|
# («все изменения лежат под caddy»), и выражать её действием, у которого
|
||||||
|
# ИЛИ по умолчанию, — значит снова повесить решение на незаметное
|
||||||
|
# умолчание: `predicate-quantifier: every` действует на ВЕСЬ блок и
|
||||||
|
# сломал бы backend/frontend/infra. Плюс два требования #3448: решение
|
||||||
|
# обязано быть ВИДНО в логе (иначе «сработало» и «просто не совпало»
|
||||||
|
# неотличимы), и оно не должно молча зависеть от того, что платформа
|
||||||
|
# кладёт в `before`.
|
||||||
|
#
|
||||||
|
# FAIL-SAFE. База не разрешилась (ручной запуск, пустой/нулевой `before`,
|
||||||
|
# коммита нет на сервере) → считаем изменённым ВЕСЬ репозиторий: лишний
|
||||||
|
# полный деплой безопаснее пропущенного. Фолбэка на `HEAD^..HEAD` тут
|
||||||
|
# намеренно нет: у мерж-коммита он дал бы верный ответ, а у push'а из
|
||||||
|
# нескольких коммитов — молча урезанный, и быстрый путь включился бы
|
||||||
|
# там, где приехал бэкенд.
|
||||||
|
- name: Определить изменённые файлы (#3448)
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
env:
|
||||||
filters: |
|
BEFORE: ${{ github.event.before }}
|
||||||
backend:
|
EVENT: ${{ github.event_name }}
|
||||||
- 'backend/**'
|
run: |
|
||||||
- 'data/sql/**'
|
set -eu
|
||||||
frontend:
|
NULL_SHA=0000000000000000000000000000000000000000
|
||||||
- 'frontend/**'
|
BASE=""
|
||||||
infra:
|
if [ "$EVENT" = "push" ] && [ -n "${BEFORE:-}" ] && [ "$BEFORE" != "$NULL_SHA" ]; then
|
||||||
- 'docker-compose.prod.yml'
|
git cat-file -e "${BEFORE}^{commit}" 2>/dev/null \
|
||||||
- 'Caddyfile'
|
|| git fetch --depth=1 --no-tags origin "$BEFORE" >/dev/null 2>&1 \
|
||||||
- 'caddy/**'
|
|| true
|
||||||
- '.forgejo/workflows/deploy.yml'
|
if git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
|
||||||
# Пара фильтров для «правка ТОЛЬКО прокси» (#2916). Одного `caddy`
|
BASE="$BEFORE"
|
||||||
# мало: он true и когда вместе с конфигом приехал бэкенд — тогда
|
else
|
||||||
# нужен обычный полный деплой. `non_caddy` матчит ВСЁ остальное,
|
echo "::warning::коммит $BEFORE недоступен в клоне — деплой будет полным"
|
||||||
# и быстрый путь включается лишь когда он false.
|
fi
|
||||||
caddy:
|
fi
|
||||||
- 'Caddyfile'
|
|
||||||
- 'caddy/**'
|
if [ -n "$BASE" ]; then
|
||||||
non_caddy:
|
FILES=$(git diff --no-renames --name-only "$BASE" HEAD)
|
||||||
- '**'
|
N=$(printf '%s\n' "$FILES" | grep -c . || true)
|
||||||
- '!Caddyfile'
|
echo "База: $BASE → $(git rev-parse HEAD); изменённых файлов: $N"
|
||||||
- '!caddy/**'
|
printf '%s\n' "$FILES" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
FILES=$(git ls-files)
|
||||||
|
N=$(printf '%s\n' "$FILES" | grep -c . || true)
|
||||||
|
echo "База не определена (event=$EVENT, before='${BEFORE:-}') — считаем изменённым весь репозиторий ($N файлов), деплой полный"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Те же наборы путей, что были в фильтрах до #3448.
|
||||||
|
CADDY_RE='^(Caddyfile$|caddy/)'
|
||||||
|
has() { printf '%s\n' "$FILES" | grep -qE "$1"; }
|
||||||
|
|
||||||
|
backend=false; frontend=false; infra=false; caddy_only=false
|
||||||
|
has '^(backend/|data/sql/)' && backend=true
|
||||||
|
has '^frontend/' && frontend=true
|
||||||
|
has '^(docker-compose\.prod\.yml$|Caddyfile$|caddy/|\.forgejo/workflows/deploy\.yml$)' && infra=true
|
||||||
|
# Быстрый путь: изменения ЕСТЬ и НИ ОДНО из них не лежит вне caddy.
|
||||||
|
# Проверка `N -gt 0` обязательна: пустой список иначе прошёл бы как
|
||||||
|
# «всё под caddy» и отключил бы сборку на ровном месте.
|
||||||
|
if [ "$N" -gt 0 ] && ! printf '%s\n' "$FILES" | grep -vE "$CADDY_RE" | grep -q .; then
|
||||||
|
caddy_only=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Флаги: backend=$backend frontend=$frontend infra=$infra caddy_only=$caddy_only"
|
||||||
|
{
|
||||||
|
echo "backend=$backend"
|
||||||
|
echo "frontend=$frontend"
|
||||||
|
echo "infra=$infra"
|
||||||
|
echo "caddy_only=$caddy_only"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
build-backend:
|
build-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
240
backend/tests/ops/test_3448_caddy_only_detection.py
Normal file
240
backend/tests/ops/test_3448_caddy_only_detection.py
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
"""Гейт: быстрый путь «правка только прокси» действительно включается (#3448).
|
||||||
|
|
||||||
|
ЧТО СЛУЧИЛОСЬ. Быстрый путь #2916 (`caddy_only` → джоба `deploy-caddy` с
|
||||||
|
`caddy reload` вместо пересоздания контейнеров) не отработал ни разу за всё
|
||||||
|
время жизни. Проверено на мерже 84920e6c, где в диффе ровно один файл
|
||||||
|
`caddy/sites/apps.caddy`: контейнеры пересозданы, в логе Caddy
|
||||||
|
`serving initial configuration` — холодный старт, а не reload.
|
||||||
|
|
||||||
|
ПРИЧИНА — НЕ пустой `github.event.before` (рабочая гипотеза #3448 опровергнута
|
||||||
|
логом задачи 29244: `before` = 204e2e09…, `git diff` вернул ровно один файл).
|
||||||
|
Причина в том, что dorny/paths-filter склеивает шаблоны одного фильтра через
|
||||||
|
`some`, то есть ИЛИ (src/filter.ts: `patterns.some(aPredicate)`; параметр
|
||||||
|
`predicate-quantifier` по умолчанию `some`). Список
|
||||||
|
|
||||||
|
non_caddy: ['**', '!Caddyfile', '!caddy/**']
|
||||||
|
|
||||||
|
значит «подходит под `**` ИЛИ не Caddyfile ИЛИ не caddy/**», а `**` матчит всё —
|
||||||
|
non_caddy был true ВСЕГДА. В логе это видно дословно:
|
||||||
|
|
||||||
|
##[group]Filter non_caddy = true
|
||||||
|
Matching files:
|
||||||
|
caddy/sites/apps.caddy [modified]
|
||||||
|
|
||||||
|
исключённый файл сам себя и «исключил».
|
||||||
|
|
||||||
|
ЗАЧЕМ ЭТОТ ФАЙЛ. У самой правки нет отрицательного признака: пропущенную джобу
|
||||||
|
Forgejo рисует зелёной, поэтому «зелёный deploy-caddy» одинаково выглядит и
|
||||||
|
когда быстрый путь сработал, и когда его вообще не было. Проверки ниже
|
||||||
|
ИСПОЛНЯЮТ шаг определения файлов из deploy.yml на настоящем временном
|
||||||
|
репозитории (включая мерж-коммит — ровно случай #3448) и смотрят на значения
|
||||||
|
флагов, а не на текст воркфлоу. Регресс к исключающим шаблонам paths-filter
|
||||||
|
ловит отдельная проверка в конце.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
# backend/tests/ops/<этот файл> → корень репозитория
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
WORKFLOWS = REPO_ROOT / ".forgejo" / "workflows"
|
||||||
|
DEPLOY = WORKFLOWS / "deploy.yml"
|
||||||
|
|
||||||
|
NULL_SHA = "0" * 40
|
||||||
|
|
||||||
|
# Файлы, которые лежат в тестовом репозитории до правки. Набор подобран так,
|
||||||
|
# чтобы фолбэк «база не определена» мог отличить полный деплой от пустого.
|
||||||
|
BASE_FILES = (
|
||||||
|
"caddy/sites/apps.caddy",
|
||||||
|
"Caddyfile",
|
||||||
|
"backend/app/main.py",
|
||||||
|
"frontend/src/page.tsx",
|
||||||
|
"data/sql/001.sql",
|
||||||
|
"docker-compose.prod.yml",
|
||||||
|
"README.md",
|
||||||
|
)
|
||||||
|
|
||||||
|
GIT_ENV = {
|
||||||
|
"GIT_AUTHOR_NAME": "t",
|
||||||
|
"GIT_AUTHOR_EMAIL": "t@example.com",
|
||||||
|
"GIT_COMMITTER_NAME": "t",
|
||||||
|
"GIT_COMMITTER_EMAIL": "t@example.com",
|
||||||
|
"GIT_CONFIG_GLOBAL": "/dev/null",
|
||||||
|
"GIT_CONFIG_SYSTEM": "/dev/null",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_script() -> str:
|
||||||
|
"""Тело шага, который считает изменённые файлы в job `changes`."""
|
||||||
|
spec = yaml.safe_load(DEPLOY.read_text(encoding="utf-8"))
|
||||||
|
job = spec["jobs"]["changes"]
|
||||||
|
steps = [s for s in job["steps"] if s.get("id") == "filter"]
|
||||||
|
assert len(steps) == 1, (
|
||||||
|
"в job `changes` нет ровно одного шага с `id: filter` — определение "
|
||||||
|
"изменённых файлов переехало, гейт #3448 ослеп"
|
||||||
|
)
|
||||||
|
step = steps[0]
|
||||||
|
assert "run" in step, (
|
||||||
|
f"шаг `filter` не считает файлы сам, а делегирует их {step.get('uses')!r}. "
|
||||||
|
"Именно так и возник #3448: у dorny/paths-filter шаблоны одного фильтра "
|
||||||
|
"склеиваются через ИЛИ, поэтому `non_caddy: ['**', '!caddy/**']` был true "
|
||||||
|
"ВСЕГДА и быстрый путь не включался ни разу."
|
||||||
|
)
|
||||||
|
assert step["run"].strip(), "шаг `filter` пуст"
|
||||||
|
return step["run"]
|
||||||
|
|
||||||
|
|
||||||
|
def _git(repo: Path, *args: str) -> None:
|
||||||
|
subprocess.run(
|
||||||
|
["git", "-C", str(repo), *args], check=True, env=dict(GIT_ENV), capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_repo(tmp_path: Path, changed: tuple[str, ...]) -> tuple[Path, str]:
|
||||||
|
"""Репозиторий с базовым коммитом и МЕРЖ-коммитом поверх него.
|
||||||
|
|
||||||
|
Мерж, а не обычный коммит, — намеренно: #3448 наблюдался именно на мерже
|
||||||
|
PR'а, и любой фолбэк вида «дифф последнего коммита» на мерж-коммите даёт
|
||||||
|
пустой список (git show у мержа без -m не печатает ничего).
|
||||||
|
"""
|
||||||
|
repo = tmp_path / "repo"
|
||||||
|
repo.mkdir(parents=True)
|
||||||
|
_git(repo, "init", "-q", "-b", "main")
|
||||||
|
for name in BASE_FILES:
|
||||||
|
path = repo / name
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("base\n", encoding="utf-8")
|
||||||
|
_git(repo, "add", "-A")
|
||||||
|
_git(repo, "commit", "-qm", "base")
|
||||||
|
base_sha = subprocess.run(
|
||||||
|
["git", "-C", str(repo), "rev-parse", "HEAD"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=dict(GIT_ENV),
|
||||||
|
).stdout.strip()
|
||||||
|
|
||||||
|
_git(repo, "checkout", "-q", "-b", "feature")
|
||||||
|
for name in changed:
|
||||||
|
path = repo / name
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("changed\n", encoding="utf-8")
|
||||||
|
_git(repo, "add", "-A")
|
||||||
|
_git(repo, "commit", "-qm", "feature")
|
||||||
|
_git(repo, "checkout", "-q", "main")
|
||||||
|
_git(repo, "merge", "-q", "--no-ff", "-m", "merge feature", "feature")
|
||||||
|
return repo, base_sha
|
||||||
|
|
||||||
|
|
||||||
|
def _run(
|
||||||
|
tmp_path: Path, changed: tuple[str, ...], *, before: str | None = None, event: str = "push"
|
||||||
|
) -> tuple[dict[str, str], str]:
|
||||||
|
repo, base_sha = _make_repo(tmp_path, changed)
|
||||||
|
out_file = tmp_path / "outputs"
|
||||||
|
out_file.touch()
|
||||||
|
env = {
|
||||||
|
"PATH": "/usr/bin:/bin:/usr/local/bin",
|
||||||
|
"BEFORE": base_sha if before is None else before,
|
||||||
|
"EVENT": event,
|
||||||
|
"GITHUB_OUTPUT": str(out_file),
|
||||||
|
**GIT_ENV,
|
||||||
|
}
|
||||||
|
proc = subprocess.run(
|
||||||
|
["bash", "-c", _detect_script()],
|
||||||
|
cwd=repo,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert proc.returncode == 0, f"шаг упал:\n{proc.stdout}\n{proc.stderr}"
|
||||||
|
outputs = dict(
|
||||||
|
line.split("=", 1)
|
||||||
|
for line in out_file.read_text(encoding="utf-8").splitlines()
|
||||||
|
if "=" in line
|
||||||
|
)
|
||||||
|
return outputs, proc.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_with_only_caddy_file_takes_the_fast_path(tmp_path: Path) -> None:
|
||||||
|
"""Случай #3448 дословно: мерж, в диффе один файл под caddy/."""
|
||||||
|
outputs, _ = _run(tmp_path, ("caddy/sites/apps.caddy",))
|
||||||
|
assert outputs["caddy_only"] == "true", (
|
||||||
|
f"быстрый путь не включился на правке ТОЛЬКО прокси: {outputs}. "
|
||||||
|
"Ровно это и есть #3448: deploy-caddy пропускается, идёт полный деплой "
|
||||||
|
"с пересозданием контейнеров, а Forgejo рисует пропуск зелёным."
|
||||||
|
)
|
||||||
|
assert outputs["backend"] == "false"
|
||||||
|
assert outputs["frontend"] == "false"
|
||||||
|
|
||||||
|
|
||||||
|
def test_caddy_plus_backend_is_a_full_deploy(tmp_path: Path) -> None:
|
||||||
|
"""Обратное направление: быстрый путь НЕ должен красть обычный деплой."""
|
||||||
|
outputs, _ = _run(tmp_path, ("caddy/sites/apps.caddy", "backend/app/main.py"))
|
||||||
|
assert outputs["caddy_only"] == "false", (
|
||||||
|
f"быстрый путь включился, хотя вместе с конфигом приехал бэкенд: {outputs}. "
|
||||||
|
"Так прод остался бы на старом образе при зелёном деплое."
|
||||||
|
)
|
||||||
|
assert outputs["backend"] == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_base_falls_back_to_full_deploy(tmp_path: Path) -> None:
|
||||||
|
"""База не разрешилась → полный деплой, а не пустой список.
|
||||||
|
|
||||||
|
Пустой список изменений — это `caddy_only` без единого caddy-файла и
|
||||||
|
отключённая сборка: отказ, который выглядит как успешный быстрый путь.
|
||||||
|
"""
|
||||||
|
for before, event in ((NULL_SHA, "push"), ("", "push"), (None, "workflow_dispatch")):
|
||||||
|
outputs, log = _run(
|
||||||
|
tmp_path / f"case-{event}-{before!r}",
|
||||||
|
("caddy/sites/apps.caddy",),
|
||||||
|
before=before,
|
||||||
|
event=event,
|
||||||
|
)
|
||||||
|
assert outputs["caddy_only"] == "false", f"before={before!r} event={event}: {outputs}"
|
||||||
|
assert outputs["backend"] == "true", f"before={before!r} event={event}: {outputs}"
|
||||||
|
assert outputs["frontend"] == "true", f"before={before!r} event={event}: {outputs}"
|
||||||
|
assert outputs["infra"] == "true", f"before={before!r} event={event}: {outputs}"
|
||||||
|
assert "деплой полный" in log
|
||||||
|
|
||||||
|
|
||||||
|
def test_decision_is_visible_in_the_log(tmp_path: Path) -> None:
|
||||||
|
"""Решение печатается: и список файлов, и итоговые флаги.
|
||||||
|
|
||||||
|
Без этого «сработало» и «просто не совпало» неотличимы — единственным
|
||||||
|
свидетелем остаётся метка Created у контейнера на проде.
|
||||||
|
"""
|
||||||
|
_, log = _run(tmp_path, ("caddy/sites/apps.caddy",))
|
||||||
|
assert "caddy/sites/apps.caddy" in log, f"шаг не печатает список файлов:\n{log}"
|
||||||
|
assert "caddy_only=true" in log and "backend=false" in log, (
|
||||||
|
f"шаг не печатает итоговые флаги:\n{log}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("path", sorted(WORKFLOWS.glob("*.yml")), ids=lambda p: p.name)
|
||||||
|
def test_no_paths_filter_relies_on_exclusion_patterns(path: Path) -> None:
|
||||||
|
"""Ни один paths-filter в репозитории не пытается вычитать пути через `!`.
|
||||||
|
|
||||||
|
Класс бага, а не единственный его случай: при `predicate-quantifier: some`
|
||||||
|
(умолчание) шаблоны фильтра склеиваются через ИЛИ, и `!` ничего не вычитает.
|
||||||
|
"""
|
||||||
|
spec = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||||
|
for job_name, job in (spec.get("jobs") or {}).items():
|
||||||
|
for step in job.get("steps") or []:
|
||||||
|
if not str(step.get("uses", "")).startswith("dorny/paths-filter"):
|
||||||
|
continue
|
||||||
|
with_ = step.get("with") or {}
|
||||||
|
if with_.get("predicate-quantifier") == "every":
|
||||||
|
continue
|
||||||
|
filters = yaml.safe_load(with_.get("filters") or "") or {}
|
||||||
|
for filter_name, patterns in filters.items():
|
||||||
|
bad = [p for p in (patterns or []) if isinstance(p, str) and p.startswith("!")]
|
||||||
|
assert not bad, (
|
||||||
|
f"{path.name}: job {job_name}, фильтр {filter_name!r} вычитает пути "
|
||||||
|
f"шаблонами {bad} — при `some` (умолчание) они склеиваются через ИЛИ "
|
||||||
|
"и фильтр становится true ВСЕГДА. Так #2916 не сработал ни разу (#3448)."
|
||||||
|
)
|
||||||
Loading…
Add table
Reference in a new issue