From 5bcb7ab90903c01cb1544f2cf8f464ae2c8db842 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 2 Jul 2026 22:48:18 +0300 Subject: [PATCH] =?UTF-8?q?ci(tradein):=20pre-merge=20=D0=B3=D0=B5=D0=B9?= =?UTF-8?q?=D1=82=20=E2=80=94=20backend=20pytest=20+=20frontend=20type-che?= =?UTF-8?q?ck/lint=20(#2208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тradein-PR мержились на пусто-зелёных чеках: ci.yml фильтрует только backend/**+frontend/** главного стека, а pytest tradein жил лишь в post-merge deploy-tradein.yml. Новый ci-tradein.yml гейтит pull_request до мержа; deploy test-job синхронизирован. - уборка устаревшего deselect: test_cian_valuation::test_cache_hit_returns_cached проходит в полном прогоне (2947 passed); остаётся только test_search_cache_hit - pytest-timeout=120 per-test (страховка от зависших тестов в CI) - uv sync --frozen в обоих test-job (workspace-лок tracked, зеркало Dockerfile) - eslint flat config для tradein-frontend (гейт lint был невозможен: next lint без конфига уходит в интерактивный prompt и падает в CI) - 2 apostrophe lint-фикса в scrapers/page.tsx Refs #2208 --- .forgejo/workflows/ci-tradein.yml | 135 ++++++++++++++++++ .forgejo/workflows/deploy-tradein.yml | 23 +-- tradein-mvp/backend/pyproject.toml | 4 + tradein-mvp/frontend/eslint.config.mjs | 13 ++ tradein-mvp/frontend/package.json | 1 + .../frontend/src/app/scrapers/page.tsx | 4 +- tradein-mvp/uv.lock | 14 ++ 7 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 .forgejo/workflows/ci-tradein.yml create mode 100644 tradein-mvp/frontend/eslint.config.mjs diff --git a/.forgejo/workflows/ci-tradein.yml b/.forgejo/workflows/ci-tradein.yml new file mode 100644 index 00000000..11396792 --- /dev/null +++ b/.forgejo/workflows/ci-tradein.yml @@ -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//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 diff --git a/.forgejo/workflows/deploy-tradein.yml b/.forgejo/workflows/deploy-tradein.yml index 92eaf0af..87f8c111 100644 --- a/.forgejo/workflows/deploy-tradein.yml +++ b/.forgejo/workflows/deploy-tradein.yml @@ -133,7 +133,7 @@ jobs: # Quality gate: pytest MUST pass before any image is built/deployed (#666). # Runs the tradein-mvp/backend suite; a red test blocks build + deploy. # 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: runs-on: ubuntu-latest needs: changes @@ -158,20 +158,21 @@ jobs: echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Sync deps (incl. dev group — pytest) - # NB: tradein-mvp/backend/uv.lock is gitignored (see tradein-mvp/.gitignore), - # so --frozen can't work in CI (no lockfile in a fresh checkout). Resolve - # from pyproject.toml like the Dockerfile does (uv sync --no-dev there). - run: uv sync + # Workspace-лок tradein-mvp/uv.lock TRACKED (с воркспейса #2137; gitignored + # только старый backend/uv.lock) → --frozen детерминирован и зеркалит + # Dockerfile (uv sync --frozen --no-dev). Актуализировано в #2208. + run: uv sync --frozen - name: Run pytest (tradein-mvp/backend) - # DESELECT (2026-05): two pre-existing tests fail only in whole-suite - # ordering (pass in isolation) due to global state leak from other test - # modules — not introduced here. Excluded so the gate is reliably green; - # tracked separately. Everything else (1087 tests) must pass. + # 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) убран — проходит в полном + # прогоне (проверено 2026-07-02: 2947 passed / 1 failed). Список обязан + # совпадать с backend-tests в ci-tradein.yml (pre-merge гейт). run: | uv run pytest -q \ - --deselect "tests/test_search_api.py::test_search_cache_hit" \ - --deselect "tests/test_cian_valuation.py::test_cache_hit_returns_cached" + --deselect "tests/test_search_api.py::test_search_cache_hit" build-backend: runs-on: ubuntu-latest diff --git a/tradein-mvp/backend/pyproject.toml b/tradein-mvp/backend/pyproject.toml index b94fdad8..dbb1b025 100644 --- a/tradein-mvp/backend/pyproject.toml +++ b/tradein-mvp/backend/pyproject.toml @@ -37,11 +37,15 @@ scraper-kit = { workspace = true } dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.24.0", + "pytest-timeout>=2.3.0", "ruff>=0.5.0", ] [tool.pytest.ini_options] 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] include = ["app*"] diff --git a/tradein-mvp/frontend/eslint.config.mjs b/tradein-mvp/frontend/eslint.config.mjs new file mode 100644 index 00000000..ae75f11e --- /dev/null +++ b/tradein-mvp/frontend/eslint.config.mjs @@ -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; diff --git a/tradein-mvp/frontend/package.json b/tradein-mvp/frontend/package.json index d23062f2..b3579017 100644 --- a/tradein-mvp/frontend/package.json +++ b/tradein-mvp/frontend/package.json @@ -20,6 +20,7 @@ "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@eslint/eslintrc": "^3.0.0", "eslint": "^9.0.0", "eslint-config-next": "^15.0.0", "typescript": "^5.9.3" diff --git a/tradein-mvp/frontend/src/app/scrapers/page.tsx b/tradein-mvp/frontend/src/app/scrapers/page.tsx index ac466fb6..03703465 100644 --- a/tradein-mvp/frontend/src/app/scrapers/page.tsx +++ b/tradein-mvp/frontend/src/app/scrapers/page.tsx @@ -1416,7 +1416,7 @@ function CianBackfillSection() {
-

7. Full Load (полный обход без anchor'ов)

+

7. Full Load (полный обход без anchor'ов)

Exhaustive Cian ЕКБ: адаптивное деление по цене, пагинация каждого бакета. Запускается в background.

@@ -1820,7 +1820,7 @@ function YandexBackfillSection() {
-

6. Full Load (полный обход без anchor'ов)

+

6. Full Load (полный обход без anchor'ов)

Exhaustive Yandex ЕКБ: адаптивное деление по цене, пагинация каждого бакета. Background.

diff --git a/tradein-mvp/uv.lock b/tradein-mvp/uv.lock index e03ef752..0db989ad 100644 --- a/tradein-mvp/uv.lock +++ b/tradein-mvp/uv.lock @@ -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" }, ] +[[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]] name = "python-dotenv" version = "1.2.2" @@ -1329,6 +1341,7 @@ dependencies = [ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-timeout" }, { name = "ruff" }, ] @@ -1362,6 +1375,7 @@ requires-dist = [ dev = [ { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, + { name = "pytest-timeout", specifier = ">=2.3.0" }, { name = "ruff", specifier = ">=0.5.0" }, ] -- 2.45.3