feat(ai_review): parse .pr-review.json:static_message + render as banner
Repos can pin a free-text notice (e.g. 'this repo is in maintenance mode',
or 'reviewers: focus on the public API only for this quarter') in
.pr-review.json:static_message. The string is stripped and capped at 400
chars (mirror of the existing instructions cap), then rendered as a
Markdown blockquote (> {msg}) directly under the REVIEW_HEADER so it
surfaces on every review without scrolling.
Plumbing: parse_repo_config exposes 'static_message'; format_review_body
accepts a static_message kwarg and inserts the blockquote before the
'### Summary of Changes' section; review_pr threads
config.get('static_message') to both call sites (salvage path + happy
path). Empty / non-string values are silently dropped, mirroring the
parser's 'ignore blank' handling for every other text field.
This commit is contained in:
+18
-1
@@ -215,12 +215,16 @@ def format_review_body(
|
||||
risks: list[str] | None = None,
|
||||
findings_for_table: list[dict] | None = None,
|
||||
inline_count: int = 0,
|
||||
static_message: str = "",
|
||||
) -> str:
|
||||
"""Format the posted review summary body.
|
||||
|
||||
Layout (per the operator's format guide):
|
||||
|
||||
* Header line (``🤖 AI Review …``).
|
||||
* Optional static banner (``> {static_message}``) — repo-wide call-out
|
||||
from `.pr-review.json:static_message`, placed under the header so
|
||||
every reviewer sees it on every review without scrolling.
|
||||
* **Summary of Changes** — 2–4 bullets of what the PR introduces
|
||||
(`summary_changes`); falls back to the opencode prose `summary` if
|
||||
the agent didn't emit the list.
|
||||
@@ -241,6 +245,11 @@ def format_review_body(
|
||||
header = REVIEW_HEADER.format(model=model, sha=sha[:8] if sha else "unknown")
|
||||
parts: list[str] = [header]
|
||||
|
||||
# Optional free-text banner. Rendered as a Markdown blockquote immediately
|
||||
# after the header — front-of-mind for any maintainer scanning the review.
|
||||
if static_message and static_message.strip():
|
||||
parts.append(f"> {static_message.strip()}")
|
||||
|
||||
# --- Summary of Changes ---
|
||||
sc = list(summary_changes or [])
|
||||
if not sc and summary:
|
||||
@@ -1137,6 +1146,7 @@ CONFIG_MAX_ITEM_CHARS = 200
|
||||
CONFIG_MAX_INSTRUCTIONS_CHARS = 4000
|
||||
CONFIG_MAX_PATTERNS_ITEMS = 16 # allow + deny separately, total 32 entries
|
||||
CONFIG_MAX_FINDINGS = 30
|
||||
CONFIG_MAX_STATIC_MESSAGE_CHARS = 400 # free-text banner, mirror of instructions
|
||||
|
||||
STYLES = frozenset(STYLE_DEFAULTS)
|
||||
SEVERITY_VALUES = frozenset(SEVERITIES)
|
||||
@@ -1152,6 +1162,7 @@ def parse_repo_config(raw: str) -> dict:
|
||||
|
||||
Recognised keys (all optional):
|
||||
focus, exclude_paths, languages, instructions — text steer
|
||||
static_message ≤ CONFIG_MAX_STATIC_MESSAGE_CHARS — banner under header
|
||||
style strict|balanced|lenient — default: balanced
|
||||
severity_threshold low|medium|high|critical — default: per style
|
||||
max_findings 1..CONFIG_MAX_FINDINGS — default: per style
|
||||
@@ -1185,6 +1196,10 @@ def parse_repo_config(raw: str) -> dict:
|
||||
if isinstance(instr, str) and instr.strip():
|
||||
out["instructions"] = instr.strip()[:CONFIG_MAX_INSTRUCTIONS_CHARS]
|
||||
|
||||
sm = data.get("static_message")
|
||||
if isinstance(sm, str) and sm.strip():
|
||||
out["static_message"] = sm.strip()[:CONFIG_MAX_STATIC_MESSAGE_CHARS]
|
||||
|
||||
style = data.get("style")
|
||||
if isinstance(style, str) and style.strip().lower() in STYLES:
|
||||
out["style"] = style.strip().lower()
|
||||
@@ -1945,7 +1960,8 @@ def review_pr(
|
||||
usage_section = _render_collapsible_usage(usage, display_model, config=config) if report_usage else ""
|
||||
post_review(api, repo, index, token, format_review_body(
|
||||
salvaged or "AI review produced no parseable output.",
|
||||
display_model, sha, usage_section=usage_section))
|
||||
display_model, sha, usage_section=usage_section,
|
||||
static_message=(config or {}).get("static_message", "")))
|
||||
return True
|
||||
else:
|
||||
user_prompt = build_user_prompt(title, body + compression_note, diff, config, prior, additional_context)
|
||||
@@ -2015,6 +2031,7 @@ def review_pr(
|
||||
risks=risks,
|
||||
findings_for_table=findings,
|
||||
inline_count=len(anchored),
|
||||
static_message=(config or {}).get("static_message", ""),
|
||||
)
|
||||
|
||||
post_inline_review(api, repo, index, token, summary_body, anchored)
|
||||
|
||||
@@ -401,6 +401,24 @@ def test_parse_repo_config_partial_and_bad():
|
||||
assert parse_repo_config('{"instructions":" "}') == {}
|
||||
|
||||
|
||||
def test_parse_repo_config_reads_static_message():
|
||||
cfg = parse_repo_config(json.dumps({"static_message": " NOTE: this repo is in maintenance mode "}))
|
||||
assert cfg == {"static_message": "NOTE: this repo is in maintenance mode"}
|
||||
|
||||
|
||||
def test_parse_repo_config_static_message_caps_length():
|
||||
long_text = "x" * 9999
|
||||
cfg = parse_repo_config(json.dumps({"static_message": long_text}))
|
||||
assert "static_message" in cfg
|
||||
assert len(cfg["static_message"]) <= 400
|
||||
|
||||
|
||||
def test_parse_repo_config_static_message_ignores_blank():
|
||||
assert "static_message" not in parse_repo_config(json.dumps({"static_message": " "}))
|
||||
assert "static_message" not in parse_repo_config(json.dumps({"static_message": ""}))
|
||||
assert "static_message" not in parse_repo_config(json.dumps({"static_message": 42}))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dedupe / prior-context parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -823,6 +841,22 @@ def test_format_review_body_no_usage_section_omitted():
|
||||
assert "AI usage" not in body
|
||||
|
||||
|
||||
def test_format_review_body_renders_static_message_banner():
|
||||
body = format_review_body(
|
||||
"", "glm-5.2:cloud", "abcdef1234567890",
|
||||
static_message="NOTE: this repo is in maintenance mode.",
|
||||
)
|
||||
assert "> NOTE: this repo is in maintenance mode." in body
|
||||
# Banner sits under the header and above the rest of the body.
|
||||
assert body.index("NOTE") > body.index("🤖")
|
||||
assert body.index("NOTE") < body.index("### Summary of Changes")
|
||||
|
||||
|
||||
def test_format_review_body_omits_static_message_when_blank():
|
||||
body = format_review_body("", "glm-5.2:cloud", "abcdef1234567890")
|
||||
assert "> " not in body
|
||||
|
||||
|
||||
def test_format_review_body_with_summary_changes_and_risks():
|
||||
body = format_review_body(
|
||||
"", "glm-5.2:cloud", "abcdef1234567890",
|
||||
|
||||
Reference in New Issue
Block a user