fix: marketplace build respects GITHUB_HOST for GHE repos#1009
fix: marketplace build respects GITHUB_HOST for GHE repos#1009danielmeppiel merged 5 commits intomainfrom
Conversation
Thread the existing default_host() / build_https_clone_url() / AuthResolver pattern (used by apm install) through the marketplace build pipeline. Changes: - RefResolver: accept optional host parameter, use build_https_clone_url() instead of hardcoded github.com for git ls-remote URLs - MarketplaceBuilder: resolve tokens against configured host, use REST API for metadata fetch on GHES/GHE Cloud (raw.githubusercontent.com is github.com-only), skip metadata for non-GitHub hosts - Fix AuthResolver import scoping so classify_host() works when auth_resolver is pre-injected - Add GHE Cloud early-exit when no token (avoids pointless 401) Tests: - Update URL assertions to use urlparse (test convention) - Add 4 RefResolver GHE host tests - Add 3 metadata fetch path tests (GHES REST API, non-GitHub skip, GHE Cloud no-token skip) - Add builder host env test Docs: - CHANGELOG: Fixed entry under [Unreleased] - marketplace-authoring guide: GHES section - apm-usage authentication skill: marketplace build example Closes #1008 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel VerdictDisposition: APPROVE (two minor pre-merge suggestions; neither is a blocker) Per-persona findingsPython Architect: This is a routine bug-fix PR: two existing classes ( OO / class diagram classDiagram
direction LR
class MarketplaceBuilder {
<<Builder>>
+_host: str
+_host_info: Optional[object]
+_github_token: Optional[str]
+_get_resolver() RefResolver
+_resolve_github_token() Optional[str]
+_fetch_remote_metadata(pkg) Optional[Dict]
+build() BuildResult
}
class RefResolver {
<<Service>>
+_host: str
+list_remote_refs(owner_repo) List[RemoteRef]
+resolve_ref_sha(owner_repo, ref) str
}
class AuthResolver {
<<Strategy>>
+classify_host(host) HostInfo
+resolve(host) AuthContext
}
class HostInfo {
<<ValueObject>>
+kind: str
+api_base: str
}
class AuthContext {
<<ValueObject>>
+token: str
+source: str
}
class default_host {
<<Pure>>
+default_host() str
+build_https_clone_url(host, repo) str
}
class ResolvedPackage {
<<ValueObject>>
+source_repo: str
+sha: str
+subdir: Optional[str]
}
MarketplaceBuilder *-- RefResolver : creates lazily
MarketplaceBuilder ..> AuthResolver : classify_host and resolve
MarketplaceBuilder ..> HostInfo : stores as _host_info
MarketplaceBuilder ..> ResolvedPackage : reads in _fetch_remote_metadata
MarketplaceBuilder ..> default_host : reads host at init
RefResolver ..> default_host : reads host at init
AuthResolver ..> HostInfo : returns
AuthResolver ..> AuthContext : returns
class MarketplaceBuilder:::touched
class RefResolver:::touched
classDef touched fill:#fff3b0,stroke:#d47600
Execution flow diagram flowchart TD
A["apm marketplace build\ncli.py"] --> B["MarketplaceBuilder.__init__()\nbuilder.py\n_host = default_host() or 'github.com'"]
B --> C["_prefetch_metadata(resolved)\nbuilder.py:589"]
C --> D["_resolve_github_token()\nbuilder.py:547\nsets _host_info AND _github_token"]
D --> E["[NET] AuthResolver.classify_host(self._host)\nsrc/apm_cli/core/auth.py:134\nreturns HostInfo(kind, api_base)"]
D --> F["[NET] resolver.resolve(self._host)\nreturns AuthContext.token"]
F --> G["pool.submit(_fetch_remote_metadata, pkg)\nbuilder.py:553\nfor each resolved package"]
G --> H{"host_kind?"}
H -->|"not github/ghe_cloud/ghes"| I["logger.debug skip\nreturn None"]
H -->|"ghe_cloud and no token"| J["logger.debug skip\nreturn None"]
H -->|"self._host == 'github.com'"| K["[NET] raw.githubusercontent.com\n/{source_repo}/{sha}/{path}/apm.yml\nurllib.request.urlopen"]
H -->|"ghes or ghe_cloud+token"| L["[NET] {api_base}/repos/{source_repo}\n/contents/{file}?ref={sha}\nAccept: application/vnd.github.raw\nurllib.request.urlopen"]
K --> M["yaml.safe_load(raw) -> dict"]
L --> M
M --> N["return metadata dict"]
Design patterns
CLI Logging Expert: All new log calls use DevX UX Expert: This is a silent behavior fix -- no new flags, no command surface changes. The key UX property preserved: Supply Chain Security Expert: Reviewed against the threat model:
No new supply-chain surface opened. Auth Expert: Activated -- the PR changes
OSS Growth Hacker: This fix completes APM's GHES story: CEO arbitrationSpecialists agree: this is a correct, well-tested, well-documented bug fix from an external contributor. The two minor suggestions (move Required actions before merge
Optional follow-ups
|
…yReference for URL sources Phase B of #1008 -- decouples authentication from marketplace generation and reuses existing resolution infrastructure for cross-source compatibility. Changes: - RefResolver: accept optional token for authenticated git ls-remote - Builder: extract lazy _ensure_auth() called from _get_resolver() so both resolve() and build() benefit from authenticated ls-remote - Builder: eagerly init resolver before thread pool (race prevention) - Builder: fix _host_info type annotation (Optional["HostInfo"] with TYPE_CHECKING guard) - resolver.py: _resolve_url_source() now delegates to DependencyReference.parse() -- accepts any valid Git URL (GitHub, GHES, GitLab, Bitbucket, ADO, SSH) instead of github.com only - 13 new tests covering token injection, lazy auth, and cross-source URL resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel VerdictDisposition: APPROVE (with one recommended docstring fix and one follow-up on URL-source semantics) Per-persona findingsPython Architect: This is a focused, well-scoped fix. Two commits: one that threads 1. OO / class diagramclassDiagram
direction LR
class MarketplaceBuilder {
<<Builder>>
-_host str
-_host_info HostInfo
-_github_token Optional[str]
-_resolver Optional[RefResolver]
-_auth_resolver Optional[AuthResolver]
+_ensure_auth() None
+_get_resolver() RefResolver
+_resolve_github_token() Optional[str]
+_fetch_remote_metadata(pkg) Optional[dict]
+resolve() ResolveResult
+build() BuildReport
}
class RefResolver {
<<IOBoundary>>
-_host str
-_token Optional[str]
-_cache RefCache
-_lock threading.Lock
+list_remote_refs(owner_repo) List[RemoteRef]
+resolve_ref_sha(owner_repo, ref) str
}
class AuthResolver {
<<Strategy>>
+resolve(host) AuthContext
+classify_host(host) HostInfo
}
class HostInfo {
<<ValueObject>>
+kind str
+api_base Optional[str]
}
class AuthContext {
<<ValueObject>>
+token Optional[str]
+source str
}
class DependencyReference {
<<ValueObject>>
+repo_url str
+reference Optional[str]
+is_local bool
+parse(url) DependencyReference
}
class _resolve_url_source {
<<Pure>>
accepts any Git URL via DependencyReference
}
MarketplaceBuilder *-- RefResolver : lazily creates
MarketplaceBuilder ..> AuthResolver : calls resolve(host)
MarketplaceBuilder ..> HostInfo : reads kind, api_base
AuthResolver ..> HostInfo : returns from classify_host()
AuthResolver ..> AuthContext : returns from resolve()
_resolve_url_source ..> DependencyReference : delegates parse()
RefResolver ..> AuthContext : uses token field
class MarketplaceBuilder:::touched
class RefResolver:::touched
class _resolve_url_source:::touched
classDef touched fill:#fff3b0,stroke:#d47600
2. Execution flow diagramflowchart TD
A["apm marketplace build\n(cli.py)"] --> B["MarketplaceBuilder.build()"]
B --> C["MarketplaceBuilder.resolve()"]
C --> D["_get_resolver()\n[TOUCHED]"]
D --> E["_ensure_auth()\n[TOUCHED]"]
E --> F{"_github_token\nalready set?"}
F -- yes --> G["return early"]
F -- no --> H["_resolve_github_token()"]
H --> I["[NET] AuthResolver.classify_host(host)\nset _host_info"]
I --> J["[NET] AuthResolver.resolve(host)\ntoken precedence chain"]
J --> K["self._github_token = token or None"]
K --> L["RefResolver(host=, token=)\n[TOUCHED]"]
L --> M["ThreadPoolExecutor spawned\nfor each package entry"]
M --> N["RefResolver.list_remote_refs(owner_repo)"]
N --> O["build_https_clone_url(host, owner_repo, token=)\nproduces x-access-token URL or plain URL"]
O --> P["[EXEC] subprocess: git ls-remote\nGIT_TERMINAL_PROMPT=0, GIT_ASKPASS=echo"]
P --> Q["ref + sha resolved"]
B --> R["_prefetch_metadata(resolved)"]
R --> S["_ensure_auth() again\n(idempotent if token set;\nre-calls _resolve_github_token if None)"]
S --> T{"host_kind?"}
T -- github.com --> U["[NET] urllib: raw.githubusercontent.com"]
T -- ghes / ghe_cloud --> V["[NET] urllib: {api_base}/repos/.../contents/...\nAccept: application/vnd.github.raw"]
T -- generic --> W["skip metadata enrichment"]
T -- ghe_cloud + no token --> X["skip metadata enrichment"]
Design patterns
Minor architecture note: CLI Logging Expert: No concerns. All new diagnostic output routes through DevX UX Expert: Transparent to the user. A marketplace author on a GHE instance sets Supply Chain Security Expert: Three surfaces reviewed.
Auth Expert: Activated (fallback self-check: YES -- the PR changes how tokens are injected into The key fix -- changing from the hardcoded The One idempotency gap (noted by Python Architect): when No auth precedence regression. No credential leakage path. No new OSS Growth Hacker: This is an enterprise unlock. Marketplace authoring previously required a Side-channel to CEO: The The CEO arbitrationSpecialists are in agreement: this is a correct, well-tested fix with no regressions. The core change -- routing Required actions before merge
Optional follow-ups
|
- Add _auth_resolved sentinel to _ensure_auth() for true idempotency - Clarify _resolve_url_source() docstring: host is not preserved (#1010) - Split CHANGELOG #1008 entry into GHE fix + URL-source expansion - Add test documenting host-is-ignored behaviour for non-GitHub URLs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review Panel Findings -- AddressedAll findings from both panel reviews have been addressed in Required fixes
Optional fixes (also implemented)
Validation
|
There was a problem hiding this comment.
Pull request overview
Fixes apm marketplace build to respect GITHUB_HOST (consistent with the install/auth infrastructure) so GHES/GHE Cloud repos can be resolved/authenticated correctly, and expands marketplace type: url parsing by delegating to DependencyReference.parse().
Changes:
- Thread
default_host()/build_https_clone_url()+ host/token handling through marketplace ref resolution (git ls-remote) and metadata fetching. - Add lazy auth resolution (
_ensure_auth) so bothresolve()andbuild()use authenticated ref resolution when available. - Update docs/changelog and add unit tests for GHE host behavior and URL parsing.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/marketplace/ref_resolver.py |
Adds host + optional token support; builds git ls-remote URLs via shared host utilities. |
src/apm_cli/marketplace/builder.py |
Uses default host, lazy auth resolution, and host-aware metadata fetch (raw CDN vs REST API). |
src/apm_cli/marketplace/resolver.py |
Delegates type: url resolution to DependencyReference.parse() instead of github.com-only matching. |
tests/unit/marketplace/test_ref_resolver.py |
Adds URL parsing assertions + GHE host/token-in-URL coverage. |
tests/unit/marketplace/test_marketplace_resolver.py |
Adds tests for URL-source parsing behavior (including host stripping). |
tests/unit/marketplace/test_builder.py |
Adds tests for host-kind branching in _fetch_remote_metadata() and _ensure_auth() behavior. |
tests/unit/commands/test_marketplace_build.py |
Confirms GITHUB_HOST is respected by MarketplaceBuilder. |
docs/src/content/docs/guides/marketplace-authoring.md |
Documents GHES usage for marketplace build. |
packages/apm-guide/.apm/skills/apm-usage/authentication.md |
Updates auth guide to mention marketplace build respects GITHUB_HOST. |
CHANGELOG.md |
Adds Unreleased Fixed entries for GHE host support and URL parsing behavior. |
- Fix _ensure_auth() offline branch to set _auth_resolved sentinel - Clarify CHANGELOG and docs: URL host is not preserved, GITHUB_HOST required - Update marketplace-authoring.md to warn against cross-host URL reliance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel VerdictDisposition: APPROVE (with one optional follow-up noted below) Per-persona findingsPython Architect: The PR is a well-scoped bug fix that decouples host-awareness from the marketplace build pipeline. Three files are modified in the problem space: 1. OO / Class DiagramclassDiagram
direction LR
class MarketplaceBuilder {
<<Service>>
-_host str
-_host_info Optional[HostInfo]
-_auth_resolved bool
-_github_token Optional[str]
-_resolver Optional[RefResolver]
+build() MarketplaceOutput
+resolve() list
-_ensure_auth() None
-_get_resolver() RefResolver
-_resolve_github_token() Optional[str]
-_fetch_remote_metadata(pkg) Optional[dict]
}
class RefResolver {
<<Service>>
-_host str
-_token Optional[str]
-_cache RefCache
-_lock Lock
+list_remote_refs(owner_repo) List[RemoteRef]
+resolve_ref_sha(owner_repo, ref) str
}
class AuthResolver {
<<Strategy>>
+classify_host(host) HostInfo
+resolve(host) AuthContext
}
class HostInfo {
<<ValueObject>>
+kind str
+api_base Optional[str]
}
class AuthContext {
<<ValueObject>>
+token str
+source str
}
class DependencyReference {
<<ValueObject>>
+repo_url str
+reference Optional[str]
+is_local bool
+parse(url) DependencyReference
}
class github_host_utils {
<<Module>>
+default_host() Optional[str]
+build_https_clone_url(host, repo, token) str
}
class resolver_module {
<<Module>>
+_resolve_url_source(source) str
}
class MarketplaceBuilder:::touched
class RefResolver:::touched
class resolver_module:::touched
MarketplaceBuilder *-- RefResolver : creates and owns
MarketplaceBuilder ..> AuthResolver : token and host classification
MarketplaceBuilder ..> HostInfo : branches on kind (github/ghes/ghe_cloud/generic)
MarketplaceBuilder ..> github_host_utils : default_host()
RefResolver ..> github_host_utils : build_https_clone_url()
AuthResolver ..> HostInfo : returns
AuthResolver ..> AuthContext : returns
resolver_module ..> DependencyReference : delegates URL parsing
note for MarketplaceBuilder "Lazy init: _ensure_auth() is idempotent\n_auth_resolved flag prevents re-entry\n_host_info set as side-effect in _resolve_github_token()"
classDef touched fill:#fff3b0,stroke:#d47600
2. Execution Flow Diagramflowchart TD
A["apm marketplace build (cli.py)"] --> B["MarketplaceBuilder.build()"]
B --> C["_get_resolver() [eager, pre-thread-pool]"]
C --> D["_ensure_auth()"]
D --> E{_auth_resolved?}
E -- yes --> F["return (idempotent)"]
E -- no --> G{offline mode?}
G -- yes --> H["_auth_resolved = True, token = None"]
G -- no --> I["_resolve_github_token()"]
I --> J["[NET] AuthResolver.classify_host(self._host) -> HostInfo"]
J --> K["[NET] AuthResolver.resolve(self._host) -> AuthContext"]
K --> L["self._github_token = ctx.token\nself._host_info = HostInfo\n_auth_resolved = True"]
L --> M["RefResolver(host=self._host, token=self._token)"]
M --> N["ThreadPoolExecutor: _resolve_references()"]
N --> O["[EXEC] RefResolver.list_remote_refs(owner_repo)"]
O --> P["build_https_clone_url(host, owner_repo, token)"]
P --> Q["[EXEC] git ls-remote --tags --heads url.git\nGIT_TERMINAL_PROMPT=0, GIT_ASKPASS=echo"]
B --> R["_prefetch_metadata(resolved)"]
R --> S["_ensure_auth() (idempotent)"]
R --> T["ThreadPoolExecutor: _fetch_remote_metadata(pkg)"]
T --> U{host_info.kind?}
U -- generic --> V["return None (skip, no HTTP)"]
U -- ghe_cloud no token --> W["return None (skip, no HTTP)"]
U -- github.com --> X["[NET] urllib GET raw.githubusercontent.com/repo/sha/apm.yml\nAuthorization: token ..."]
U -- ghes/ghe_cloud with token --> Y["[NET] urllib GET api_base/repos/repo/contents/apm.yml?ref=sha\nAccept: application/vnd.github.raw\nAuthorization: token ..."]
X --> Z["yaml.safe_load(raw) -> dict"]
Y --> Z
3. Design patternsDesign patterns
One code smell (not blocking): CLI Logging Expert: No output path changes. The PR adds only DevX UX Expert: No CLI surface changes -- no new flags, no new commands, no help text changes. The fix is transparent for The docs addition in One note: the Supply Chain Security Expert: No new security surface introduced.
Auth Expert: Activated -- the PR changes token resolution from hardcoded Token resolution chain: Thread safety: Offline mode: Side-effect coupling (minor): ADO / non-GitHub hosts: No regressions to AuthResolver precedence, host classification, or credential leakage surface. OSS Growth Hacker: This fix closes a gap that blocked enterprise customers from using Story angle for release notes: "APM marketplace now works with GitHub Enterprise Server -- The CHANGELOG entries are clean raw material. The Side-channel to CEO: GHES parity across CEO arbitrationThe five specialists and the Auth Expert are in strong agreement: this is a correct, well-tested, well-documented bug fix. The lazy-init pattern with The Growth Hacker's framing is sound: GHES parity across Ratification: APPROVE. The change ships the right fix at the right scope. Required actions before merge
Optional follow-ups
|
Description
apm marketplace buildhardcodedgithub.comin four places, soGITHUB_HOSThad no effect on ref resolution, token lookup, or metadata fetch. This PR threads the existingdefault_host()/build_https_clone_url()/AuthResolverpattern (already used byapm install) through the marketplace build pipeline, and decouples auth from marketplace generation by reusing existing resolution infrastructure.Fixes #1008
Related: #1010 (ADO marketplace support -- not covered here; URL parsing accepts ADO forms but downstream resolution still uses
GITHUB_HOST)Changes
Phase A -- Bug fix (commit
11a9d27)ref_resolver.py--RefResolveraccepts an optionalhostparameter (defaults toGITHUB_HOSTorgithub.com). Bothlist_remote_refs()andresolve_ref_sha()usebuild_https_clone_url()instead of hardcodedgithub.com.builder.py--MarketplaceBuilderstores a normalised host andHostInfo:_resolve_github_token()resolves against the configured host, not"github.com"_fetch_remote_metadata()uses the GitHub REST API for GHES/GHE Cloud (sinceraw.githubusercontent.comis github.com-only), skips metadata for non-GitHub hosts, and short-circuits tokenless GHE Cloud requestsAuthResolverimport moved to top oftryblock to fix a scoping issue whenauth_resolveris pre-injectedPhase B -- Resolution decoupling (commit
239064d)ref_resolver.py--RefResolveraccepts an optionaltokenparameter. When set,git ls-remoteuses authenticated URLs (x-access-token), so private GHES repos work without separate git credential setup.builder.py-- Extracted lazy_ensure_auth()method with_auth_resolvedsentinel for true idempotency (including offline mode). Called from_get_resolver()so bothresolve()andbuild()benefit from authenticatedgit ls-remote. Resolver is eagerly initialised before the thread pool to prevent a race condition. Fixed_host_infotype annotation (Optional["HostInfo"]withTYPE_CHECKINGguard).resolver.py--_resolve_url_source()now delegates toDependencyReference.parse()instead of hardcodinggithub.comprefix matching. This reuses the existing resolution infrastructure (as suggested by @danielmeppiel) and gives marketplacetype: urlsources broader URL form acceptance. Note: the URL's host is not preserved -- downstream resolution uses the configuredGITHUB_HOST. True cross-host resolution is tracked in #1010.Review feedback (commits
51a1760,94fe220)Addressed findings from both the APM Review Panel and Copilot code review:
_auth_resolvedsentinel to_ensure_auth()for true idempotency (panel + Copilot)_resolve_url_source()docstring: host is not preserved (panel)GITHUB_HOSTdrives resolution (panel + Copilot)Tests and docs
Type of change
Testing