Skip to content

fix: marketplace install auth host on *.ghe.com (closes #1285)#1292

Open
edenfunf wants to merge 2 commits into
microsoft:mainfrom
edenfunf:fix/marketplace-ghe-host-prefix-1285
Open

fix: marketplace install auth host on *.ghe.com (closes #1285)#1292
edenfunf wants to merge 2 commits into
microsoft:mainfrom
edenfunf:fix/marketplace-ghe-host-prefix-1285

Conversation

@edenfunf
Copy link
Copy Markdown
Contributor

TL;DR

For marketplaces registered on *.ghe.com (GHE Cloud) hosts, plugin install resolved auth against github.com instead of the registered enterprise host. This change backfills the marketplace host on the resolver's canonical string so DependencyReference.parse recovers the correct host downstream. Closes #1285.

Problem

apm install <plugin>@<marketplace> against a marketplace registered with --host corp.ghe.com failed with an authentication error. The verbose trace showed:

Resolving my-plugin@my-marketplace via marketplace...
Resolved to: myorg/my-marketplace/plugins/my-plugin        <-- missing corp.ghe.com/ prefix
Auth resolved: host=github.com, org=myorg, ...             <-- wrong host
[i] plugins/my-plugin/apm.yml@main (Authentication failed for myorg/my-marketplace ...)

DependencyReference.parse defaults missing hosts to github.com, so every *.ghe.com marketplace mis-routes auth. The same plugin installed via fully-qualified path worked correctly:

apm install corp.ghe.com/myorg/my-marketplace/plugins/my-plugin

i.e. the rest of the install pipeline already handles GHE Cloud correctly; only the marketplace → canonical step dropped the host.

A second, quieter symptom: the bare canonical also produced an apm.yml entry with git: https://github.com/... even though the marketplace was on corp.ghe.com, poisoning the lockfile with the wrong origin.

Root cause

resolve_marketplace_plugin uses _marketplace_host_needs_explicit_git_path(source.host) to decide whether to build a structured DependencyReference (explicit git: URL + path:). That helper returns False for any GitHub-family host (github.com + *.ghe.com) because shorthand owner/repo[/path] is unambiguous on those hosts -- no GitLab-style nested-group ambiguity.

The flaw: that single condition conflated two orthogonal concerns.

  • Clone-path semantics: do we need explicit git: + path: to disambiguate the clone target? No for GitHub family. ✓
  • Auth-host semantics: does the canonical carry enough info for DependencyReference.parse to recover the correct host? No for *.ghe.com; yes for github.com because it is the documented parse default.

Both concerns were gated on the same return value, so *.ghe.com correctly skipped the structured-ref path but incorrectly also dropped the host hint.

Approach

Backfill the host onto the canonical post-hoc, scoped narrowly. The structured-ref path for GitLab-family hosts is left untouched -- this only fills the gap for the GitHub-family enterprise branch.

A new pure helper _needs_canonical_host_prefix(canonical, host) answers a single question; the call site in resolve_marketplace_plugin adds five lines after the existing structured-ref block:

if (
    dep_ref is None
    and _is_in_marketplace_source(plugin, source)
    and _needs_canonical_host_prefix(canonical, source.host)
):
    canonical = f"{source.host}/{canonical}"

Three guards justify the scope:

  1. dep_ref is None -- the structured-ref path (GitLab family, self-managed FQDN) already carries host via to_canonical(). Don't double-handle.
  2. _is_in_marketplace_source(plugin, source) -- only sources whose host is unambiguously the registered marketplace host get backfilled. Cross-repo dict sources without explicit host qualification are deliberately untouched; treating them as on-host would silently change routing for existing manifests.
  3. _needs_canonical_host_prefix(canonical, host) -- narrows to GitHub-family enterprise hosts (is_github_hostname(host) and host != "github.com") and is idempotent (case-insensitive) against dict sources that already carry a host-qualified repo.

dependency_reference remains None for GitHub-family hosts -- the clone-path shorthand semantics are preserved, only the auth-routing string is corrected.

Tests

New class TestResolveMarketplacePluginGHECloud in tests/unit/marketplace/test_marketplace_resolver.py (7 cases). Each test locks one branch of the helper or one invariant the fix must preserve:

Test Locks
test_relative_source_carries_host_in_canonical Primary bug case; parse round-trip recovers corp.ghe.com
test_dict_github_source_bare_repo_carries_host Dict-source variant of the same in-marketplace fix
test_dict_github_source_host_qualified_repo_not_double_prefixed Idempotent guard against double-prefix
test_dict_github_source_mixed_case_host_qualified_not_double_prefixed Idempotent guard is case-insensitive
test_cross_repo_source_not_prefixed Cross-repo references explicitly out of scope
test_version_spec_override_preserves_host_prefix Ref override stacks on top of host-prefixed canonical
test_github_com_canonical_remains_bare Regression: github.com path unchanged

Validation

End-to-end equivalence check against real AuthResolver (mock marketplace fetch only):

Pre-fix bare canonical This PR Workaround FQDN install
canonical myorg/my-marketplace/plugins/my-plugin corp.ghe.com/myorg/my-marketplace/plugins/my-plugin corp.ghe.com/myorg/my-marketplace/plugins/my-plugin
AuthResolver target host github.com corp.ghe.com corp.ghe.com
HostInfo.kind github ghe_cloud ghe_cloud
clone URL https://github.com/myorg/my-marketplace https://corp.ghe.com/myorg/my-marketplace https://corp.ghe.com/myorg/my-marketplace
apm.yml entry git https://github.com/... https://corp.ghe.com/... https://corp.ghe.com/...

After this change the marketplace path produces byte-identical resolver / auth / lockfile output to the documented workaround (apm install corp.ghe.com/owner/repo/path).

How to test

uv run pytest tests/unit/marketplace/test_marketplace_resolver.py -x

For a manual reproduction:

  1. Register a marketplace on a *.ghe.com host: apm marketplace add --host corp.ghe.com myorg/my-marketplace
  2. Run apm install my-plugin@my-marketplace --verbose
  3. Before this change: Resolved to: myorg/my-marketplace/plugins/my-plugin and Auth resolved: host=github.com (auth fails).
  4. After this change: Resolved to: corp.ghe.com/myorg/my-marketplace/plugins/my-plugin and Auth resolved: host=corp.ghe.com (auth succeeds).

Marketplaces registered on `*.ghe.com` (GHE Cloud) resolved plugin
install auth against `github.com` instead of the registered enterprise
host. The marketplace resolver emitted a canonical without the host
prefix (`owner/repo/path`); `DependencyReference.parse` defaults missing
hosts to `github.com`, mis-routing every downstream auth lookup -- and
poisoning the resulting `apm.yml` entry with the wrong `git:` URL.

`_marketplace_host_needs_explicit_git_path` conflated two orthogonal
concerns. GitHub-family hosts (github.com + *.ghe.com) correctly skip
the GitLab-style structured-ref path because they have no nested-group
ambiguity -- but the same gate also dropped the host hint needed by
`DependencyReference.parse` to recover the correct enterprise host.
`github.com` survived because parse defaults to it; `*.ghe.com` broke.

Backfill the host onto the canonical for in-marketplace sources only,
scoped to GitHub-family enterprise hosts. Idempotent against dict
sources that already carry a host-qualified `repo`. Cross-repo sources
without explicit host qualification stay out of scope -- they are a
separate manifest-author concern and inferring host there would change
existing semantics.

Round-trip stable through `DependencyReference.parse` /
`to_canonical()`, so `apm.yml` lockfile entries now record the
correct enterprise `git:` URL instead of the (silently wrong)
`https://github.com/...` URL produced before the fix.
Copilot AI review requested due to automatic review settings May 12, 2026 13:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes marketplace plugin installs for GHE Cloud (*.ghe.com) hosts by ensuring the resolved canonical reference carries the enterprise host prefix, so downstream DependencyReference.parse() (and auth routing) does not default to github.com (closes #1285).

Changes:

  • Add _needs_canonical_host_prefix() and a scoped host-prefix backfill in resolve_marketplace_plugin() for GitHub-family enterprise hosts.
  • Add a focused unit test suite covering GHE Cloud canonical host prefixing, idempotency, cross-repo non-prefixing, and version ref override behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/apm_cli/marketplace/resolver.py Adds helper + conditional canonical host-prefix backfill for *.ghe.com marketplace resolutions.
tests/unit/marketplace/test_marketplace_resolver.py Adds unit tests validating correct canonical host behavior for GHE Cloud marketplaces and key regressions.

Comment thread src/apm_cli/marketplace/resolver.py
Manifests that put a full URL or SSH SCP shorthand in a dict source's
`repo` field reach `_needs_canonical_host_prefix` via
`_resolve_github_source` (which validates only that `/` is present) and
`_is_in_marketplace_source` (whose normalizer accepts URL/SSH paths).
Before this guard the prefix step produced malformed canonicals like
`corp.ghe.com/https://corp.ghe.com/...` that `DependencyReference.parse`
rejects with `ValueError` -- a regression versus the pre-fix tolerance
where parse recovered host from the URL form natively.

Detect URL/SSH form by `:` in the first slash-split segment of the
canonical (both `https:` and `git@host:owner` carry a colon; bare owner
names and bare host shorthand do not) and return False, leaving the
canonical untouched. Downstream parse already handles both URL and SSH
forms natively for routing, so no host backfill is needed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host

2 participants