Skip to content

Commit eb82dc3

Browse files
committed
fix: close stale monthly task issues
1 parent d1293db commit eb82dc3

2 files changed

Lines changed: 95 additions & 14 deletions

File tree

scripts/fanout_monthly_optimization_tasks.py

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ def build_issue_body(plan: dict[str, Any], owner_repo: str, planner_issue_url: s
8080
return "\n".join(lines).strip() + "\n"
8181

8282

83+
def build_closed_issue_body(plan: dict[str, Any], owner_repo: str, planner_issue_url: str | None = None) -> str:
84+
lines = [
85+
build_marker(plan, owner_repo),
86+
f"# Monthly Optimization Tasks · {owner_repo}",
87+
"",
88+
"No repo-scoped tasks remain in the current monthly optimization plan.",
89+
"This issue is being closed to avoid leaving stale automation targets behind.",
90+
]
91+
if planner_issue_url:
92+
lines.extend(["", f"- Planner issue: {planner_issue_url}"])
93+
return "\n".join(lines).strip() + "\n"
94+
95+
8396
def github_request(method: str, url: str, token: str, payload: dict[str, Any] | None = None) -> Any:
8497
data = None
8598
headers = {
@@ -119,13 +132,8 @@ def ensure_label(api_url: str, repo: str, token: str) -> None:
119132

120133

121134
def upsert_issue(*, api_url: str, repo: str, token: str, title: str, body: str) -> tuple[str, int, str]:
122-
issues = github_request(
123-
"GET",
124-
f"{api_url}/repos/{repo}/issues?state=open&labels={urllib.parse.quote(LABEL_NAME)}&per_page=100",
125-
token,
126-
)
127135
marker = build_marker_from_body(body)
128-
existing = next((issue for issue in issues if build_marker_from_body(issue.get("body", "")) == marker), None)
136+
existing = find_existing_issue(api_url=api_url, repo=repo, token=token, marker=marker)
129137
payload = {"title": title, "body": body, "labels": [LABEL_NAME]}
130138
if existing:
131139
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)
134142
return "created", int(created["number"]), str(created["html_url"])
135143

136144

145+
def find_existing_issue(*, api_url: str, repo: str, token: str, marker: str) -> dict[str, Any] | None:
146+
issues = github_request(
147+
"GET",
148+
f"{api_url}/repos/{repo}/issues?state=open&labels={urllib.parse.quote(LABEL_NAME)}&per_page=100",
149+
token,
150+
)
151+
return next((issue for issue in issues if build_marker_from_body(issue.get("body", "")) == marker), None)
152+
153+
154+
def close_existing_issue(
155+
*,
156+
api_url: str,
157+
repo: str,
158+
token: str,
159+
title: str,
160+
body: str,
161+
) -> tuple[bool, int | None, str | None]:
162+
marker = build_marker_from_body(body)
163+
existing = find_existing_issue(api_url=api_url, repo=repo, token=token, marker=marker)
164+
if not existing:
165+
return False, None, None
166+
github_request(
167+
"PATCH",
168+
f"{api_url}/repos/{repo}/issues/{existing['number']}",
169+
token,
170+
{"title": title, "body": body, "state": "closed", "labels": [LABEL_NAME]},
171+
)
172+
return True, int(existing["number"]), str(existing["html_url"])
173+
174+
137175
def build_result(
138176
*,
139177
owner_repo: str,
@@ -186,13 +224,40 @@ def main() -> int:
186224
plan = json.loads(args.plan_file.read_text(encoding="utf-8"))
187225
actions = _repo_actions(plan, args.owner_repo)
188226
if not actions:
189-
result = build_result(
190-
owner_repo=args.owner_repo,
191-
target_repo=args.repo,
192-
plan=plan,
193-
status="skipped_no_actions",
194-
reason="No recommended actions for this repo in the current optimization plan.",
195-
)
227+
title = build_issue_title(plan, args.owner_repo)
228+
body = build_closed_issue_body(plan, args.owner_repo, planner_issue_url=args.planner_issue_url)
229+
try:
230+
closed, issue_number, issue_url = close_existing_issue(
231+
api_url=args.api_url.rstrip("/"),
232+
repo=args.repo,
233+
token=token,
234+
title=title,
235+
body=body,
236+
)
237+
result = build_result(
238+
owner_repo=args.owner_repo,
239+
target_repo=args.repo,
240+
plan=plan,
241+
status="closed_no_actions" if closed else "skipped_no_actions",
242+
issue_number=issue_number,
243+
issue_url=issue_url,
244+
reason=None if closed else "No recommended actions for this repo in the current optimization plan.",
245+
)
246+
except urllib.error.HTTPError as exc:
247+
detail = exc.read().decode("utf-8", errors="replace")
248+
if args.allow_permission_skip and exc.code in {403, 404}:
249+
result = build_result(
250+
owner_repo=args.owner_repo,
251+
target_repo=args.repo,
252+
plan=plan,
253+
status="skipped_permission",
254+
reason=f"{exc.code}: {detail or 'permission denied or repo not accessible'}",
255+
)
256+
write_result(args.output_file, result)
257+
print(json.dumps(result, ensure_ascii=False))
258+
return 0
259+
print(f"GitHub API request failed: {exc.code} {detail}", file=sys.stderr)
260+
return 1
196261
write_result(args.output_file, result)
197262
print(json.dumps(result, ensure_ascii=False))
198263
return 0

tests/test_fanout_monthly_optimization_tasks.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
import unittest
44

5-
from scripts.fanout_monthly_optimization_tasks import build_issue_body, build_issue_title, build_marker
5+
from scripts.fanout_monthly_optimization_tasks import (
6+
build_closed_issue_body,
7+
build_issue_body,
8+
build_issue_title,
9+
build_marker,
10+
)
611

712

813
class FanoutMonthlyOptimizationTasksTests(unittest.TestCase):
@@ -73,6 +78,17 @@ def test_build_issue_body_lists_repo_specific_actions_and_flags(self) -> None:
7378
self.assertIn("Add zero-trade diagnostics [auto-pr-safe]", body)
7479
self.assertIn("Source: [QuantStrategyLab/BinancePlatform #9]", body)
7580

81+
def test_build_closed_issue_body_marks_repo_as_resolved(self) -> None:
82+
body = build_closed_issue_body(
83+
self.plan,
84+
"CryptoStrategies",
85+
planner_issue_url="https://github.com/QuantStrategyLab/CryptoLeaderRotation/issues/20",
86+
)
87+
88+
self.assertIn("<!-- monthly-optimization-task:CryptoStrategies:", body)
89+
self.assertIn("No repo-scoped tasks remain", body)
90+
self.assertIn("This issue is being closed", body)
91+
7692

7793
if __name__ == "__main__":
7894
unittest.main()

0 commit comments

Comments
 (0)