Skip to content

Commit a4ce981

Browse files
lselvarclaude
andcommitted
fix(bundle): pass github_provider_hosts() for GHES private release downloads
Extends the GHES support pattern from extensions and presets (#2855, #3157) to the bundle manifest download path: resolve_github_release_asset_api_url now receives github_hosts=github_provider_hosts() so browser release URLs from GitHub Enterprise Server instances are resolved via /api/v3 rather than falling back to the unauthenticated download path. Also adds a contract test covering the GHES resolution path for _download_remote_manifest (analogous to the existing github.com tests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7f1b20b commit a4ce981

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

src/specify_cli/commands/bundle/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ def _download_remote_manifest(entry_id: str, url: str):
807807

808808
import yaml as _yaml
809809

810-
from ...authentication.http import open_url
810+
from ...authentication.http import github_provider_hosts, open_url
811811
from ..._github_http import resolve_github_release_asset_api_url
812812
from ...bundler.models.manifest import BundleManifest
813813

@@ -823,7 +823,9 @@ def _validate_redirect(old_url: str, new_url: str) -> None:
823823
# can download the actual file.
824824
extra_headers = None
825825
effective_url = url
826-
resolved = resolve_github_release_asset_api_url(url, open_url, timeout=30)
826+
resolved = resolve_github_release_asset_api_url(
827+
url, open_url, timeout=30, github_hosts=github_provider_hosts()
828+
)
827829
if resolved:
828830
effective_url = resolved
829831
_require_https(f"bundle '{entry_id}'", effective_url)

tests/contract/test_bundle_cli.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,3 +669,58 @@ def fake_open_url(url, timeout=None, extra_headers=None, redirect_validator=None
669669

670670
# Error output must be actionable (not a raw traceback)
671671
assert "Error:" in result.output
672+
673+
674+
def test_bundle_info_resolves_ghes_browser_release_url(project: Path):
675+
"""bundle info resolves a GHES private-repo browser release URL via /api/v3."""
676+
ghes_host = "ghes.example"
677+
browser_url = f"https://{ghes_host}/org/repo/releases/download/v1.0/bundle.yml"
678+
api_asset_url = f"https://{ghes_host}/api/v3/repos/org/repo/releases/assets/42"
679+
680+
captured = []
681+
manifest_yaml = yaml.safe_dump(valid_manifest_dict()).encode()
682+
683+
def fake_open_url(url, timeout=None, extra_headers=None, redirect_validator=None):
684+
captured.append((url, extra_headers))
685+
if "/api/v3/repos/" in url and "releases/tags/" in url:
686+
return FakeBundleResponse(
687+
json.dumps({
688+
"assets": [{"name": "bundle.yml", "url": api_asset_url}]
689+
}).encode(),
690+
url=url,
691+
)
692+
return FakeBundleResponse(manifest_yaml, url=api_asset_url)
693+
694+
catalog = project / "catalog.json"
695+
write_catalog_file(
696+
catalog,
697+
{"demo-bundle": catalog_entry_dict("demo-bundle", download_url=browser_url)},
698+
)
699+
_make_catalog_config(catalog, project)
700+
701+
ghes_entry = {
702+
"hosts": [ghes_host],
703+
"provider": "github",
704+
"auth": "bearer",
705+
"token": "ghes-test-token",
706+
}
707+
708+
with patch("specify_cli.authentication.http.open_url", side_effect=fake_open_url), \
709+
patch("specify_cli.authentication.http.github_provider_hosts", return_value=(ghes_host,)):
710+
result = runner.invoke(app, ["bundle", "info", "demo-bundle", "--json"])
711+
712+
assert result.exit_code == 0, result.output
713+
714+
# The GHES /api/v3 tags lookup must have fired
715+
tag_calls = [url for url, _ in captured if "releases/tags/" in url]
716+
assert len(tag_calls) == 1
717+
assert f"{ghes_host}/api/v3/repos/org/repo/releases/tags/v1.0" in tag_calls[0]
718+
719+
# Asset download must use the resolved GHES API URL with octet-stream
720+
asset_calls = [(url, h) for url, h in captured if "releases/assets/" in url]
721+
assert len(asset_calls) == 1
722+
assert asset_calls[0][0] == api_asset_url
723+
assert asset_calls[0][1] == {"Accept": "application/octet-stream"}
724+
725+
payload = json.loads(result.output)
726+
assert payload["id"] == "demo-bundle"

0 commit comments

Comments
 (0)