refactor(telegram): drop the flood-stack compat shims #3598
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # Triggers — CI runs only on `main` and on pull requests (owner decision, #1051). | |
| # push: [main] — post-merge verification that main itself stays green. | |
| # pull_request: — fires on PR open AND on every push inside an open PR | |
| # (the `synchronize` event), so PR commits are still covered. | |
| # Both run the same full test suite; the CI speedup from #1090 is the parallel | |
| # job split (lint | static-checks | tests), not test selection (#1097 §5). | |
| # Dropped the old `fix/**` / `codex/**` push triggers: they double-ran CI (once | |
| # for the branch push, once for the PR) and burned runners on intermediate, | |
| # draft, PR-less pushes. A push to a feature branch with no open PR no longer | |
| # triggers CI — open the PR to get a run. | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| # Docs-only changes skip the suite (#1051). paths-ignore is "all-or-nothing": | |
| # the run is skipped only when EVERY changed file matches a pattern below, so | |
| # a PR touching both code and docs still runs the full CI. The branch-name | |
| # guard (check-branch-name) still runs because it is an independent job that | |
| # github evaluates after the path filter at the workflow level — a docs-only | |
| # PR simply has no test job to run. | |
| paths-ignore: | |
| - "docs/**" | |
| - "**/*.md" | |
| - "mkdocs.yml" | |
| # Cancel an in-progress run when a newer commit is pushed to the same ref | |
| # (#1051). Without this, every push to an open PR left the previous run burning | |
| # a runner to completion. Grouping by workflow+ref keeps main and each PR | |
| # independent; cancel-in-progress reclaims the runner for the latest commit. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| check-branch-name: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| # A trivial github-script job; the timeout is a hang guard, not a budget. | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Reject branch names with "+" | |
| if: contains(github.head_ref, '+') | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: '⛔ Символ `+` недопустим в названии ветки (`' + context.payload.pull_request.head.ref + '`). Переименуйте ветку без `+` (используйте `-` или `.`).' | |
| }); | |
| core.setFailed('Branch name contains "+", which is not allowed.'); | |
| # Lint is its own job (#1097 §5) so a style slip reds CI in seconds, in | |
| # parallel with the slower static-checks and tests jobs — no waiting on pytest | |
| # to learn ruff is unhappy. | |
| lint: | |
| runs-on: ubuntu-latest | |
| # Just `pip install` + ruff; minutes at most. 10 leaves headroom for a cold | |
| # pip cache. | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install dependencies | |
| run: pip install -e ".[dev]" | |
| - name: Ruff | |
| run: ruff check src tests conftest.py | |
| # Static gates run as their own job so they fail fast and in parallel with the | |
| # long tests job (#944, #1097 §5). Holds the import-architecture contracts and | |
| # the blocking complexity gate, plus the advisory security/duplication scans. | |
| static-checks: | |
| runs-on: ubuntu-latest | |
| # Import contracts + complexity gate + security scans + strict docs build. | |
| # Typically a couple of minutes; 10 leaves headroom over a cold pip install. | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install dependencies | |
| run: pip install -e ".[dev]" | |
| - name: Import architecture contracts | |
| run: lint-imports --config .importlinter | |
| # Blocking cyclomatic-complexity gate (#923). After #922/#923 reduced the | |
| # last rank-F functions, main is at F:0 — this keeps it there: any function | |
| # that grows back to radon rank F (CC ≥ 41) reds CI. radon/vulture ship in | |
| # the [dev] extra so `pip install -e ".[dev]"` above already provides them. | |
| - name: Cyclomatic-complexity gate (no rank-F functions) | |
| run: python scripts/code_health.py --fail-on F | |
| # Non-blocking duplication guard (#807 dedup round 2). Current level is | |
| # ~0.6%; threshold 1% leaves headroom while surfacing regressions. Kept | |
| # advisory (continue-on-error) so it never reds CI on its own. jscpd is | |
| # version-pinned so CI never executes an unvetted future npm release | |
| # (review #918: @claude/@codex supply-chain note). | |
| - name: Code duplication guard (advisory) | |
| continue-on-error: true | |
| run: npx -y jscpd@5.0.10 --min-tokens 70 --format python --threshold 1 src | |
| # Dependency CVE scan (#1053, #1097 §4). Advisory for now (continue-on-error) | |
| # so a pre-existing transitive CVE never reds CI — pip-audit LOCATES, | |
| # Dependabot (.github/dependabot.yml) FIXES via update PRs. Ratchet to | |
| # blocking later once the current findings are triaged. pip-audit ships in | |
| # the [dev] extra installed above. | |
| - name: Dependency vulnerability scan (advisory) | |
| continue-on-error: true | |
| run: pip-audit --desc | |
| # Static security analysis of our own code (#1053). Advisory like the | |
| # duplication guard: the codebase has ~hundreds of broad `except Exception` | |
| # blocks that bandit flags (B110/B112) — kept non-blocking to avoid noise | |
| # until a baseline/skip-list is curated. Config lives in [tool.bandit] in | |
| # pyproject.toml (recurses src, excludes tests). | |
| - name: Code security scan (advisory) | |
| continue-on-error: true | |
| run: bandit -c pyproject.toml -r src -q | |
| # Doc-coverage (interrogate) is intentionally NOT a CI step (#1097 §3): it | |
| # stays a LOCAL script (scripts/doc_coverage.py, #1072) the owner runs on | |
| # demand. Wiring it into CI added noise without a gate, so it was removed. | |
| # Гейт строгой сборки доков (#1071). Тестовые job'ы ставят только [dev], | |
| # где нет mkdocs/mkdocstrings, поэтому build-тест в test_docs_api_reference.py | |
| # там пропускается — strict-сборка не была бы PR-гейтом, а ловилась бы лишь | |
| # post-merge в docs.yml. Ставим [docs] здесь и гоняем `mkdocs build --strict` | |
| # напрямую, чтобы битые ссылки / нерезолвленные autoref'ы валили PR заранее. | |
| - name: Docs build (strict) | |
| run: | | |
| pip install -e ".[docs]" | |
| mkdocs build --strict | |
| - name: Enforce pytest warnings as errors | |
| run: | | |
| python - <<'PY' | |
| import sys | |
| import tomllib | |
| with open("pyproject.toml", "rb") as f: | |
| config = tomllib.load(f) | |
| actual = config["tool"]["pytest"]["ini_options"].get("filterwarnings") | |
| expected = ["error"] | |
| if actual != expected: | |
| print(f"Expected tool.pytest.ini_options.filterwarnings = {expected!r}, got {actual!r}") | |
| sys.exit(1) | |
| PY | |
| # The full test gate — runs on every PR and on main (#1051). Split into its | |
| # own parallel job (#1097 §5) so it fans out alongside lint and static-checks | |
| # instead of serializing behind them. | |
| # | |
| # NOTE (#1090): we evaluated pytest-testmon for a selective PR run and dropped | |
| # it. testmon only deselects in a single process (no-op under `-n auto`) and | |
| # testmon-collection + xdist + coverage INTERNALERRORs — so a testmon PR gate | |
| # would have to run single-process without coverage, which on this large suite | |
| # is often SLOWER than the full `-n auto` sweep, not faster. The real CI | |
| # speedup here is the parallel-job split, not test selection. The full suite | |
| # stays the gate on both PR and main. | |
| tests: | |
| runs-on: ubuntu-latest | |
| # smoke + full parallel + serial pytest with coverage. The suite runs in a | |
| # few minutes on CI; 15 is a generous hang guard well under the 6h GitHub | |
| # default that would otherwise let a deadlocked run idle (#1051). | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install dependencies | |
| run: pip install -e ".[dev]" | |
| # Fast offline preflight: a sub-second-ish sanity gate over real critical | |
| # paths (package import, CLI parser, DB schema, web /health). Runs first | |
| # so a fundamentally broken build fails before the full suite starts. | |
| # Live smoke (real_provider_smoke / codex_*) stays opt-in and is skipped | |
| # here — no gate env vars are set. | |
| - name: Pytest (smoke preflight) | |
| run: pytest tests -q -m smoke | |
| # On CI conftest.py uses every runner core (the load throttle is dev-only), | |
| # so `-n auto` fans out across all vCPUs. --durations surfaces the slowest | |
| # tests each run for ongoing tuning (#944). | |
| # | |
| # Coverage (#1052): both real test runs measure `src` and the per-process | |
| # .coverage.* files are combined afterwards. The parallel run starts the | |
| # dataset (no --cov-append); the serial run appends so its lines aren't | |
| # lost. `[tool.coverage.run] parallel = true` already keeps the xdist | |
| # worker datafiles separate; we pass --cov-report= here to defer reporting | |
| # (and the pyproject fail_under gate) to the single combined step below. | |
| - name: Pytest (parallel-safe) | |
| if: ${{ !cancelled() }} | |
| run: pytest tests -q -m "not aiosqlite_serial" -n auto --durations=25 --cov=src --cov-report= | |
| # Keep each SQLite-backed test file together, but let independent files | |
| # run on separate workers. This preserves within-file SQLite assumptions; | |
| # the old single-process command left all 986 tests serialized. | |
| - name: Pytest (aiosqlite serial, per-file parallel) | |
| if: ${{ !cancelled() }} | |
| run: pytest tests -q -m aiosqlite_serial -n auto --dist=loadfile --cov=src --cov-append --cov-report= | |
| # Report the combined parallel + serial coverage. pytest-cov already merged | |
| # the per-worker datafiles into a single `.coverage` at the end of each run | |
| # (`[tool.coverage.run] parallel = true`), and the serial run's | |
| # `--cov-append` added its lines to that same file — so the dataset is | |
| # already combined here. We deliberately do NOT run `coverage combine`: with | |
| # no leftover `.coverage.*` suffix files it exits 1 ("No data to combine"), | |
| # which under the Actions default `bash -e` would red the step before the | |
| # report runs. `coverage report` reads `.coverage` directly and applies the | |
| # fail_under=88 ratchet from [tool.coverage.report] (#1052, baseline for the | |
| # #1024 bug-hunt epic). The XML artifact lets PRs diff coverage without | |
| # re-running the suite. | |
| - name: Coverage report (combined) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| coverage report | |
| coverage xml -o coverage.xml | |
| - name: Upload coverage artifact | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-xml | |
| path: coverage.xml | |
| if-no-files-found: ignore |