#!/usr/bin/env python3 """CI guard: fail on undefined-name references (the pyflakes F821 class). This is the exact bug class behind the case-rename 500 (PR #249): a name referenced but never imported/defined. It is invisible to tests when it sits inside a rarely-hit branch or a fire-and-forget ``background_tasks`` callable — it only NameErrors when that code path runs in production. pyflakes catches it statically, before merge. Scope is deliberately narrow — we gate ONLY on undefined names, not on the other pyflakes findings (unused imports, f-strings without placeholders, unused locals). Those are style noise, not runtime crashes; gating on them would make the check too noisy to keep green. Keep this gate high-signal. Requires pyflakes importable by the running interpreter (the workflow installs it into a throwaway venv and runs this script with that venv's python). """ from __future__ import annotations import subprocess import sys # Paths that ship into the running app / are executed operationally. TARGETS = ["web", "mcp-server/src", "scripts"] # pyflakes messages that mean "this reference will NameError at runtime". FATAL_MARKERS = ("undefined name", "may be undefined") def main() -> int: proc = subprocess.run( [sys.executable, "-m", "pyflakes", *TARGETS], capture_output=True, text=True, ) # pyflakes exits non-zero whenever it has ANY finding; we re-classify so # that only the fatal class fails the build. lines = (proc.stdout + proc.stderr).splitlines() fatal = [ln for ln in lines if any(m in ln for m in FATAL_MARKERS)] if fatal: print("❌ undefined name(s) detected — these crash at runtime:\n") for ln in fatal: print(f" {ln}") print( f"\n{len(fatal)} undefined-name finding(s). Import or define the " "name, or delete the dead reference." ) return 1 print(f"✓ no undefined names in: {', '.join(TARGETS)}") return 0 if __name__ == "__main__": raise SystemExit(main())