From 56b1839fbae6bd51d39760eedeac913a55bbc691 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Wed, 29 Jul 2026 01:45:38 +0500 Subject: [PATCH] fix: handle tags containing / in GitHub release asset URL resolution (#3767) The tag extraction in resolve_github_release_asset_api_url split the URL path on / and assumed the tag was a single segment at index 4. Tags containing literal / (e.g. feature/v1) would be split across multiple segments, causing the tag to be truncated to only the first part and the asset name to include leftover tag segments. Fix by reconstructing the tag as all segments between 'download' and the final asset segment: tag = '/'.join(parts[4:-1]), asset = parts[-1]. --- src/specify_cli/_github_http.py | 5 +++-- tests/test_github_http.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/_github_http.py b/src/specify_cli/_github_http.py index 03b822788..017f50b5d 100644 --- a/src/specify_cli/_github_http.py +++ b/src/specify_cli/_github_http.py @@ -159,8 +159,9 @@ def resolve_github_release_asset_api_url( if len(parts) < 6 or parts[2:4] != ["releases", "download"]: return None - owner, repo, tag = parts[0], parts[1], parts[4] - asset_name = "/".join(parts[5:]) + owner, repo = parts[0], parts[1] + tag = "/".join(parts[4:-1]) + asset_name = parts[-1] encoded_tag = quote(tag, safe="") release_url = f"{api_base}/repos/{owner}/{repo}/releases/tags/{encoded_tag}" diff --git a/tests/test_github_http.py b/tests/test_github_http.py index 91cfb5e6b..b31d82990 100644 --- a/tests/test_github_http.py +++ b/tests/test_github_http.py @@ -379,6 +379,31 @@ class TestResolveGitHubReleaseAssetApiUrl: assert result == "https://api.github.com/repos/org/repo/releases/assets/99" assert captured == ["https://api.github.com/repos/org/repo/releases/tags/v1.0"] + def test_tag_with_literal_slash_in_path(self): + """A tag containing a literal '/' (e.g. feature/v1.0.0) splits across + multiple URL path segments. The implementation must join all segments + between 'download/' and the asset name to reconstruct the full tag.""" + captured_urls = [] + asset_url = "https://api.github.com/repos/org/repo/releases/assets/77" + + @contextmanager + def capturing_open(url, timeout=None, extra_headers=None): + captured_urls.append(url) + resp = MagicMock() + resp.read.side_effect = io.BytesIO(json.dumps({ + "assets": [{"name": "asset.zip", "url": asset_url}] + }).encode()).read + yield resp + + result = resolve_github_release_asset_api_url( + "https://github.com/org/repo/releases/download/feature/v1.0.0/asset.zip", + capturing_open, + ) + assert result == asset_url + # Tag must be the full "feature/v1.0.0", not just "v1.0.0" + assert len(captured_urls) == 1 + assert "releases/tags/feature%2Fv1.0.0" in captured_urls[0] + class TestGitHubRedirectAuth: """Tests for GitHub-owned redirect auth handling."""