#!/usr/bin/env python3 """Decide whether AI observability deploy QA should run. Input is a list of changed files via argv or stdin. The script prints matched files and can write `needed=true|false` to a GitHub/Gitea Actions output file. It exits 0 for both true and false so workflow control stays explicit. """ from __future__ import annotations import argparse import os import re from pathlib import Path TRIGGER_PATTERNS = ( r"^templates/admin/.*", r"^templates/ewoooc_base\.html$", r"^templates/components/_ewoooc_shell\.html$", r"^static/css/observability-system\.css$", r"^web/static/css/observability-system\.css$", r"^routes/admin_observability_routes\.py$", r"^scripts/check_observability_.*", r"^scripts/check_observability_suite\.sh$", r"^scripts/observability_contract\.py$", r"^scripts/quick_review\.sh$", r"^scripts/sync_observability_css\.py$", r"^docs/guides/observability_ui_governance\.md$", r"^docs/guides/deployment_sop\.md$", ) SELF_TEST_POSITIVE = ( "templates/admin/business_intel.html", "templates/ewoooc_base.html", "templates/components/_ewoooc_shell.html", "static/css/observability-system.css", "web/static/css/observability-system.css", "routes/admin_observability_routes.py", "scripts/check_observability_ui.py", "scripts/check_observability_pages.py", "scripts/check_observability_suite.sh", "scripts/observability_contract.py", "scripts/quick_review.sh", "scripts/sync_observability_css.py", "docs/guides/observability_ui_governance.md", "docs/guides/deployment_sop.md", ) SELF_TEST_NEGATIVE = ( "services/pricing_service.py", "docs/memory/history_logs.md", "README.md", "migrations/099_example.sql", ) def normalize_paths(raw_paths: list[str]) -> list[str]: paths: list[str] = [] for raw in raw_paths: for line in raw.splitlines(): path = line.strip() if path: paths.append(path) return paths def match_paths(paths: list[str]) -> list[str]: regexes = [re.compile(pattern) for pattern in TRIGGER_PATTERNS] return [path for path in paths if any(regex.search(path) for regex in regexes)] def write_output(output_path: str | None, needed: bool) -> None: if not output_path: return path = Path(output_path) with path.open("a", encoding="utf-8") as handle: handle.write(f"needed={'true' if needed else 'false'}\n") def run_self_test() -> int: positive_misses = [ path for path in SELF_TEST_POSITIVE if path not in match_paths([path]) ] negative_false_positives = [ path for path in SELF_TEST_NEGATIVE if path in match_paths([path]) ] if positive_misses or negative_false_positives: print("observability_deploy_gate_self_test=FAIL") if positive_misses: print("positive_misses:") for path in positive_misses: print(f"- {path}") if negative_false_positives: print("negative_false_positives:") for path in negative_false_positives: print(f"- {path}") return 1 print("observability_deploy_gate_self_test=PASS") print(f"- positive_cases={len(SELF_TEST_POSITIVE)}") print(f"- negative_cases={len(SELF_TEST_NEGATIVE)}") return 0 def main() -> int: parser = argparse.ArgumentParser(description="Check observability deploy QA trigger") parser.add_argument("paths", nargs="*", help="Changed file paths") parser.add_argument("--stdin", action="store_true", help="Read changed paths from stdin") parser.add_argument("--self-test", action="store_true", help="Run built-in trigger tests") parser.add_argument( "--github-output", default=os.environ.get("GITHUB_OUTPUT"), help="Actions output file to append needed=true|false.", ) args = parser.parse_args() if args.self_test: return run_self_test() raw_paths = list(args.paths) if args.stdin: import sys raw_paths.append(sys.stdin.read()) paths = normalize_paths(raw_paths) matches = match_paths(paths) needed = bool(matches) write_output(args.github_output, needed) print(f"observability_qa_needed={'true' if needed else 'false'}") if matches: print("matched_files:") for path in matches: print(f"- {path}") else: print("matched_files: none") return 0 if __name__ == "__main__": raise SystemExit(main())