Skip to content

Commit 8347a32

Browse files
authored
fix: recover stale crane completions without pr gate (#109)
1 parent 6bc2cdc commit 8347a32

2 files changed

Lines changed: 151 additions & 30 deletions

File tree

.github/workflows/scripts/crane_scheduler.py

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,18 @@
7171
def parse_machine_state(content):
7272
"""Parse the [*] Machine State table from a state file. Returns a dict."""
7373
state = {}
74-
m = re.search(r"## [*] Machine State.*?\n(.*?)(?=\n## |\Z)", content, re.DOTALL)
74+
m = re.search(
75+
r"##\s+(?:\[\*\]|\*)\s+Machine State.*?\n(.*?)(?=\n## |\Z)",
76+
content,
77+
re.DOTALL,
78+
)
7579
if not m:
7680
return state
7781
section = m.group(0)
7882
for row in re.finditer(r"\|\s*(.+?)\s*\|\s*(.+?)\s*\|", section):
7983
raw_key = row.group(1).strip()
8084
raw_val = row.group(2).strip()
81-
if raw_key.lower() in ("field", "---", ":---", ":---:", "---:"):
85+
if raw_key.lower() == "field" or re.fullmatch(r":?-+:?", raw_key):
8286
continue
8387
key = raw_key.lower().replace(" ", "_")
8488
val = None if raw_val in ("--", "-", "") else raw_val
@@ -231,6 +235,60 @@ def check_skip_conditions(state, issue_active=False):
231235
return False, None
232236

233237

238+
def evaluate_completed_label_recovery(
239+
name,
240+
state,
241+
issue_active,
242+
issue_completed_label,
243+
repo,
244+
github_token,
245+
find_pr=None,
246+
check_gate=None,
247+
):
248+
"""Return stale-completion recovery state for issue-based migrations.
249+
250+
Completed-label issues are only trustworthy when Crane can positively
251+
confirm the current PR-head gate. A missing PR, pending checks, failing
252+
checks, or unavailable gate evidence means the completed state is stale and
253+
the migration should be selected again.
254+
"""
255+
if find_pr is None:
256+
find_pr = find_existing_pr_for_branch
257+
if check_gate is None:
258+
check_gate = get_pr_head_check_gate
259+
260+
has_stale_completed_state = issue_active and is_completed_state(state)
261+
recovered_completed_issue = False
262+
recovery_event = None
263+
264+
if issue_completed_label and is_completed_state(state) and not issue_active:
265+
existing_pr_for_recovery = find_pr(repo, name, github_token)
266+
if existing_pr_for_recovery:
267+
gate_passed, gate_reason = check_gate(
268+
repo, existing_pr_for_recovery, github_token
269+
)
270+
if gate_passed is True:
271+
recovery_event = (
272+
"confirmed",
273+
existing_pr_for_recovery,
274+
gate_reason,
275+
)
276+
else:
277+
has_stale_completed_state = True
278+
recovered_completed_issue = True
279+
recovery_event = (
280+
"stale_gate",
281+
existing_pr_for_recovery,
282+
gate_reason or "gate-unavailable",
283+
)
284+
else:
285+
has_stale_completed_state = True
286+
recovered_completed_issue = True
287+
recovery_event = ("stale_no_pr", None, "no-open-migration-pr")
288+
289+
return has_stale_completed_state, recovered_completed_issue, recovery_event
290+
291+
234292
# ---------------------------------------------------------------------------
235293
# I/O helpers
236294
# ---------------------------------------------------------------------------
@@ -765,37 +823,33 @@ def main():
765823
else:
766824
print(" {}: no state found (first run)".format(name))
767825

768-
has_stale_completed_state = issue_active and is_completed_state(state)
769-
recovered_completed_issue = False
770-
if issue_completed_label and is_completed_state(state) and not issue_active:
771-
existing_pr_for_recovery = find_existing_pr_for_branch(repo, name, github_token)
772-
if existing_pr_for_recovery:
773-
gate_passed, gate_reason = get_pr_head_check_gate(
774-
repo, existing_pr_for_recovery, github_token
775-
)
776-
if gate_passed is False:
777-
has_stale_completed_state = True
778-
recovered_completed_issue = True
779-
print(
780-
" {}: crane-completed label is stale; PR #{} gate is {}".format(
781-
name, existing_pr_for_recovery, gate_reason
782-
)
783-
)
784-
elif gate_passed is True:
785-
print(
786-
" {}: crane-completed label confirmed by PR #{} gate {}".format(
787-
name, existing_pr_for_recovery, gate_reason
788-
)
826+
has_stale_completed_state, recovered_completed_issue, recovery_event = (
827+
evaluate_completed_label_recovery(
828+
name,
829+
state,
830+
issue_active,
831+
issue_completed_label,
832+
repo,
833+
github_token,
834+
)
835+
)
836+
if recovery_event:
837+
event_kind, recovery_pr, gate_reason = recovery_event
838+
if event_kind == "confirmed":
839+
print(
840+
" {}: crane-completed label confirmed by PR #{} gate {}".format(
841+
name, recovery_pr, gate_reason
789842
)
790-
else:
791-
print(
792-
" {}: could not evaluate completed-label recovery for PR #{} ({})".format(
793-
name, existing_pr_for_recovery, gate_reason
794-
)
843+
)
844+
elif event_kind == "stale_gate":
845+
print(
846+
" {}: crane-completed label is stale; PR #{} gate is {}".format(
847+
name, recovery_pr, gate_reason
795848
)
796-
else:
849+
)
850+
elif event_kind == "stale_no_pr":
797851
print(
798-
" {}: crane-completed label present, but no open migration PR was found".format(
852+
" {}: crane-completed label is stale; no open migration PR was found".format(
799853
name
800854
)
801855
)

tests/unit/test_crane_scheduler.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ def test_machine_state_completed_string_is_recognized() -> None:
4444
assert crane_scheduler.is_completed_state({"completed": "true"}) is True
4545

4646

47+
def test_parse_machine_state_accepts_bracketed_status_heading() -> None:
48+
state = crane_scheduler.parse_machine_state(
49+
"""# Crane: sample
50+
51+
## [*] Machine State
52+
53+
| Field | Value |
54+
|-------|-------|
55+
| Last Run | 2026-06-05T16:10:36Z |
56+
| Iteration Count | 67 |
57+
| PR | #104 |
58+
| Completed | true |
59+
| Recent Statuses | accepted, rejected |
60+
61+
---
62+
63+
## [list] Migration Info
64+
"""
65+
)
66+
67+
assert state["last_run"] == "2026-06-05T16:10:36Z"
68+
assert state["iteration_count"] == 67
69+
assert state["completed"] is True
70+
assert state["recent_statuses"] == ["accepted", "rejected"]
71+
assert "-------" not in state
72+
73+
4774
def test_issue_label_detection_accepts_github_label_payloads() -> None:
4875
issue = {"labels": [{"name": "crane-completed"}, "automation"]}
4976

@@ -52,6 +79,46 @@ def test_issue_label_detection_accepts_github_label_payloads() -> None:
5279
assert crane_scheduler._issue_has_label(issue, "crane-migration") is False
5380

5481

82+
def test_completed_label_without_open_pr_is_recovered_as_stale() -> None:
83+
stale, recovered, event = crane_scheduler.evaluate_completed_label_recovery(
84+
"crane-migration-python-to-go-full-apm-cli-rewrite",
85+
{"completed": True},
86+
issue_active=False,
87+
issue_completed_label=True,
88+
repo="githubnext/apm",
89+
github_token="token",
90+
find_pr=lambda *_args: None,
91+
)
92+
93+
assert stale is True
94+
assert recovered is True
95+
assert event == ("stale_no_pr", None, "no-open-migration-pr")
96+
97+
should_skip, reason = crane_scheduler.check_skip_conditions(
98+
{"completed": True},
99+
issue_active=recovered,
100+
)
101+
assert should_skip is False
102+
assert reason is None
103+
104+
105+
def test_completed_label_with_unknown_pr_gate_is_recovered_as_stale() -> None:
106+
stale, recovered, event = crane_scheduler.evaluate_completed_label_recovery(
107+
"crane-migration-python-to-go-full-apm-cli-rewrite",
108+
{"completed": True},
109+
issue_active=False,
110+
issue_completed_label=True,
111+
repo="githubnext/apm",
112+
github_token="token",
113+
find_pr=lambda *_args: 104,
114+
check_gate=lambda *_args: (None, "checks-unavailable:2699b7d"),
115+
)
116+
117+
assert stale is True
118+
assert recovered is True
119+
assert event == ("stale_gate", 104, "checks-unavailable:2699b7d")
120+
121+
55122
def test_pr_head_gate_fails_when_any_check_is_not_success() -> None:
56123
def fake_http_get_json(url, _headers, timeout=30):
57124
del timeout

0 commit comments

Comments
 (0)