From 439d5336c18fbd70e4b37cf12274f4065f33276a Mon Sep 17 00:00:00 2001 From: mrjf Date: Fri, 5 Jun 2026 09:21:31 -0700 Subject: [PATCH] fix: recover stale crane completions without pr gate --- .github/workflows/scripts/crane_scheduler.py | 114 ++++++++++++++----- tests/unit/test_crane_scheduler.py | 67 +++++++++++ 2 files changed, 151 insertions(+), 30 deletions(-) diff --git a/.github/workflows/scripts/crane_scheduler.py b/.github/workflows/scripts/crane_scheduler.py index 41929c5d..23ceb28f 100644 --- a/.github/workflows/scripts/crane_scheduler.py +++ b/.github/workflows/scripts/crane_scheduler.py @@ -71,14 +71,18 @@ def parse_machine_state(content): """Parse the [*] Machine State table from a state file. Returns a dict.""" state = {} - m = re.search(r"## [*] Machine State.*?\n(.*?)(?=\n## |\Z)", content, re.DOTALL) + m = re.search( + r"##\s+(?:\[\*\]|\*)\s+Machine State.*?\n(.*?)(?=\n## |\Z)", + content, + re.DOTALL, + ) if not m: return state section = m.group(0) for row in re.finditer(r"\|\s*(.+?)\s*\|\s*(.+?)\s*\|", section): raw_key = row.group(1).strip() raw_val = row.group(2).strip() - if raw_key.lower() in ("field", "---", ":---", ":---:", "---:"): + if raw_key.lower() == "field" or re.fullmatch(r":?-+:?", raw_key): continue key = raw_key.lower().replace(" ", "_") val = None if raw_val in ("--", "-", "") else raw_val @@ -231,6 +235,60 @@ def check_skip_conditions(state, issue_active=False): return False, None +def evaluate_completed_label_recovery( + name, + state, + issue_active, + issue_completed_label, + repo, + github_token, + find_pr=None, + check_gate=None, +): + """Return stale-completion recovery state for issue-based migrations. + + Completed-label issues are only trustworthy when Crane can positively + confirm the current PR-head gate. A missing PR, pending checks, failing + checks, or unavailable gate evidence means the completed state is stale and + the migration should be selected again. + """ + if find_pr is None: + find_pr = find_existing_pr_for_branch + if check_gate is None: + check_gate = get_pr_head_check_gate + + has_stale_completed_state = issue_active and is_completed_state(state) + recovered_completed_issue = False + recovery_event = None + + if issue_completed_label and is_completed_state(state) and not issue_active: + existing_pr_for_recovery = find_pr(repo, name, github_token) + if existing_pr_for_recovery: + gate_passed, gate_reason = check_gate( + repo, existing_pr_for_recovery, github_token + ) + if gate_passed is True: + recovery_event = ( + "confirmed", + existing_pr_for_recovery, + gate_reason, + ) + else: + has_stale_completed_state = True + recovered_completed_issue = True + recovery_event = ( + "stale_gate", + existing_pr_for_recovery, + gate_reason or "gate-unavailable", + ) + else: + has_stale_completed_state = True + recovered_completed_issue = True + recovery_event = ("stale_no_pr", None, "no-open-migration-pr") + + return has_stale_completed_state, recovered_completed_issue, recovery_event + + # --------------------------------------------------------------------------- # I/O helpers # --------------------------------------------------------------------------- @@ -765,37 +823,33 @@ def main(): else: print(" {}: no state found (first run)".format(name)) - has_stale_completed_state = issue_active and is_completed_state(state) - recovered_completed_issue = False - if issue_completed_label and is_completed_state(state) and not issue_active: - existing_pr_for_recovery = find_existing_pr_for_branch(repo, name, github_token) - if existing_pr_for_recovery: - gate_passed, gate_reason = get_pr_head_check_gate( - repo, existing_pr_for_recovery, github_token - ) - if gate_passed is False: - has_stale_completed_state = True - recovered_completed_issue = True - print( - " {}: crane-completed label is stale; PR #{} gate is {}".format( - name, existing_pr_for_recovery, gate_reason - ) - ) - elif gate_passed is True: - print( - " {}: crane-completed label confirmed by PR #{} gate {}".format( - name, existing_pr_for_recovery, gate_reason - ) + has_stale_completed_state, recovered_completed_issue, recovery_event = ( + evaluate_completed_label_recovery( + name, + state, + issue_active, + issue_completed_label, + repo, + github_token, + ) + ) + if recovery_event: + event_kind, recovery_pr, gate_reason = recovery_event + if event_kind == "confirmed": + print( + " {}: crane-completed label confirmed by PR #{} gate {}".format( + name, recovery_pr, gate_reason ) - else: - print( - " {}: could not evaluate completed-label recovery for PR #{} ({})".format( - name, existing_pr_for_recovery, gate_reason - ) + ) + elif event_kind == "stale_gate": + print( + " {}: crane-completed label is stale; PR #{} gate is {}".format( + name, recovery_pr, gate_reason ) - else: + ) + elif event_kind == "stale_no_pr": print( - " {}: crane-completed label present, but no open migration PR was found".format( + " {}: crane-completed label is stale; no open migration PR was found".format( name ) ) diff --git a/tests/unit/test_crane_scheduler.py b/tests/unit/test_crane_scheduler.py index 13ef75c7..0f9dc648 100644 --- a/tests/unit/test_crane_scheduler.py +++ b/tests/unit/test_crane_scheduler.py @@ -44,6 +44,33 @@ def test_machine_state_completed_string_is_recognized() -> None: assert crane_scheduler.is_completed_state({"completed": "true"}) is True +def test_parse_machine_state_accepts_bracketed_status_heading() -> None: + state = crane_scheduler.parse_machine_state( + """# Crane: sample + +## [*] Machine State + +| Field | Value | +|-------|-------| +| Last Run | 2026-06-05T16:10:36Z | +| Iteration Count | 67 | +| PR | #104 | +| Completed | true | +| Recent Statuses | accepted, rejected | + +--- + +## [list] Migration Info +""" + ) + + assert state["last_run"] == "2026-06-05T16:10:36Z" + assert state["iteration_count"] == 67 + assert state["completed"] is True + assert state["recent_statuses"] == ["accepted", "rejected"] + assert "-------" not in state + + def test_issue_label_detection_accepts_github_label_payloads() -> None: issue = {"labels": [{"name": "crane-completed"}, "automation"]} @@ -52,6 +79,46 @@ def test_issue_label_detection_accepts_github_label_payloads() -> None: assert crane_scheduler._issue_has_label(issue, "crane-migration") is False +def test_completed_label_without_open_pr_is_recovered_as_stale() -> None: + stale, recovered, event = crane_scheduler.evaluate_completed_label_recovery( + "crane-migration-python-to-go-full-apm-cli-rewrite", + {"completed": True}, + issue_active=False, + issue_completed_label=True, + repo="githubnext/apm", + github_token="token", + find_pr=lambda *_args: None, + ) + + assert stale is True + assert recovered is True + assert event == ("stale_no_pr", None, "no-open-migration-pr") + + should_skip, reason = crane_scheduler.check_skip_conditions( + {"completed": True}, + issue_active=recovered, + ) + assert should_skip is False + assert reason is None + + +def test_completed_label_with_unknown_pr_gate_is_recovered_as_stale() -> None: + stale, recovered, event = crane_scheduler.evaluate_completed_label_recovery( + "crane-migration-python-to-go-full-apm-cli-rewrite", + {"completed": True}, + issue_active=False, + issue_completed_label=True, + repo="githubnext/apm", + github_token="token", + find_pr=lambda *_args: 104, + check_gate=lambda *_args: (None, "checks-unavailable:2699b7d"), + ) + + assert stale is True + assert recovered is True + assert event == ("stale_gate", 104, "checks-unavailable:2699b7d") + + def test_pr_head_gate_fails_when_any_check_is_not_success() -> None: def fake_http_get_json(url, _headers, timeout=30): del timeout