11"""GitHub API client for workflow operations."""
22
3+ import json
34import re
45import time
56import uuid
6- from typing import Dict , List , Optional
7+ from typing import Any , Dict , List , Optional
78import requests
89
910from rich .console import Console
@@ -238,22 +239,41 @@ def wait_for_workflow_completion(
238239 conclusion = WorkflowConclusion .SUCCESS ,
239240 )
240241
241- def get_workflow_artifacts (self , repo : str , run_id : int ) -> List [str ]:
242- """Get artifact URLs from a completed workflow.
242+ def get_workflow_artifacts (self , repo : str , run_id : int ) -> Dict [str , Dict ]:
243+ """Get artifacts from a completed workflow.
243244
244245 Args:
245246 repo: Repository name
246247 run_id: Workflow run ID
247248
248249 Returns:
249- List of artifact URLs
250+ Dictionary with artifact names as keys and artifact details as values.
251+ Each artifact dictionary contains: id, archive_download_url, created_at,
252+ expires_at, updated_at, size_in_bytes, digest
250253 """
251254 console .print (f"[blue]Getting artifacts for workflow { run_id } in { repo } [/blue]" )
252255
253256 if self .dry_run :
254- return [
255- f"https://github.com/{ repo } /actions/runs/{ run_id } /artifacts/mock-artifact"
256- ]
257+ return {
258+ "release_handle" : {
259+ "id" : 12345 ,
260+ "archive_download_url" : f"https://api.github.com/repos/{ repo } /actions/artifacts/12345/zip" ,
261+ "created_at" : "2023-01-01T00:00:00Z" ,
262+ "expires_at" : "2023-01-31T00:00:00Z" ,
263+ "updated_at" : "2023-01-01T00:00:00Z" ,
264+ "size_in_bytes" : 1048576 ,
265+ "digest" : "sha256:mock-digest"
266+ },
267+ "mock-artifact" : {
268+ "id" : 67890 ,
269+ "archive_download_url" : f"https://api.github.com/repos/{ repo } /actions/artifacts/67890/zip" ,
270+ "created_at" : "2023-01-01T00:00:00Z" ,
271+ "expires_at" : "2023-01-31T00:00:00Z" ,
272+ "updated_at" : "2023-01-01T00:00:00Z" ,
273+ "size_in_bytes" : 2048576 ,
274+ "digest" : "sha256:mock-digest-2"
275+ }
276+ }
257277
258278 # Real GitHub API call to get artifacts
259279 url = f"https://api.github.com/repos/{ repo } /actions/runs/{ run_id } /artifacts"
@@ -268,22 +288,29 @@ def get_workflow_artifacts(self, repo: str, run_id: int) -> List[str]:
268288 response .raise_for_status ()
269289
270290 data = response .json ()
271- artifacts = []
291+ artifacts = {}
272292
273293 for artifact_data in data .get ("artifacts" , []):
274294 artifact_name = artifact_data .get ("name" , "unknown" )
275- artifact_id = artifact_data .get ("id" )
276- size_mb = round (
277- artifact_data .get ("size_in_bytes" , 0 ) / (1024 * 1024 ), 2
278- )
279295
280- artifact_url = f"https://github.com/{ repo } /actions/runs/{ run_id } /artifacts/{ artifact_id } "
281- artifacts .append (f"{ artifact_name } ({ size_mb } MB) - { artifact_url } " )
296+ # Extract the required fields from the GitHub API response
297+ artifact_info = {
298+ "id" : artifact_data .get ("id" ),
299+ "archive_download_url" : artifact_data .get ("archive_download_url" ),
300+ "created_at" : artifact_data .get ("created_at" ),
301+ "expires_at" : artifact_data .get ("expires_at" ),
302+ "updated_at" : artifact_data .get ("updated_at" ),
303+ "size_in_bytes" : artifact_data .get ("size_in_bytes" ),
304+ "digest" : artifact_data .get ("workflow_run" , {}).get ("head_sha" ) # Using head_sha as digest
305+ }
306+
307+ artifacts [artifact_name ] = artifact_info
282308
283309 if artifacts :
284310 console .print (f"[green]Found { len (artifacts )} artifacts[/green]" )
285- for artifact in artifacts :
286- console .print (f"[dim] { artifact } [/dim]" )
311+ for artifact_name , artifact_info in artifacts .items ():
312+ size_mb = round (artifact_info .get ("size_in_bytes" , 0 ) / (1024 * 1024 ), 2 )
313+ console .print (f"[dim] { artifact_name } ({ size_mb } MB) - ID: { artifact_info .get ('id' )} [/dim]" )
287314 else :
288315 console .print (
289316 "[yellow]No artifacts found for this workflow run[/yellow]"
@@ -293,7 +320,79 @@ def get_workflow_artifacts(self, repo: str, run_id: int) -> List[str]:
293320
294321 except requests .exceptions .RequestException as e :
295322 console .print (f"[red]Failed to get artifacts: { e } [/red]" )
296- return []
323+ return {}
324+
325+ def extract_release_handle (self , repo : str , artifacts : Dict [str , Dict ]) -> Optional [Dict [str , Any ]]:
326+ """Extract release_handle JSON from artifacts.
327+
328+ Args:
329+ repo: Repository name
330+ artifacts: Dictionary of artifacts from get_workflow_artifacts
331+
332+ Returns:
333+ Parsed JSON content from release_handle.json file, or None if not found
334+ """
335+ if "release_handle" not in artifacts :
336+ console .print ("[yellow]No release_handle artifact found[/yellow]" )
337+ return None
338+
339+ release_handle_artifact = artifacts ["release_handle" ]
340+ artifact_id = release_handle_artifact .get ("id" )
341+
342+ if not artifact_id :
343+ console .print ("[red]release_handle artifact has no ID[/red]" )
344+ return None
345+
346+ console .print (f"[blue]Extracting release_handle from artifact { artifact_id } [/blue]" )
347+
348+ if self .dry_run :
349+ console .print ("[yellow] (DRY RUN - returning mock release_handle)[/yellow]" )
350+ return {
351+ "mock" : True ,
352+ "version" : "1.0.0" ,
353+ "build_info" : {
354+ "timestamp" : "2023-01-01T00:00:00Z" ,
355+ "commit" : "mock-commit-hash"
356+ }
357+ }
358+
359+ # Download the artifact and extract release_handle.json
360+ download_url = release_handle_artifact .get ("archive_download_url" )
361+ if not download_url :
362+ console .print ("[red]release_handle artifact has no download URL[/red]" )
363+ return None
364+
365+ headers = {
366+ "Authorization" : f"Bearer { self .token } " ,
367+ "Accept" : "application/vnd.github.v3+json" ,
368+ "X-GitHub-Api-Version" : "2022-11-28" ,
369+ }
370+
371+ try :
372+ # Download the artifact zip file
373+ response = requests .get (download_url , headers = headers , timeout = 30 )
374+ response .raise_for_status ()
375+
376+ # Extract release_handle.json from the zip
377+ import zipfile
378+ import io
379+
380+ with zipfile .ZipFile (io .BytesIO (response .content )) as zip_file :
381+ if "release_handle.json" in zip_file .namelist ():
382+ with zip_file .open ("release_handle.json" ) as json_file :
383+ release_handle_data = json .load (json_file )
384+ console .print ("[green]Successfully extracted release_handle.json[/green]" )
385+ return release_handle_data
386+ else :
387+ console .print ("[red]release_handle.json not found in artifact[/red]" )
388+ return None
389+
390+ except requests .exceptions .RequestException as e :
391+ console .print (f"[red]Failed to download release_handle artifact: { e } [/red]" )
392+ return None
393+ except (zipfile .BadZipFile , json .JSONDecodeError , KeyError ) as e :
394+ console .print (f"[red]Failed to extract release_handle.json: { e } [/red]" )
395+ return None
297396
298397 def _get_recent_workflow_runs (
299398 self , repo : str , workflow_file : str , limit : int = 10
0 commit comments