from __future__ import annotations import os import re from datetime import datetime from typing import Optional import requests from intel.config import GENERATED_DIR, ROOT from intel.utils import read_json, run PR_PATHS = [ "README.md", "05-defense/secure-code", "07-framework-security", "08-threat-intel", "requirements-intel.txt", "scripts/intel", ] def create_branch_name() -> str: return "codex/intel-" + datetime.now().strftime("%Y%m%d-%H%M") def _parse_origin() -> Optional[dict]: result = run(["git", "-C", str(ROOT), "remote", "get-url", "origin"], check=False) if result.returncode != 0: return None url = result.stdout.strip() match = re.match(r"https://([^/]+)/([^/]+)/([^/.]+)(?:\.git)?", url) if not match: return None return {"host": match.group(1), "owner": match.group(2), "repo": match.group(3), "url": url} def _changed_paths() -> list[str]: status = run(["git", "-C", str(ROOT), "status", "--short", "--", *PR_PATHS], check=False) lines = [line.rstrip() for line in status.stdout.splitlines() if line.strip()] return lines def open_pr(base_branch: str = "main", dry_run: bool = False) -> str: origin = _parse_origin() if not origin: raise RuntimeError("Unable to parse origin remote URL") changed = _changed_paths() if not changed: return "No intel-related changes to submit" branch = create_branch_name() if dry_run: preview = "\n".join(f"- {line}" for line in changed[:40]) return f"Dry run only; would create branch {branch} with these paths:\n{preview}" run(["git", "-C", str(ROOT), "checkout", "-b", branch]) run(["git", "-C", str(ROOT), "add", "--", *PR_PATHS]) run(["git", "-C", str(ROOT), "commit", "-m", f"intel: automated advisory ingest {branch}"]) run(["git", "-C", str(ROOT), "push", "-u", "origin", branch]) token = os.environ.get("GITEA_TOKEN") if not token: return f"Pushed branch {branch}, but GITEA_TOKEN is not set; PR not created" summary = read_json(GENERATED_DIR / "run-summary.json", default={}) or {} body_lines = [ "Automated advisory ingest update.", "", f"- New advisories: {summary.get('new_count', 0)}", f"- Updated advisories: {summary.get('updated_count', 0)}", f"- Triage count: {summary.get('triage_count', 0)}", f"- Failure count: {len(summary.get('failures', []))}", ] if summary.get("systems_touched"): body_lines.append(f"- Systems touched: {', '.join(summary['systems_touched'])}") if summary.get("failures"): body_lines.extend(["", "Failed source adapters:"]) for failure in summary["failures"]: body_lines.append(f"- {failure}") payload = { "title": f"Intel ingest {branch}", "head": branch, "base": base_branch, "body": "\n".join(body_lines), } response = requests.post( f"https://{origin['host']}/api/v1/repos/{origin['owner']}/{origin['repo']}/pulls", headers={"Authorization": f"token {token}", "Content-Type": "application/json"}, json=payload, timeout=30, ) response.raise_for_status() pr_url = response.json().get("html_url") or response.json().get("url") return f"Created PR: {pr_url}"