ci(tradein): pre-merge гейт — backend pytest + frontend type-check/lint (#2208) #2220
7 changed files with 181 additions and 13 deletions
135
.forgejo/workflows/ci-tradein.yml
Normal file
135
.forgejo/workflows/ci-tradein.yml
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
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 }}
|
||||||
|
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'
|
||||||
|
- '.forgejo/workflows/ci-tradein.yml'
|
||||||
|
frontend:
|
||||||
|
- 'tradein-mvp/frontend/**'
|
||||||
|
- '.forgejo/workflows/ci-tradein.yml'
|
||||||
|
|
||||||
|
backend-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.backend == 'true'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./tradein-mvp/backend
|
||||||
|
env:
|
||||||
|
# psycopg v3 требует parseable URL на импорте; реального коннекта нет —
|
||||||
|
# DB-тесты мокаются (mirror deploy-tradein.yml test-job).
|
||||||
|
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
# 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: Run pytest (tradein-mvp/backend)
|
||||||
|
# DESELECT (актуализировано 2026-07-02, #2208): test_search_cache_hit падает
|
||||||
|
# ТОЛЬКО в whole-suite ordering (401 vs 200; в изоляции проходит) — global-state
|
||||||
|
# leak из другого test-модуля, pre-existing. Второй исторический deselect
|
||||||
|
# (test_cian_valuation::test_cache_hit_returns_cached) убран — проходит в
|
||||||
|
# полном прогоне (проверено локально: 2947 passed / 1 failed). Список обязан
|
||||||
|
# совпадать с test-job в deploy-tradein.yml.
|
||||||
|
run: |
|
||||||
|
uv run pytest -q \
|
||||||
|
--deselect "tests/test_search_api.py::test_search_cache_hit"
|
||||||
|
|
||||||
|
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).
|
||||||
|
# npm-кэш setup-node НЕ настраиваем: в tradein-mvp/frontend нет
|
||||||
|
# package-lock.json (Dockerfile ставит через npm install), а cache=npm
|
||||||
|
# без lockfile падает. Кэш wheels/node тут не критичен для type-check/lint.
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- name: Install deps (npm install, no lockfile)
|
||||||
|
# ТОЧНЫЕ флаги из tradein-mvp/frontend/Dockerfile (deps stage):
|
||||||
|
# --legacy-peer-deps — Tailwind/React 19 peer-dep mismatches;
|
||||||
|
# --no-audit --no-fund — тише и быстрее в CI. `install` (не `ci`):
|
||||||
|
# в tradein-mvp/frontend НЕТ package-lock.json (есть pnpm-lock.yaml, но
|
||||||
|
# Dockerfile ставит именно npm install) → `npm ci` упал бы.
|
||||||
|
run: npm install --legacy-peer-deps --no-audit --no-fund
|
||||||
|
|
||||||
|
- name: Type-check (tsc --noEmit)
|
||||||
|
# Blocking: любая TS-ошибка → job RED.
|
||||||
|
run: npm run type-check
|
||||||
|
|
||||||
|
- name: Lint (next lint)
|
||||||
|
# Blocking: любая ESLint-ошибка → job RED.
|
||||||
|
run: npm run lint
|
||||||
|
|
@ -133,7 +133,7 @@ jobs:
|
||||||
# Quality gate: pytest MUST pass before any image is built/deployed (#666).
|
# Quality gate: pytest MUST pass before any image is built/deployed (#666).
|
||||||
# Runs the tradein-mvp/backend suite; a red test blocks build + deploy.
|
# Runs the tradein-mvp/backend suite; a red test blocks build + deploy.
|
||||||
# Tests use mocks + a stub DATABASE_URL — no real Postgres/Redis needed.
|
# Tests use mocks + a stub DATABASE_URL — no real Postgres/Redis needed.
|
||||||
# 2 pre-existing order-dependent tests are deselected (see DESELECT note below).
|
# 1 pre-existing order-dependent test is deselected (see DESELECT note below).
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: changes
|
needs: changes
|
||||||
|
|
@ -158,20 +158,21 @@ jobs:
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Sync deps (incl. dev group — pytest)
|
- name: Sync deps (incl. dev group — pytest)
|
||||||
# NB: tradein-mvp/backend/uv.lock is gitignored (see tradein-mvp/.gitignore),
|
# Workspace-лок tradein-mvp/uv.lock TRACKED (с воркспейса #2137; gitignored
|
||||||
# so --frozen can't work in CI (no lockfile in a fresh checkout). Resolve
|
# только старый backend/uv.lock) → --frozen детерминирован и зеркалит
|
||||||
# from pyproject.toml like the Dockerfile does (uv sync --no-dev there).
|
# Dockerfile (uv sync --frozen --no-dev). Актуализировано в #2208.
|
||||||
run: uv sync
|
run: uv sync --frozen
|
||||||
|
|
||||||
- name: Run pytest (tradein-mvp/backend)
|
- name: Run pytest (tradein-mvp/backend)
|
||||||
# DESELECT (2026-05): two pre-existing tests fail only in whole-suite
|
# DESELECT (актуализировано 2026-07-02, #2208): test_search_cache_hit падает
|
||||||
# ordering (pass in isolation) due to global state leak from other test
|
# ТОЛЬКО в whole-suite ordering (401 vs 200; в изоляции проходит) — global-state
|
||||||
# modules — not introduced here. Excluded so the gate is reliably green;
|
# leak из другого test-модуля, pre-existing. Второй исторический deselect
|
||||||
# tracked separately. Everything else (1087 tests) must pass.
|
# (test_cian_valuation::test_cache_hit_returns_cached) убран — проходит в полном
|
||||||
|
# прогоне (проверено 2026-07-02: 2947 passed / 1 failed). Список обязан
|
||||||
|
# совпадать с backend-tests в ci-tradein.yml (pre-merge гейт).
|
||||||
run: |
|
run: |
|
||||||
uv run pytest -q \
|
uv run pytest -q \
|
||||||
--deselect "tests/test_search_api.py::test_search_cache_hit" \
|
--deselect "tests/test_search_api.py::test_search_cache_hit"
|
||||||
--deselect "tests/test_cian_valuation.py::test_cache_hit_returns_cached"
|
|
||||||
|
|
||||||
build-backend:
|
build-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,15 @@ scraper-kit = { workspace = true }
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.0.0",
|
"pytest>=8.0.0",
|
||||||
"pytest-asyncio>=0.24.0",
|
"pytest-asyncio>=0.24.0",
|
||||||
|
"pytest-timeout>=2.3.0",
|
||||||
"ruff>=0.5.0",
|
"ruff>=0.5.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
|
# Страховка от зависших тестов в CI (per-test wall-clock limit, секунды).
|
||||||
|
# Покрывает и pre-merge гейт (ci-tradein.yml), и post-merge test-job (deploy).
|
||||||
|
timeout = 120
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["app*"]
|
include = ["app*"]
|
||||||
|
|
|
||||||
13
tradein-mvp/frontend/eslint.config.mjs
Normal file
13
tradein-mvp/frontend/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// ESLint flat config для tradein-mvp/frontend (#2208 CI-гейт).
|
||||||
|
// eslint-config-next в flat-формате через FlatCompat (next lint deprecated в Next 16,
|
||||||
|
// поэтому конфиг сразу совместим и с прямым `eslint .`).
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const compat = new FlatCompat({ baseDirectory: import.meta.dirname });
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
{ ignores: [".next/**", "node_modules/**", "out/**"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@eslint/eslintrc": "^3.0.0",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-next": "^15.0.0",
|
"eslint-config-next": "^15.0.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|
|
||||||
|
|
@ -1416,7 +1416,7 @@ function CianBackfillSection() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scraper-section">
|
<div className="scraper-section">
|
||||||
<h3>7. Full Load (полный обход без anchor'ов)</h3>
|
<h3>7. Full Load (полный обход без anchor'ов)</h3>
|
||||||
<p className="scraper-hint">
|
<p className="scraper-hint">
|
||||||
Exhaustive Cian ЕКБ: адаптивное деление по цене, пагинация каждого бакета. Запускается в background.
|
Exhaustive Cian ЕКБ: адаптивное деление по цене, пагинация каждого бакета. Запускается в background.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -1820,7 +1820,7 @@ function YandexBackfillSection() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scraper-section">
|
<div className="scraper-section">
|
||||||
<h3>6. Full Load (полный обход без anchor'ов)</h3>
|
<h3>6. Full Load (полный обход без anchor'ов)</h3>
|
||||||
<p className="scraper-hint">
|
<p className="scraper-hint">
|
||||||
Exhaustive Yandex ЕКБ: адаптивное деление по цене, пагинация каждого бакета. Background.
|
Exhaustive Yandex ЕКБ: адаптивное деление по цене, пагинация каждого бакета. Background.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
14
tradein-mvp/uv.lock
generated
14
tradein-mvp/uv.lock
generated
|
|
@ -998,6 +998,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-timeout"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
|
@ -1329,6 +1341,7 @@ dependencies = [
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "pytest-timeout" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1362,6 +1375,7 @@ requires-dist = [
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pytest", specifier = ">=8.0.0" },
|
{ name = "pytest", specifier = ">=8.0.0" },
|
||||||
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
|
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
|
||||||
|
{ name = "pytest-timeout", specifier = ">=2.3.0" },
|
||||||
{ name = "ruff", specifier = ">=0.5.0" },
|
{ name = "ruff", specifier = ">=0.5.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue