@@ -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+
8396def 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
121134def 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+
137175def 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
0 commit comments