From fd3a6f02827d195499d1cedbebb2c08a8bd1abfc Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 2 Apr 2026 05:04:26 +0800 Subject: [PATCH] fix: close stale monthly task issues --- scripts/fanout_monthly_optimization_tasks.py | 91 ++++++++++++++++--- .../test_fanout_monthly_optimization_tasks.py | 18 +++- 2 files changed, 95 insertions(+), 14 deletions(-) diff --git a/scripts/fanout_monthly_optimization_tasks.py b/scripts/fanout_monthly_optimization_tasks.py index 7c1a912..de95b15 100644 --- a/scripts/fanout_monthly_optimization_tasks.py +++ b/scripts/fanout_monthly_optimization_tasks.py @@ -80,6 +80,19 @@ def build_issue_body(plan: dict[str, Any], owner_repo: str, planner_issue_url: s return "\n".join(lines).strip() + "\n" +def build_closed_issue_body(plan: dict[str, Any], owner_repo: str, planner_issue_url: str | None = None) -> str: + lines = [ + build_marker(plan, owner_repo), + f"# Monthly Optimization Tasks ยท {owner_repo}", + "", + "No repo-scoped tasks remain in the current monthly optimization plan.", + "This issue is being closed to avoid leaving stale automation targets behind.", + ] + if planner_issue_url: + lines.extend(["", f"- Planner issue: {planner_issue_url}"]) + return "\n".join(lines).strip() + "\n" + + def github_request(method: str, url: str, token: str, payload: dict[str, Any] | None = None) -> Any: data = None headers = { @@ -119,13 +132,8 @@ def ensure_label(api_url: str, repo: str, token: str) -> None: def upsert_issue(*, api_url: str, repo: str, token: str, title: str, body: str) -> tuple[str, int, str]: - issues = github_request( - "GET", - f"{api_url}/repos/{repo}/issues?state=open&labels={urllib.parse.quote(LABEL_NAME)}&per_page=100", - token, - ) marker = build_marker_from_body(body) - existing = next((issue for issue in issues if build_marker_from_body(issue.get("body", "")) == marker), None) + existing = find_existing_issue(api_url=api_url, repo=repo, token=token, marker=marker) payload = {"title": title, "body": body, "labels": [LABEL_NAME]} if existing: github_request("PATCH", f"{api_url}/repos/{repo}/issues/{existing['number']}", token, payload) @@ -134,6 +142,36 @@ def upsert_issue(*, api_url: str, repo: str, token: str, title: str, body: str) return "created", int(created["number"]), str(created["html_url"]) +def find_existing_issue(*, api_url: str, repo: str, token: str, marker: str) -> dict[str, Any] | None: + issues = github_request( + "GET", + f"{api_url}/repos/{repo}/issues?state=open&labels={urllib.parse.quote(LABEL_NAME)}&per_page=100", + token, + ) + return next((issue for issue in issues if build_marker_from_body(issue.get("body", "")) == marker), None) + + +def close_existing_issue( + *, + api_url: str, + repo: str, + token: str, + title: str, + body: str, +) -> tuple[bool, int | None, str | None]: + marker = build_marker_from_body(body) + existing = find_existing_issue(api_url=api_url, repo=repo, token=token, marker=marker) + if not existing: + return False, None, None + github_request( + "PATCH", + f"{api_url}/repos/{repo}/issues/{existing['number']}", + token, + {"title": title, "body": body, "state": "closed", "labels": [LABEL_NAME]}, + ) + return True, int(existing["number"]), str(existing["html_url"]) + + def build_result( *, owner_repo: str, @@ -186,13 +224,40 @@ def main() -> int: plan = json.loads(args.plan_file.read_text(encoding="utf-8")) actions = _repo_actions(plan, args.owner_repo) if not actions: - result = build_result( - owner_repo=args.owner_repo, - target_repo=args.repo, - plan=plan, - status="skipped_no_actions", - reason="No recommended actions for this repo in the current optimization plan.", - ) + title = build_issue_title(plan, args.owner_repo) + body = build_closed_issue_body(plan, args.owner_repo, planner_issue_url=args.planner_issue_url) + try: + closed, issue_number, issue_url = close_existing_issue( + api_url=args.api_url.rstrip("/"), + repo=args.repo, + token=token, + title=title, + body=body, + ) + result = build_result( + owner_repo=args.owner_repo, + target_repo=args.repo, + plan=plan, + status="closed_no_actions" if closed else "skipped_no_actions", + issue_number=issue_number, + issue_url=issue_url, + reason=None if closed else "No recommended actions for this repo in the current optimization plan.", + ) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + if args.allow_permission_skip and exc.code in {403, 404}: + result = build_result( + owner_repo=args.owner_repo, + target_repo=args.repo, + plan=plan, + status="skipped_permission", + reason=f"{exc.code}: {detail or 'permission denied or repo not accessible'}", + ) + write_result(args.output_file, result) + print(json.dumps(result, ensure_ascii=False)) + return 0 + print(f"GitHub API request failed: {exc.code} {detail}", file=sys.stderr) + return 1 write_result(args.output_file, result) print(json.dumps(result, ensure_ascii=False)) return 0 diff --git a/tests/test_fanout_monthly_optimization_tasks.py b/tests/test_fanout_monthly_optimization_tasks.py index 89370ce..d464f6b 100644 --- a/tests/test_fanout_monthly_optimization_tasks.py +++ b/tests/test_fanout_monthly_optimization_tasks.py @@ -2,7 +2,12 @@ import unittest -from scripts.fanout_monthly_optimization_tasks import build_issue_body, build_issue_title, build_marker +from scripts.fanout_monthly_optimization_tasks import ( + build_closed_issue_body, + build_issue_body, + build_issue_title, + build_marker, +) class FanoutMonthlyOptimizationTasksTests(unittest.TestCase): @@ -73,6 +78,17 @@ def test_build_issue_body_lists_repo_specific_actions_and_flags(self) -> None: self.assertIn("Add zero-trade diagnostics [auto-pr-safe]", body) self.assertIn("Source: [QuantStrategyLab/BinancePlatform #9]", body) + def test_build_closed_issue_body_marks_repo_as_resolved(self) -> None: + body = build_closed_issue_body( + self.plan, + "CryptoStrategies", + planner_issue_url="https://github.com/QuantStrategyLab/CryptoLeaderRotation/issues/20", + ) + + self.assertIn("