Prevents recurrence of the case-rename 500 (PR #249), whose root cause was an undefined name (`paperclip_client`) sitting in a background_tasks callable — invisible until that code path ran in production. - scripts/check_undefined_names.py: runs pyflakes on web/, mcp-server/src, scripts/ and fails ONLY on "undefined name" / "may be undefined" (the runtime-crash class). Unused imports / f-strings are NOT gated — keeps the check high-signal and green. - .gitea/workflows/lint.yaml: runs the guard on every PR and push to main, in a throwaway venv (PEP-668 safe). - db.py: `from datetime import date` → `date, datetime`. The guard surfaced a real latent undefined name — `insert_panel_round`'s `round_ts: datetime` annotation referenced an unimported `datetime` (benign only because of `from __future__ import annotations`; now correct). - SCRIPTS.md: documented the new guard. Verified: clean tree → exit 0; injected undefined name → exit 1. Invariants: engineering rule §6 (no silent failures shipping to runtime). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
2.0 KiB
Python
57 lines
2.0 KiB
Python
#!/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())
|