fix(extensions,presets,workflows): resolve private GHES release assets via /api/v3#3157
Merged
mnriem merged 7 commits intoJun 25, 2026
Merged
Conversation
282b7ba to
e2852e0
Compare
2 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes authenticated downloads of private GitHub Enterprise Server (GHES) release assets across specify extension add, specify preset add, and specify workflow add by translating browser-style release download URLs into GHES REST API asset URLs under /api/v3 (with Accept: application/octet-stream), gated by the user’s opt-in ~/.specify/auth.json host allowlist.
Changes:
- Add
github_provider_hosts()to derive the GHES host allowlist fromauth.jsongithubprovider entries. - Generalize
resolve_github_release_asset_api_url()to support GHES REST API URL construction ({scheme}://{host[:port]}/api/v3/...) while preserving existinggithub.combehavior. - Wire GHES host allowlisting into all resolver call sites and add unit + CLI/integration tests, plus authentication docs updates.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/authentication/http.py |
Adds github_provider_hosts() to expose configured github provider hosts for GHES classification. |
src/specify_cli/_github_http.py |
Extends release-asset URL resolution to support GHES /api/v3 with an allowlisted host gate. |
src/specify_cli/extensions/__init__.py |
Threads github_provider_hosts() into extension catalog asset URL resolution. |
src/specify_cli/presets/__init__.py |
Threads github_provider_hosts() into preset catalog asset URL resolution. |
src/specify_cli/presets/_commands.py |
Ensures preset add --from uses GHES-aware resolver + octet-stream download behavior. |
src/specify_cli/__init__.py |
Ensures both workflow add <url> and workflow add <id> paths use GHES-aware resolver + octet-stream downloads. |
tests/test_github_http.py |
Adds resolver unit tests for GHES behavior, scheme/port preservation, and allowlist gating. |
tests/test_authentication.py |
Adds tests verifying github_provider_hosts() behavior. |
tests/test_presets.py |
Adds CLI-level GHES download test and end-to-end wiring test for preset catalog wrapper. |
tests/test_extensions.py |
Adds end-to-end wiring test for extension catalog wrapper GHES resolution. |
tests/test_workflows.py |
Adds CLI-level tests covering GHES release URL resolution for workflow add (URL + catalog paths). |
docs/reference/authentication.md |
Documents the GHES auth.json recipe and explains why the bare host must be listed. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 12/12 changed files
- Comments generated: 1
Collaborator
|
Please address Copilot feedback |
HeroSizy
added a commit
to HeroSizy/spec-kit
that referenced
this pull request
Jun 25, 2026
Addresses Copilot review on PR github#3157: drop unnecessary __import__("io") in test_preset_add_from_ghes_release_url_resolves_via_api_v3 since io is already imported at module level.
…auth.json Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
Generalizes resolve_github_release_asset_api_url to GitHub Enterprise Server hosts (gated by auth.json github hosts), fixing private GHES extension/preset downloads. github#3147 Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
…olver Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
…lease resolvers Wires preset add --from and workflow add through github_provider_hosts() so private GHES release assets resolve via /api/v3 there too. github#3147 Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
Addresses Copilot review on PR github#3157: drop unnecessary __import__("io") in test_preset_add_from_ghes_release_url_resolves_via_api_v3 since io is already imported at module level.
364be42 to
76c15ba
Compare
Collaborator
|
Please address Copilot feedback |
Addresses Copilot review on PR github#3157. A direct GHES /api/v3 release asset URL was only returned as already-resolved when its host was in the allowlist; otherwise the resolver returned None and the caller downloaded the same URL without 'Accept: application/octet-stream', fetching JSON metadata instead of the binary. Gate the passthrough on path shape alone, mirroring the github.com case. This is safe: passthrough returns the input URL unchanged and the caller fetches it either way, so no new request to an arbitrary host is induced; the token stays independently gated by auth.json in open_url. The allowlist remains the anti-SSRF gate on the tag-lookup resolving path. Add test_passthrough_for_unlisted_ghes_api_asset_url.
Collaborator
|
Thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes private GitHub Enterprise Server (GHES) release-asset downloads for
specify extension add,specify preset add, andspecify workflow add. Closes #3147 (approach proposed and discussed in #3147 (comment)).Background. Catalog fetch from a private GHES host already works today via the opt-in
~/.specify/auth.jsonmechanism (#2393) — that was simply undocumented, so the issue assumed catalogs had to be public. The real gap is the release-asset download.Root cause. Release-asset downloads first translate a browser release URL (
https://<host>/<owner>/<repo>/releases/download/<tag>/<asset>) into the REST API asset URL and addAccept: application/octet-stream; otherwise a private repo's browser URL redirects to an SSO/HTML page instead of the binary. That translation lives in one shared helper,_github_http.resolve_github_release_asset_api_url, which hardcodedgithub.com/api.github.comand returnedNonefor any GHES host — so for GHES the URL was never translated and the header never set, and the download was corrupt even with a valid token.What this does.
github_provider_hosts()— enumerates the hosts a user has listed under agithubprovider inauth.json.{scheme}://{host[:port]}/api/v3/...) for those hosts. Publicgithub.comhandling is unchanged (github_hosts=()reproduces prior behavior byte-for-byte).preset add --from, and bothworkflow addpaths).auth.jsonrecipe indocs/reference/authentication.md.Design notes.
auth.jsonentry supplies the token and classifies the host as GitHub Enterprise.auth.jsonallowlist is the anti-SSRF gate — only hosts the user explicitly trusts locally get/api/v3treatment, so a remote catalog can never induce an API request to an arbitrary host. The host-classification list is injected into_github_http(which imports nothing from the auth layer), keeping that module's dependency boundary intact.Scope: the issue's
GH_HOST/GH_ENTERPRISE_TOKEN"zero-config" idea was intentionally not implemented —auth.jsonalready carries the token and preserves the file-based opt-in model; reusing env vars would auto-send credentials based on environment alone. Easy to add later if there's demand.Testing
uv run specify --help(and exercised the realspecify preset add --fromCLI in the manual test below).venv/bin/python -m pytest tests -q(the repo's recommended form over bareuv run pytest, per CONTRIBUTING/AGENTS.md). New: 6 resolver unit tests,github_provider_hosts()unit tests, and CLI/integration tests for every wired caller.Manual test results
Agent: Claude Code (Opus 4.8) | OS/Shell: macOS / zsh
specify preset add --from <private GHES release URL>…/api/v3/repos/…/releases/assets/<id>, authenticated with theauth.jsonbearer token, downloaded withAccept: application/octet-stream, preset installed (exit 0).ExtensionCatalogresolver path)/api/v3and authenticated-downloaded a valid extension archive (verified valid zip + contents).auth.json(local mock)/api/v3attempted → request 401s → command exits non-zero.specify extension add·specify preset add <id>·specify workflow addCliRunnerintegration tests (GHES resolution +Accept: application/octet-stream).Note on the suite: all new tests pass. Three failures present in my environment are pre-existing and unrelated to this branch:
test_get_pack_infoandtest_default_active_catalogscome from a developer-global preset catalog leaking into tests that don't isolate~/.specify(they pass once that global registry is moved aside);test_timestamp_branches…[go AI now]fails on pristinemainas well. Happy to file separate issues for the test-isolation gaps.AI Disclosure
This change was developed with Claude Code (Anthropic), and the loop was AI-driven, disclosed as such:
preset add --frominstall — and confirmed the results.Assisted-by: Claude Code (model: …, autonomous)trailer, and the issue comment proposing this approach was likewise AI-drafted and disclosed.Happy to walk through any part in more detail.