mirror of
https://github.com/github/spec-kit.git
synced 2026-07-08 23:59:55 +08:00
feat(extensions,presets): authenticate GitHub-hosted catalog and download requests with GITHUB_TOKEN/GH_TOKEN (#2331)
* feat(extensions,presets): authenticate GitHub-hosted catalog and download requests with GITHUB_TOKEN/GH_TOKEN Squashed from #2087 (original author: @anasseth). Adds GitHub-token authentication to extension and preset catalog fetching and ZIP downloads so private GitHub repos work when GITHUB_TOKEN/GH_TOKEN is set, while preventing credential leakage to non-GitHub hosts. - Introduces shared _github_http module with build_github_request() and open_github_url() helpers - Routes ExtensionCatalog and PresetCatalog network calls through GitHub-auth-aware opener - Adds comprehensive unit/integration tests for auth header behavior - Updates user docs for both extensions and presets Co-authored-by: anasseth <16745089+anasseth@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(auth): address review feedback from #2087 - Fix redirect handler to preserve Authorization on GitHub-to-GitHub redirects (e.g. github.com → codeload.github.com). The previous implementation relied on super().redirect_request() which strips auth on cross-host redirects, breaking private repo archive downloads. - Add codeload.github.com to documented host lists in both EXTENSION-USER-GUIDE.md and presets/README.md - Add redirect auth-preservation and auth-stripping tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(auth): use Bearer scheme instead of token for consistency Aligns with the rest of the codebase (e.g. __init__.py:1721) and GitHub's current API guidance. Updates all test assertions accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address second round of Copilot review feedback - Fix docstring to say Bearer instead of token (matches implementation) - Remove unused imports/fixtures from redirect tests (GITHUB_HOSTS, MagicMock, temp_dir, monkeypatch) - Replace __import__('io').BytesIO() with normal import io pattern in test_presets.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: anasseth <16745089+anasseth@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1831,6 +1831,22 @@ class PresetCatalog:
|
||||
"Catalog URL must be a valid URL with a host."
|
||||
)
|
||||
|
||||
def _make_request(self, url: str):
|
||||
"""Build a urllib Request, adding a GitHub auth header when available.
|
||||
|
||||
Delegates to :func:`specify_cli._github_http.build_github_request`.
|
||||
"""
|
||||
from specify_cli._github_http import build_github_request
|
||||
return build_github_request(url)
|
||||
|
||||
def _open_url(self, url: str, timeout: int = 10):
|
||||
"""Open a URL with GitHub auth, stripping the header on cross-host redirects.
|
||||
|
||||
Delegates to :func:`specify_cli._github_http.open_github_url`.
|
||||
"""
|
||||
from specify_cli._github_http import open_github_url
|
||||
return open_github_url(url, timeout)
|
||||
|
||||
def _load_catalog_config(self, config_path: Path) -> Optional[List[PresetCatalogEntry]]:
|
||||
"""Load catalog stack configuration from a YAML file.
|
||||
|
||||
@@ -2013,10 +2029,7 @@ class PresetCatalog:
|
||||
pass
|
||||
|
||||
try:
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
with urllib.request.urlopen(entry.url, timeout=10) as response:
|
||||
with self._open_url(entry.url, timeout=10) as response:
|
||||
catalog_data = json.loads(response.read())
|
||||
|
||||
if (
|
||||
@@ -2109,10 +2122,7 @@ class PresetCatalog:
|
||||
pass
|
||||
|
||||
try:
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
with urllib.request.urlopen(catalog_url, timeout=10) as response:
|
||||
with self._open_url(catalog_url, timeout=10) as response:
|
||||
catalog_data = json.loads(response.read())
|
||||
|
||||
if (
|
||||
@@ -2231,7 +2241,6 @@ class PresetCatalog:
|
||||
Raises:
|
||||
PresetError: If pack not found or download fails
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
pack_info = self.get_pack_info(pack_id)
|
||||
@@ -2283,7 +2292,7 @@ class PresetCatalog:
|
||||
zip_path = target_dir / zip_filename
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(download_url, timeout=60) as response:
|
||||
with self._open_url(download_url, timeout=60) as response:
|
||||
zip_data = response.read()
|
||||
|
||||
zip_path.write_bytes(zip_data)
|
||||
|
||||
Reference in New Issue
Block a user