Sync from dev @ 497baf0

Source: main (497baf0)
Excluded: live tenant exports, generated artifacts, and dev-only tooling.
This commit is contained in:
2026-04-21 22:21:43 +02:00
parent b6ac9524f7
commit 2c41eaca44
25 changed files with 2258 additions and 79 deletions

View File

@@ -54,6 +54,14 @@ TICKET_BLOCK_END = "<!-- AUTO-CHANGE-TICKETS:END -->"
AUTO_TICKET_THREAD_PREFIX = "AUTO-CHANGE-TICKET:"
AUTO_AI_REVIEW_THREAD_PREFIX = "AUTO-AI-REVIEW:"
COMPACT_AI_THREAD_NOTE = "_Full AI reviewer narrative is posted in a dedicated PR thread due PR description limits._"
AUTO_DETERMINISTIC_THREAD_PREFIX = "AUTO-DETERMINISTIC-SUMMARY:"
COMPACT_DETERMINISTIC_THREAD_NOTE = (
"_Full deterministic summary (including Top Risk Items) is posted in a dedicated PR thread "
"due to Azure DevOps description size limits._"
)
ADO_PR_DESCRIPTION_MAX_LEN = 4000
AUTO_REVIEWER_GUIDE_THREAD_PREFIX = "AUTO-REVIEWER-GUIDE:"
COMPACT_REVIEWER_GUIDE_NOTE = "> 📋 Full **reviewer guide** is posted in a dedicated PR thread."
THREAD_STATUS_ACTIVE = 1
THREAD_STATUS_FIXED = 2
@@ -2035,6 +2043,29 @@ def _compact_deterministic_summary(deterministic_summary: str) -> str:
return deterministic_summary[:idx].strip()
def _compact_reviewer_guide(description: str) -> str:
"""Replace the legacy long reviewer guide with a compact reference."""
description = description or ""
marker = "## Reviewer Quick Actions"
idx = description.find(marker)
if idx == -1:
return description
prefix = description[:idx].rstrip()
if not prefix:
return COMPACT_REVIEWER_GUIDE_NOTE + "\n"
return prefix + "\n\n" + COMPACT_REVIEWER_GUIDE_NOTE + "\n"
def _append_reviewer_guide_note(description: str) -> str:
"""Append the compact reviewer guide note if not already present."""
description = description or ""
if COMPACT_REVIEWER_GUIDE_NOTE in description:
return description
if description.endswith("\n"):
return description + COMPACT_REVIEWER_GUIDE_NOTE + "\n"
return description + "\n\n" + COMPACT_REVIEWER_GUIDE_NOTE + "\n"
def _remove_marked_block(description: str, start_marker: str, end_marker: str) -> str:
description = description or ""
pattern = re.compile(
@@ -2273,6 +2304,185 @@ def _sync_full_ai_review_thread(
return True
def _deterministic_thread_marker(workload: str) -> str:
return f"Automation marker: {AUTO_DETERMINISTIC_THREAD_PREFIX}{workload.strip().lower()}"
def _build_full_deterministic_thread_content(workload: str, deterministic_summary: str) -> str:
marker = _deterministic_thread_marker(workload)
return (
"Automated review summary (full)\n\n"
"PR description uses a compact review summary because of Azure DevOps description size limits.\n\n"
f"{deterministic_summary}\n\n"
f"{marker}"
).strip()
def _create_deterministic_thread(
repo_api: str,
pr_id: int,
token: str,
workload: str,
deterministic_summary: str,
) -> None:
content = _build_full_deterministic_thread_content(workload, deterministic_summary)
_request_json(
f"{repo_api}/pullrequests/{pr_id}/threads?api-version=7.1",
token=token,
method="POST",
body={
"comments": [
{
"parentCommentId": 0,
"content": content,
"commentType": 1,
}
],
"status": THREAD_STATUS_ACTIVE,
},
)
def _sync_deterministic_thread(
repo_api: str,
pr_id: int,
token: str,
workload: str,
deterministic_summary: str,
) -> bool:
marker = _deterministic_thread_marker(workload)
desired_content = _build_full_deterministic_thread_content(workload, deterministic_summary)
threads_payload = _request_json(
f"{repo_api}/pullrequests/{pr_id}/threads?api-version=7.1",
token=token,
)
threads = threads_payload.get("value", []) if isinstance(threads_payload, dict) else []
thread = _find_marked_thread(threads, marker)
if thread is None:
_create_deterministic_thread(repo_api, pr_id, token, workload, deterministic_summary)
return True
comments = thread.get("comments", []) if isinstance(thread.get("comments"), list) else []
if _thread_has_matching_comment(comments, desired_content):
return False
thread_id = _thread_id(thread)
if thread_id <= 0:
_create_deterministic_thread(repo_api, pr_id, token, workload, deterministic_summary)
return True
if _is_thread_resolved(thread):
_set_thread_status(repo_api, pr_id, thread_id, token, THREAD_STATUS_ACTIVE)
_add_thread_comment(repo_api, pr_id, thread_id, token, desired_content)
return True
def _close_deterministic_thread(
repo_api: str,
pr_id: int,
token: str,
workload: str,
) -> bool:
marker = _deterministic_thread_marker(workload)
threads_payload = _request_json(
f"{repo_api}/pullrequests/{pr_id}/threads?api-version=7.1",
token=token,
)
threads = threads_payload.get("value", []) if isinstance(threads_payload, dict) else []
thread = _find_marked_thread(threads, marker)
if thread is None:
return False
thread_id = _thread_id(thread)
if thread_id <= 0:
return False
if _is_thread_resolved(thread):
return False
_set_thread_status(repo_api, pr_id, thread_id, token, THREAD_STATUS_CLOSED)
return True
def _reviewer_guide_thread_marker(workload: str) -> str:
return f"Automation marker: {AUTO_REVIEWER_GUIDE_THREAD_PREFIX}{workload.strip().lower()}"
def _build_full_reviewer_guide_thread_content(workload: str) -> str:
marker = _reviewer_guide_thread_marker(workload)
return (
"## Reviewer Quick Actions\n\n"
"### 1) Accept all changes\n"
"- Merge PR to accept drift into baseline.\n\n"
"### 2) Reject whole PR and revert\n"
"- Set reviewer vote to **Reject**.\n"
"- Abandon PR.\n"
"- Auto-remediation queues restore (if `AUTO_REMEDIATE_ON_PR_REJECTION=true`).\n\n"
"### 3) Reject only selected policy changes\n"
"- In each `Change Needed` policy thread, comment `/reject` for changes you do not want.\n"
"- Optional: use `/accept` for changes you want to keep.\n"
"- Wait for review-sync pipeline (about 5 minutes) to update PR diff.\n"
"- Merge remaining accepted changes.\n"
"- Post-merge auto-remediation queues restore to reconcile tenant to merged baseline "
"(if `AUTO_REMEDIATE_AFTER_MERGE=true`).\n\n"
f"{marker}"
).strip()
def _create_reviewer_guide_thread(
repo_api: str,
pr_id: int,
token: str,
workload: str,
) -> None:
content = _build_full_reviewer_guide_thread_content(workload)
_request_json(
f"{repo_api}/pullrequests/{pr_id}/threads?api-version=7.1",
token=token,
method="POST",
body={
"comments": [
{
"parentCommentId": 0,
"content": content,
"commentType": 1,
}
],
"status": THREAD_STATUS_ACTIVE,
},
)
def _sync_reviewer_guide_thread(
repo_api: str,
pr_id: int,
token: str,
workload: str,
) -> bool:
marker = _reviewer_guide_thread_marker(workload)
desired_content = _build_full_reviewer_guide_thread_content(workload)
threads_payload = _request_json(
f"{repo_api}/pullrequests/{pr_id}/threads?api-version=7.1",
token=token,
)
threads = threads_payload.get("value", []) if isinstance(threads_payload, dict) else []
thread = _find_marked_thread(threads, marker)
if thread is None:
_create_reviewer_guide_thread(repo_api, pr_id, token, workload)
return True
comments = thread.get("comments", []) if isinstance(thread.get("comments"), list) else []
if _thread_has_matching_comment(comments, desired_content):
return False
thread_id = _thread_id(thread)
if thread_id <= 0:
_create_reviewer_guide_thread(repo_api, pr_id, token, workload)
return True
if _is_thread_resolved(thread):
_set_thread_status(repo_api, pr_id, thread_id, token, THREAD_STATUS_ACTIVE)
_add_thread_comment(repo_api, pr_id, thread_id, token, desired_content)
return True
def _set_thread_status(
repo_api: str,
pr_id: int,
@@ -2530,12 +2740,16 @@ def main() -> int:
)
full_pr = _request_json(f"{repo_api}/pullrequests/{pr_id}?api-version=7.1", token=token)
current_description = full_pr.get("description", "")
current_description = full_pr.get("description") or ""
pr_is_draft = bool(full_pr.get("isDraft"))
existing_fingerprint = _existing_change_fingerprint(current_description)
existing_summary_version = _existing_summary_version(current_description)
current_auto_body = _auto_block_body(current_description)
deterministic_already_present = deterministic in current_auto_body if current_auto_body else False
compact_deterministic = _compact_deterministic_summary(deterministic)
deterministic_already_present = (
(deterministic in current_auto_body)
or (compact_deterministic in current_auto_body)
) if current_auto_body else False
ai_fallback_in_current_block = _auto_block_contains_ai_fallback(current_auto_body)
refresh_on_fallback = _env_bool("PR_AI_FORCE_REFRESH_ON_FALLBACK", default=True)
if existing_fingerprint and existing_fingerprint == changes_fingerprint:
@@ -2549,7 +2763,7 @@ def main() -> int:
repo_api=repo_api,
token=token,
pr_id=int(pr_id),
title=full_pr.get("title", pr.get("title", f"{args.workload} drift review (rolling)")),
title=full_pr.get("title") or pr.get("title") or f"{args.workload} drift review (rolling)",
description=current_description,
is_draft=pr_is_draft,
)
@@ -2625,29 +2839,44 @@ def main() -> int:
updated_description = _upsert_auto_block(current_description, auto_block)
# Cleanup legacy description-based ticket checklist if present.
updated_description = _remove_marked_block(updated_description, TICKET_BLOCK_START, TICKET_BLOCK_END)
# Strip legacy long reviewer guide and ensure compact note is present.
updated_description = _compact_reviewer_guide(updated_description)
updated_description = _append_reviewer_guide_note(updated_description)
patch_url = f"{repo_api}/pullrequests/{pr_id}?api-version=7.1"
patch_title = full_pr.get("title", pr.get("title", f"{args.workload} drift review (rolling)"))
patch_title = full_pr.get("title") or pr.get("title") or f"{args.workload} drift review (rolling)"
summary_updated = False
final_description = current_description
description_compacted = False
print(
f"DEBUG summary: pr_id={pr_id} workload={args.workload} "
f"status={full_pr.get('status')} isDraft={full_pr.get('isDraft')} "
f"mergeStatus={full_pr.get('mergeStatus')} title_len={len(patch_title)} "
f"current_desc_len={len(current_description or '')} updated_desc_len={len(updated_description or '')}"
)
# Proactively compact if we are near the Azure DevOps PR description limit.
if len(updated_description) > (ADO_PR_DESCRIPTION_MAX_LEN - 100):
description_compacted = True
if updated_description != current_description:
try:
_request_json(
patch_url,
token=token,
method="PATCH",
body={
"title": patch_title,
"description": updated_description,
},
)
summary_updated = True
final_description = updated_description
except RuntimeError as exc:
if not _is_description_limit_error(exc):
raise
description_compacted = True
if not description_compacted:
try:
_request_json(
patch_url,
token=token,
method="PATCH",
body={
"title": patch_title,
"description": updated_description,
},
)
summary_updated = True
final_description = updated_description
except RuntimeError as exc:
if not _is_description_limit_error(exc):
raise
description_compacted = True
if description_compacted:
compact_ai_block = ""
if ai_summary:
compact_ai_block = "\n### AI Reviewer Narrative\n" + COMPACT_AI_THREAD_NOTE
@@ -2660,6 +2889,8 @@ def main() -> int:
"",
f"- **Summary Version:** `{AUTO_SUMMARY_VERSION}`",
_compact_deterministic_summary(deterministic),
"",
COMPACT_DETERMINISTIC_THREAD_NOTE,
compact_ai_block,
AUTO_BLOCK_END,
]
@@ -2670,10 +2901,11 @@ def main() -> int:
)
if compact_description == updated_description:
raise
print(
"WARNING: Full PR summary update failed; retrying with compact summary block. "
f"Reason: {exc}"
)
if not summary_updated:
print(
"INFO: Full PR summary exceeds Azure DevOps description limit; "
"using compact summary in description and posting full details to a PR thread."
)
try:
_request_json(
patch_url,
@@ -2697,6 +2929,7 @@ def main() -> int:
f"- **Summary Version:** `{AUTO_SUMMARY_VERSION}`",
_compact_deterministic_summary(deterministic),
"",
COMPACT_DETERMINISTIC_THREAD_NOTE,
COMPACT_AI_THREAD_NOTE,
AUTO_BLOCK_END,
]
@@ -2720,6 +2953,34 @@ def main() -> int:
else:
final_description = updated_description
if description_compacted:
try:
thread_updated = _sync_deterministic_thread(
repo_api=repo_api,
pr_id=int(pr_id),
token=token,
workload=args.workload,
deterministic_summary=deterministic,
)
if thread_updated:
print(f"Updated full deterministic summary thread for PR #{pr_id} ({args.workload}).")
else:
print(f"Full deterministic summary thread already up to date for PR #{pr_id} ({args.workload}).")
except Exception as exc:
print(f"WARNING: Failed to sync full deterministic summary thread for PR #{pr_id}: {exc}")
else:
try:
closed = _close_deterministic_thread(
repo_api=repo_api,
pr_id=int(pr_id),
token=token,
workload=args.workload,
)
if closed:
print(f"Closed full deterministic summary thread for PR #{pr_id} ({args.workload}) because description now fits.")
except Exception as exc:
print(f"WARNING: Failed to close deterministic summary thread for PR #{pr_id}: {exc}")
if summary_updated:
print(f"Updated automated review summary for PR #{pr_id} ({args.workload}).")
else:
@@ -2739,6 +3000,19 @@ def main() -> int:
print(f"Full AI reviewer narrative thread already up to date for PR #{pr_id} ({args.workload}).")
except Exception as exc:
print(f"WARNING: Failed to sync full AI reviewer narrative thread for PR #{pr_id}: {exc}")
try:
guide_updated = _sync_reviewer_guide_thread(
repo_api=repo_api,
pr_id=int(pr_id),
token=token,
workload=args.workload,
)
if guide_updated:
print(f"Updated reviewer guide thread for PR #{pr_id} ({args.workload}).")
else:
print(f"Reviewer guide thread already up to date for PR #{pr_id} ({args.workload}).")
except Exception as exc:
print(f"WARNING: Failed to sync reviewer guide thread for PR #{pr_id}: {exc}")
if _publish_draft_pr(
repo_api=repo_api,
token=token,