Skip to content

fix: add support for custom github endpoints (host.tld/owner/repo) as apm.yml entrypoint for command apm pack (marketplace)#1288

Open
abi-jey wants to merge 5 commits into
microsoft:mainfrom
abi-jey:main
Open

fix: add support for custom github endpoints (host.tld/owner/repo) as apm.yml entrypoint for command apm pack (marketplace)#1288
abi-jey wants to merge 5 commits into
microsoft:mainfrom
abi-jey:main

Conversation

@abi-jey
Copy link
Copy Markdown
Contributor

@abi-jey abi-jey commented May 12, 2026

Description

Currently apm pack (on apm yaml for marketplace block) does validate only github.com or local file. This PR extends the validation and package entries to support the items hosted in github enterprise for example with including the host (optional if exists).

Fixes # (issue)

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass
  • Added tests for new functionality (if applicable)

abi-jey and others added 5 commits May 11, 2026 21:07
Allow apm.yml package entries to specify a non-default git host inline
via the 'host.tld/owner/repo' source form, removing the need to set
GITHUB_HOST when building marketplaces against GitHub Enterprise or
other self-hosted git servers.

- yml_schema: extend SOURCE_RE to accept 3-segment host-prefixed form,
  add split_host_from_source() helper, add 'host' field on PackageEntry
  populated by the loader.
- builder: thread host through ResolvedPackage; per-host RefResolver
  cache (pre-warmed before worker dispatch to avoid races); emit full
  https://host/owner/repo URLs in the generated marketplace.json so
  Claude Code consumers can clone from the correct host. Non-subdir
  packages on a non-default host emit a 'url' source instead of the
  github shorthand which is hard-coded to github.com.
…s apm.yml

- test_yml_schema: add TestHostPrefixedSource covering local / default-host /
  host-prefixed source forms, including subdir + host combinations and
  rejection of malformed 4-segment / dotless 3-segment paths.
- test_builder: add emitter cases verifying github shorthand for default host,
  source:url for host-prefixed without subdir, git-subdir+url for host-prefixed
  with subdir, plus per-host RefResolver isolation. Adds _build_with_host_mock
  helper that stubs _get_resolver_for_host so non-default hosts resolve through
  the in-memory mock.
- publisher: route through load_marketplace_config so 'apm publish' works with
  the apm.yml form, not only the legacy marketplace.yml.
Copilot AI review requested due to automatic review settings May 12, 2026 07:13
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

This PR extends marketplace packaging (apm pack marketplace block) to accept host-prefixed repository sources (e.g. ghe.example.com/owner/repo) in addition to the existing owner/repo and local ./path forms, and threads the parsed host through resolution and marketplace.json emission.

Changes:

  • Broadened marketplace source validation and parsing to split host.tld/owner/repo into PackageEntry.host + owner/repo.
  • Added per-host RefResolver support in MarketplaceBuilder and emit full https://host/owner/repo URLs for non-default hosts in marketplace.json.
  • Added unit tests covering schema parsing and marketplace.json emission for host-prefixed sources.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/apm_cli/marketplace/yml_schema.py Accepts and parses host-prefixed source strings; adds PackageEntry.host.
src/apm_cli/marketplace/builder.py Introduces per-host resolver caching and emits URL-based marketplace sources when a host override is present.
src/apm_cli/marketplace/publisher.py Switches lazy-load to the unified marketplace config loader (apm.yml or legacy marketplace.yml).
tests/unit/marketplace/test_yml_schema.py Adds schema tests for host-prefixed source parsing/splitting rules.
tests/unit/marketplace/test_builder.py Adds build/output tests for host-prefixed sources and a helper to mock per-host resolver creation.

Comment on lines +56 to 58
from .migration import load_marketplace_config
from .yml_schema import load_marketplace_yml # noqa: F401 — kept for back-compat

