#!/usr/bin/env python3 """CI validation checks for zulip-platform-plugins. The inline validation steps this script replaced were never valid YAML in the first place (the `run: |` blocks dedented out of their block scalar), so the whole `validate` job silently never ran. Keeping the logic in a real file means it is testable locally — run `python3 ci_check.py all` before pushing. Usage: python3 ci_check.py {workflows|config|secrets|all} """ import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent CODE_GLOBS = ("*.py", "*.ts", "*.yaml", "*.yml", "*.cjs", "*.sh") # scheme://user:pass@host — embedded HTTP Basic credentials CRED_RE = re.compile(r"://[^/ \"']+:[^/ \"'@]+@") API_KEY_RE = re.compile(r"""api_key["']?\s*[:=]\s*["']?[A-Za-z0-9]{20,}""") # Allowlist: placeholder patterns, not real secrets PLACEHOLDER = ("${", "{{", "user:pass", "USER:TOKEN", "user:token", "example") def iter_code_files(): for path in sorted(ROOT.rglob("*")): if not path.is_file(): continue if any(part in {".git", "__pycache__", "node_modules"} for part in path.parts): continue if path.name == Path(__file__).name: # this file documents the patterns continue if any(path.match(glob) for glob in CODE_GLOBS): yield path def check_workflows() -> bool: try: import yaml except ModuleNotFoundError: print("⚠️ pyyaml not installed — workflow parse check skipped") return True ok = True for f in sorted((ROOT / ".gitea" / "workflows").glob("*.yml")): try: doc = yaml.safe_load(f.read_text()) assert isinstance(doc, dict) and "jobs" in doc, "missing 'jobs' mapping" print(f"✅ {f.relative_to(ROOT)} valid — jobs: {list(doc['jobs'])}") except Exception as e: print(f"❌ {f.relative_to(ROOT)}: {e}") ok = False return ok def check_config() -> bool: try: import yaml except ModuleNotFoundError: print("⚠️ pyyaml not installed — config check skipped") return True try: cfg = yaml.safe_load((ROOT / "config.yaml.example").read_text()) assert isinstance(cfg, dict) and "zulip" in cfg, "missing 'zulip' section" print("✅ config.yaml.example valid") return True except Exception as e: print(f"❌ config.yaml.example: {e}") return False def check_secrets() -> bool: hits = [] for path in iter_code_files(): try: text = path.read_text(errors="ignore") except OSError: continue for lineno, line in enumerate(text.splitlines(), 1): for rx in (CRED_RE, API_KEY_RE): if rx.search(line) and not any(p in line for p in PLACEHOLDER): hits.append(f"{path.relative_to(ROOT)}:{lineno}: {line.strip()[:100]}") break if hits: print("❌ Embedded credentials detected:") for h in hits: print(f" {h}") return False print("✅ No embedded credentials in tracked code/workflow files") return True CHECKS = {"workflows": check_workflows, "config": check_config, "secrets": check_secrets} def main() -> int: args = sys.argv[1:] or ["all"] names = list(CHECKS) if "all" in args else args failed = [n for n in names if n not in CHECKS or not CHECKS[n]()] if failed: print(f"❌ CI checks failed: {', '.join(failed)}") return 1 return 0 if __name__ == "__main__": sys.exit(main())