diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c12fffc..e82153d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ name: CI # way. If you rename it, update the ruleset in the same change. # # So the two triggers do different jobs: `pull_request` GATES the merge, `push` VERIFIES the result -# of it. `release.yml` remains the gate that matters for money -- it re-runs `ruff` + `pytest` +# of it. `release.yml` remains the gate that matters for money -- it re-runs `ruff` + `mypy` + `pytest` # itself before it will build an artifact, so a red `main` cannot become a wheel, a tag, or a # release asset even if both of these were removed. on: @@ -60,6 +60,18 @@ jobs: - name: Lint run: uv run ruff check keel tests packages + # A STEP in this job, deliberately not a job of its own. The `main` ruleset requires the + # status context `test`, which this job produces; a separate `typecheck:` job would report + # a context nothing requires, so a red mypy would not block a merge -- the same failure + # shape the header above describes for a renamed job. As a step it inherits that gate. + # + # Paths come from `[tool.mypy]`'s `files` in pyproject.toml and are deliberately NOT + # repeated here: `keel.*` came off the `ignore_errors` list in #266, and the point of this + # step is that it cannot drift back silently. A package moving in or out of the checked + # set is then one edit there, not one edit mirrored across two workflows. + - name: Type-check + run: uv run mypy + - name: Test run: uv run pytest -q diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95483d67..fc4319df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,11 @@ jobs: - name: Lint run: uv run ruff check keel tests packages + # Re-run here rather than trusting CI's: this workflow is dispatched against whatever + # `main` is at the time, which need not be the commit any CI run went green on. + - name: Type-check + run: uv run mypy + - name: Test run: uv run pytest -q diff --git a/tests/test_packaging.py b/tests/test_packaging.py index dc16b899..f1ac7be2 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -144,6 +144,32 @@ def test_the_strict_module_list_is_not_empty(): ) +def test_keel_is_not_exempt_from_type_checking(): + """`keel.*` must not reappear on an `ignore_errors` override. + + CI runs `mypy` (`ci.yml`, `release.yml`), which catches a type ERROR in `keel/`. It cannot + catch the other direction: re-adding `keel.*` here silences the whole package, mypy goes + green, and the ungating done in #266 is undone with nothing to show for it. That is the + failure mode this file already guards for strictness, in the opposite direction -- an + exemption that reads as a passing build. + + `tests.*` and `keel_core.*` are still legitimately exempt (see the comments beside each in + pyproject.toml); this pins only the one that was deliberately brought under the checker. + """ + exempt = [] + for override in _mypy_overrides(): + if not override.get("ignore_errors"): + continue + entry = override["module"] + exempt.extend([entry] if isinstance(entry, str) else entry) + + assert "keel.*" not in exempt, ( + "`keel.*` is back on an `ignore_errors` override, which silently un-checks the entire " + "package -- mypy will pass while checking nothing there. It was ungated deliberately " + f"(#266); currently exempt: {sorted(exempt)!r}" + ) + + def test_broker_strict_flags_match_mypy_strict(): """The expanded flag list must stay equal to what `--strict` actually turns on.