fix(prs): read StatusContext state when classifying CI

gh's statusCheckRollup mixes CheckRun entries (status/conclusion) with
StatusContext entries, the commit statuses external CI such as Jenkins or
CircleCI posts, which carry only a state. _parse_ci never read state, so a
failing external CI showed as NONE, or as SUCCESS beside a passing check
run, and the PR was classified READY instead of CI-FAIL in pr list,
triage and the MCP PR tools. Fold state into the conclusions, with ERROR
as a failure and PENDING/EXPECTED as pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rudy CelekliandClaude Opus 5.5 authored and safishamsi committed 2026-10-20 10:50:36 +01:00
1 parent 8b06e7033c
commit 8d39c75d02
2 files changed
+34 -3

No files matched your search

+7 -3
View File
@@ -190,11 +190,15 @@ _CI_FAILURE_CONCLUSIONS = frozenset({"FAILURE", "CANCELLED", "TIMED_OUT", "ACTIO
def _parse_ci(rollup: list) -> str:
if not rollup:
return "NONE"
conclusions = {r.get("conclusion") for r in rollup if r.get("conclusion")}
if conclusions & _CI_FAILURE_CONCLUSIONS:
# statusCheckRollup mixes two entry types: CheckRun (GitHub Actions and other
# check apps) carries status/conclusion, while StatusContext (commit statuses
# from external CI such as Jenkins or CircleCI) carries only state, one of
# SUCCESS, FAILURE, ERROR, PENDING or EXPECTED. Read both.
conclusions = {r.get("conclusion") or r.get("state") for r in rollup} - {None, ""}
if conclusions & (_CI_FAILURE_CONCLUSIONS | {"ERROR"}):
return "FAILURE"
statuses = {r.get("status") for r in rollup}
if "IN_PROGRESS" in statuses or "QUEUED" in statuses:
if "IN_PROGRESS" in statuses or "QUEUED" in statuses or conclusions & {"PENDING", "EXPECTED"}:
return "PENDING"
if "SUCCESS" in conclusions:
return "SUCCESS"
+27
View File
@@ -134,6 +134,33 @@ class TestParseCi:
assert _parse_ci(rollup) == "FAILURE"
def test_status_context_failure_is_failure(self):
"""A commit status from external CI (StatusContext) carries only `state`,
no conclusion/status; a failing one must not read as NONE."""
rollup = [{"__typename": "StatusContext", "context": "ci/jenkins", "state": "FAILURE"}]
assert _parse_ci(rollup) == "FAILURE"
def test_status_context_error_is_failure(self):
rollup = [{"__typename": "StatusContext", "context": "ci/jenkins", "state": "ERROR"}]
assert _parse_ci(rollup) == "FAILURE"
def test_status_context_failure_beside_passing_check_run_is_failure(self):
rollup = [
{"__typename": "CheckRun", "conclusion": "SUCCESS", "status": "COMPLETED"},
{"__typename": "StatusContext", "context": "ci/jenkins", "state": "FAILURE"},
]
assert _parse_ci(rollup) == "FAILURE"
def test_status_context_pending_is_pending(self):
for state in ("PENDING", "EXPECTED"):
rollup = [{"__typename": "StatusContext", "context": "ci/jenkins", "state": state}]
assert _parse_ci(rollup) == "PENDING", state
def test_status_context_success_is_success(self):
rollup = [{"__typename": "StatusContext", "context": "ci/jenkins", "state": "SUCCESS"}]
assert _parse_ci(rollup) == "SUCCESS"
# ── _path_match ───────────────────────────────────────────────────────────────
class TestPathMatch: