From 6dec7d411af1226a37c798da5f98f55be80a703f Mon Sep 17 00:00:00 2001 From: FZ2000 Date: Thu, 30 Jul 2026 21:44:10 -0700 Subject: [PATCH] ci: cache pip, bound every job, and turn on five free mypy flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-publication hardening pass. Three findings, each measured. PIP CACHING — 19 installs, no cache `setup-python` was configured without `cache:` anywhere, so all nine setup steps re-downloaded the dev toolchain (ruff, mypy, pytest, pytest-cov and their graph) on every run. The runtime has zero dependencies, which is exactly why this was easy to miss — the thing being installed is never shipped. Now `cache: pip` keyed on pyproject.toml. Measured baseline for comparison on the next run: 8.3 runner-minutes total, with the five test-matrix legs at ~60s each and most of that spent installing. TIMEOUTS — 2 of 16 jobs had one GitHub's default job timeout is 360 minutes. A hung job — a network call that never returns, a test that deadlocks — burns six hours of the account's runner budget before it is killed. Every job now carries an explicit `timeout-minutes`, sized to roughly 15x its observed runtime so a slow runner cannot trip it: test / integration / artifact / cassette-replay / network-anonymous 15 lint / smoke / pip-audit / secret-scan / verified-secret-scan 10 codespell / markdownlint / link-check / discoverability / ci-gate 5 MYPY — five strict flags that already passed check_untyped_defs, no_implicit_optional, strict_equality, disallow_untyped_calls and disallow_incomplete_defs were all off. I ran each individually and then all five together against the package before enabling any of them: zero findings in every case. So they cost nothing today and stop a regression tomorrow, which is the only kind of strictness worth adding retroactively. These are specifically the strict flags that do not require annotating every local variable — the ones this codebase already satisfied without declaring it. The seven disabled error codes stay disabled; those would be real work, and the README badge no longer claims "strict" anyway. The same five flags are added to the mypy hook in .pre-commit-config.yaml. That file previously drifted from pyproject (its `files:` pattern matched nothing at all, fixed earlier), so the agreement is now asserted rather than assumed: pyproject's enabled flags and the hook's args are the same set, verified both directions. Verified: ruff, format, mypy, doc references, doc flags + usage lines all clean; full suite green; actionlint clean on all three workflows. --- .github/workflows/ci.yml | 56 ++++++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 5 ++++ pyproject.toml | 9 +++++++ 3 files changed, 70 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fa5895..e32763e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,11 +68,17 @@ concurrency: jobs: lint: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.13" + # Caches the pip download/wheel cache keyed on pyproject.toml. The + # runtime has zero dependencies, but the dev extra pulls ruff, mypy, + # pytest and their graph, and 19 jobs were each fetching it fresh. + cache: pip + cache-dependency-path: pyproject.toml - name: Install dev deps run: | python -m pip install --upgrade pip @@ -113,11 +119,14 @@ jobs: # registered with that label. python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyproject.toml - name: Install dev deps run: | python -m pip install --upgrade pip @@ -150,11 +159,17 @@ jobs: integration: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.13" + # Caches the pip download/wheel cache keyed on pyproject.toml. The + # runtime has zero dependencies, but the dev extra pulls ruff, mypy, + # pytest and their graph, and 19 jobs were each fetching it fresh. + cache: pip + cache-dependency-path: pyproject.toml - name: Install dev deps run: | python -m pip install --upgrade pip @@ -172,11 +187,17 @@ jobs: smoke: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.13" + # Caches the pip download/wheel cache keyed on pyproject.toml. The + # runtime has zero dependencies, but the dev extra pulls ruff, mypy, + # pytest and their graph, and 19 jobs were each fetching it fresh. + cache: pip + cache-dependency-path: pyproject.toml - name: Run `da --version` (stdlib-only — no deps needed) run: python3 da --version - name: Run `da --help` @@ -243,12 +264,18 @@ jobs: # installer that flattens one, passes all of them. This job runs only # what a user would actually receive. runs-on: ubuntu-latest + timeout-minutes: 15 needs: test steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.13" + # Caches the pip download/wheel cache keyed on pyproject.toml. The + # runtime has zero dependencies, but the dev extra pulls ruff, mypy, + # pytest and their graph, and 19 jobs were each fetching it fresh. + cache: pip + cache-dependency-path: pyproject.toml - name: Build wheel + sdist run: | @@ -336,6 +363,7 @@ jobs: # --------------------------------------------------------------------------- secret-scan: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v7 with: @@ -368,6 +396,7 @@ jobs: # approach as the gitleaks job above). --fail → exit 183 on any # reportable finding, which gates CI. runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v7 with: @@ -384,6 +413,7 @@ jobs: codespell: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v7 - name: Install codespell @@ -411,12 +441,18 @@ jobs: # deps). da-cli itself has 0 runtime deps, so this is purely about # the dev environment. runs-on: ubuntu-latest + timeout-minutes: 10 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.13" + # Caches the pip download/wheel cache keyed on pyproject.toml. The + # runtime has zero dependencies, but the dev extra pulls ruff, mypy, + # pytest and their graph, and 19 jobs were each fetching it fresh. + cache: pip + cache-dependency-path: pyproject.toml - name: Install dev deps run: | python -m pip install --upgrade pip @@ -436,6 +472,7 @@ jobs: # advisory and the corpus had drifted to 56 violations. Run it here # so "configured" and "enforced" mean the same thing. runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v7 - name: markdownlint @@ -449,6 +486,7 @@ jobs: # deterministic, no network, gates every push/PR. External URLs are # covered by link-check-external below. runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v7 - name: Install lychee @@ -468,6 +506,7 @@ jobs: # unrelated PRs. Exclusions and accepted status codes live in # lychee.toml. runs-on: ubuntu-latest + timeout-minutes: 10 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' steps: - uses: actions/checkout@v7 @@ -487,11 +526,17 @@ jobs: # so unlike the live jobs below it can gate every push, and it is # what actually catches a DA response-shape change. runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.13" + # Caches the pip download/wheel cache keyed on pyproject.toml. The + # runtime has zero dependencies, but the dev extra pulls ruff, mypy, + # pytest and their graph, and 19 jobs were each fetching it fresh. + cache: pip + cache-dependency-path: pyproject.toml - name: Install dev + integration deps run: | python -m pip install --upgrade pip @@ -531,12 +576,18 @@ jobs: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest + timeout-minutes: 15 needs: test # don't bother if unit tests are broken steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.13" + # Caches the pip download/wheel cache keyed on pyproject.toml. The + # runtime has zero dependencies, but the dev extra pulls ruff, mypy, + # pytest and their graph, and 19 jobs were each fetching it fresh. + cache: pip + cache-dependency-path: pyproject.toml - name: Install dev + integration deps run: | python -m pip install --upgrade pip @@ -571,6 +622,11 @@ jobs: - uses: actions/setup-python@v7 with: python-version: "3.13" + # Caches the pip download/wheel cache keyed on pyproject.toml. The + # runtime has zero dependencies, but the dev extra pulls ruff, mypy, + # pytest and their graph, and 19 jobs were each fetching it fresh. + cache: pip + cache-dependency-path: pyproject.toml - name: Discoverability and metadata checks run: python3 tools/check_discoverability.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24e67a5..9f531e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,6 +48,11 @@ repos: - "--warn-unreachable" - "--warn-redundant-casts" - "--warn-unused-ignores" + - "--check-untyped-defs" + - "--no-implicit-optional" + - "--strict-equality" + - "--disallow-untyped-calls" + - "--disallow-incomplete-defs" - "--disable-error-code=type-arg" - "--disable-error-code=no-any-return" - "--disable-error-code=attr-defined" diff --git a/pyproject.toml b/pyproject.toml index 6716fda..b0a0ad4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -248,6 +248,15 @@ warn_unreachable = true warn_redundant_casts = true warn_unused_ignores = true disallow_untyped_decorators = true +# All five verified to pass with zero findings before being enabled, so they +# cost nothing today and stop a regression tomorrow. They are the strict flags +# that do not require annotating every local — the ones this codebase already +# satisfies without knowing it. +check_untyped_defs = true # type-check bodies of unannotated functions +no_implicit_optional = true # `def f(x: int = None)` is an error, not Optional +strict_equality = true # `x == y` where the types cannot overlap +disallow_untyped_calls = true # calling an unannotated function from a typed one +disallow_incomplete_defs = true # a signature annotated only in part disallow_untyped_defs = true no_implicit_reexport = true # Relax the rules that produce noise from JSON-shaped values