Incremental pyright in CI: Strict Types Without Hour-Long Monorepo Gates

Incremental pyright in CI: Strict Types Without Hour-Long Monorepo Gates
5 views
Strict pyright on a giant monorepo is correct—and useless if every PR waits an hour. The unfair advantage is affected-package analysis: typecheck only dirty projects plus dependents, keep a warm cache, and reserve full-fleet runs for main/nightly.
⚡ TL;DR: Partition with pyrightconfig.json / polyrepo packages; compute changed paths → package set → dependents; run pyright -p per package with CI cache on pyright artifacts; full check on main. Pair with TypeScript Project References and AST-Guided Edits.

Incremental pyright in CI: production guidance

A single root invocation:
  • Re-walks the entire import graph on every PR
  • Contends for CPU with tests and lint in the same job
  • Encourages teams to flip typeCheckingMode to basic “temporarily”
  • Hides which package introduced the break when the log is 40k lines
Incremental CI keeps strict mode and shrinks the workset, not the rules.

Package graph, not file list

bash
# ✅ Example: git-diff → packages (illustrative)
git diff --name-only origin/main...HEAD \
 | awk -F/ '/^packages\//{print $2}' \
 | sort -u > /tmp/changed_pkgs.txt
python
# ✅ Expand dependents from a simple adjacency file
# packages/deps.json: {"payments": ["ledger", "api"], "api": []}
import json
from pathlib import Path

deps = json.loads(Path("packages/deps.json").read_text())
# reverse edges: who imports me?
rev: dict[str, set[str]] = {k: set() for k in deps}
for pkg, uses in deps.items():
 for u in uses:
 rev.setdefault(u, set()).add(pkg)

def closure(changed: set[str]) -> set[str]:
 out = set(changed)
 stack = list(changed)
 while stack:
 p = stack.pop()
 for d in rev.get(p, ()):
 if d not in out:
 out.add(d)
 stack.append(d)
 return out
yaml
# ✅ CI job sketch
# - restore cache key: pyright-${{ hashFiles('**/pyrightconfig.json', 'uv.lock') }}
# - for pkg in $(cat affected); do pyright -p packages/$pkg; done
# - on main: pyright -p . # full
Generate deps.json from your packaging tool (Pants, Bazel, uv workspaces, poetry plugins) so humans do not hand-edit edges forever. Treat drift as a CI failure.

Config that keeps strictness

json
{
 "include": ["src"],
 "exclude": ["**/tests/fixtures", "**/generated"],
 "typeCheckingMode": "strict",
 "venvPath": ".",
 "venv": ".venv",
 "reportMissingImports": true,
 "pythonVersion": "3.12"
}
Per-package configs should extend a shared base rather than fork rules:
json
{
 "extends": "../../pyright.base.json",
 "include": ["src"],
 "executionEnvironments": [{ "root": "src" }]
}
Mode When
Affected + dependents PR / push branches
Full workspace main, nightly, release
Changed files only ❌ misses cross-package breakages
❌ pyright $(git diff --name-only) on raw files without dependents — you green PRs that break importers.

Caching that actually hits

  • Cache key: pyright-${{ runner.os }}-${{ hashFiles('**/pyrightconfig*.json', 'uv.lock', '**/py.typed') }}
  • Store the pyright binary / nodeenv separately from package type artifacts
  • Prefer uv sync --frozen before pyright so stub packages are identical across runners
  • Shard by package on large PRs (matrix.package) but merge results into one check run

Agent-friendly build graph

Coding agents thrash CI when they cannot see package boundaries. Check in a machine-readable graph (same idea as TypeScript Project References) and teach agents to edit within one package + run that package’s pyright locally before opening a PR. Pair with AST-Guided Edits so refactors do not spray Any across the monorepo.

Escape hatches that do not rot

Allow # pyright: ignore only with a tracking issue ID in the same line. Ban repo-wide typeCheckingMode: off. If a generated client is untyped, put it under exclude and wrap it with a typed facade package.

Nightly full gate as the contract

PR incremental checks are a filter, not the source of truth. Nightly (and every merge to main) must still run the full strict workspace. If nightly fails while PRs were green, you have a graph bug—usually a missing reverse edge—or an unchecked generated path. Page the owning platform team; do not silently downgrade strictness overnight.

Measuring the win

Track PR wall-clock for the typecheck job before/after incremental rollout. Healthy targets: median PR typecheck under 5–8 minutes on a 1M-LOC monorepo, with nightly full under whatever budget finance already accepted. Publish a weekly dashboard of “PRs that would have missed a dependent break under file-only mode” by replaying diffs — that proves why dependents stay mandatory.

Closing checklist

✅ Dos – ✅ Maintain an explicit package dependency graph – ✅ Typecheck dependents of changed packages – ✅ Cache pyright / nodeenv between CI runs – ✅ Keep main full-strict as the source of truth – ✅ Fail CI on config drift (pyrightconfig owned) – ✅ Shard by package on large affected sets – ✅ Require issue IDs on inline ignores ❌ Don’ts – ❌ Don’t lower typeCheckingMode to save wall time – ❌ Don’t skip dependents to “go faster” – ❌ Don’t commit generated files into the typecheck set without need – ❌ Don’t run one giant process with no cache on every PR – ❌ Don’t let agents open PRs without a local package-scoped pyright run

Related reading

Last updated on September 11, 2026 Related: Cursor Agent Mode: Multi-File Refactors That Survive CI Gates; Cursor Background Agents: CI Gates That Catch Hallucinated AWS Changes; WebSockets vs Server-Sent Events vs Long Polling: Real-Time Communication Explained.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.