Skip to content

Commit 17e8386

Browse files
committed
Structurized artifacts and release_handle support
1 parent 26c0369 commit 17e8386

File tree

4 files changed

+132
-22
lines changed

4 files changed

+132
-22
lines changed

src/redis_release/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ def status(
126126
else:
127127
build_status = "[yellow]Cancelled[/yellow]"
128128

129-
if pkg_state.artifact_urls:
130-
artifacts = f"[green]{len(pkg_state.artifact_urls)} artifacts[/green]"
129+
if pkg_state.artifacts:
130+
artifacts = f"[green]{len(pkg_state.artifacts)} artifacts[/green]"
131131
else:
132132
artifacts = "[dim]None[/dim]"
133133

src/redis_release/github_client.py

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""GitHub API client for workflow operations."""
22

3+
import json
34
import re
45
import time
56
import uuid
6-
from typing import Dict, List, Optional
7+
from typing import Any, Dict, List, Optional
78
import requests
89

910
from 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

src/redis_release/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from datetime import datetime
44
from enum import Enum
5-
from typing import Dict, List, Optional
5+
from typing import Any, Dict, Optional
66

77
from pydantic import BaseModel, Field
88

@@ -53,7 +53,8 @@ class PackageState(BaseModel):
5353

5454
package_type: PackageType
5555
build_workflow: Optional[WorkflowRun] = None
56-
artifact_urls: List[str] = Field(default_factory=list)
56+
artifacts: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
57+
release_handle: Optional[Dict[str, Any]] = None
5758
build_completed: bool = False
5859

5960

src/redis_release/orchestrator.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,17 @@ def _execute_build_phase(
326326
artifacts = github_client.get_workflow_artifacts(
327327
completed_run.repo, completed_run.run_id
328328
)
329-
docker_state.artifact_urls = artifacts
329+
docker_state.artifacts = artifacts
330+
331+
# Extract release_handle from artifacts
332+
release_handle = github_client.extract_release_handle(
333+
completed_run.repo, artifacts
334+
)
335+
if release_handle is None:
336+
console.print("[red]Failed to extract release_handle from artifacts[/red]")
337+
return False
338+
339+
docker_state.release_handle = release_handle
330340
console.print("[green]Docker build completed successfully[/green]")
331341
elif completed_run.conclusion == WorkflowConclusion.FAILURE:
332342
docker_state.build_completed = True # completed, but failed

0 commit comments

Comments
 (0)