#!/usr/bin/env python3 """ci/approval-override.py — select the admin-override comment on a PR, if there is a valid one. This script decides NOTHING about authority. It parses a PR's comment list and returns the single override *candidate* that ci/approval-policy.sh should then try to authorise against the live Gitea permission API. Keeping the two apart is the point: * parsing is pure, offline, and therefore TESTABLE (ci/tests/approval-override.test.sh); * authorisation needs the API and must stay in the stage that fails closed. A candidate is a comment whose body contains, AT THE START OF A LINE, the directive followed by a non-empty reason: /approve-override the gate cannot gate itself and I am the only human reviewer WHY EACH RULE IS HERE * START OF A LINE, not "anywhere in the body". Otherwise quoting someone else's override ("you could just /approve-override this") re-arms it. Markdown quotes are stripped of a leading '>' before matching for the same reason -- a quoted directive must NOT count, so lines beginning with '>' are DROPPED, not unquoted. * A REASON IS MANDATORY. A bare directive records that a review was waived but not why, which is the difference between an audit trail and a rubber stamp. * STALE OVERRIDES ARE DISCARDED. An override earned at commit A must not silently cover commit B. Any candidate created at or before the head commit's timestamp is dropped -- the same semantics Gitea's own `dismiss_stale_approvals` applies to reviews. Push new commits and you must re-override. * THE LATEST CANDIDATE WINS, so a second override comment supersedes the first rather than both being live. Output: one TSV line (login, created_at, reason) on stdout, or nothing at all when there is no valid candidate. Never exits non-zero for "no override" -- absence is a normal answer, and the caller must not be able to confuse it with a parse crash. Input, all via env so nothing lands in `ps`: COMMENTS_JSON the Gitea /issues/{n}/comments array HEAD_TIME ISO-8601 timestamp of the PR head commit DIRECTIVE the override directive, e.g. /approve-override """ import json import os import re import sys from datetime import datetime, timezone def parse_ts(value): """Gitea emits RFC3339. Return an aware datetime, or None if it is unparseable. Unparseable is NOT 'treat as fresh': the caller drops such a candidate, because a timestamp we cannot read is a staleness check we cannot perform. """ if not value: return None text = str(value).strip() # Python < 3.11 cannot read a trailing 'Z' in fromisoformat. if text.endswith(("Z", "z")): text = text[:-1] + "+00:00" try: parsed = datetime.fromisoformat(text) except ValueError: return None if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed def extract_reason(body, directive): """Return the reason from the first line that STARTS with the directive, else None.""" pattern = re.compile( r"^[ \t]*" + re.escape(directive) + r"(?=[ \t:]|$)[ \t:]*(.*)$" ) for raw in (body or "").splitlines(): # A quoted line is somebody repeating an override, not casting one. if raw.lstrip().startswith(">"): continue match = pattern.match(raw) if match: reason = match.group(1).strip() return reason or None return None def main(): directive = os.environ.get("DIRECTIVE", "/approve-override").strip() head_time = parse_ts(os.environ.get("HEAD_TIME", "")) try: comments = json.loads(os.environ.get("COMMENTS_JSON") or "[]") except (ValueError, TypeError): # The caller validated this JSON already; if it is broken here, say so loudly on # stderr and emit no candidate. Silence on stdout means "no override". print("approval-override: COMMENTS_JSON is not valid JSON", file=sys.stderr) return 0 if not isinstance(comments, list): print("approval-override: COMMENTS_JSON is not a list", file=sys.stderr) return 0 candidates = [] for comment in comments: if not isinstance(comment, dict): continue login = ((comment.get("user") or {}).get("login") or "").strip() reason = extract_reason(comment.get("body"), directive) if not login or not reason: continue created_raw = comment.get("created_at") or comment.get("updated_at") or "" created = parse_ts(created_raw) if created is None: print( "approval-override: dropping override by '%s' — unreadable timestamp %r" % (login, created_raw), file=sys.stderr, ) continue # An override must post-date the commit it covers. if head_time is not None and created <= head_time: print( "approval-override: dropping STALE override by '%s' (%s) — not newer than " "head commit (%s). Re-comment to override the current commit." % (login, created.isoformat(), head_time.isoformat()), file=sys.stderr, ) continue candidates.append((created, login, reason, created_raw)) if not candidates: return 0 # Latest wins: a later override supersedes an earlier one. created, login, reason, created_raw = max(candidates, key=lambda c: c[0]) # Keep the reason on one line — this becomes a TSV field. reason = " ".join(reason.split()) print("%s\t%s\t%s" % (login, created_raw, reason)) return 0 if __name__ == "__main__": sys.exit(main())