Skip to content

fix(marketplace): enhance repository input parsing for GitLab subgroups and HTTPS URLs#1034

Open
Antonin-Rouxel-LaPoste-BGPN wants to merge 5 commits intomicrosoft:mainfrom
anrouxel:fix/marketplace-url
Open

fix(marketplace): enhance repository input parsing for GitLab subgroups and HTTPS URLs#1034
Antonin-Rouxel-LaPoste-BGPN wants to merge 5 commits intomicrosoft:mainfrom
anrouxel:fix/marketplace-url

Conversation

@Antonin-Rouxel-LaPoste-BGPN
Copy link
Copy Markdown

Note

Closes #1027

TL;DR

apm marketplace add was limited to exactly 2 or 3 path segments, making it impossible to register a GitLab marketplace hosted under subgroups (e.g. mycompany/myorg/specs-and-standards/repo). This PR replaces the rigid segment counter with a unified parser that accepts any N-segment shorthand path and full HTTPS URLs.


Problem

Why the old parser failed

The previous implementation split the input on / and checked len(parts) == 2 or len(parts) == 3. Any deeper path — which GitLab subgroups require — was rejected with "Expected 'OWNER/REPO'".

$ apm marketplace add gitlab.com/mycompany/myorg/specs-and-standards/internal-marketplace
[x] Invalid format: '...'. Expected 'OWNER/REPO'

Two separate root causes were reported in #1027:

  1. Parser rejects subgroup paths — only OWNER/REPO (2 parts) or HOST/OWNER/REPO (3 parts) were accepted.
  2. Full HTTPS URLs were not supported — users pasting a browser URL received the same rejection error.

Important

The MarketplaceSource model already stores owner as a plain string with no segment-count constraint, so the fix is entirely in the parser; no schema migration is required.


Approach

Input form Before After
OWNER/REPO
HOST/OWNER/REPO
HOST/group/sub/.../REPO
OWNER/group/sub/.../REPO
https://host/group/sub/.../repo[.git]

The rule is: everything except the last segment is owner; the last segment is repo. This mirrors how dependency/reference.py already handles generic hosts for apm add (see _resolve_shorthand_to_parsed_url), giving both surfaces consistent behaviour.


Implementation

Files changed

src/apm_cli/commands/marketplace.pyadd command parser

Replaced the 2/3-segment if/elif/else block with a three-branch strategy:

HTTPS URL  →  urlparse → strip .git → split path → owner = all-but-last, repo = last
Shorthand with FQDN first segment  →  host = parts[0], owner = parts[1..-2], repo = last
Shorthand without host  →  owner = parts[0..-2], repo = last

Path-traversal sequences (.., .) in the parsed owner and repo_name are validated through the existing validate_path_segments guard (required by the path-security rules in copilot-instructions.md). Conflicting --host flags are still caught in all branches.

src/apm_cli/marketplace/errors.pyMarketplaceNotFoundError

Updated the user-facing hint to mention the HTTPS URL form.

tests/unit/marketplace/test_marketplace_commands.py

Added 6 new test cases to TestMarketplaceAdd:

  • GitLab subgroup shorthand (HOST/group/sub/.../repo)
  • Full HTTPS URL
  • .git suffix stripping
  • Conflicting --host flag with HTTPS URL
  • Single-segment URL path (rejected)
  • Path traversal injection (rejected)

Flow diagram

The diagram below shows the updated parse strategy inside the add command.

flowchart TD
    A(["apm marketplace add INPUT"]) --> B{"starts with https://?"}
    B -- yes --> C["urlparse / strip .git"]
    C --> D{"path segments >= 2?"}
    D -- no --> ERR1(["error: need OWNER/REPO"])
    D -- yes --> E["owner = all-but-last\nrepo = last segment"]
    B -- no --> F["split on / filter empty"]
    F --> G{"segments >= 2?"}
    G -- no --> ERR2(["error: need OWNER/REPO"])
    G -- yes --> H{"first segment is valid FQDN?"}
    H -- yes --> I{"segments >= 3?"}
    I -- no --> ERR3(["error: HOST/OWNER/REPO required"])
    I -- yes --> J["host = first\nowner = middle segments\nrepo = last"]
    H -- no --> K["owner = all-but-last\nrepo = last segment"]
    E & J & K --> L["validate_path_segments\nowner + repo_name"]
    L --> M["resolve --host flag\nor default_host"]
    M --> N(["_auto_detect_path + register"])
Loading

Trade-offs

  • owner is a multi-segment stringMarketplaceSource.owner may now be "mycompany/myorg/specs-and-standards". The model already stored it as a plain string and the _github_contents_url builder inlines it directly, so the API URL is assembled correctly. No other callers were found to assume a single-segment owner.
  • No GitLab API support yet — fetching marketplace contents still uses the GitHub Contents API path. GitLab-hosted marketplaces will fail at fetch time (after parsing succeeds). This is a separate issue; the parser fix unblocks the manual workaround documented in [FEATURE] Add a marketplace with just a full repository URL #1027 and makes the MarketplaceSource storable via apm marketplace add.
  • http:// accepted alongside https:// — kept for parity with dependency/reference.py; production usage is expected to be HTTPS-only.

Validation

21/21 unit tests pass
$ uv run pytest tests/unit/marketplace/test_marketplace_commands.py -q
.....................                                                    [100%]
21 passed in 0.82s
Full unit suite — 6656 tests, 0 failures
$ uv run pytest tests/unit tests/test_console.py -x -q 2>&1 | tail -5
6656 passed, 1 warning, 26 subtests passed in 104.45s (0:01:44)
Live invocation with a 4-segment subgroup path
$ uv run apm marketplace add solutions-distributeurs/yz_-alf_framework/sandbox/github-copilot-agents \
    --host gitlab.udd.net.intra.laposte.fr --verbose
[*] Registering marketplace 'github-copilot-agents'...
    Repository: solutions-distributeurs/yz_-alf_framework/sandbox/github-copilot-agents
    Branch: main
    Host: gitlab.udd.net.intra.laposte.fr

Parser correctly sets owner = "solutions-distributeurs/yz_-alf_framework/sandbox", repo = "github-copilot-agents", host = "gitlab.udd.net.intra.laposte.fr". (Fetch aborted manually — no live GitLab API support yet.)


How to test

  • uv run pytest tests/unit/marketplace/test_marketplace_commands.py -v — all 21 tests green
  • uv run apm marketplace add gitlab.com/myorg/subgroup/my-marketplace --name my-mkt — parses cleanly, fails at fetch (expected without a live GitLab API)
  • uv run apm marketplace add https://gitlab.com/myorg/subgroup/my-marketplace.git --name my-mkt — same result, .git stripped
  • uv run apm marketplace add gitlab.com/myorg/repo --host github.com — exits with "Conflicting host" error
  • uv run apm marketplace add gitlab.com/myorg/../evil/repo — exits with traversal-rejection error

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

Enhances apm marketplace add repository input parsing to support GitLab subgroup paths (N-segment owners) and full HTTP(S) repository URLs, addressing the limitation that previously only accepted 2- or 3-segment shorthands.

Changes:

  • Replace rigid 2/3-segment parsing with a unified parser that supports N-segment subgroup paths and full HTTP(S) URLs.
  • Update user-facing hint text to mention the HTTPS URL form.
  • Add unit tests covering GitLab subgroup shorthand, HTTPS URL parsing, .git stripping, and rejection cases.

Reviewed changes

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

File Description
src/apm_cli/commands/marketplace.py Implements the new unified parsing logic for marketplace repo inputs and updates CLI messaging.
src/apm_cli/marketplace/errors.py Expands the “not registered” hint to mention full HTTPS URLs.
tests/unit/marketplace/test_marketplace_commands.py Adds new test cases for subgroup shorthands, HTTPS URLs, .git stripping, and invalid inputs.

Comment thread src/apm_cli/commands/marketplace.py Outdated
Comment thread src/apm_cli/commands/marketplace.py Outdated
Comment on lines +355 to +360
owner = "/".join(parts[1:-1])
repo_name = parts[-1]
else:
# OWNER/.../REPO (no host prefix, any number of segments)
owner = "/".join(parts[:-1])
repo_name = parts[-1]
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

