Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Failing after 55s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m3s
CI Trade-In / frontend-checks (pull_request) Successful in 1m47s
CI / openapi-codegen-check (pull_request) Successful in 2m26s
CI / backend-tests (pull_request) Successful in 17m51s
Блокер переезда (#2990). Чистый старт на пустом томе падал: 077 читает foreign table gendesign_rosreestr_deals, а USER MAPPING создаёт бэкенд при старте (app/core/fdw.py), то есть ПОСЛЕ docker-entrypoint-initdb.d. Контейнер не поднимался вообще. Путь «пустой том» на реальном железе не исполнялся ни разу, а CI этот файл явно пропускал — гейт, который должен был поймать, был ослаблен. Проверено по всем 14 миграциям, упоминающим FDW-таблицы: читает ровно одна — 077. Остальные только CREATE/DROP FOREIGN TABLE и COMMENT, им ни USER MAPPING, ни связь с чужой БД не нужны. 077 не удалена, а сделана самозащитной: гард считает строки в md5-форме и выходит раньше обращения к FDW, если мигрировать нечего. На чистой БД таких строк нет по определению. Удаление файла было бы неверным — прод помнит миграции по bare-filename в _schema_migrations, и test_applied_migration_is_not_renamed_or_deleted падает на удалении. Исключение в ci-tradein.yml снято: теперь цепочка применяется целиком, то есть CI сам стал репетицией чистого старта. Отдельно закрыт тихий отказ в deploy-tradein.yml. Ветка baseline срабатывала по одному лишь отсутствию _schema_migrations, а это состояние неоднозначно: так выглядит и наполненный прод до внедрения tracking, и пустая БД нового сервера. Во втором случае baseline пометил бы все миграции применёнными, ни одной не прогнав, и деплой уехал бы зелёным на пустой схеме. Добавлен sentinel по listings: пусто → baseline пропускается, цепочка применяется с нуля. Refs #2990, #2989
369 lines
25 KiB
YAML
369 lines
25 KiB
YAML
name: CI Trade-In
|
||
|
||
# Forgejo Actions pre-merge gate for the SUBPROJECT tradein-mvp/.
|
||
# WHY THIS FILE EXISTS (#2208): основной .forgejo/workflows/ci.yml гейтит
|
||
# ТОЛЬКО backend/** + frontend/** главного стека — tradein-PR проходили на
|
||
# пусто-зелёных чеках (paths-filter no-op), а pytest tradein жил лишь в
|
||
# post-merge deploy-tradein.yml. Итог: сломанный tradein-код мержился в main
|
||
# и обнаруживался только на деплое. Этот workflow добавляет РЕАЛЬНЫЙ pre-merge
|
||
# gate: tradein-backend pytest + tradein-frontend type-check/lint ДО мержа.
|
||
on:
|
||
# ТОЛЬКО pull_request — НЕТ push-триггера на feature-ветки (CI-шторм #1709,
|
||
# см. подробное обоснование в ci.yml). Кратко: раньше push+pull_request на один
|
||
# SHA давали разный github.ref → разные concurrency-группы → 2× прогон на
|
||
# дефицитных раннерах. В bot-пайплайне каждый коммит идёт через PR, так что
|
||
# pull_request гейтит его полностью; push-прогон был чистым дублем.
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
concurrency:
|
||
# github.ref стабилен на весь PR (refs/pull/<N>/merge) → новый push в ветку PR
|
||
# отменяет предыдущий незавершённый прогон ЭТОГО PR вместо накопления.
|
||
group: ci-tradein-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
# Paths-filter: гейт бежит ТОЛЬКО когда поменялся tradein-код.
|
||
# PR не трогающий tradein-mvp/ → оба job'а no-op'ятся → дёшево.
|
||
changes:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
backend: ${{ steps.filter.outputs.backend }}
|
||
frontend: ${{ steps.filter.outputs.frontend }}
|
||
browser: ${{ steps.filter.outputs.browser }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: dorny/paths-filter@v3
|
||
id: filter
|
||
with:
|
||
filters: |
|
||
backend:
|
||
- 'tradein-mvp/backend/**'
|
||
- 'tradein-mvp/packages/**'
|
||
# workspace root: lock-only bump (uv lock --upgrade) или правка
|
||
# [tool.uv.workspace] меняют реальные зависимости → гейт обязан бежать.
|
||
- 'tradein-mvp/uv.lock'
|
||
- 'tradein-mvp/pyproject.toml'
|
||
# auth/roles.yaml — общий RBAC-конфиг обоих стеков, лежит В КОРНЕ
|
||
# репы и монтируется в tradein-backend (/app/auth/roles.yaml).
|
||
# tests/test_rbac.py читает именно его, поэтому правка ролей обязана
|
||
# гонять и этот гейт. Без строки правка roles.yaml не запускала НИ
|
||
# ОДИН сьют (та же дыра закрыта симметрично в ci.yml) — так на main
|
||
# уехал красный test_get_role_known_users (2026-07-30 → PR #2587).
|
||
- 'auth/**'
|
||
# Реестр городов — фронтовый файл, но его читает БЭКЕНДОВЫЙ тест
|
||
# (tests/test_public_mera_api.py сверяет то, что мы предлагаем
|
||
# выбрать, с тем, на что умеет отвечать проба покрытия). Без этой
|
||
# строки правка одного лишь дропдауна не гоняла бы сверку — а
|
||
# разошлись списки ровно так: город добавили на фронте, в пороги
|
||
# покрытия не внесли, и житель Серова получал «вы вне области».
|
||
- 'tradein-mvp/frontend/src/lib/city-registry.ts'
|
||
- '.forgejo/workflows/ci-tradein.yml'
|
||
frontend:
|
||
- 'tradein-mvp/frontend/**'
|
||
# Caddyfile — по той же причине, что auth/** у бэкенда: он лежит в
|
||
# КОРНЕ репы, но его читает фронтовый тест
|
||
# (mera-public/__tests__/public-perimeter.test.ts) — тот сверяет,
|
||
# что каждый маршрут публичного сайта действительно раздаётся на
|
||
# meraocenka.ru. Без этой строки правка одного лишь Caddyfile не
|
||
# запускала бы НИ ОДИН гейт, и удаление короткого адреса из
|
||
# allowlist уехало бы на main зелёным — а на сайте кнопка «Проверить»
|
||
# стала бы ссылкой в 404.
|
||
- 'Caddyfile'
|
||
- '.forgejo/workflows/ci-tradein.yml'
|
||
browser:
|
||
# Сайдкар — сервис ВНЕ uv-воркспейса (tradein-mvp/pyproject.toml
|
||
# members = backend + packages/*), со своим Dockerfile и без pyproject,
|
||
# поэтому и фильтр отдельный: backend-гейт его тестов не видел вовсе.
|
||
- 'tradein-mvp/browser/**'
|
||
- '.forgejo/workflows/ci-tradein.yml'
|
||
|
||
backend-tests:
|
||
runs-on: ubuntu-latest
|
||
needs: changes
|
||
if: needs.changes.outputs.backend == 'true'
|
||
# Postgres-сервис (#2745). ДО него лэйн был mock-only: DATABASE_URL указывал на
|
||
# заведомо мёртвый `localhost:5432/test`, и девять тестов с `_live_session()`
|
||
# self-skip'ались — в CI они не бежали НИ РАЗУ. Так и разъехался со схемой
|
||
# test_house_dedup_merge (#2740: houses.url стал NOT NULL), а
|
||
# test_gar_flats_loader вообще падал до первого утверждения (#2744).
|
||
#
|
||
# Замер перед включением: полный сьют на mock-лэйне 122с / 3858 passed / 10 skipped,
|
||
# тот же сьют против живой БД — 106с / 3867 passed / 1 skipped. Живая БД не
|
||
# медленнее, поэтому НЕ добавляем второй job, а чиним этот: один прогон, на
|
||
# девять реальных проверок больше. Накладные — только подъём контейнера и
|
||
# bootstrap схемы (219 файлов, ~20с).
|
||
defaults:
|
||
run:
|
||
working-directory: ./tradein-mvp/backend
|
||
env:
|
||
# Имя контейнера уникально на прогон: параллельные PR не дерутся за него.
|
||
CI_PG: ci-pg-tradein-${{ github.run_id }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
# ПОЛНАЯ история, а не дефолтный depth=1 (#2683).
|
||
# tests/test_migration_numbering.py сверяет номер новой миграции с
|
||
# origin/main и с точкой ветвления. Ровно этот флаг их и даёт: при
|
||
# depth=0 checkout идёт refspec'ом `+refs/heads/*:refs/remotes/origin/*`
|
||
# (видно в логе прогона), при depth=1 — только `+<sha>:refs/remotes/
|
||
# pull/N/head`, то есть ни ветки main, ни общего предка в клоне нет.
|
||
# Дотянуть main отдельным `git fetch` НЕЛЬЗЯ: из job-контейнера
|
||
# git.gendsgn.ru:443 недостижим (проверено, run 6977 — connection
|
||
# refused), сеть есть только у самого checkout.
|
||
#
|
||
# Гейт при отсутствии эталона краснеет, а не пропускается: молча
|
||
# пропущенная проверка и есть тот зелёный, который ничего не проверяет.
|
||
# Пак репозитория ~33 MiB — полный fetch дешевле разбора коллизии на проде.
|
||
fetch-depth: 0
|
||
|
||
- name: Поднять Postgres и собрать схему tradein
|
||
working-directory: .
|
||
# ПОЧЕМУ НЕ `services:` И ПОЧЕМУ БЕЗ ПУБЛИКАЦИИ ПОРТА.
|
||
# Раннер запускает и job, и сервис-контейнеры с `--network host` (видно в
|
||
# логе прогона: `docker create image=... network="host"`), а на 5432 того
|
||
# же хоста слушает ПРОДОВЫЙ Postgres. Попытка через `services:` +
|
||
# `ports: 5432:5432` кончилась тем, что сервис-контейнер не смог занять
|
||
# порт, а psql из job'а ушёл В ПРОД и получил
|
||
# `password authentication failed for user "tradein"`. То есть
|
||
# `localhost:5432` из job'а на этом раннере — боевая база, а не тестовая.
|
||
# Поэтому контейнер поднимаем сами, в bridge-сети, БЕЗ публикации порта,
|
||
# и ходим по его собственному IP: прод недостижим в принципе, параллельные
|
||
# прогоны не конфликтуют, psql берём из самого контейнера.
|
||
#
|
||
# ОДИН шаг, а не два: между шагами контейнер успевал исчезнуть, и
|
||
# bootstrap падал на `container is not running`.
|
||
#
|
||
# `pg_isready -h 127.0.0.1`, а НЕ через unix-сокет: на время initdb образ
|
||
# поднимает ВРЕМЕННЫЙ сервер с listen_addresses='' — по сокету он уже
|
||
# отвечает «accepting connections», хотя снаружи БД ещё не существует, а
|
||
# впереди рестарт. Проба по TCP зеленеет только на настоящем сервере —
|
||
# том самом, к которому пойдут тесты.
|
||
#
|
||
# postgis, не plain postgres: tests/tasks/test_cadastral_geo_match.py
|
||
# проверяет KNN по geometry (PostGIS_Version() в connectivity-probe).
|
||
# Имя БД ОБЯЗАНО отличаться от `test`: `_live_session()` считает DSN с
|
||
# `localhost:5432/test` заглушкой и вернул бы None — контейнер поднялся
|
||
# бы, а тесты всё равно скипались.
|
||
run: |
|
||
set -u
|
||
docker rm -fv "$CI_PG" >/dev/null 2>&1 || true
|
||
docker run -d --name "$CI_PG" \
|
||
-e POSTGRES_DB=tradein -e POSTGRES_USER=tradein -e POSTGRES_PASSWORD=tradein \
|
||
postgis/postgis:16-3.4
|
||
|
||
ready=""
|
||
for _ in $(seq 1 45); do
|
||
if docker exec "$CI_PG" pg_isready -h 127.0.0.1 -U tradein -q 2>/dev/null; then
|
||
ready=1; break
|
||
fi
|
||
[ "$(docker inspect -f '{{.State.Status}}' "$CI_PG" 2>/dev/null)" = "running" ] || break
|
||
sleep 2
|
||
done
|
||
if [ -z "$ready" ]; then
|
||
echo "::error::Postgres не поднялся; статус=$(docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' "$CI_PG" 2>&1)"
|
||
docker logs --tail 50 "$CI_PG" 2>&1 || true
|
||
exit 1
|
||
fi
|
||
|
||
ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CI_PG")
|
||
[ -n "$ip" ] || { echo "::error::не удалось узнать IP контейнера $CI_PG"; exit 1; }
|
||
echo "DATABASE_URL=postgresql+psycopg://tradein:tradein@${ip}:5432/tradein" >> "$GITHUB_ENV"
|
||
echo "✓ Postgres на ${ip}:5432 (контейнер $CI_PG)"
|
||
|
||
# Тот же порядок и тот же строгий режим, что в deploy-tradein.yml:
|
||
# `ls | sort` + ON_ERROR_STOP=on, падение любой миграции → job RED.
|
||
# Никаких «применилось как получилось»: схема в CI либо та же, что на
|
||
# проде, либо гейта нет.
|
||
docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q -c \
|
||
"CREATE EXTENSION IF NOT EXISTS postgis;
|
||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||
CREATE ROLE gendesign_reader;"
|
||
# Исключений НЕТ (#2990). Раньше здесь пропускалась 077 — единственная
|
||
# миграция, читающая foreign table через postgres_fdw, которой в CI нет.
|
||
# Пропуск означал, что гейт не проверял ровно тот файл, который потом
|
||
# ронял чистый старт на реальном железе. Теперь 077 сама выходит раньше
|
||
# обращения к FDW, если мигрировать нечего, и в CI проходит честно.
|
||
for sql_file in $(ls -1 tradein-mvp/backend/data/sql/*.sql | sort); do
|
||
fname=$(basename "$sql_file")
|
||
docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q < "$sql_file" \
|
||
|| { echo "::error::миграция $fname не применилась"; docker logs --tail 20 "$CI_PG" 2>&1 || true; exit 1; }
|
||
done
|
||
echo "✓ схема собрана: $(docker exec "$CI_PG" psql -U tradein -d tradein -tAc \
|
||
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public'") таблиц"
|
||
|
||
- 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).
|
||
# continue-on-error чтобы сбой cache-бэкенда раннера НИКОГДА не ронял gate.
|
||
# Ключ по workspace-локу tradein-mvp/uv.lock (tracked с воркспейса #2137).
|
||
uses: actions/cache@v4
|
||
continue-on-error: true
|
||
with:
|
||
path: ~/.cache/uv
|
||
key: uv-tradein-${{ runner.os }}-${{ hashFiles('tradein-mvp/uv.lock') }}
|
||
restore-keys: |
|
||
uv-tradein-${{ runner.os }}-
|
||
|
||
- name: Sync deps (incl. dev group — pytest, ruff)
|
||
# Workspace-лок tradein-mvp/uv.lock TRACKED (с воркспейса #2137; gitignored
|
||
# только старый backend/uv.lock) → --frozen детерминирован и зеркалит
|
||
# Dockerfile (uv sync --frozen --no-dev там). uv находит workspace root
|
||
# вверх от cwd.
|
||
run: uv sync --frozen
|
||
|
||
- name: Lint (ruff check)
|
||
# Правила выбраны в tradein-mvp/backend/pyproject.toml ([tool.ruff.lint]
|
||
# select = E F I B UP N RUF), но до этого шага их никто не гонял в CI —
|
||
# "дерево чистое" было непроверенным утверждением, а не гарантией.
|
||
# Версия ruff — та же, что в tradein-mvp/uv.lock (--frozen из шага выше),
|
||
# т.е. ровно то, что видит `uv sync --frozen` в Dockerfile.
|
||
# Blocking: любое нарушение → job RED (не декоративно).
|
||
run: uv run ruff check .
|
||
|
||
- name: Run pytest (tradein-mvp/backend)
|
||
# БЕЗ deselect'ов — сьют гоняется целиком (#2722).
|
||
#
|
||
# Здесь два года жил `--deselect tests/test_search_api.py::test_search_cache_hit`
|
||
# с объяснением «падает ТОЛЬКО в whole-suite ordering, в изоляции проходит —
|
||
# global-state leak из другого модуля». Объяснение было неверным в обеих
|
||
# половинах: тест падал и в изоляции тоже (401 vs 200), потому что он —
|
||
# единственный HTTP-тест в своём файле — ходил в /api/v1/search БЕЗ заголовка
|
||
# X-Authenticated-User, а RBAC-гард отвечает на такое 401 (ровно то, что
|
||
# фиксирует tests/test_estimate_idor.py). Причина была в тесте, а не в порядке;
|
||
# заголовок добавлен, deselect снят, полный прогон зелёный.
|
||
#
|
||
# Не добавлять сюда новые deselect'ы: молча выключенный тест — это тот же
|
||
# класс дефекта, что каталог вне пайплайна (#2722). Тест либо чинится, либо
|
||
# помечается xfail с причиной В КОДЕ, где её видно рядом с самим тестом.
|
||
#
|
||
# NB: в deploy-tradein.yml (post-merge test-job) свой экземпляр этого
|
||
# deselect'а — он остаётся до #2680, который правит тот файл. Расхождение
|
||
# безвредно: pre-merge гейт тест гоняет, post-merge просто пропустит зелёный.
|
||
#
|
||
# `-rs` (#2745) — КАЖДЫЙ пропуск печатает свою причину в лог job'а. Без него
|
||
# `-q` рисует пропуск точкой `s`, неотличимой на глаз от прогона: ровно так
|
||
# девять DB-тестов «шли зелёными», ничего не проверяя. Пропуск, который не
|
||
# называет себя вслух, со временем перестаёт быть верным.
|
||
run: uv run pytest -q -rs
|
||
|
||
- name: Снести тестовый Postgres
|
||
# if: always() — контейнер уходит и когда сьют красный, и когда прогон
|
||
# отменён concurrency-группой. Иначе на раннере копятся мёртвые контейнеры.
|
||
if: always()
|
||
working-directory: .
|
||
run: docker rm -fv "$CI_PG" >/dev/null 2>&1 || true
|
||
|
||
# Тесты браузерного сайдкара (#2722). До этого job'а они не бежали НИГДЕ:
|
||
# ci-tradein гейтил только backend/frontend, deploy-tradein — тоже, а каталог
|
||
# вне uv-воркспейса, так что и `uv run pytest` из backend их не собирал. Итог:
|
||
# 4 теста лежали красными на main (с 2026-06-20 и 2026-07-02), файл при этом
|
||
# правился, и никто не узнал. Починка — PR #2724, этот job закрывает причину.
|
||
#
|
||
# Почему НЕ переиспользуем backend-job:
|
||
# 1. сайдкар не член воркспейса → `uv sync --frozen` его не ставит;
|
||
# 2. aiohttp (единственная не-stdlib зависимость сьюта) нет в tradein-mvp/uv.lock;
|
||
# 3. разный scope paths-filter: правка browser/ не должна гонять backend-сьют.
|
||
browser-tests:
|
||
runs-on: ubuntu-latest
|
||
needs: changes
|
||
if: needs.changes.outputs.browser == 'true'
|
||
# Сьют идёт ~15с. Лимит — страховка от зависшего теста: у сайдкара нет своего
|
||
# pyproject, а значит и pytest-timeout'а backend'а (timeout=120). Дешевле
|
||
# взять нативный job-таймаут, чем тащить плагин ради одного каталога.
|
||
timeout-minutes: 10
|
||
defaults:
|
||
run:
|
||
working-directory: ./tradein-mvp/browser
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
# 3.12 — как в browser/Dockerfile (FROM python:3.12-slim).
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: "3.12"
|
||
|
||
- name: Install test deps
|
||
# ВЕСЬ список: pytest + aiohttp. Ни playwright, ни camoufox, ни закачки
|
||
# Firefox — camoufox импортируется ЛЕНИВО внутри _launch_browser
|
||
# (server.py, `from camoufox.async_api import AsyncCamoufox`), а сами тесты
|
||
# мокают _ensure_browser/_do_fetch и грузят server.py по пути через importlib.
|
||
# pytest-asyncio тоже НЕ нужен: ни одного `async def test_` — каждый тест сам
|
||
# крутит asyncio.run(). Проверено локально на venv ровно из этих двух пакетов.
|
||
#
|
||
# aiohttp без пина — ровно как в browser/Dockerfile (`pip install ... aiohttp`),
|
||
# то есть гейт видит ту же версию, что уедет в образ. Пин здесь означал бы
|
||
# проверку версии, которой в проде нет.
|
||
run: pip install pytest aiohttp
|
||
|
||
- name: Run pytest (tradein-mvp/browser)
|
||
# Каталог без pyproject/pytest.ini → дефолтная конфигурация, ничего
|
||
# не deselect'ится. Ожидание: 108 passed, 0 failed, 0 skipped.
|
||
# `-rs`: если однажды появится пропуск, он назовёт причину в логе, а не
|
||
# растворится в строке точек.
|
||
run: pytest -q -rs
|
||
|
||
frontend-checks:
|
||
runs-on: ubuntu-latest
|
||
needs: changes
|
||
if: needs.changes.outputs.frontend == 'true'
|
||
defaults:
|
||
run:
|
||
working-directory: ./tradein-mvp/frontend
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Set up Node
|
||
# Node 20 — major из tradein-mvp/frontend/Dockerfile (node:20-alpine).
|
||
# cache: npm включён с #2770 — package-lock.json теперь tracked.
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "20"
|
||
cache: npm
|
||
cache-dependency-path: tradein-mvp/frontend/package-lock.json
|
||
|
||
- name: Install deps (npm ci)
|
||
# ТОЧНЫЕ флаги из tradein-mvp/frontend/Dockerfile (deps stage), чтобы гейт
|
||
# видел то же дерево, что уедет в образ. `ci`, а не `install` (#2770): до
|
||
# него лока не было вовсе (лежал мёртвый pnpm-lock.yaml, из которого никто
|
||
# не ставил), и версии в CI и в прод-образе выбирались независимо по дате
|
||
# сборки — гейт проверял не тот код, который деплоится.
|
||
#
|
||
# Правишь package.json — регенерируй лок в том же PR: `npm ci` требует
|
||
# точного match и иначе роняет и этот job, и build образа.
|
||
run: npm ci --legacy-peer-deps --no-audit --no-fund
|
||
|
||
- name: Type-check (tsc --noEmit)
|
||
# Blocking: любая TS-ошибка → job RED.
|
||
run: npm run type-check
|
||
|
||
- name: Run tests (vitest)
|
||
# Blocking (#2766). До этого шага у tradein-фронта не бежало НИ ОДНОЙ
|
||
# проверки поведения: лэйн гейтил только типы и статический анализ, а оба
|
||
# молчат про то, что видит пользователь — пустое поле, погашенное число,
|
||
# отказ по частоте. Инфраструктура не изобретена, а взята у соседнего
|
||
# frontend/ (vitest + jsdom + testing-library), где сьют живёт давно.
|
||
#
|
||
# Пропусков в сьюте нет и быть не должно: сторож пропусков
|
||
# (tests/skip_allowlist.txt) — pytest-only, у vitest такого нет, поэтому
|
||
# пропуск здесь стал бы ровно тем незаметным «зелёным», который #2722
|
||
# запретил на бэкенде. Тест либо чинится, либо помечается `.fails`
|
||
# с причиной В КОДЕ.
|
||
run: npm test
|
||
|
||
- name: Lint (next lint)
|
||
# Blocking: любая ESLint-ошибка → job RED.
|
||
run: npm run lint
|
||
|
||
- name: Mera-public isolation guard (#2631)
|
||
# Blocking: статический import-graph публичного лэндинга не должен
|
||
# достигать закрытого контура (useMe/lib/api/sessionId/isPathAllowed/
|
||
# GuardedRoute вне next/dynamic). Инвариант этапа 1 #2545.
|
||
run: npm run check:mera-public-isolation
|