Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/apm_cli/utils/github_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,16 @@ def build_artifactory_archive_url(host: str, prefix: str, owner: str, repo: str,
"""Build Artifactory VCS archive download URLs.

Returns a tuple of URLs to try in order. Because Artifactory proxies
the upstream server's native URL scheme, we attempt both GitHub-style
and GitLab-style archive paths so the caller does not need to know
what sits behind the Artifactory remote repository.
the upstream server's native URL scheme, we attempt GitHub-style,
GitLab-style, and codeload.github.com-style archive paths so the caller
does not need to know what sits behind the Artifactory remote repository.

Organizations using private GitHub repositories must configure their
Artifactory upstream as ``codeload.github.com`` (instead of ``github.com``)
because Artifactory cannot follow GitHub's cross-host redirect (which
carries short-lived tokens) to codeload. When the upstream is
``codeload.github.com``, the required archive path is
``/{owner}/{repo}/zip/refs/heads/{ref}`` (no ``.zip`` extension).

Args:
host: Artifactory hostname (e.g., 'artifactory.example.com')
Expand All @@ -301,6 +308,12 @@ def build_artifactory_archive_url(host: str, prefix: str, owner: str, repo: str,
f"{base}/-/archive/{ref}/{repo}-{ref}.zip",
# GitHub-style tags fallback
f"{base}/archive/refs/tags/{ref}.zip",
# codeload.github.com-style: /zip/refs/heads/{ref}
# Required when Artifactory upstream is configured as codeload.github.com
# (workaround for private repos where github.com redirects to codeload with tokens
# that Artifactory cannot follow across hosts)
f"{base}/zip/refs/heads/{ref}",
f"{base}/zip/refs/tags/{ref}",
)


Expand Down
48 changes: 46 additions & 2 deletions tests/unit/test_artifactory_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,49 @@ def test_real_artifactory_host(self):
assert parsed.hostname == "artifactory.example.com"
assert parsed.path == "/artifactory/github/microsoft/apm-sample-package/archive/refs/heads/main.zip"

def test_codeload_upstream_heads_ref(self):
"""When Artifactory upstream targets codeload.github.com, generate codeload-style archive URLs.

codeload.github.com uses /zip/refs/heads/{ref} (no .zip extension) instead of
the github.com-style /archive/refs/heads/{ref}.zip.
"""
urls = build_artifactory_archive_url(
"art.example.com", "artifactory/github", "owner", "repo", ref="main"
)
assert any("/zip/refs/heads/main" in u and not u.endswith("archive/refs/heads/main") for u in urls), (
"codeload-style /zip/refs/heads/{ref} URL must be present"
)

def test_codeload_upstream_tags_ref(self):
"""Tags fallback for codeload-style upstream — /zip/refs/tags/{ref}."""
urls = build_artifactory_archive_url(
"art.example.com", "artifactory/github", "owner", "repo", ref="v2.0.0"
)
assert any("/zip/refs/tags/v2.0.0" in u for u in urls), (
"codeload-style /zip/refs/tags/{ref} URL must be present for tags fallback"
)

def test_github_archive_urls_unchanged(self):
"""Existing github.com archive URL patterns must not be broken by codeload support."""
urls = build_artifactory_archive_url(
"art.example.com", "artifactory/github", "owner", "repo", ref="main"
)
assert any("/archive/refs/heads/main.zip" in u for u in urls), (
"github.com-style /archive/refs/heads/{ref}.zip must still be present"
)
assert any("/archive/refs/tags/main.zip" in u for u in urls), (
"github.com-style /archive/refs/tags/{ref}.zip must still be present"
)

def test_gitlab_archive_urls_unchanged(self):
"""Existing GitLab archive URL pattern must not be broken by codeload support."""
urls = build_artifactory_archive_url(
"art.example.com", "artifactory/github", "owner", "repo", ref="main"
)
assert any("/-/archive/main/repo-main.zip" in u for u in urls), (
"GitLab-style /-/archive/{ref}/{repo}-{ref}.zip must still be present"
)


# ── apm_package.py: DependencyReference Artifactory parsing ──

Expand Down Expand Up @@ -1323,8 +1366,9 @@ def test_entry_download_returns_none_on_404(self):
)

assert result is None
# Should have tried all 3 URL patterns
assert mock_get.call_count == 3
# Should have tried all 5 URL patterns (GitHub heads, GitLab, GitHub tags,
# codeload heads, codeload tags)
assert mock_get.call_count == 5

def test_entry_download_returns_none_on_connection_error(self):
"""Returns None when the HTTP call raises an exception."""
Expand Down