This parser now allows multi-segment owner values even when the resolved host is the GitHub API backend (default github.com). Downstream, _github_contents_url() interpolates source.owner directly into /repos/{owner}/{repo}/..., so an owner containing / will generate a malformed/ambiguous API path and can accidentally target the wrong repo/path. Consider rejecting owner values containing / when resolved_host is a GitHub host (or otherwise ensuring the client builds URLs safely for multi-segment owners).

Copilot uses AI. Check for mistakes.
Comment thread src/apm_cli/commands/marketplace.py Outdated
Comment on lines 313 to 317
if host and host.lower() != url_host:
logger.error(
f"Invalid host: '{parts[0]}'. "
f"Use 'OWNER/REPO' or 'HOST/OWNER/REPO' format."
f"Conflicting host: --host '{host}' vs '{url_host}' in URL."
)
sys.exit(1)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

Host comparisons use host.lower() without stripping whitespace (e.g. --host "gitlab.com "), which can raise a false "Conflicting host" error even though the normalized host is the same. Consider normalizing the --host value once up front (strip + lower) and using that for both conflict checks and later validation.

Copilot uses AI. Check for mistakes.
Comment on lines +290 to 291
"""Register a marketplace from OWNER/REPO, HOST/OWNER/.../REPO, or a full HTTPS URL."""
logger = CommandLogger("marketplace-add", verbose=verbose)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The CLI now accepts full HTTP(S) URLs and N-segment subgroup paths, but documentation still shows only OWNER/REPO and HOST/OWNER/REPO forms. Please update the relevant Starlight docs pages (e.g. docs/src/content/docs/guides/marketplaces.md, docs/src/content/docs/reference/cli-commands.md, docs/src/content/docs/guides/marketplace-authoring.md) and the APM guide resource (packages/apm-guide/.apm/skills/apm-usage/commands.md) to include the HTTPS URL form and subgroup examples.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +96
# ------------------------------------------------------------------
# GitLab subgroup / deep-path support
# ------------------------------------------------------------------

@patch("apm_cli.marketplace.client.fetch_marketplace")
@patch("apm_cli.marketplace.client._auto_detect_path")
def test_add_gitlab_subgroup_shorthand(self, mock_detect, mock_fetch, runner):
"""HOST/group/subgroup/.../repo shorthand stores all intermediate segments in owner."""
from apm_cli.commands.marketplace import marketplace
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The new parsing behavior around host detection vs OWNER/REPO (especially for owners containing dots like foo.bar/repo) is not covered by tests here. Adding a regression test that asserts foo.bar/repo is treated as owner="foo.bar", repo="repo" (not as a host-prefixed shorthand) would prevent reintroducing the ambiguity fix.

Copilot uses AI. Check for mistakes.
@Antonin-Rouxel-LaPoste-BGPN
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="La Poste Groupe"

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 29, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Two independent panelists flagged the (redacted) acceptance as a direct MITM vector and UX contract breach; percent-encoding bypasses validate_path_segments; and neither cli-commands.md nor CHANGELOG.md were updated. The feature strategy is correct -- fix the blocking issues and it ships clean.

