#!/usr/bin/env python3 """Post a standardized Codex state update comment to a Gitea issue.""" from __future__ import annotations import argparse import os import pathlib import sys from gitea_common import GiteaClient, GiteaError, infer_target, load_dotenv, repo_path, repo_root, require_token STATUS_LABELS = { "started": "status/in-progress", "progress": "status/in-progress", "blocked": "status/blocked", "needs-info": "status/needs-info", "ready": "status/ready", "done": "", "note": "", } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path.cwd(), help="repository root or child path") parser.add_argument("--remote", default="origin", help="git remote to use for target inference") parser.add_argument("--env-file", type=pathlib.Path, help="dotenv file to read before using GITEA_* values") parser.add_argument("--issue", type=int, required=True, help="Gitea issue number") parser.add_argument("--status", choices=sorted(STATUS_LABELS), default="note") parser.add_argument("--summary", action="append", default=[], help="summary bullet; may be repeated") parser.add_argument("--changed", action="append", default=[], help="changed file path; may be repeated") parser.add_argument("--test", action="append", default=[], help="verification command/result; may be repeated") parser.add_argument("--next", action="append", default=[], help="next step or blocker; may be repeated") parser.add_argument("--body-file", type=pathlib.Path, help="additional Markdown body to append") parser.add_argument("--close", action="store_true", help="close the issue after posting the note") parser.add_argument("--apply", action="store_true", help="post the comment and optional state update") args = parser.parse_args() try: root = repo_root(args.root) load_dotenv(args.env_file or root / ".env") target = infer_target(root, args.remote) token = require_token() if args.apply else os.environ.get("GITEA_TOKEN") body = build_body(args) print(f"Target: {target.display}") print(f"Issue: #{args.issue}") print(body) if not args.apply: print("Dry run only. Re-run with --apply and GITEA_TOKEN to post.") return 0 client = GiteaClient(target, token) client.request_json( "POST", repo_path(target.owner, target.repo, f"/issues/{args.issue}/comments"), body={"body": body}, ) if args.close: client.request_json( "PATCH", repo_path(target.owner, target.repo, f"/issues/{args.issue}"), body={"state": "closed"}, ) print("Posted Codex note.") return 0 except GiteaError as exc: print(f"error: {exc}", file=sys.stderr) return 1 def build_body(args: argparse.Namespace) -> str: lines = [f"## Codex State: {args.status}", ""] append_section(lines, "Summary", args.summary) append_section(lines, "Changed Files", [f"`{item}`" for item in args.changed]) append_section(lines, "Verification", [f"`{item}`" for item in args.test]) append_section(lines, "Next / Blocked", args.next) if STATUS_LABELS[args.status]: lines.extend(["", f"Suggested status label: `{STATUS_LABELS[args.status]}`"]) if args.body_file: lines.extend(["", args.body_file.read_text(encoding="utf-8").strip()]) return "\n".join(lines).rstrip() + "\n" def append_section(lines: list[str], title: str, items: list[str]) -> None: if not items: return lines.extend([f"### {title}", ""]) lines.extend(f"- {item}" for item in items) lines.append("") if __name__ == "__main__": raise SystemExit(main())