backend-tests и openapi-codegen-check тратят ~90% времени не на работу (дамп схемы + pytest), а на пересборку окружения с нуля каждый прогон. Доминанта — 'uv sync --frozen' тянет весь geo/WeasyPrint-стек заново, т.к. ~/.cache/uv не кэшировался между прогонами (npm уже кэширован setup-node). Изменения: - Cache uv (~/.cache/uv) через actions/cache в обоих job, ключ по backend/uv.lock, restore-keys для частичного хита. continue-on-error → сбой cache-бэкенда раннера не ломает gate. - openapi-codegen-check: 'uv sync --frozen --no-dev' — job только импортирует app.main для дампа OpenAPI, dev-группа (pytest/ruff/coverage) не нужна. Dockerfile тоже --no-dev, поэтому импорт гарантирован. Refs #1709
299 lines
16 KiB
YAML
299 lines
16 KiB
YAML
name: CI
|
||
|
||
# Forgejo Actions pytest gate for the MAIN backend (backend/).
|
||
# WHY THIS FILE EXISTS: Forgejo runs ONLY .forgejo/workflows/* — the
|
||
# .github/workflows/ci.yml pytest gate does NOT execute on git.gendsgn.ru
|
||
# (proven: Forgejo Actions runs показывают только deploy/build jobs). Без этого
|
||
# backend-изменения мержились + деплоились БЕЗ автотестов → live-баг #994
|
||
# (district 500) уехал в прод необнаруженным. Этот workflow добавляет реальный
|
||
# gate: backend-сьют GREEN (1687 passed / 0 failed) после CI-rehab 1+2.
|
||
#
|
||
# Lane = MOCK-ONLY (day 1): нет postgres service-контейнера — сьют мокает БД,
|
||
# единственный real-Postgres тест (tests/sql/ mv_layout) self-skip'ается через
|
||
# connectivity-probe. PDF-тесты (WeasyPrint) РЕАЛЬНО ИДУТ здесь (libpango
|
||
# установлен ниже), тогда как на macOS-dev они runtime-skip'аются.
|
||
# FUTURE: добавить `postgis/postgis:16-3.4` service + гонять mv_layout — см.
|
||
# .github/workflows/ci.yml как образец service-блока.
|
||
on:
|
||
# ТОЛЬКО pull_request — НЕТ push-триггера на feature-ветки (CI-шторм #1709).
|
||
# WHY: раньше был и push: [feat/**,fix/**,...]. Каждый коммит в ветку с открытым
|
||
# PR триггерил ДВА прогона на ОДИН SHA: push-событие (github.ref=refs/heads/<branch>)
|
||
# и pull_request-событие (github.ref=refs/pull/<N>/merge). Разный github.ref →
|
||
# разные concurrency-группы (см. ниже) → прогоны НЕ отменяют друг друга → 2× job
|
||
# при и так дефицитных раннерах. В bot-пайплайне каждый коммит идёт через PR, так
|
||
# что pull_request гейтит его полностью; push-прогон был чистым дублем.
|
||
# Trade-off: push в feature-ветку БЕЗ открытого PR не получит CI до открытия PR
|
||
# (бот открывает PR сразу после первого push) — приемлемо.
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
concurrency:
|
||
# Теперь, когда остался только pull_request, github.ref стабилен на весь PR
|
||
# (refs/pull/<N>/merge) → новый push в ветку PR отменяет предыдущий незавершённый
|
||
# прогон ЭТОГО PR (cancel-in-progress) вместо накопления параллельных.
|
||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
# Paths-filter: gate бежит ТОЛЬКО когда поменялся backend-код или SQL,
|
||
# которые этот backend читает (mirror deploy.yml changes-job). На чисто
|
||
# frontend/docs PR job no-op'ится → дёшево.
|
||
changes:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
backend: ${{ steps.filter.outputs.backend }}
|
||
frontend: ${{ steps.filter.outputs.frontend }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: dorny/paths-filter@v3
|
||
id: filter
|
||
with:
|
||
filters: |
|
||
backend:
|
||
- 'backend/**'
|
||
- 'data/sql/**'
|
||
- '.forgejo/workflows/ci.yml'
|
||
frontend:
|
||
- 'frontend/**'
|
||
- '.forgejo/workflows/ci.yml'
|
||
|
||
backend-tests:
|
||
runs-on: ubuntu-latest
|
||
needs: changes
|
||
if: needs.changes.outputs.backend == 'true'
|
||
defaults:
|
||
run:
|
||
working-directory: backend
|
||
env:
|
||
# TESTING=1 активирует RBAC-bypass (app/main.py rbac_guard пропускает
|
||
# запросы при settings.testing=True) — иначе 401 на всём /api/v1.
|
||
TESTING: "1"
|
||
# Stub DSN: psycopg v3 требует parseable URL на импорте; реального коннекта
|
||
# нет — DB-тесты мокаются, real-DB тест (tests/sql/) self-skip'ается через
|
||
# connectivity-probe к этому хосту (5432 недоступен → skip).
|
||
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
||
REDIS_URL: redis://localhost:6379/0
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: "3.12"
|
||
|
||
- name: Install uv
|
||
# Официальный standalone-инсталлер. НЕ astral-sh/setup-uv — он ломается
|
||
# на Forgejo-runner с PEP 668 externally-managed-environment (#666 CI).
|
||
run: |
|
||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||
|
||
- name: Cache uv packages
|
||
# Кросс-прогонный кэш скачанных/собранных wheel'ов (~/.cache/uv по умолч.).
|
||
# `uv sync --frozen` без него каждый прогон тянет весь geo-стек заново —
|
||
# доминирующая часть времени job (#1709). Ключ по uv.lock; continue-on-error
|
||
# чтобы сбой cache-бэкенда раннера НИКОГДА не ронял gate.
|
||
uses: actions/cache@v4
|
||
continue-on-error: true
|
||
with:
|
||
path: ~/.cache/uv
|
||
key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }}
|
||
restore-keys: |
|
||
uv-${{ runner.os }}-
|
||
|
||
- name: Install system deps for geo + WeasyPrint
|
||
# libpq/gdal/proj/geos — geo-стек (geopandas/shapely/pyproj).
|
||
# libcairo2/libpango* — нативные либы WeasyPrint: с ними PDF-тесты
|
||
# (tests/test_layout_tz_pdf.py) РЕАЛЬНО ИДУТ на CI (на macOS-dev они
|
||
# module-skip'аются probe'ом native-libs). Mirror .github ci.yml:55-57.
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y libpq-dev libgdal-dev libproj-dev libgeos-dev \
|
||
libcairo2 libpango-1.0-0 libpangoft2-1.0-0
|
||
|
||
- name: Install Python deps (incl. dev group — ruff, pytest)
|
||
# backend/uv.lock закоммичен → --frozen (детерминированно, fail при
|
||
# дрейфе lock vs pyproject). dev-группа ставится по умолчанию (ruff/pytest).
|
||
run: uv sync --frozen
|
||
|
||
- name: Lint (ruff check)
|
||
# Дешёвый fail-fast. NB: `ruff format --check` НАМЕРЕННО НЕ в gate —
|
||
# на момент создания 17 файлов repo-wide дали бы day-1 red. Отдельный
|
||
# format-pass = future enhancement. `ruff check .` сейчас green.
|
||
run: uv run ruff check .
|
||
|
||
- name: Test (pytest + coverage gate)
|
||
# --ignore=tests/smoke: prod-smoke бьёт по live https://gendsgn.ru
|
||
# (помечены @pytest.mark.prod_smoke; pyproject addopts уже их deselect'ит,
|
||
# но --ignore — belt-and-suspenders на случай сбора фикстур).
|
||
# tests/integration self-skip'ается через requires_test_db (skipif на
|
||
# TEST_DATABASE_URL, который тут не задан) → НЕ игнорим, оно чисто skip'ается.
|
||
# tests/sql/ mv_layout self-skip'ается через Postgres-connectivity probe
|
||
# (5432 недоступен в этом mock-lane) → SKIP. Это intended.
|
||
# PDF-тесты ИДУТ (libpango выше). Target: 0 failed, skips OK.
|
||
#
|
||
# Coverage-gate (#68): --cov=app меряет покрытие пакета app/.
|
||
# --cov-fail-under=65 → job RED если покрытие упало ниже baseline
|
||
# (измерено 2026-06: mock-lane сьют ~71%, см. [tool.coverage] в pyproject;
|
||
# 65 = floor с запасом, не flaky). На CI PDF-тесты РЕАЛЬНО идут (libpango),
|
||
# поэтому реальное CI-покрытие ≥ локально-измеренного floor.
|
||
# coverage.xml — артефакт для будущего Codecov/Coveralls upload (#68 badge).
|
||
# term-missing → видно непокрытые строки прямо в job-логе.
|
||
run: |
|
||
uv run pytest -q --ignore=tests/smoke \
|
||
--cov=app \
|
||
--cov-report=term-missing:skip-covered \
|
||
--cov-report=xml:coverage.xml \
|
||
--cov-fail-under=65
|
||
|
||
- name: Coverage summary → job output
|
||
# Дешёвый human-readable итог. Бежит даже если gate упал (if: always) —
|
||
# чтобы было видно НАСКОЛЬКО просело покрытие, а не только "fail".
|
||
# GITHUB_STEP_SUMMARY поддержан не во всех версиях Forgejo act_runner →
|
||
# если переменная пустая/файла нет, печатаем в обычный лог (fallback).
|
||
if: always()
|
||
run: |
|
||
[ -f coverage.xml ] || { echo "coverage.xml отсутствует — пропускаю summary"; exit 0; }
|
||
report="$(uv run coverage report --skip-covered --sort=cover | tail -40)"
|
||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||
{ echo '```'; echo "$report"; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
||
else
|
||
echo "$report"
|
||
fi
|
||
|
||
frontend-tests:
|
||
runs-on: ubuntu-latest
|
||
needs: changes
|
||
if: needs.changes.outputs.frontend == 'true'
|
||
defaults:
|
||
run:
|
||
working-directory: frontend
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Set up Node
|
||
# Node 20 — совпадает с major из frontend/Dockerfile (node:20-alpine).
|
||
# cache=npm + cache-dependency-path на lockfile → переиспользуем ~/.npm
|
||
# между прогонами (mirror Dockerfile's `--mount=type=cache,target=/root/.npm`).
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "20"
|
||
cache: npm
|
||
cache-dependency-path: frontend/package-lock.json
|
||
|
||
- name: Install deps (npm ci, frozen lockfile)
|
||
# ТОЧНЫЕ флаги из frontend/Dockerfile (deps stage):
|
||
# --legacy-peer-deps — Tailwind 4 alpha + React 19 peer-dep mismatches;
|
||
# --no-audit --no-fund — тише и быстрее в CI. `ci` (не `install`) =
|
||
# детерминированно из package-lock.json, fail при дрейфе lock vs package.json.
|
||
run: npm ci --legacy-peer-deps --no-audit --no-fund
|
||
|
||
- name: Test (vitest)
|
||
# `npm run test` = `vitest run` (single-shot, не watch). Только тесты —
|
||
# `next build` НАМЕРЕННО не здесь (тяжёлый, verified в deploy.yml build).
|
||
run: npm run test
|
||
|
||
# OpenAPI → TS codegen drift-gate (#69). Защищает фронт от молчаливого
|
||
# рассинхрона типов: если backend изменил OpenAPI-схему, а
|
||
# frontend/src/lib/api-types.ts не перегенерён (`npm run codegen`) — job RED.
|
||
#
|
||
# Бежит когда поменялся backend ИЛИ frontend (backend-change может застейлить
|
||
# типы даже без правок во frontend/). Отдельный job (не внутри frontend-tests),
|
||
# чтобы vitest и codegen-gate скейлились независимо.
|
||
#
|
||
# WHY no running server: `npm run codegen` бьёт по http://localhost:8000/openapi.json,
|
||
# но схема = app.openapi() — её можно сдампить из python БЕЗ uvicorn/DB
|
||
# (app.main импортируется под TESTING=1 со stub-DSN, как в backend-tests).
|
||
# Это убирает flaky port-wait. openapi-typescript v7 принимает локальный файл.
|
||
#
|
||
# WHY prettier: committed api-types.ts форматируется prettier'ом через
|
||
# pre-commit hook (mirrors-prettier, no config → defaults, 2-space). Raw
|
||
# openapi-typescript отдаёт 4-space → diff-шум. Прогоняем prettier (defaults)
|
||
# на regen, чтобы сравнивать ТОЛЬКО контент, не форматирование.
|
||
openapi-codegen-check:
|
||
runs-on: ubuntu-latest
|
||
needs: changes
|
||
if: |
|
||
needs.changes.outputs.backend == 'true' ||
|
||
needs.changes.outputs.frontend == 'true'
|
||
env:
|
||
# Те же stub-переменные, что backend-tests: psycopg требует parseable URL
|
||
# на импорте; реального коннекта нет (схему дампим, не обслуживаем запросы).
|
||
TESTING: "1"
|
||
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
||
REDIS_URL: redis://localhost:6379/0
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: "3.12"
|
||
|
||
- name: Set up Node
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "20"
|
||
cache: npm
|
||
cache-dependency-path: frontend/package-lock.json
|
||
|
||
- name: Install uv
|
||
run: |
|
||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||
|
||
- name: Cache uv packages
|
||
# См. backend-tests: кросс-прогонный кэш ~/.cache/uv, тот же ключ по uv.lock.
|
||
uses: actions/cache@v4
|
||
continue-on-error: true
|
||
with:
|
||
path: ~/.cache/uv
|
||
key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }}
|
||
restore-keys: |
|
||
uv-${{ runner.os }}-
|
||
|
||
- name: Install system deps for geo + WeasyPrint
|
||
# app.main транзитивно тянет geo/PDF-модули. На macOS-dev импорт схемы
|
||
# проходит и без этих либ, но на ubuntu ставим как backend-tests
|
||
# (belt-and-suspenders, чтобы import app.main точно не падал).
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y libpq-dev libgdal-dev libproj-dev libgeos-dev \
|
||
libcairo2 libpango-1.0-0 libpangoft2-1.0-0
|
||
|
||
- name: Install backend deps (uv sync --frozen --no-dev)
|
||
working-directory: backend
|
||
# --no-dev: этот job только дампит app.openapi() (нужен runtime app.main).
|
||
# pytest/ruff/coverage не используются → не ставим dev-группу (быстрее).
|
||
# Dockerfile тоже собирает с --no-dev → импорт app.main гарантированно ок.
|
||
run: uv sync --frozen --no-dev
|
||
|
||
- name: Install frontend deps (npm ci)
|
||
working-directory: frontend
|
||
run: npm ci --legacy-peer-deps --no-audit --no-fund
|
||
|
||
- name: Dump OpenAPI schema from app (no server)
|
||
working-directory: backend
|
||
run: uv run python -c "import json; from app.main import app; print(json.dumps(app.openapi()))" > /tmp/openapi.json
|
||
|
||
- name: Regenerate api-types.ts + format (prettier defaults)
|
||
working-directory: frontend
|
||
# 1) openapi-typescript из дампнутого файла (эквивалент `npm run codegen`,
|
||
# который читает ту же схему по URL). 2) prettier (defaults, как
|
||
# pre-commit hook) → формат совпадает с committed-файлом.
|
||
run: |
|
||
npx openapi-typescript /tmp/openapi.json -o src/lib/api-types.ts
|
||
npx prettier --write src/lib/api-types.ts
|
||
|
||
- name: Assert api-types.ts is up-to-date
|
||
working-directory: frontend
|
||
# git diff --exit-code: 0 если файл не изменился (типы актуальны),
|
||
# 1 если regen дал другой результат (типы устарели → fail с подсказкой).
|
||
run: |
|
||
if ! git diff --exit-code -- src/lib/api-types.ts; then
|
||
echo "::error::frontend/src/lib/api-types.ts устарел относительно backend OpenAPI." \
|
||
"Запусти: cd frontend && npm run codegen (с backend на :8000), затем закоммить." \
|
||
"Pre-commit prettier отформатирует автоматически."
|
||
exit 1
|
||
fi
|
||
echo "✓ api-types.ts актуален относительно backend OpenAPI-схемы."
|