Required before merge (12 items)

  • [python-architect] 50-line parsing block inline in add() -- extract to _parse_repo_argument() at src/apm_cli/commands/marketplace.py:368

    • Why: The add() command body now contains ~50 lines of URL/path parsing logic. This violates the single-responsibility shape of a Click handler and makes the function untestable in isolation. The logic has a clear stable interface: (repo_input: str, host_flag: str | None) -> tuple[str, str, str].
    • Suggested fix: Add def _parse_repo_argument(repo_input: str, host_flag: str | None) -> tuple[str, str, str]:. Raise a structured exception on bad input, catch it once in add(). This keeps the command handler to ~30 lines of orchestration.
  • [python-architect] display_name = name or repo_name at line 454 is immediately overwritten -- dead assignment with a latent behavioral difference at src/apm_cli/commands/marketplace.py:454

    • Why: The assignment is dead code (the three-tier alias block below overwrites it unconditionally), but it silently changes meaning if an exception is added between the two sites. Remove it.
    • Suggested fix: Delete the display_name = name or repo_name line at 454. The three-tier block below already handles all cases correctly.
  • [cli-logging-expert] PathTraversalError passthrough may expose internal message not written for users at src/apm_cli/commands/marketplace.py

    • Why: logger.error(str(exc)) delegates the user-facing message entirely to the exception class. If PathTraversalError.str reads like an internal diagnostic rather than an actionable instruction, the user gets no fix. Every error must name the thing and include the fix.
    • Suggested fix: Catch PathTraversalError and emit a controlled message: logger.error(f"Invalid {exc.context}: path traversal sequences ('..', '~') are not allowed.") -- or verify PathTraversalError.str already meets the standard and document that contract.
  • [cli-logging-expert] Empty-hostname error Invalid host in URL: '' is not actionable at src/apm_cli/commands/marketplace.py

    • Why: When _parsed.hostname is empty (e.g. https:///owner/repo), the message reads "Invalid host in URL: ''.". The user sees an empty quoted string with no idea what a valid host looks like.
    • Suggested fix: Add a guard: if not url_host: logger.error("Invalid URL: no host found. Use '(host/redacted) before calling is_valid_fqdn`.
  • [devx-ux-expert] cli-commands.md not updated -- change is incomplete by definition at docs/src/content/docs/reference/cli-commands.md:1152

    • Why: The docs still show HOST/OWNER/REPO (fixed 3-segment) and have no mention of full URL input or subgroup paths. Per panel rule: if a CLI change is not reflected in cli-commands.md in the same PR, that change is incomplete.
    • Suggested fix: Update the usage block to show all accepted forms (OWNER/REPO, HOST/OWNER/.../REPO, `(host/redacted), update the Arguments section, and add a GitLab subgroup URL example.
  • [devx-ux-expert] (redacted) URLs are silently accepted -- a package manager must not do this at src/apm_cli/commands/marketplace.py:384`

    • Why: Every other APM fetch path that allows HTTP requires --allow-insecure. Silently fetching a marketplace over plaintext breaks the established security contract and enables MITM attacks on the plugin registry.
    • Suggested fix: Either reject (redacted) with logger.error("Only HTTPS URLs are accepted. Use https:// for security.")or require an explicit--allow-insecure` flag matching the existing pattern.
  • [devx-ux-expert] Docstring says 'HTTPS URL' but code accepts HTTP -- user-visible lie at src/apm_cli/commands/marketplace.py:368

    • Why: The function docstring and list_cmd help text both say 'full HTTPS URL', but the implementation accepts both schemes. Help text is the contract.
    • Suggested fix: If (redacted) is intentionally supported, update docstring/help to say 'a full URL (https:// or (redacted) If not intentional, remove `(redacted) from the startswith check.
  • [devx-ux-expert] MarketplaceNotFoundError error hint doesn't mention subgroup paths at src/apm_cli/marketplace/errors.py:17

    • Why: When a user gets a MarketplaceNotFoundError, the recovery hint says only 'OWNER/REPO (or a full HTTPS URL)'. It does not mention HOST/OWNER/.../REPO, the most relevant new feature for GitLab users who hit this error.
    • Suggested fix: Update the hint to: Run 'apm marketplace add OWNER/REPO', 'HOST/OWNER/.../REPO', or a full HTTPS URL to register it.
  • [supply-chain-security-expert] HTTP URLs accepted for marketplace source -- plaintext fetch enables MITM at src/apm_cli/commands/marketplace.py:375

    • Why: `startswith(("https://", "(redacted) explicitly allows (redacted) scheme. An on-path attacker can substitute a malicious marketplace manifest, injecting arbitrary package references. Because marketplace.json defines what packages are installable, this is a direct supply-chain injection point (fail-closed violation).
    • Suggested fix: Replace the combined check with an HTTPS-only gate. If input starts with (redacted) (not https://), call logger.error("Only HTTPS URLs are accepted for marketplace sources.")andsys.exit(1)`.
  • [supply-chain-security-expert] Percent-encoded traversal sequences bypass validate_path_segments at src/apm_cli/commands/marketplace.py:412

    • Why: urllib.parse.urlparse does NOT decode percent-encoding in _parsed.path. A URL like https://github.com/%2E%2E/evil yields owner '%2E%2E'. validate_path_segments checks for '..' and '.' but '%2E%2E' does not match -- the security control is bypassed silently.
    • Suggested fix: Before calling validate_path_segments, decode: import urllib.parse; owner = urllib.parse.unquote(owner); repo_name = urllib.parse.unquote(repo_name). Apply unquote to each element of path_parts before joining.
  • [supply-chain-security-expert] Multi-segment owner interpolated into API URL without per-segment URL-encoding -- potential URL injection at src/apm_cli/marketplace/client.py:187

    • Why: client.py builds f"{api_base}/repos/{source.owner}/{source.repo}/contents/...". With multi-segment owner (myorg/subgroup/team), validate_path_segments only rejects .. but allows ?, #, @, space. An owner segment containing ?injected=1 would inject query parameters into the API request.
    • Suggested fix: URL-encode each segment of source.owner and source.repo individually with urllib.parse.quote(seg, safe='') before interpolating into the API URL, then rejoin with /.
  • [oss-growth-hacker] No CHANGELOG entry for GitLab subgroup / HTTPS URL support at CHANGELOG.md:9

    • Why: The [Unreleased] section has zero mention of PR fix(marketplace): enhance repository input parsing for GitLab subgroups and HTTPS URLs #1034's changes. This is the canonical anti-pattern: feature shipped, no one knows it exists in 30 days. Enterprise GitLab teams will search release notes before adopting.
    • Suggested fix: Add under ## [Unreleased] / ### Added: - apm marketplace add now accepts full HTTPS URLs (e.g. (gitlab.com/redacted) and N-segment GitLab subgroup paths -- paste directly from your browser. Works with any HTTPS-hosted Git marketplace. (#1034)

Nits (11 items, skip if you want)

  • [python-architect] import urllib.parse as _urlparse and import re are lazy imports inside the function for no circular-dep reason -- move both to top-level stdlib imports
  • [python-architect] New tests use ... (Ellipsis) as stub body -- replace with a comment or remove the ellipsis
  • [python-architect] Ambiguous host-detection heuristic: 2-segment github.com/repo falls through to OWNER/REPO branch silently with no warning
  • [cli-logging-expert] Trailing period inconsistency: "Invalid host in URL: '{url_host}'." ends with a period; all other new error messages do not
  • [cli-logging-expert] Conflicting-host errors don't include the fix -- append "Remove --host to let the URL determine the host, or omit the host from the argument."
  • [cli-logging-expert] 'Invalid format' error example acme-org/plugin-marketplace is GitHub-specific; extend to include a multi-host variant
  • [devx-ux-expert] --host flag interaction with full URL is confusing -- consider a warning when both are provided and they agree
  • [devx-ux-expert] Invalid-format error message doesn't mention the new URL form; extend hint to include the full HTTPS URL example
  • [supply-chain-security-expert] FQDN heuristic is ambiguous for org names that look like domains (e.g. my.org); emit a warning when promoting a segment to host without explicit --host
  • [oss-growth-hacker] README hero doesn't mention multi-host / GitLab support -- consider a 'Works with GitHub, GitLab, and any HTTPS-hosted Git repository' signal within 1-2 releases
  • [oss-growth-hacker] MANIFESTO 'Portability over Vendor Lock-in' is the perfect hook anchor -- reference it in the PR description and CHANGELOG entry

CEO arbitration

PR #1034 ships a genuinely valuable feature -- paste-from-browser HTTPS URLs and GitLab subgroup paths -- but it arrives with three categories of blocking defect that must be resolved before merge. First, and most critically, the implementation accepts (redacted) scheme URLs with no warning, no opt-in flag, and no documentation of the risk. Two panelists (DevX/UX and Supply Chain Security) flag this independently: from a UX standpoint it breaks the established --allow-insecure contract every other APM fetch path honors; from a security standpoint it is a direct MITM injection point for marketplace manifests. These two angles reinforce each other and are not in conflict -- the fix is the same: reject (redacted) at the parser boundary and document that only https:// is accepted. Second, the PR introduces a percent-encoding bypass in validate_path_segments ('%2E%2E' evades the '..' check) and multi-segment owner strings interpolated into API URLs without per-segment encoding, leaving '?', '#', and '@' characters as live URL-injection vectors. These are supply-chain correctness issues, not edge cases. Third, the 50-line parsing block embedded in the Click handler conflates command orchestration with URL/path resolution logic, making the new behavior untestable in isolation; extraction to _parse_repo_argument() with a defined return type is a structural prerequisite for the security fixes above, since it creates one auditable entry point.

Two process gaps must close in the same PR: cli-commands.md must reflect the new HOST/OWNER/.../REPO and full-URL input forms (the docs currently show only the fixed 3-segment shape, making the new feature undiscoverable from help alone), and CHANGELOG.md must carry an [Unreleased] entry. Neither is cosmetic -- the docs gap means GitLab users who need subgroup support cannot find the syntax without reading source, and the CHANGELOG gap makes the feature invisible to evaluators deciding whether to upgrade.

The recommended path forward is: (1) land the _parse_repo_argument() extraction first as it is the foundation for auditable security fixes; (2) add urllib.parse.unquote() normalization before validate_path_segments and per-segment urllib.parse.quote() encoding before URL construction; (3) restrict accepted schemes to https:// only, raising a clear actionable error for (redacted) (4) update cli-commands.md and CHANGELOG.md; (5) replace the placeholder '...' stub test bodies with real assertions. The feature strategy is correct and the growth signal is real -- ship it clean.

Dissent resolved: No genuine inter-panelist disagreement exists. The (redacted) finding surfaces from both DevX/UX (UX contract) and Supply Chain Security (MITM risk) and the two rationales are complementary -- a single fix satisfies both. CLI Logging Expert's PathTraversalError message finding and Python Architect's structural extraction finding are also complementary: extraction to _parse_repo_argument() creates the natural site for a single user-readable error raise, resolving both concerns simultaneously.

Growth/positioning note: The paste-from-browser UX (full HTTPS URL input) is a concrete, demonstrable hook for a 'GitLab teams: APM works for you' campaign. OSS Growth Hacker recommends a standalone social beat -- a tweet thread or dev.to post anchored to the MANIFESTO 'Portability over Vendor Lock-in' principle -- rather than bundling the feature into a release roundup. Schedule it as soon as the fixed PR merges.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR
    class add {
        <<IOBoundary>>
        +repo: str
        +name: str
        +branch: str
        +host: str
        +verbose: bool
    }
    class MarketplaceSource {
        <<ValueObject>>
        +host: str
        +owner: str
        +repo: str
        +branch: str
        +name: str
    }
    class CommandLogger {
        <<Base>>
        +error()
        +progress()
        +complete()
    }
    class validate_path_segments {
        <<Pure>>
    }
    class PathTraversalError {
        <<Exception>>
    }
    class is_valid_fqdn {
        <<Pure>>
    }
    class default_host {
        <<Pure>>
    }
    class fetch_marketplace {
        <<IOBoundary>>
    }
    class _auto_detect_path {
        <<IOBoundary>>
    }
    class MarketplaceNotFoundError {
        <<Exception>>
    }

    add --> CommandLogger : uses
    add --> MarketplaceSource : constructs
    add --> fetch_marketplace : calls
    add --> _auto_detect_path : calls
    add --> validate_path_segments : calls
    add --> is_valid_fqdn : calls
    add --> default_host : calls
    validate_path_segments ..> PathTraversalError : raises
    MarketplaceNotFoundError --> MarketplaceSource : references

    class add:::touched
    class PathTraversalError:::touched
    class validate_path_segments:::touched
    class MarketplaceNotFoundError:::touched
    classDef touched fill:#ffe0b2,stroke:#e65100
Loading
flowchart TD
    A([CLI: apm marketplace add REPO]) --> B[add -- marketplace.py:368]
    B --> C{starts with https:// ?}
    C -- yes --> D[urlparse repo_input pure parse]
    D --> E{is_valid_fqdn url_host ?}
    E -- no --> ERR1[logger.error + sys.exit 1]
    E -- yes --> F{--host conflicts?}
    F -- yes --> ERR2[logger.error + sys.exit 1]
    F -- no --> G[split path to owner and repo_name]
    C -- no --> H[split by slash to parts]
    H --> I{len parts ge 3 and is_valid_fqdn parts0 ?}
    I -- yes --> J[resolved_host=parts0 owner=join parts1 to -1 repo_name=parts-1]
    I -- no --> K[owner=join parts to -1 repo_name=parts-1]
    G --> L[validate_path_segments owner and repo_name]
    J --> L
    K --> L
    L -- PathTraversalError --> ERR3[logger.error str exc + sys.exit 1]
    L -- ok --> M{resolved_host is None?}
    M -- yes --> N{--host flag set?}
    N -- yes --> O[is_valid_fqdn host check]
    O -- invalid --> ERR4[logger.error + sys.exit 1]
    O -- valid --> P[resolved_host = normalized host]
    N -- no --> Q[resolved_host = default_host FS read env/config]
    M -- no --> R[validate --name flag via _is_valid_alias]
    P --> R
    Q --> R
    R -- invalid --> ERR5[logger.error + sys.exit 1]
    R -- ok --> S[_auto_detect_path probe_source NET HTTP probe]
    S -- None --> ERR6[logger.error + sys.exit 1]
    S -- path --> T[fetch_marketplace fetch_source NET download JSON]
    T --> U[three-tier alias resolution: --name > manifest.name > repo_name]
    U --> V[add_marketplace source FS write registry]
    V --> W([logger.complete exit 0])
Loading

Design patterns

  • Used in this PR: Inline procedural parsing -- all URL/path disambiguation logic is written sequentially inside the Click handler with early-exit guards, following an imperative decision tree rather than a strategy or parser object.
  • Pragmatic suggestion: Extract Function -- move the ~50-line parsing block to _parse_repo_argument(repo_input, host_flag) -> tuple[str, str, str]; the three branches (HTTPS URL / HOST/.../REPO / OWNER/.../REPO) map cleanly to private helpers called from that one function, keeping add() as pure orchestration.

Required findings: (see aggregated list above)

Nits:

  • import urllib.parse as _urlparse and import re are lazy imports inside the function for no circular-dep reason -- move to top-level imports.
  • New tests use ... (Ellipsis) as stub body -- replace with a comment or remove.
  • Ambiguous host-detection heuristic: 2-segment github.com/repo falls through to OWNER/REPO branch silently.

CLI Logging Expert

Required findings: (see aggregated list above)

Nits:

  • Trailing period inconsistency on "Invalid host in URL: '{url_host}'." -- drop the trailing period.
  • Conflicting-host errors don't include the fix -- append the recovery action.
  • 'Invalid format' error example is GitHub-specific -- extend to include a multi-host variant.
  • import re inside function body -- move to top of file.

DevX UX Expert

Required findings: (see aggregated list above)

Nits:

  • --host flag interaction with full URL is confusing -- consider a warning when both are provided and they agree.
  • Invalid-format error message doesn't mention the new URL form.

Supply Chain Security Expert

Required findings: (see aggregated list above)

Nits:

  • FQDN heuristic is ambiguous for org names that look like domains (e.g. my.org) -- emit a warning when promoting a segment to host without explicit --host.
  • (redacted) scheme is silently normalised inside is_valid_fqdnbefore FQDN validation rejects it -- add an explicit guard rejecting inputs containing://`.

Auth Expert

Inactive -- PR changes marketplace source URL parsing only and does not modify AuthResolver, token management, or credential resolution logic (touched files: src/apm_cli/commands/marketplace.py, src/apm_cli/marketplace/errors.py, tests only).

OSS Growth Hacker

Required findings: (see aggregated list above)

Nits:

  • README hero doesn't mention multi-host / GitLab support -- consider adding a 'Works with GitHub, GitLab, and any HTTPS-hosted Git repository' signal within 1-2 releases.
  • MANIFESTO 'Portability over Vendor Lock-in' is the perfect hook anchor -- reference it in the PR description and CHANGELOG entry.

Side-channel: This is the right feature to anchor a 'GitLab teams: APM works for you' tweet thread or dev.to post. The paste-from-browser UX (full HTTPS URL) is the concrete, demonstrable hook. Recommend scheduling this as a standalone social beat rather than bundling it into a release roundup.

Verdict computed deterministically: 12 required findings across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

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 #1034 · ● 1.7M ·

@github-actions github-actions Bot added panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. and removed panel-review Trigger the apm-review-panel gh-aw workflow labels Apr 29, 2026
…nce for improved validation

Co-authored-by: Copilot <copilot@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add a marketplace with just a full repository URL

4 participants