Skip to content

Commit 7399c7e

Browse files
abrichrclaude
andauthored
fix: use binary-mode subprocess for GitHub release asset downloads (#6)
The release asset scanner was failing to download all assets because: 1. _download_asset used _gh_api() which runs with text=True, corrupting binary asset content 2. _download_asset_binary first tried gh release download --pattern * without specifying a release tag (defaulted to latest instead of the target release) 3. The fallback used --output which is not a valid gh api flag 4. Even the fallback used run_cmd() with text=True Replace both methods with a single _download_asset_binary that calls gh api with Accept: application/octet-stream in binary mode (text=False) and writes raw bytes to disk. gh api handles auth and redirect-following automatically. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4f11cd5 commit 7399c7e

1 file changed

Lines changed: 29 additions & 39 deletions

File tree

tidy/artifacts/github_releases.py

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -76,54 +76,44 @@ def _list_assets(self, release_id: int) -> List[Dict[str, Any]]:
7676
return []
7777
return data
7878

79-
def _download_asset(self, asset_url: str, dest_path: str) -> bool:
80-
"""Download a release asset to *dest_path*.
79+
def _download_asset_binary(self, asset_id: int, dest_path: str) -> bool:
80+
"""Download a release asset as binary using ``gh api``.
81+
82+
GitHub release asset downloads require:
83+
1. ``Accept: application/octet-stream`` header to get the raw binary
84+
(otherwise the API returns JSON metadata).
85+
2. Binary-mode I/O — ``text=True`` would corrupt non-UTF-8 content.
8186
82-
Uses ``gh api`` with the octet-stream accept header to download
83-
binary assets.
87+
``gh api`` handles authentication automatically and follows the
88+
redirect that GitHub's asset endpoint returns.
8489
"""
90+
import subprocess as _sp
91+
8592
try:
86-
result = _gh_api(
87-
asset_url,
88-
accept="application/octet-stream",
93+
cmd = [
94+
"gh", "api",
95+
f"/repos/{self.repo}/releases/assets/{asset_id}",
96+
"-H", "Accept: application/octet-stream",
97+
]
98+
result = _sp.run(
99+
cmd,
100+
capture_output=True,
101+
text=False, # binary mode — crucial for non-text assets
89102
check=False,
90103
)
91104
if result.returncode != 0:
92-
log(f"Failed to download asset {asset_url}: {result.stderr}", level="WARN")
105+
stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
106+
log(
107+
f"Failed to download asset {asset_id}: {stderr.strip()}",
108+
level="WARN",
109+
)
93110
return False
94-
with open(dest_path, "w", encoding="utf-8", errors="replace") as f:
111+
if not result.stdout:
112+
log(f"Empty response for asset {asset_id}", level="WARN")
113+
return False
114+
with open(dest_path, "wb") as f:
95115
f.write(result.stdout)
96116
return True
97-
except Exception as exc:
98-
log(f"Error downloading asset {asset_url}: {exc}", level="WARN")
99-
return False
100-
101-
def _download_asset_binary(self, asset_id: int, dest_path: str) -> bool:
102-
"""Download a release asset as binary using gh CLI."""
103-
try:
104-
result = run_cmd(
105-
[
106-
"gh", "release", "download",
107-
"--repo", self.repo,
108-
"--pattern", "*",
109-
"--dir", os.path.dirname(dest_path),
110-
"--clobber",
111-
],
112-
check=False,
113-
)
114-
# Fallback: use curl with the gh token
115-
if result.returncode != 0:
116-
# Use the direct API download
117-
result = run_cmd(
118-
[
119-
"gh", "api",
120-
f"/repos/{self.repo}/releases/assets/{asset_id}",
121-
"-H", "Accept: application/octet-stream",
122-
"--output", dest_path,
123-
],
124-
check=False,
125-
)
126-
return result.returncode == 0
127117
except Exception as exc:
128118
log(f"Error downloading asset {asset_id}: {exc}", level="WARN")
129119
return False

0 commit comments

Comments
 (0)