Skip to content

Commit 4f67689

Browse files
fix: preserve duplicate required check identities
Aggregate same-name check runs and commit statuses fail-closed after reducing each identity to its latest state so DeployBot does not mark a required context passed while another matching entry is failed or pending. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent 7466043 commit 4f67689

2 files changed

Lines changed: 71 additions & 7 deletions

File tree

‎src/agent_merge_queue/cli.py‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,15 @@ def normalize_check_state(check: dict[str, Any]) -> str:
134134
return "UNKNOWN"
135135

136136

137+
def check_identity(check: dict[str, Any], name: str) -> tuple[str, str]:
138+
typename = str(check.get("__typename") or "")
139+
if not typename:
140+
typename = "StatusContext" if check.get("context") else "CheckRun"
141+
return (typename, name)
142+
143+
137144
def check_states(checks: Iterable[dict[str, Any]]) -> dict[str, str]:
138-
grouped: dict[str, tuple[str, int, str]] = {}
145+
grouped: dict[tuple[str, tuple[str, str]], tuple[str, int, str]] = {}
139146
for index, check in enumerate(checks):
140147
name = str(check.get("name") or check.get("context") or "")
141148
if not name:
@@ -156,15 +163,19 @@ def check_states(checks: Iterable[dict[str, Any]]) -> dict[str, str]:
156163
# of letting an older success hide that pending rerun.
157164
order = "\uffff" if not timestamp and state != "SUCCESS" else timestamp
158165
candidate = (order, index, state)
159-
if name not in grouped or candidate[:2] > grouped[name][:2]:
160-
grouped[name] = candidate
166+
key = (name, check_identity(check, name))
167+
if key not in grouped or candidate[:2] > grouped[key][:2]:
168+
grouped[key] = candidate
169+
170+
states_by_name: dict[str, list[str]] = {}
171+
for (name, _identity), value in grouped.items():
172+
states_by_name.setdefault(name, []).append(value[2])
161173

162174
result: dict[str, str] = {}
163-
for name, value in grouped.items():
164-
state = value[2]
165-
if state in FAILED_CHECK_STATES:
175+
for name, states in states_by_name.items():
176+
if any(state in FAILED_CHECK_STATES for state in states):
166177
result[name] = "failed"
167-
elif state == "SUCCESS":
178+
elif all(state == "SUCCESS" for state in states):
168179
result[name] = "passed"
169180
else:
170181
result[name] = "pending"

‎tests/test_cli.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,59 @@ def test_undated_queued_rerun_hides_older_success(self) -> None:
12361236

12371237
self.assertEqual(states["CI"], "pending")
12381238

1239+
def test_same_name_check_run_and_failed_status_blocks_required_check(self) -> None:
1240+
states = check_states(
1241+
[
1242+
{"__typename": "CheckRun", "name": "CI", "conclusion": "SUCCESS"},
1243+
{"__typename": "StatusContext", "context": "CI", "state": "FAILURE"},
1244+
]
1245+
)
1246+
self.assertEqual(states["CI"], "failed")
1247+
1248+
value = entry(1)
1249+
value.checks = states
1250+
value.classify(CONFIG)
1251+
1252+
self.assertEqual(value.state, "blocked")
1253+
self.assertIn("CI failed", value.reasons)
1254+
1255+
def test_same_name_check_run_and_pending_status_waits_required_check(self) -> None:
1256+
states = check_states(
1257+
[
1258+
{"__typename": "CheckRun", "name": "CI", "conclusion": "SUCCESS"},
1259+
{"__typename": "StatusContext", "context": "CI", "state": "PENDING"},
1260+
]
1261+
)
1262+
self.assertEqual(states["CI"], "pending")
1263+
1264+
value = entry(1)
1265+
value.checks = states
1266+
value.classify(CONFIG)
1267+
1268+
self.assertEqual(value.state, "waiting")
1269+
self.assertIn("CI is not complete", value.reasons)
1270+
1271+
def test_exact_check_run_replaces_stale_rollup_check_run_identity(self) -> None:
1272+
states = check_states(
1273+
[
1274+
{
1275+
"__typename": "CheckRun",
1276+
"name": "CI",
1277+
"status": "QUEUED",
1278+
"workflowName": "Build",
1279+
"startedAt": "2026-06-20T00:00:00Z",
1280+
},
1281+
{
1282+
"name": "CI",
1283+
"conclusion": "SUCCESS",
1284+
"app": {"slug": "github-actions"},
1285+
"started_at": "2026-06-20T00:01:00Z",
1286+
},
1287+
]
1288+
)
1289+
1290+
self.assertEqual(states["CI"], "passed")
1291+
12391292
def test_review_verdicts_are_classified_generically(self) -> None:
12401293
value = entry(1)
12411294
value.review_verdicts = (ReviewVerdict("Any bot", "blocked", ("one finding",)),)

0 commit comments

Comments
 (0)