Coverage for .github / scripts / verify_lambda_imports.py: 100.00%
43 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
1#!/usr/bin/env python3
2"""Import every deployable Python Lambda handler in an isolated process."""
4from __future__ import annotations
6import argparse
7import os
8import subprocess
9import sys
10from dataclasses import dataclass
11from pathlib import Path
13ENTRYPOINT_OVERRIDES = {
14 "analytics-cleanup": "handler",
15 "helm-orchestrator": "on_event",
16}
17DEFAULT_ENTRYPOINT = "lambda_handler"
18IMPORT_CHECK = (
19 "import importlib,sys;"
20 "sys.path.insert(0,sys.argv[1]);"
21 "module=importlib.import_module('handler');"
22 "entrypoint=getattr(module,sys.argv[2]);"
23 "raise SystemExit(0 if callable(entrypoint) else 1)"
24)
27@dataclass(frozen=True)
28class HandlerTarget:
29 """One handler module and the callable configured as its entrypoint."""
31 directory: Path
32 entrypoint: str
35def discover_handlers(root: Path) -> list[HandlerTarget]:
36 """Return every non-generated ``lambda/*/handler.py`` target."""
37 lambda_root = root.resolve() / "lambda"
38 targets = [
39 HandlerTarget(
40 directory=handler_path.parent,
41 entrypoint=ENTRYPOINT_OVERRIDES.get(
42 handler_path.parent.name,
43 DEFAULT_ENTRYPOINT,
44 ),
45 )
46 for handler_path in sorted(lambda_root.glob("*/handler.py"))
47 if not handler_path.parent.name.endswith("-build")
48 ]
49 if not targets:
50 raise RuntimeError(f"no Python Lambda handlers found under {lambda_root}")
52 discovered_names = {target.directory.name for target in targets}
53 stale_overrides = set(ENTRYPOINT_OVERRIDES) - discovered_names
54 if stale_overrides:
55 names = ", ".join(sorted(stale_overrides))
56 raise RuntimeError(f"entrypoint overrides reference missing handlers: {names}")
57 return targets
60def verify_handler(target: HandlerTarget, *, timeout: int = 30) -> None:
61 """Import one handler in a fresh interpreter and require a callable entrypoint."""
62 environment = os.environ.copy()
63 environment.setdefault("AWS_DEFAULT_REGION", "us-east-1")
64 environment.setdefault("AWS_REGION", "us-east-1")
65 environment.setdefault("AWS_EC2_METADATA_DISABLED", "true")
66 subprocess.run(
67 [sys.executable, "-c", IMPORT_CHECK, str(target.directory), target.entrypoint],
68 check=True,
69 env=environment,
70 timeout=timeout,
71 )
74def verify_handlers(targets: list[HandlerTarget]) -> None:
75 """Verify every discovered target without sharing imported module state."""
76 for target in targets:
77 relative_directory = target.directory.name
78 print(f"=== lambda/{relative_directory} ({target.entrypoint}) ===", flush=True)
79 verify_handler(target)
80 print(f"Imported {len(targets)} Python Lambda handlers.")
83def main(argv: list[str] | None = None) -> int:
84 parser = argparse.ArgumentParser(description=__doc__)
85 parser.add_argument(
86 "--root",
87 type=Path,
88 default=Path(__file__).resolve().parents[2],
89 help="Repository root (defaults to the checkout containing this script)",
90 )
91 args = parser.parse_args(argv)
92 verify_handlers(discover_handlers(args.root))
93 return 0
96if __name__ == "__main__":
97 raise SystemExit(main())