diff --git a/graphify/prs.py b/graphify/prs.py index 23886f163..55045f41c 100644 --- a/graphify/prs.py +++ b/graphify/prs.py @@ -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" diff --git a/tests/test_prs.py b/tests/test_prs.py index d5b8ca855..a2e453c51 100644 --- a/tests/test_prs.py +++ b/tests/test_prs.py @@ -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: