Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ec101e7
fix(governance): dispatch failed-check RCA directly
seonghobae Sep 1, 2026
62ab710
test(governance): ignore cancelled check runs
seonghobae Sep 1, 2026
8add1d6
fix(governance): validate direct RCA evidence
seonghobae Sep 1, 2026
b1f52df
Merge remote-tracking branch 'origin/main' into fix/draft-governance-…
seonghobae Sep 1, 2026
2add607
test(governance): cover package import fallback
seonghobae Sep 1, 2026
a0874b8
test(governance): reproduce direct RCA arbitration gaps
seonghobae Sep 1, 2026
7367ed1
fix(governance): prioritize complete failed-check RCA evidence
seonghobae Sep 1, 2026
a18261d
test(governance): isolate status pagination failures
seonghobae Sep 1, 2026
2485812
fix: isolate complete RCA check evidence
seonghobae Sep 1, 2026
660b755
ci(governance): apply isolated pagination repair
seonghobae Sep 1, 2026
ee6dc91
ci(governance): remove superseded repair workflow
seonghobae Sep 1, 2026
80ee9bf
test(governance): preserve REST workflow identity
seonghobae Sep 1, 2026
a783375
ci(governance): apply REST workflow identity repair
seonghobae Sep 1, 2026
4f9515a
fix(governance): preserve REST workflow identity
Sep 1, 2026
2d0f976
ci(governance): retrigger exact-head validation
seonghobae Sep 1, 2026
5c43c61
ci(governance): finalize exact-head validation retrigger
seonghobae Sep 1, 2026
a4a86af
test(governance): preserve name-only Strix evidence
seonghobae Sep 1, 2026
eb0d485
ci(governance): apply Strix sentinel repair
seonghobae Sep 1, 2026
5ecd929
fix(governance): preserve name-only Strix evidence
Sep 1, 2026
152af99
ci(governance): retrigger final exact-head validation
seonghobae Sep 1, 2026
79f4dff
ci(governance): finalize final exact-head validation retrigger
seonghobae Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions scripts/ci/pr_review_autofix_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
from pathlib import Path
from typing import Any

try:
from pr_review_fix_scheduler import current_head_failed_checks
except ModuleNotFoundError:
from scripts.ci.pr_review_fix_scheduler import current_head_failed_checks


REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$")
Expand Down Expand Up @@ -327,19 +332,22 @@ def write_context(
reviews = current_reviews(repo, number, head_sha)
threads = review_threads(repo, number)
detected_rca_mode = review_requires_rca(reviews)
direct_rca_mode = bool(current_head_failed_checks(pr))
if repair_mode is None:
rca_mode = detected_rca_mode
elif repair_mode == "conflict":
# Conflict repair has an independently sealed unresolved-path scope.
# Failed-check reviews may coexist on the same head, but they must not
# widen this approved conflict-only invocation to every changed path.
rca_mode = False
elif (repair_mode == "rca") != detected_rca_mode:
elif repair_mode == "rca" and not (detected_rca_mode or direct_rca_mode):
raise RuntimeError(
"requested repair mode does not match exact-head review evidence"
"requested RCA mode lacks exact-head review or failed-check evidence"
)
elif repair_mode != "rca" and detected_rca_mode:
raise RuntimeError("requested repair mode does not match exact-head review evidence")
else:
rca_mode = detected_rca_mode
rca_mode = repair_mode == "rca"
Comment thread
seonghobae marked this conversation as resolved.
if failed_check_evidence_path is not None and not rca_mode:
raise RuntimeError(
"failed-check evidence is accepted only for exact-head RCA repair"
Expand Down
156 changes: 127 additions & 29 deletions scripts/ci/pr_review_fix_scheduler.py
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,30 @@

try:
from pr_review_merge_scheduler import (
complete_paginated_pr_contexts,
fetch_open_prs,
fetch_pr,
context_nodes,
has_current_head_approval,
has_current_head_changes_requested,
is_opencode_review,
latest_check_run_attempts,
REST_UNKNOWN_GITHUB_ACTIONS_WORKFLOW,
review_matches_current_head,
run,
unresolved_thread_count,
)
except ModuleNotFoundError:
from scripts.ci.pr_review_merge_scheduler import (
complete_paginated_pr_contexts,
fetch_open_prs,
fetch_pr,
context_nodes,
has_current_head_approval,
has_current_head_changes_requested,
is_opencode_review,
latest_check_run_attempts,
REST_UNKNOWN_GITHUB_ACTIONS_WORKFLOW,
review_matches_current_head,
run,
unresolved_thread_count,
Expand Down Expand Up @@ -69,6 +77,26 @@
"sast semgrep failed",
"codeql failed",
)
RCA_IGNORED_CHECK_NAMES = frozenset(
{
"metadata-only gate evaluation",
"opencode-review",
"PR governance metadata controller",
"scan-pr-queue",
}
)
RCA_IGNORED_WORKFLOW_NAMES = frozenset(
{
"OpenCode Review",
"Required OpenCode Review",
"OpenCode PR Review",
REST_UNKNOWN_GITHUB_ACTIONS_WORKFLOW,
}
)
FAILED_CHECK_CONCLUSIONS = frozenset(
{"FAILURE", "STARTUP_FAILURE", "TIMED_OUT"}
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
FAILED_STATUS_STATES = frozenset({"ERROR", "FAILURE"})
Comment thread
seonghobae marked this conversation as resolved.


def run_json(args: list[str]) -> Any:
Expand Down Expand Up @@ -203,14 +231,52 @@ def needs_autofix(pr: dict[str, Any]) -> tuple[bool, tuple[str, ...]]:

def needs_rca_repair(pr: dict[str, Any]) -> tuple[bool, tuple[str, ...]]:
"""Return whether exact-head failed-check evidence warrants RCA and repair."""
if not (
review_requires_rca = (
has_current_head_changes_requested(pr)
and change_request_requires_rca(pr)
):
)
failed_checks = current_head_failed_checks(pr)
if not review_requires_rca and not failed_checks:
return False, ()
if failed_checks:
return True, (
"current-head failed check(s) require RCA: " + ", ".join(failed_checks),
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
return True, ("current-head failed-check blocker requires RCA",)


def current_head_failed_checks(pr: dict[str, Any]) -> tuple[str, ...]:
"""Return terminal failed checks that can carry source-backed RCA evidence."""
failed: list[str] = []
rollup = pr.get("statusCheckRollup") or {}
nodes = rollup if isinstance(rollup, list) else context_nodes(pr)
for node in latest_check_run_attempts(nodes):
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
if node.get("__typename") == "CheckRun":
name = str(node.get("name") or "").strip()
workflow_name = str(
(
((node.get("checkSuite") or {}).get("workflowRun") or {}).get(
"workflow"
)
or {}
).get("name")
or ""
).strip()
Comment thread
seonghobae marked this conversation as resolved.
conclusion = str(node.get("conclusion") or "").upper()
if (
name not in RCA_IGNORED_CHECK_NAMES
and workflow_name not in RCA_IGNORED_WORKFLOW_NAMES
and conclusion in FAILED_CHECK_CONCLUSIONS
):
failed.append(name or "unnamed check")
else:
name = str(node.get("context") or "").strip()
state = str(node.get("state") or "").upper()
if name not in RCA_IGNORED_CHECK_NAMES and state in FAILED_STATUS_STATES:
failed.append(name or "unnamed status")
Comment thread
seonghobae marked this conversation as resolved.
return tuple(dict.fromkeys(failed))


CONFLICT_MERGE_STATES = frozenset({"DIRTY", "CONFLICTING"})


Expand Down Expand Up @@ -336,8 +402,6 @@ def inspect_pr(
) -> tuple[str, tuple[str, ...]]:
"""Inspect one PR and optionally dispatch a bounded repair."""
number = int(pr["number"])
if pr.get("isDraft"):
return "skip", ("draft PR",)
if not _base_branch_matches(pr, args.base_branch):
return "skip", (
f"base branch is {pr.get('baseRefName')}; expected {args.base_branch}",
Expand All @@ -347,28 +411,39 @@ def inspect_pr(
"external PR head is not writable by repository workflow credentials",
)

needs_fix, reasons = needs_autofix(pr)
repair_mode = "review"
resolve_conflict = False
if not needs_fix:
needs_rca, rca_reasons = needs_rca_repair(pr)
if needs_rca:
repair_mode = "rca"
reasons = rca_reasons
else:
needs_resolve, resolve_reasons = needs_conflict_resolution(
pr,
allow_unreviewed=bool(
getattr(args, "resolve_unreviewed_conflicts", False)
),
)
if not needs_resolve:
return "skip", (
"no current-head autofixable review, failed-check RCA, or approved merge conflict",
)
resolve_conflict = True
repair_mode = "conflict"
reasons = resolve_reasons
conflicted = str(pr.get("mergeStateStatus") or "").upper() in CONFLICT_MERGE_STATES
if conflicted:
if pr.get("isDraft"):
return "skip", ("draft PR",)
needs_resolve, resolve_reasons = needs_conflict_resolution(
pr,
allow_unreviewed=bool(
getattr(args, "resolve_unreviewed_conflicts", False)
),
)
if not needs_resolve:
return "skip", ("merge conflict is not authorized for repair",)
needs_fix = True
reasons = resolve_reasons
repair_mode = "conflict"
resolve_conflict = True
else:
needs_fix, reasons = needs_autofix(pr)
repair_mode = "review"
resolve_conflict = False

needs_rca, rca_reasons = needs_rca_repair(pr)
if pr.get("isDraft") and not needs_rca:
return "skip", ("draft PR",)

if not conflicted and needs_rca:
needs_fix = True
repair_mode = "rca"
reasons = rca_reasons
elif not needs_fix and not conflicted:
return "skip", (
"no current-head autofixable review, failed-check RCA, or approved merge conflict",
)

if comments is None:
comments = issue_comments(repo, number)
Expand Down Expand Up @@ -400,13 +475,24 @@ def process_queue(args: argparse.Namespace) -> int:
if args.pr_number
else fetch_open_prs(args.repo, args.max_prs)
)
pagination_errors: set[int] = set()
for pr in prs:
if not _base_branch_matches(pr, args.base_branch):
continue
if not same_repository_head(args.repo, pr):
continue
try:
complete_paginated_pr_contexts(args.repo, pr)
except RuntimeError:
pagination_errors.add(int(pr["number"]))

dispatched = 0
inspected = 0
decisions: list[dict[str, Any]] = []

prs_needing_comments = []
for pr in prs:
if pr.get("isDraft"):
if int(pr["number"]) in pagination_errors:
continue
if not _base_branch_matches(pr, args.base_branch):
continue
Expand All @@ -420,7 +506,9 @@ def process_queue(args: argparse.Namespace) -> int:
getattr(args, "resolve_unreviewed_conflicts", False)
),
)
if needs_fix or needs_rca or needs_resolve:
if (needs_fix and not pr.get("isDraft")) or needs_rca or (
needs_resolve and not pr.get("isDraft")
):
Comment thread
seonghobae marked this conversation as resolved.
prs_needing_comments.append(pr)

comments_by_pr: dict[int, list[dict[str, Any]]] = {}
Expand Down Expand Up @@ -462,6 +550,17 @@ def fetch_comments(

for pr in prs:
inspected += 1
pr_number = int(pr["number"])
if pr_number in pagination_errors:
reasons = (
"status-context pagination failed; deferring this PR without "
"evaluating partial check evidence",
)
decisions.append(
{"pr": pr["number"], "action": "wait", "reasons": list(reasons)}
)
print(f"PR #{pr['number']}: wait: {reasons[0]}")
continue
if dispatched >= args.max_dispatches:
decisions.append(
{
Expand All @@ -471,7 +570,6 @@ def fetch_comments(
}
)
continue
pr_number = int(pr["number"])
if pr_number in comment_fetch_errors:
decisions.append(
{
Expand Down
Loading
Loading