Skip to content

refactor(telegram): drop the flood-stack compat shims #3598

refactor(telegram): drop the flood-stack compat shims

refactor(telegram): drop the flood-stack compat shims #3598

Workflow file for this run

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