Coverage for scripts / example_job_validation / __main__.py: 100.00%
110 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"""CLI entry: ``python -m scripts.example_job_validation``.
3Mirrors ``scripts.live_release_validation.__main__`` (same identity flags,
4consent posture, checkpoint/resume semantics) plus example selection and a
5fully offline ``--static-only`` mode.
6"""
8from __future__ import annotations
10import argparse
11import contextlib
12import os
13import re
14import sys
15import traceback
16from datetime import UTC, datetime
17from pathlib import Path
19from scripts.live_release_validation.cli_args import (
20 path_from_root,
21 repository_root,
22 split_csv_names,
23)
24from scripts.live_release_validation.models import (
25 ValidationReport,
26 utc_now,
27)
28from scripts.live_release_validation.runner import (
29 LiveValidationRunner,
30 require_local_execution,
31)
33from .models import ExampleRunSettings
34from .registry import build_action_registry
35from .specs import EXAMPLE_SPECS
36from .static_checks import run_static_checks
38REPORT_TITLE = "GCO Example Job Validation"
39REPORT_STEM = "example-job-validation"
42def _split_names(value: str) -> tuple[str, ...]:
43 names = tuple(dict.fromkeys(item.strip() for item in value.split(",") if item.strip()))
44 if not names:
45 raise argparse.ArgumentTypeError("expected at least one name")
46 return names
49def _build_parser() -> argparse.ArgumentParser:
50 registry = build_action_registry()
51 parser = argparse.ArgumentParser(
52 prog="python -m scripts.example_job_validation",
53 description=(
54 "Deploy the configured GCO topology, run every selected example "
55 "through its documented submission path, verify its success "
56 "criteria, and always destroy what was deployed. Reports carry "
57 "account-specific identifiers; post only sanitized summaries."
58 ),
59 )
60 parser.add_argument("--repo-root", help="GCO checkout (default: current Git root)")
61 parser.add_argument(
62 "--expected-account",
63 default=os.environ.get("GCO_LIVE_EXPECTED_ACCOUNT"),
64 help="Exact 12-digit AWS account ID (or GCO_LIVE_EXPECTED_ACCOUNT)",
65 )
66 parser.add_argument(
67 "--expected-sha",
68 default=os.environ.get("GCO_LIVE_EXPECTED_SHA"),
69 help="Exact 40-character Git commit (or GCO_LIVE_EXPECTED_SHA)",
70 )
71 parser.add_argument(
72 "--expected-branch",
73 default=os.environ.get("GCO_LIVE_EXPECTED_BRANCH"),
74 help="Exact local branch identity (or GCO_LIVE_EXPECTED_BRANCH)",
75 )
76 parser.add_argument(
77 "--actions",
78 type=split_csv_names,
79 default=("all",),
80 metavar="NAME[,NAME...]",
81 help="Selectable actions; dependencies are added automatically (default: all)",
82 )
83 parser.add_argument("--list-actions", action="store_true", help="List actions and exit")
84 parser.add_argument(
85 "--examples",
86 type=_split_names,
87 default=(),
88 metavar="NAME[,NAME...]",
89 help="Only validate these examples (file stems; default: every example)",
90 )
91 parser.add_argument(
92 "--skip-examples",
93 type=_split_names,
94 default=(),
95 metavar="NAME[,NAME...]",
96 help="Exclude these examples from the selection",
97 )
98 parser.add_argument(
99 "--static-only",
100 action="store_true",
101 help="Run only the offline checks (no AWS access) and exit",
102 )
103 parser.add_argument(
104 "--max-parallel",
105 type=int,
106 default=0,
107 metavar="N",
108 help=(
109 "Maximum examples running concurrently in the examples action "
110 "(default 0 = all selected at once; 1 = serial)"
111 ),
112 )
113 parser.add_argument("--run-id", help="Stable run/checkpoint identifier")
114 parser.add_argument(
115 "--report-dir", help="Report directory (default: .example-job-validation/<run-id>)"
116 )
117 parser.add_argument(
118 "--checkpoint", help="Checkpoint JSON path (default: <report-dir>/checkpoint.json)"
119 )
120 parser.add_argument(
121 "--resume", action="store_true", help="Resume an exact identity-matched checkpoint"
122 )
123 parser.add_argument(
124 "--protected-stack",
125 action="append",
126 default=[],
127 metavar="NAME",
128 help="Additional non-project CloudFormation stack to preserve exactly",
129 )
130 parser.add_argument(
131 "--confirm-kms-key-deletion",
132 action="store_true",
133 help=(
134 "Explicitly authorize scheduling only this run's exact retained EKS "
135 "KMS keys for deletion after stack teardown"
136 ),
137 )
138 parser.epilog = (
139 "Actions: " + ", ".join(registry) + ". Examples: " + ", ".join(sorted(EXAMPLE_SPECS))
140 )
141 return parser
144def _select_examples(parser: argparse.ArgumentParser, args: argparse.Namespace) -> tuple[str, ...]:
145 selected = list(args.examples) if args.examples else sorted(EXAMPLE_SPECS)
146 unknown = sorted({*args.examples, *args.skip_examples} - set(EXAMPLE_SPECS))
147 if unknown:
148 parser.error(f"Unknown example name(s): {', '.join(unknown)}")
149 return tuple(name for name in selected if name not in set(args.skip_examples))
152def _settings_from_args(
153 parser: argparse.ArgumentParser, args: argparse.Namespace
154) -> ExampleRunSettings:
155 if not args.expected_account or not re.fullmatch(r"\d{12}", args.expected_account):
156 parser.error("--expected-account must be an exact 12-digit AWS account ID")
157 if not args.expected_sha or not re.fullmatch(r"[0-9a-fA-F]{40}", args.expected_sha):
158 parser.error("--expected-sha must be an exact 40-character commit SHA")
159 if not args.expected_branch or not args.expected_branch.strip():
160 parser.error("--expected-branch is required")
161 if args.run_id and not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,79}", args.run_id):
162 parser.error("--run-id must be 1-80 safe filename characters")
163 if args.max_parallel < 0:
164 parser.error("--max-parallel must be >= 0 (0 = all selected at once)")
165 root = repository_root(args.repo_root)
166 run_id = args.run_id or (
167 datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ") + "-" + args.expected_sha[:12].lower()
168 )
169 report_dir = path_from_root(root, args.report_dir, Path(".example-job-validation") / run_id)
170 checkpoint = path_from_root(root, args.checkpoint, report_dir / "checkpoint.json")
171 protected = tuple(dict.fromkeys(("CDKToolkit", "GCOGitHubOIDCStack", *args.protected_stack)))
172 return ExampleRunSettings(
173 run_id=run_id,
174 repo_root=root,
175 report_dir=report_dir,
176 checkpoint_path=checkpoint,
177 expected_account=args.expected_account,
178 expected_sha=args.expected_sha.lower(),
179 expected_branch=args.expected_branch.strip(),
180 profile="configured",
181 requested_actions=args.actions,
182 protected_stack_names=protected,
183 confirm_kms_key_deletion=args.confirm_kms_key_deletion,
184 resume=args.resume,
185 selected_examples=_select_examples(parser, args),
186 max_parallel_examples=args.max_parallel,
187 )
190def _run_static_only(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
191 root = repository_root(args.repo_root)
192 names = list(_select_examples(parser, args))
193 findings = run_static_checks(root, names)
194 failed = [finding for finding in findings if not finding.passed]
195 for finding in findings:
196 marker = "ok " if finding.passed else "FAIL"
197 detail = f" — {finding.detail}" if finding.detail else ""
198 print(f"[{marker}] {finding.example}: {finding.check}{detail}")
199 print(f"{len(findings)} checks, {len(failed)} failed")
200 return 1 if failed else 0
203def main() -> int:
204 parser = _build_parser()
205 args = parser.parse_args()
206 if args.list_actions:
207 for definition in build_action_registry().values():
208 dependencies = ", ".join(definition.dependencies) or "none"
209 print(f"{definition.name:16} {definition.description} [depends: {dependencies}]")
210 return 0
211 if args.static_only:
212 return _run_static_only(parser, args)
213 try:
214 require_local_execution()
215 except RuntimeError as exc:
216 print(f"Example validation could not start: {exc}", file=sys.stderr)
217 return 1
218 settings: ExampleRunSettings | None = None
219 try:
220 settings = _settings_from_args(parser, args)
221 runner = LiveValidationRunner(settings, registry=build_action_registry())
222 runner.report.title = REPORT_TITLE
223 runner.report.report_stem = REPORT_STEM
224 return runner.run()
225 except KeyboardInterrupt:
226 print("Example validation interrupted before the runner initialized", file=sys.stderr)
227 return 130
228 except BaseException as exc:
229 print(f"Example validation could not start: {type(exc).__name__}: {exc}", file=sys.stderr)
230 if settings is not None:
231 report = ValidationReport(
232 run_id=settings.run_id,
233 identity=settings.identity(),
234 selected_actions=list(settings.requested_actions),
235 started_at=utc_now(),
236 ended_at=utc_now(),
237 status="failed",
238 fatal_error="".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
239 title=REPORT_TITLE,
240 report_stem=REPORT_STEM,
241 )
242 with contextlib.suppress(OSError):
243 report.write(settings.report_dir)
244 return 1
247if __name__ == "__main__":
248 sys.exit(main())