Comment on lines 317 to 321
def _load_yml(self):
"""Lazy-load marketplace.yml."""
"""Lazy-load marketplace config (apm.yml or legacy marketplace.yml)."""
if self._yml is None:
yml_path = self._root / "marketplace.yml"
self._yml = load_marketplace_yml(yml_path)
self._yml = load_marketplace_config(self._root)
return self._yml
Comment on lines +71 to +77
# segment must look like a hostname, i.e. contain a dot)
# - ``./...`` (local path within the same repo)
# Used by both yml_schema and yml_editor for source field validation.
SOURCE_RE = re.compile(r"^(?:[^/\s]+\.[^/\s]+/[^/\s]+/[^/\s]+|[^/\s]+/[^/\s]+|\./.*)$")
LOCAL_SOURCE_RE = re.compile(r"^\./")
# Matches ``host.tld/owner/repo`` (3 segments, first is FQDN-ish).
_HOST_PREFIXED_SOURCE_RE = re.compile(r"^([^/\s]+\.[^/\s]+)/([^/\s]+/[^/\s]+)$")
Comment on lines +68 to +72
# Source field accepts:
# - ``owner/repo`` (remote, default host)
# - ``host.tld/owner/repo`` (remote on a non-default host -- the first
# segment must look like a hostname, i.e. contain a dot)
# - ``./...`` (local path within the same repo)
Comment on lines +299 to +307
# Reuse the resolved token only when the override host matches the
# default host class; otherwise leave token unset and rely on
# ambient git credentials (SSH key / git credential helper).
token = self._github_token if host == self._host else None
resolver = RefResolver(
timeout_seconds=self._options.timeout_seconds,
offline=self._options.offline,
host=host,
token=token,
Comment on lines +908 to 912
if pkg.host:
source_obj["url"] = f"https://{pkg.host}/{pkg.source_repo}"
else:
source_obj["url"] = pkg.source_repo
source_obj["path"] = pkg.subdir
Comment on lines +1039 to +1040
def test_per_host_resolvers_isolated(self, tmp_path: Path) -> None:
"""Two entries on different hosts each get their own resolver instance."""
Comment on lines +231 to +233
# Per-host RefResolver cache for entries that override the host via
# the ``host.tld/owner/repo`` source form in apm.yml.
self._host_resolvers: dict[str, RefResolver] = {}
@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label May 12, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel: needs_rework

PR #1288 unlocks GitHub Enterprise and self-hosted GitLab as first-class package sources in apm.yml -- a meaningful enterprise addressable-market expansion that ships with one concrete security regression and one AuthResolver bypass that must be resolved before the feature is safe to expose.

cc @abi-jey -- a fresh advisory pass is ready for your review.

The architectural foundation of this PR is sound: schema-layer parsing, per-host resolver caching with pre-warm, and frozen-dataclass host field placement are all well-executed. Python-architect's praise is warranted and I weight it as the structural baseline. The two areas requiring rework are not architectural -- they are sharp edges on the security perimeter and auth contract that, if shipped as-is, would create a worse situation than not shipping the feature at all.

The supply-chain-security blocking finding is the highest-priority signal in this panel: the _HOST_PREFIXED_SOURCE_RE host segment permits @, which means evil.com@github.com/owner/repo passes validation and propagates into composed marketplace JSON as (evil.com/redacted)@github.com/owner/repo. RFC 3986 userinfo injection via an ambient credential helper is a real confused-deputy attack surface on a feature specifically designed to handle non-default hosts with non-default credentials. This must be fixed before ship. The auth-expert's finding is architecturally coupled: _get_resolver_for_host never calls AuthResolver.resolve(host) for non-default hosts, meaning GITHUB_APM_PAT, GITHUB_APM_PAT_{ORG}, and GH_TOKEN are all silently ignored for any host that is not self._host. The dead ternary on line 302 (flagged independently by python-architect, supply-chain, and auth-expert) is the symptom; the AuthResolver bypass is the root cause. These two issues -- regex hardening and AuthResolver wiring -- are the minimum bar for ship.

The docs-and-discoverability cluster (CHANGELOG, manifest-schema.md, error message hint for the new 3-segment syntax) is a fast-follow requirement, not a rework blocker, but the growth-hacker is correct that an enterprise unlock that ships with no CHANGELOG entry effectively does not exist for the enterprise evaluator segment. The CHANGELOG entry and manifest-schema.md update should be bundled into the same commit that fixes the security issues.

Dissent. Severity grading on the dead ternary at builder.py line 302 differs: python-architect marks it blocking on architectural-clarity grounds; supply-chain marks it nit; auth-expert marks it recommended because the more important issue is the AuthResolver bypass the dead code masks. I side with auth-expert's framing: the dead ternary is a symptom, not the defect. Fixing the AuthResolver bypass will necessarily eliminate the dead branch.

Aligned with: portable-by-manifest: host intent now encoded in the manifest, advancing portability across GitHub.com, GHE, and self-hosted GitLab. multi-harness-multi-host: first-class concept in the resolver layer. pragmatic-as-npm: natural shorthand syntax familiar to developers. secure-by-default: currently regresses pre-ship due to @ injection and AuthResolver bypass -- both must be corrected. oss-community-driven: at risk without CHANGELOG and schema reference updates.

Growth signal. GHE and self-hosted GitLab support removes the single most-cited blocker for enterprise platform engineering teams evaluating APM. The pitch -- "APM now works wherever your code lives" -- is the correct headline for the CHANGELOG entry and any release note. Platform eng teams who hit this path are high-propensity contributors: they file precise issues, send targeted PRs, and evangelize internally. Capturing them with clear docs at first contact (manifest-schema.md example, CHANGELOG bullet, error message hint) is worth more than any feature in the backlog. Do not let this ship quietly.

Panel summary

Persona B R N Takeaway
Python Architect 1 1 2 Sound layered design; dead token ternary is the primary structural defect; missing threading.Lock is the only fragility.
CLI Logging Expert 0 3 0 Three logging/UX gaps: publisher.py drops DEPRECATION_MESSAGE silently; no verbose trace for per-host resolver creation; no user-facing signal for ambient-credential fallback.
DevX UX Expert 0 4 1 Feature ships undiscoverable (no docs, no help text, error message hides the syntax); silent auth fallback on credential failure; source:url output shape undocumented for consumers.
Supply Chain Security Expert 1 3 1 @ userinfo injection in host regex is a confirmed confused-deputy attack surface; port injection unblocked; no host allowlist for arbitrary external connections.
OSS Growth Hacker 0 2 1 Strong enterprise unlock; ships invisible without CHANGELOG entry and manifest-schema.md example.
Auth Expert 0 2 2 Dead ternary masks AuthResolver bypass for non-default hosts; GITHUB_APM_PAT and per-org env vars silently ignored; no diagnostic on ambient-credential fallback.
Doc Writer 0 3 1 Zero documentation updated; manifest-schema.md source field description and section 7.6 example do not reflect the new form; CHANGELOG has no entry.
Test Coverage Expert 0 2 0 Token isolation for non-default hosts and pre-warm race-prevention loop are both untested; all other critical surfaces have unit coverage.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [Supply Chain Security Expert] (blocking-severity) Harden _HOST_PREFIXED_SOURCE_RE host segment to valid hostname chars only -- block @, :, ?, # from the host segment to prevent RFC 3986 userinfo injection -- Confirmed injection attack surface: evil.com@github.com/owner/repo passes current regex and propagates into composed marketplace JSON as a credential-confusion URL.
  2. [Auth Expert] (blocking-severity) Wire non-default host resolvers through AuthResolver.resolve(host) so GITHUB_APM_PAT, GITHUB_APM_PAT_{ORG}, and GH_TOKEN are consulted before falling back to ambient credentials -- Current implementation silently ignores all APM-managed tokens for non-default hosts; this fix also eliminates the dead ternary on line 302 flagged by three panelists.
  3. [Test Coverage Expert] (blocking-severity) Add test asserting token=None isolation for non-default host resolvers (test_get_resolver_for_host_non_default_host_gets_no_token) -- All existing host-prefixed tests patch _get_resolver_for_host away; the token isolation guarantee has no automated verification on the secure-by-default surface.
  4. [Doc Writer + OSS Growth Hacker] Add CHANGELOG [Unreleased] entry and update manifest-schema.md source field description to include the host.tld/owner/repo form with a commented GHE example -- An enterprise unlock with no CHANGELOG entry and no schema reference effectively does not ship for the enterprise evaluator audience. Two file edits, high impact.
  5. [DevX UX Expert] Update validation error message to hint at all three accepted source forms: <owner>/<repo>, <host.tld>/<owner>/<repo>, or ./<path> -- Current error message omits the new syntax entirely; enterprise users who mistype a host-prefixed source see a message that makes the feature appear unsupported.

Architecture

classDiagram
    direction LR

    class PackageEntry {
        <<ValueObject>>
        +name str
        +source str
        +subdir str|None
        +version str|None
        +ref str|None
        +is_local bool
        +host str|None
    }

    class MarketplaceConfig {
        <<ValueObject>>
        +packages tuple~PackageEntry~
        +name str
        +output str
    }

    class ResolvedPackage {
        <<ValueObject>>
        +name str
        +source_repo str
        +subdir str|None
        +ref str
        +sha str
        +host str|None
    }

    class MarketplaceBuilder {
        <<Orchestrator>>
        -_host str
        -_github_token str|None
        -_resolver RefResolver
        -_host_resolvers dict
        -_auth_resolved bool
        +resolve() ResolveResult
        +build() BuildReport
        +compose_marketplace_json() dict
        -_get_resolver() RefResolver
        -_get_resolver_for_host(host) RefResolver
        -_ensure_auth() None
        -_resolve_entry(entry) ResolvedPackage
    }

    class RefResolver {
        <<Service>>
        +host str
        +token str|None
        +ls_remote(repo) list
    }

    class split_host_from_source {
        <<Pure>>
        +split_host_from_source(source) tuple
    }

    note for MarketplaceBuilder "Cache pattern: _host_resolvers\npre-warmed on main thread before\nThreadPoolExecutor spawns workers"
    note for PackageEntry "host is derived by loader\nfrom host.tld/owner/repo form"

    MarketplaceBuilder *-- RefResolver : default resolver
    MarketplaceBuilder *-- RefResolver : per-host cache (_host_resolvers)
    MarketplaceBuilder ..> PackageEntry : reads
    MarketplaceBuilder ..> ResolvedPackage : produces
    MarketplaceConfig *-- PackageEntry : contains
    MarketplaceBuilder ..> MarketplaceConfig : loads

    class PackageEntry:::touched
    class ResolvedPackage:::touched
    class MarketplaceBuilder:::touched
    class split_host_from_source:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A(["apm.yml / marketplace.yml"]) -->|"yaml.safe_load"| B["_parse_package_entry\nyml_schema.py"]
    B -->|"source field present"| C{"LOCAL_SOURCE_RE match?"}
    C -->|"yes: ./path"| D["is_local=True, host=None"]
    C -->|"no: remote"| E["split_host_from_source()\n_HOST_PREFIXED_SOURCE_RE"]
    E -->|"3-segment host.tld/owner/repo"| F["PackageEntry\nhost='host.tld'\nsource='owner/repo'"]
    E -->|"2-segment owner/repo"| G["PackageEntry\nhost=None\nsource='owner/repo'"]
    D --> H["MarketplaceConfig.packages"]
    F --> H
    G --> H

    H --> I["MarketplaceBuilder.resolve()\nbuilder.py:~530"]
    I -->|"main thread"| J["_ensure_auth()\n_resolve_github_token()"]
    J -->|"AuthResolver.resolve()"| K["self._github_token set"]
    K --> L["pre-warm loop\nfor entry.host in entries"]
    L -->|"entry.host not None"| M["_get_resolver_for_host(host)\nbuilder.py:287"]
    M -->|"host==self._host or None"| N["_get_resolver() default"]
    M -->|"cache miss, host!=self._host"| O["token=None [dead ternary]\nnew RefResolver(host=host, token=None)\n[no lock, no AuthResolver]"]
    O --> P["_host_resolvers[host] = resolver"]
    L -->|"all resolvers cached"| Q["ThreadPoolExecutor\nworkers"]
    Q -->|"parallel"| R["_resolve_entry(entry)\nbuilder.py:342"]
    R -->|"is_local"| S["ResolvedPackage(source_repo='')"]
    R -->|"remote"| T["_get_resolver_for_host(entry.host)"]
    T -->|"cache hit"| U["resolver.ls_remote(owner_repo)"]
    U --> V["ResolvedPackage(host=entry.host)"]
    V --> W["compose_marketplace_json()"]
    W -->|"pkg.host not None, pkg.subdir"| X["source=git-subdir\nurl=(host/redacted)
    W -->|"pkg.host not None, no subdir"| Y["source=url\nurl=(host/redacted)
    W -->|"pkg.host None"| Z["source=github\nrepo=owner/repo"]
    X --> AA(["marketplace.json"])
    Y --> AA
    Z --> AA
Loading

Recommendation

The feature design is worth shipping -- multi-host support is a genuine APM differentiator for enterprise shops and the architectural layering is clean. But two issues must be resolved before this is safe to expose: the @ userinfo injection in the host regex (a concrete confused-deputy attack surface) and the AuthResolver bypass for non-default hosts (which silently drops all APM-managed tokens for the exact use case this feature targets). These are not large changes -- a tightened regex pattern and a call to AuthResolver.resolve(host) -- but they are the minimum bar for a security-property-introducing feature. Bundle the CHANGELOG entry and manifest-schema.md update into the same revision; the combined changeset then tells a complete and auditable story. Once those four items land, this PR is ship-ready and the growth signal is strong.


Full per-persona findings

Python Architect

  • [blocking] Token condition on line 302 is dead code -- token is always None for non-default hosts at src/apm_cli/marketplace/builder.py:302
    The guard at line 293 already returns early when host == self._host, so at line 302 host == self._host is always False. Currently safe-by-accident but signals incorrect intent and could cause a regression if the early return is relaxed.
    Suggested: Replace token = self._github_token if host == self._host else None with token = None # override hosts rely on ambient git credentials; GITHUB_TOKEN is scoped to self._host only

  • [recommended] _host_resolvers dict has no lock -- pre-warm invariant is the only thread-safety guarantee at src/apm_cli/marketplace/builder.py:295
    _get_resolver_for_host writes to _host_resolvers without a lock. Safe now due to pre-warm but fragile for future refactors.
    Suggested: Add self._host_resolvers_lock = threading.Lock() in __init__ and wrap the check-then-set pattern.

  • [nit] host field placement in PackageEntry is slightly inconsistent with is_local grouping at src/apm_cli/marketplace/yml_schema.py:246
    Both are derived fields; grouping under one comment block would be cleaner.

  • [nit] Design pattern note: Lazy Cache Factory is appropriate; no architecture changes needed at this scope.

CLI Logging Expert

  • [recommended] publisher.py drops the deprecation warning into stdlib logging instead of surfacing via _rich_warning at src/apm_cli/marketplace/publisher.py
    load_marketplace_config accepts a warn_callback for routing through _rich_warning. publisher.py passes none, so the deprecation message is invisible in CLI context.
    Suggested: Pass warn_callback=lambda msg: _rich_warning(f'[!] {msg}') to load_marketplace_config.

  • [recommended] No verbose-mode trace when a per-host resolver is created for a non-default host at src/apm_cli/marketplace/builder.py
    _get_resolver_for_host is completely silent. For --verbose users debugging resolution failures against a non-default host, there is no signal.
    Suggested: Emit logger.debug('Creating resolver for host %s (token=%s)', host, 'set' if token else 'unset') after creating the resolver.

  • [recommended] Silent auth fallback to ambient credentials for non-default hosts gives no user-facing signal at src/apm_cli/marketplace/builder.py
    When token is None, the resulting git error arrives with no prior context naming the host or the expected credential mechanism.
    Suggested: Emit logger.debug or _rich_info('[i] No token for host %s -- using ambient git credentials', host) when token=None.

DevX UX Expert

  • [recommended] Validation error message does not mention the new 3-segment syntax at src/apm_cli/marketplace/yml_schema.py
    When source: myhost/org/repo (no dot) fails, the error lists only <owner>/<repo> or ./<path> -- never hints at host.tld/owner/repo.
    Suggested: Update error to: must match '<owner>/<repo>', '<host.tld>/<owner>/<repo>', or './<path>' shape

  • [recommended] No user-visible signal when falling back to ambient credentials for non-default host at src/apm_cli/marketplace/builder.py
    Auth failures for non-default hosts produce an opaque git error with no APM-level context.

  • [recommended] New source: url output shape has no documented schema contract for consumers at src/apm_cli/marketplace/builder.py
    Consumers (Claude Code, CI) that pattern-match on source == "github" will silently skip non-default-host packages. No changelog entry or version bump.

  • [recommended] Feature is invisible in all user-facing docs and help text
    No entry in apm.yml reference, quick-start, or packages/apm-guide/.apm/skills/apm-usage/commands.md mentions host.tld/owner/repo.

  • [nit] git-subdir URL asymmetry between default and non-default host is undocumented in code comments at src/apm_cli/marketplace/builder.py

Supply Chain Security Expert

  • [blocking] @ userinfo injection in host segment passes regex, producing credential-confusion URLs in marketplace JSON at src/apm_cli/marketplace/yml_schema.py:77
    _HOST_PREFIXED_SOURCE_RE uses [^/\s]+ which permits @. Confirmed: evil.com@github.com/owner/repo matches, sets host='evil.com@github.com', and compose emits url: (evil.com/redacted)@github.com/owner/repo. RFC 3986 userinfo: git and HTTP clients interpret evil.com as username credential for github.com -- a confused-deputy credential leak.
    Suggested: Restrict host segment to valid hostname chars: [a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+ -- blocks @, :, ?, # from the host segment.

  • [recommended] Port injection in host is unblocked: evil.com:8080/owner/repo passes validation at src/apm_cli/marketplace/yml_schema.py:77
    host='evil.com:8080' produces `url: (evil.com/redacted) APM opens network connections to non-standard ports on arbitrary hosts.

  • [recommended] Query strings and fragments in source_repo segment are unblocked at src/apm_cli/marketplace/yml_schema.py:74
    [^/\s]+ in the repo segment permits ?, #, @. These propagate into git URLs.
    Suggested: Restrict owner/repo to [A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+

  • [recommended] No host allowlist: any FQDN-shaped string causes APM to open network connections to arbitrary external hosts at src/apm_cli/marketplace/builder.py:287
    A compromised apm.yml can redirect resolution to any host.
    Suggested: Add optional allowed_hosts to MarketplaceOptions. Default to deny in enterprise mode.

  • [nit] Dead code at builder.py line 302: token = self._github_token if host == self._host is always False at that point at src/apm_cli/marketplace/builder.py:302

OSS Growth Hacker

  • [recommended] No CHANGELOG entry -- enterprise unlock ships invisible
    GHE/self-hosted support is exactly what enterprise evaluators search the changelog for.
    Suggested: Add under [Unreleased] ### Added: "Marketplace packages can now be sourced from GitHub Enterprise and self-hosted Git servers using the host.tld/owner/repo shorthand in apm.yml -- no extra flags required."

  • [recommended] Marketplace authoring docs missing a host-prefixed source example
    manifest-schema.md packages examples only show owner/repo shorthand. Enterprise users scan examples first.
    Suggested: Add commented example: source: ghe.corp.example.com/org/agents # GitHub Enterprise or self-hosted GitLab

  • [nit] The dot-in-first-segment host detection heuristic is implicit -- error path is opaque for orgs with dots in their names.

Auth Expert

  • [recommended] Dead ternary on line 302: token is always None for non-default hosts -- the host-class check is not implemented at src/apm_cli/marketplace/builder.py:302
    Comment implies host-class comparison (GitHub-flavored vs. GitLab) but code does strict hostname equality. A GHE user with a compatible token on a different hostname gets no token.
    Suggested: Use is_github_hostname(host) and is_github_hostname(self._host) for the token-sharing decision.

  • [recommended] Non-default host resolvers bypass AuthResolver entirely -- GITHUB_APM_PAT and per-org env vars silently ignored at src/apm_cli/marketplace/builder.py:303
    _get_resolver_for_host never calls AuthResolver.resolve(host). Enterprise user with token for secondary GHE instance silently falls back to ambient credentials.
    Suggested: Call AuthResolver().resolve(host).token for non-default hosts; fall back to None only if unresolved. Emit [!] warning when no token found.

  • [nit] Comment on lines 299-301 is misleading -- describes host-class matching but implements hostname equality at src/apm_cli/marketplace/builder.py:299

  • [nit] Pre-warm loop calls _ensure_auth() redundantly -- already called at resolve() entry at src/apm_cli/marketplace/builder.py:550

Doc Writer

  • [recommended] manifest-schema.md source field description does not mention host-prefixed form at docs/src/content/docs/reference/manifest-schema.md
    Line 609 documents source as accepting only (owner)/(repo) or ./(path). The new form is valid but invisible.
    Suggested: Update line 609 to list all three accepted forms; add commented host-prefixed entry in section 7.6 example.

  • [recommended] Credential behavior for host-prefixed source is undocumented at docs/src/content/docs/reference/manifest-schema.md
    Non-default hosts resolve via ambient git credentials, not the GitHub token. Critical operational detail for enterprise users.
    Suggested: Add short note under section 7.5 or callout about ambient credential requirement; cross-reference authentication docs.

  • [recommended] CHANGELOG [Unreleased] has no entry for this feature at CHANGELOG.md
    Feature is user-facing and belongs in the Added section.
    Suggested: Add: "marketplace.packages[].source now accepts host.tld/owner/repo for non-default git hosts (fix: add support for custom github endpoints (host.tld/owner/repo) as apm.yml entrypoint for command apm pack (marketplace) #1288)"

  • [nit] Section 7.6 complete example contains only github.com-style sources at docs/src/content/docs/reference/manifest-schema.md

Test Coverage Expert

  • [recommended] No test exercises _get_resolver_for_host with a real non-default host to assert token=None on the resulting RefResolver at tests/unit/marketplace/test_builder.py
    All host-prefixed tests patch _get_resolver_for_host away entirely. The token isolation guarantee has no automated verification.
    Proof (missing): tests/unit/marketplace/test_builder.py::test_get_resolver_for_host_non_default_host_gets_no_token -- proves: A token resolved for the default GitHub host is never forwarded to a non-default GHE host [secure-by-default]
    resolver = builder._get_resolver_for_host('ghe.example.com'); assert resolver._token is None

  • [recommended] Pre-warm loop in resolve() is untested -- race prevention guarantee is unverified at tests/unit/marketplace/test_builder.py
    The loop pre-populates _host_resolvers before ThreadPoolExecutor starts. No test verifies it runs before any future is submitted.
    Proof (missing): tests/unit/marketplace/test_builder.py::test_resolve_prewarms_per_host_resolvers_before_worker_spawn -- proves: Per-host resolvers are created on the main thread before workers start [devx]
    with patch.object(builder, '_get_resolver_for_host', wraps=...) as spy: builder.resolve(...); assert spy.call_count >= unique_host_count

This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #1288 · ● 4.8M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label May 12, 2026
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.

3 participants