fix(artifactory): deterministic boundary probe for nested GitLab paths#1472
Conversation
Mirrors the native GitLab resolver pattern -- but uses HEAD on Artifactory archive URLs as the existence signal, so no separate metadata API is needed. Covers both routing modes: Mode 1 -- explicit FQDN deps (host/artifactory/key/owner/repo/...). Mode 2 -- bare shorthand under PROXY_REGISTRY_URL+PROXY_REGISTRY_ONLY. The resolver walks candidate (owner, repo, virtual_path) splits shallow-first and locks in the first one whose archive responds 2xx-3xx. For unambiguous paths it returns the parse-time ref unchanged. When every candidate is rejected it raises -- distinguishing "missing repo" from auth (401/403) errors so users get an actionable hint instead of silently anchoring on a wrong guess. ``allow_redirects=False`` on the HEAD call keeps the Bearer token from leaking cross-host. Removes the parse-time marker-segment heuristic (_VIRTUAL_PATH_ROOT_SEGMENTS / _ARTIFACTORY_VIRTUAL_MARKERS / _ARTIFACTORY_VIRTUAL_FILE_EXTENSIONS) that previously decided the boundary based on hard-coded directory names like ``skills/`` or ``prompts/``. Parse-time now defaults to all-as-repo with a structural file-extension rule on the last segment; the install-time resolver is the authoritative source of the boundary. Also fixes a pre-existing URL-roundtrip bug where ``to_github_url`` -> ``parse`` folded the ``artifactory/<key>`` prefix into ``repo_url``, causing the downloader to build double-prefixed archive URLs and 404. ``_validate_url_repo_path`` now strips the prefix before returning the bare ``owner/repo`` slug.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR adds deterministic install-time resolution for Artifactory proxy “repo vs virtual path” boundaries (including GitLab subgroup-style nested repo paths), replacing parse-time heuristics and updating docs/tests accordingly.
Changes:
- Added an install-time Artifactory boundary probe that HEAD-checks candidate archive URLs and rebuilds dependency refs at the verified split.
- Updated Artifactory path parsing and archive URL construction to better support nested GitLab subgroup repos (basename-based GitLab archive filenames).
- Expanded unit tests and documentation for proxy/registry-only behavior and nested-group repos.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_artifactory_support.py | Adds coverage for nested subgroup parsing, candidate enumeration, archive URL naming, and boundary probe behavior. |
| src/apm_cli/utils/github_host.py | Introduces Artifactory boundary candidate iterator; simplifies parse-time split; updates GitLab archive filename logic. |
| src/apm_cli/models/dependency/reference.py | Adds Artifactory boundary probe constructor; changes bare shorthand parsing rules under registry-only mode; strips Artifactory prefix in URL validation. |
| src/apm_cli/install/package_resolution.py | Allows optional Artifactory boundary resolver to rebuild refs and trigger structured persistence. |
| src/apm_cli/install/artifactory_resolver.py | New authoritative resolver that probes candidate boundaries against the proxy and rebuilds refs deterministically. |
| src/apm_cli/deps/artifactory_orchestrator.py | Updates owner/repo splitting and subdirectory download to preserve nested repo slugs. |
| src/apm_cli/commands/install.py | Wires the Artifactory resolver into install-time dependency normalization/persistence. |
| docs/src/content/docs/enterprise/registry-proxy.md | Documents nested-group repos behind proxy, probing behavior, and troubleshooting. |
| CHANGELOG.md | Notes new resolver behavior, removed heuristics, and fixes for nested-group installs and prefix round-tripping. |
- parse_artifactory_path: reject ``owner//virtual`` (empty segment before the explicit boundary) instead of falling through and returning an empty ``repo`` slug. - _proxy_routing_target: thread the proxy ``scheme`` (https/http) through the resolver into ``build_artifactory_archive_url`` so installs that intentionally route through an ``http://`` proxy (under ``PROXY_REGISTRY_ALLOW_HTTP=1``) probe over the same transport. - _CandidateStatus.EXISTS: update inline comment to say ``2xx or 3xx`` (redirect-inclusive), matching the actual classification in ``_candidate_archive_status``. - CHANGELOG: split the parse-time-behavior note into explicit-FQDN vs bare-shorthand modes (the all-as-repo default applies only under ``PROXY_REGISTRY_ONLY``; Mode 1 stays shallow). - package_resolution: clarify the ``direct_virtual_resolved`` docstring -- GitLab path gates on ``is_virtual``, Artifactory path gates on probe rebuild; both signal "persist as structured ``git:`` + ``path:`` entry". - Add two regression tests for the empty-segment edge case (parser + iterator).
…h-support # Conflicts: # CHANGELOG.md # src/apm_cli/models/dependency/reference.py
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| python-architect | 0 | 2 | 3 | Well-structured install-time resolver mirrors GitLab prober pattern; module placement correct; two recommended improvements for coupling and SRP. |
| cli-logging-expert | 0 | 1 | 2 | Verbose probe output uses raw print() instead of canonical _rich_echo dim helper; error messages well-structured and token-safe but miss actionable fix hint. |
| devx-ux-expert | 0 | 3 | 4 | Ship-worthy enterprise fix; default-path silent multi-second network I/O and ambiguous network-vs-missing error are the two ergonomic gaps; lockfile env-dependence is documented but not surfaced at runtime. |
| supply-chain-security-expert | 0 | 1 | 2 | Token-leak guard (allow_redirects=False) and audience separation correctly implemented; no blocking security issues; one recommended hardening and two nits. |
| oss-growth-hacker | 0 | 1 | 2 | Strong enterprise-credibility beat: community contributor from Check Point unblocks GitLab-shop Artifactory users. Docs land in right sidebar; CHANGELOG dense. |
| auth-expert | 0 | 0 | 1 | Auth claims VERIFIED. Mode 1 delegates to AuthResolver.resolve_for_dep correctly; Mode 2 bypasses AuthResolver and sources bearer from RegistryConfig.from_env() (avoids github.com token leak); allow_redirects=False prevents cross-host token leakage. No blocking issues. |
| doc-writer | 0 | 2 | 4 | Docs accurate vs code. Fix CHANGELOG PR-number suffix and propagate PROXY_REGISTRY_ONLY semantic drift to env-var reference and auth pages. |
| test-coverage-expert | 1 | 4 | 1 | New resolver module ships ~280 lines of critical install-pipeline logic with ZERO automated tests; unit tests cover adjacent surfaces but the probe itself, its auth-audience split, redirect-leak prevention, and error discrimination are untested. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [test-coverage-expert] (blocking-severity) Add integration-with-fixtures tests for
_resolve_artifactory_boundarycovering: boundary probe happy path,allow_redirects=Falsemutation-break, 401/403 vs 4xx error discrimination, Mode 2 token audience (proxy bearer not github.com PAT), and 3+ segment subgroup folding. -- The entire new 280-line module is untested. On a critical-promise surface (install pipeline), missing tests are a regression-trap gap equivalent to a blocking defect. A single refactor could silently break token-audience separation or redirect-leak prevention with no CI signal. - [supply-chain-security-expert] Add INCONCLUSIVE candidate status: fail closed on transient network errors instead of silently skipping candidates. -- Silent exception swallow on DNS/TLS/timeout can lock in a wrong boundary or surface misleading "missing repo" error. Raised independently by devx-ux and supply-chain-security -- convergent signal on a security-relevant surface.
- [doc-writer] Fix CHANGELOG entries: add
(#1472)suffix, replace internal symbol names with user-outcome language, and cross-referencePROXY_REGISTRY_ONLYsemantic drift to env-var reference page. -- Missing PR-number suffix violates a hard project rule (.github/instructions/changelog.instructions.md). Internal symbol leakage in CHANGELOG erodes the user-facing narrative. - [devx-ux-expert] Add progress indicator (spinner or dim echo) for HEAD-probe network I/O on the default install path; surface timeout feedback. -- 15s timeout x N candidates x 4 URL shapes can produce 60+ seconds of silent hang. Users will assume APM froze. Low effort to add via existing
_rich_echo(dim)pattern. - [cli-logging-expert] Replace bare
print()in verbose probe output with_rich_echo(dim)orCommandLoggerto respect--quiet, styling, and test capture. -- Bypasses the canonical output helpers; will not be suppressed by--quietand cannot be captured in integration tests.
Architecture
classDiagram
class iter_artifactory_boundary_candidates {
+iter() Iterator~ArtifactoryBoundaryCandidate~
}
class ArtifactoryBoundaryCandidate {
+owner: str
+repo: str
+depth: int
}
class _resolve_artifactory_boundary {
+resolve(dep_ref, ctx) DependencyReference
}
class _candidate_archive_status {
+probe(url, headers) _CandidateStatus
}
class _proxy_probe_headers {
+build(dep_ref, ctx) dict
}
class _proxy_routing_target {
+build(dep_ref) str
}
class RegistryConfig {
+from_env() RegistryConfig
+bearer_token: str
}
class AuthResolver {
+resolve_for_dep(dep_ref) AuthContext
}
_resolve_artifactory_boundary --> iter_artifactory_boundary_candidates
_resolve_artifactory_boundary --> _candidate_archive_status
_resolve_artifactory_boundary --> _proxy_probe_headers
_resolve_artifactory_boundary --> _proxy_routing_target
_proxy_probe_headers ..> RegistryConfig : Mode 2 lazy import
_proxy_probe_headers ..> AuthResolver : Mode 1
_proxy_routing_target ..> ArtifactoryOrchestrator : lazy import
flowchart LR
install[install.py] --> resolve[_resolve_artifactory_boundary]
resolve --> iter[iter_artifactory_boundary_candidates<br/>github_host.py]
resolve --> probe[_candidate_archive_status<br/>HEAD probe]
probe --> headers[_proxy_probe_headers]
probe --> target[_proxy_routing_target]
headers -.Mode 1.-> auth[AuthResolver]
headers -.Mode 2.-> regconf[RegistryConfig.from_env]
target -.lazy.-> orch[ArtifactoryOrchestrator]
Recommendation
The fix is correct and valuable, but shipping a new 280-line install-pipeline module with zero automated test coverage violates APM's regression-trap floor for critical-promise surfaces. Recommended path: ask the contributor to add integration-with-fixtures tests for the boundary resolver (offer to pair or co-author if needed to unblock), fix the CHANGELOG entries mechanically, then ship. The network-exception and progress-indicator items can land as fast follow-up issues to avoid blocking the contributor further. Prioritize keeping this contributor engaged -- the enterprise credibility signal is high.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Adds focused regression-trap tests for the new install-pipeline boundary resolver (microsoft#1472) covering the contracts a future refactor could silently break: - allow_redirects=False on every HEAD probe (token-leak guard against proxy-issued cross-host redirects) - Mode 2 Authorization header is sourced from RegistryConfig (proxy bearer), never from AuthResolver (which returns the github.com PAT and would leak it to the proxy host) - 403 (like 401) classifies as AUTH; mixed 401/non-auth-4xx demotes the candidate set to MISSING so the user-facing error stays accurate (avoids misreporting a missing repo as auth failure) - 429 and other non-{401,403} 4xx are MISSING, not AUTH (guards against accidental broadening of the AUTH set) - _split_owner_repo subgroup folding for 3+ segment GitLab boundaries (group/sub1/sub2/repo correctly folds to owner=group, repo=sub1/sub2/repo) Both mutation-break gates were exercised: flipping allow_redirects to True and rewiring Mode 2 to AuthResolver each made the corresponding test fail; restoring the source code made them pass. Also applies ruff format pass to bring three files in line with the canonical formatter (pre-existing drift on the PR branch that would have failed CI lint check). Co-authored-by: chkp-roniz <chkp-roniz@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Follow-ups from the apm-review-panel pass have landed. Summary:
New tests landed:
The existing happy-path boundary-probe coverage in Regression-trap evidence (mutation-break gate):
This commit also applies a Lint contract: CI: local test suite for the three affected test files (175 tests) all pass. The full GitHub Actions suite is not auto-triggered for this external-fork PR (microsoft/apm policy gates fork-PR workflows behind maintainer approval); the only registered check on the head commit is Ready for maintainer review. |
Conflict resolution: - CHANGELOG.md: combined the [Unreleased] entries from origin/main (semver ranges, ``apm deps why``, install exit-code BREAKING change, pinned-constraint fix) with this PR's Artifactory entries. Panel follow-ups addressed inline: - doc-writer: added ``(microsoft#1472)`` suffix to every CHANGELOG entry belonging to this PR, per ``.github/instructions/changelog.instructions.md``. - supply-chain-security-expert / devx-ux-expert (convergent): the boundary probe used to silently swallow transport errors (``requests.RequestException``), which could lock in a wrong owner/repo split or surface a misleading "missing repo" error during a transient DNS/TLS/timeout outage. Added a fourth candidate status ``_CandidateStatus.INCONCLUSIVE`` that fires when every URL shape for a candidate raised a transport error (no HTTP response observed at all). The resolver now fails closed on all-INCONCLUSIVE: raises a network-specific ``ValueError`` carrying the underlying exception and the proxy host, instead of mis-classifying as MISSING. Per-URL-shape transport errors are still tolerated as long as at least one URL shape per candidate produced an HTTP response. Two regression-trap tests landed in ``tests/unit/install/test_artifactory_resolver.py``: * ``TestInconclusiveFailsClosed::test_all_transport_errors_raise_network_specific_error`` * ``TestInconclusiveFailsClosed::test_partial_transport_failure_does_not_fail_closed``
|
Thanks for the panel pass — addressed in 57a3c02 (on top of @danielmeppiel's 4ae18ca). Addressed in this pushdoc-writer (mechanical): Added supply-chain-security-expert + devx-ux-expert (convergent): The silent network-exception swallow in Two regression-trap tests landed in
Punted to follow-up issues (per panel guidance)The panel explicitly said:
Network-exception is now in this PR. The remaining nits I'd suggest land as their own follow-ups so this PR stays scoped:
Happy to open both as separate issues once this lands — they're low-effort, low-risk, and easier to review on their own. Test + lint status
PR is mergeable again. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 2 | 2 | Clean new resolver module mirrors GitLab pattern without structural duplication; _CandidateStatus state machine is well-designed with correct fail-closed semantics; recommend encapsulating Mode routing into a small dataclass. |
| CLI Logging Expert | 0 | 1 | 2 | Verbose probe output bypasses CommandLogger/verbose_detail, using raw print() with a non-standard prefix; error messages use markdown backticks that won't render in a terminal. |
| DevX UX Expert | 0 | 1 | 2 | Solid error ergonomics; INCONCLUSIVE error should hint at the // escape hatch like the UNRESOLVED error does. |
| Supply Chain Security Expert | 0 | 0 | 2 | Token-leak guards (allow_redirects=False, Mode 2 audience separation) are correctly implemented; no blocking security issues. |
| OSS Growth Hacker | 0 | 1 | 1 | Repeat enterprise contributor (8 commits) from Check Point validates the regulated-airgap segment; docs additions excellent; CHANGELOG entries maintainer-voice -- mine for a launch beat. |
| Auth Expert | 0 | 0 | 2 | No auth bypass or token leak. Mode 1 HTTPS-only and Mode 2 audience separation are sound. Two hardening nits only. |
| Doc Writer | 0 | 3 | 2 | registry-proxy.md page reads cleanly and CHANGELOG rework addresses prior Copilot ask; two drift gaps (apm-usage/dependencies.md, reference/manifest-schema.md) and one missing failure-mode row. |
| Test Coverage Expert | 0 | 1 | 1 | Unit coverage is thorough for all 7 surfaces; one recommended gap: no integration-with-fixtures test exercises the full install pipeline through the boundary probe. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Test Coverage Expert] Add integration-with-fixtures test exercising full install pipeline through boundary probe -- Install pipeline is a critical-promise surface (portability-by-manifest). Unit tests cover resolver in isolation but no automated test wires parse->probe->rebuild->lockfile end-to-end. Missing integration coverage is a regression-trap gap on a critical surface.
- [Doc Writer] Fix drift in dependencies.md and manifest-schema.md to document // boundary marker and env-gated nested-group behavior -- Two real documentation drift gaps: dependencies.md still claims object-form is required for nested groups, and the formal manifest spec omits // entirely. Users reading docs after this ships will get contradictory guidance.
- [CLI Logging Expert] Route verbose output through CommandLogger.verbose_detail() instead of raw print() -- Converging signal from three panelists (cli-logging, devx-ux, security). Raw print() bypasses --quiet suppression, stderr routing, and structured capture. Also eliminates the credential-echo risk flagged by supply-chain.
- [DevX UX Expert] Add // bypass hint to the INCONCLUSIVE error message -- INCONCLUSIVE (network failure) is exactly when users most need the escape hatch. Recovery-action principle: every error names one concrete next action. The UNRESOLVED error already does this; INCONCLUSIVE should match.
- [OSS Growth Hacker] Rewrite CHANGELOG Fixed bullet as user-facing story for release notes -- Current wording references internal function names (parse_artifactory_path, build_artifactory_archive_url). Users searching "nested GitLab subgroup" or "Artifactory 404" won't find it. Release note should tell the user story.
Architecture
classDiagram
direction LR
class DependencyReference {
<<DataClass>>
+repo_url str
+host str
+reference str
+virtual_path str
+artifactory_prefix str
+parse(package) DependencyReference
+from_artifactory_boundary_probe() DependencyReference
+from_gitlab_shorthand_probe() DependencyReference
+is_artifactory() bool
}
class _CandidateStatus {
<<ValueObject>>
+EXISTS str
+MISSING str
+AUTH str
+INCONCLUSIVE str
}
class AuthResolver {
<<Strategy>>
+resolve_for_dep(dep_ref) AuthContext
}
class ArtifactoryRouter {
<<Facade>>
+should_use_proxy(dep_ref) bool
+parse_proxy_config() tuple
}
class RegistryConfig {
<<ValueObject>>
+from_env() RegistryConfig
+get_headers() dict
}
class github_host {
<<PureUtility>>
+iter_artifactory_boundary_candidates(segs) Iterator
+build_artifactory_archive_url(host,prefix,owner,repo,ref) tuple
+sanitize_token_url_in_message(pkg,host) str
}
class artifactory_resolver {
<<Module>>
+_resolve_artifactory_boundary(pkg,auth) DependencyReference
+_candidate_archive_status(host,...) tuple
+_proxy_probe_headers(dep,auth,mode) dict
+_proxy_routing_target(dep) tuple
+_rebuild_dep_ref(dep,...) DependencyReference
}
class gitlab_resolver {
<<Module>>
+_try_resolve_gitlab_direct_shorthand(pkg,auth) DependencyReference
}
class package_resolution {
<<Orchestrator>>
+resolve_parsed_dependency_reference() tuple
}
package_resolution ..> artifactory_resolver : delegates
package_resolution ..> gitlab_resolver : delegates
artifactory_resolver ..> DependencyReference : reads and rebuilds
artifactory_resolver ..> _CandidateStatus : returns
artifactory_resolver ..> AuthResolver : resolves Mode 1 token
artifactory_resolver ..> RegistryConfig : resolves Mode 2 token
artifactory_resolver ..> ArtifactoryRouter : detects proxy routing
artifactory_resolver ..> github_host : URL generation
gitlab_resolver ..> DependencyReference : reads and rebuilds
class artifactory_resolver:::touched
class _CandidateStatus:::touched
class DependencyReference:::touched
class github_host:::touched
class package_resolution:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["apm install owner/sub/repo"] --> B["install.py: resolve_parsed_dependency_reference()"]
B --> C{"needs_gitlab_direct_shorthand_probing?"}
C -->|yes| D["gitlab_resolver._try_resolve_gitlab_direct_shorthand"]
C -->|no| E{"resolve_artifactory_boundary is not None?"}
E -->|no| F["return dep_ref unchanged"]
E -->|yes| G["artifactory_resolver._resolve_artifactory_boundary()"]
G --> H["_proxy_routing_target(dep_ref)"]
H --> I{"routes through proxy?"}
I -->|no| F
I -->|yes| J["iter_artifactory_boundary_candidates(path_segments)"]
J --> K{"len(candidates) == 1?"}
K -->|yes| F
K -->|no| L["_proxy_probe_headers(dep_ref, auth, is_mode_1)"]
L --> M["Loop: for each candidate shallow-first"]
M --> N["[NET] _candidate_archive_status: HEAD probe archive URLs"]
N --> O{"status?"}
O -->|EXISTS| P["_rebuild_dep_ref or return original"]
O -->|MISSING| Q["continue to next candidate"]
O -->|AUTH| Q
O -->|INCONCLUSIVE| Q
Q --> M
M -->|exhausted| R{"failure mode?"}
R -->|all INCONCLUSIVE| S["raise ValueError: network unreachable"]
R -->|all AUTH| T["raise ValueError: authentication problem"]
R -->|mixed| U["raise ValueError: boundary unresolved"]
P --> V["return (dep_ref, direct_virtual_resolved)"]
sequenceDiagram
participant User
participant Install as install.py
participant PkgRes as package_resolution
participant ArtRes as artifactory_resolver
participant Router as ArtifactoryRouter
participant RegCfg as RegistryConfig
participant GHHost as github_host
participant Proxy as Artifactory Proxy
User->>Install: apm install group/sub/repo
Install->>PkgRes: resolve_parsed_dependency_reference(pkg)
PkgRes->>ArtRes: _resolve_artifactory_boundary(pkg, auth, dep_ref)
ArtRes->>Router: should_use_proxy(dep_ref)
Router-->>ArtRes: True (PROXY_REGISTRY_ONLY=1)
ArtRes->>Router: parse_proxy_config()
Router-->>ArtRes: (host, prefix, scheme)
ArtRes->>GHHost: iter_artifactory_boundary_candidates(segs)
GHHost-->>ArtRes: [(pfx,group,sub,repo/None), (pfx,group,sub/repo,None)]
ArtRes->>RegCfg: from_env().get_headers() [Mode 2]
RegCfg-->>ArtRes: {Authorization: Bearer proxy-token}
loop each candidate shallow-first
ArtRes->>GHHost: build_artifactory_archive_url(host,pfx,owner,repo,ref)
GHHost-->>ArtRes: (url1, url2, url3, url4, url5)
ArtRes->>Proxy: HEAD url1 (allow_redirects=False)
Proxy-->>ArtRes: 404
ArtRes->>Proxy: HEAD url2 (allow_redirects=False)
Proxy-->>ArtRes: 302 (EXISTS)
end
ArtRes->>ArtRes: _rebuild_dep_ref(dep, is_mode_1=False)
ArtRes-->>PkgRes: rebuilt DependencyReference
PkgRes-->>Install: (dep_ref, direct_virtual_resolved=True)
Recommendation
Ship this PR. Zero blocking findings, sound architecture, correct security posture, and it unblocks a real enterprise workflow from a proven contributor. Track the integration-test gap and the two doc-drift fixes as immediate follow-ups -- the maintainer should open a tracking issue for those three items before or shortly after merge. The verbose-output and error-wording polish can land in the same follow-up pass.
Full per-persona findings
Python Architect
- [recommended] _proxy_routing_target mixes routing-detection with config resolution; consider a ProxyTarget dataclass at
src/apm_cli/install/artifactory_resolver.py:150
Dataclass for structured data flowing between components. 4-tuple (host, prefix, scheme, is_mode_1) flows through three functions; positional unpacking at every call site. Future-proofs against adding modes. - [recommended] _CandidateStatus should be an enum.StrEnum not a class with string constants at
src/apm_cli/install/artifactory_resolver.py:61
Equality comparisons are raw string checks with no type-checker help. enum.StrEnum (3.11+) gives exhaustiveness checking and prevents typos. Standard Python discriminated-union pattern. - [nit] from_artifactory_boundary_probe classmethod placement is correct
Mirrors from_gitlab_shorthand_probe; consistent with codebase convention. - [nit] _proxy_routing_target placement in the resolver is correct
Couples Artifactory detection + proxy detection + config resolution - all install-time concerns.
CLI Logging Expert
- [recommended] Verbose output uses raw print() instead of logger.verbose_detail() at
src/apm_cli/install/artifactory_resolver.py:242
Every other verbose path in src/apm_cli/install/ routes through logger.verbose_detail(). New resolver falls back to print(), bypassing CommandLogger lifecycle (no --quiet suppression, no stderr routing, no structured capture). - [nit] Verbose prefix 'artifactory-resolve:' doesn't use STATUS_SYMBOLS convention at
src/apm_cli/install/artifactory_resolver.py - [nit] Error constant uses double-backtick markdown notation (
//) that won't render in a terminal atsrc/apm_cli/install/artifactory_resolver.py:50
DevX UX Expert
- [recommended] INCONCLUSIVE (network) error omits the // bypass hint that the UNRESOLVED error includes at
src/apm_cli/install/artifactory_resolver.py:275
INCONCLUSIVE is exactly when the user needs the // escape hatch most (transient outage). Recovery-action principle: every error names one concrete next action. - [nit] Docs use 'Mode 1' / 'Mode 2' labels that are internal jargon at
docs/src/content/docs/enterprise/registry-proxy.md - [nit] The // notation only documented in enterprise/registry-proxy page, not in cli-commands.md at
docs/src/content/docs/reference/cli-commands.md
Supply Chain Security Expert
- [nit] verbose print echoes host/path to stdout without sanitization (user:pass@ in PROXY_REGISTRY_URL could leak) at
src/apm_cli/install/artifactory_resolver.py:243 - [nit] INCONCLUSIVE info mixed with MISSING candidates is silently dropped from the final error at
src/apm_cli/install/artifactory_resolver.py:270
OSS Growth Hacker
- [recommended] CHANGELOG entries are maintainer-facing; rewrite Fixed bullet as user-facing story for next release note at
CHANGELOG.md
'parse_artifactory_path, build_artifactory_archive_url...' won't surface for users Googling 'nested GitLab subgroup' or 'Artifactory 404'. - [nit] New troubleshooting rows in registry-proxy.md are SEO surface; link from top-level troubleshooting at
docs/src/content/docs/enterprise/registry-proxy.md
Auth Expert
- [nit] Mode 1 hard-codes scheme='https' without upstream enforcement in resolver module at
src/apm_cli/install/artifactory_resolver.py:170
_proxy_routing_target constructs the target URL with scheme='https' which is correct, but nothing in the resolver module asserts or rejects an http:// upstream if one were passed in. - [nit] Duck-typed getattr token access instead of typed AuthContext.token attribute at
src/apm_cli/install/artifactory_resolver.py:138
Bypasses static type checking; a refactor that renames the token field would silently produce unauthenticated probes rather than raising AttributeError at development time.
Doc Writer
- [recommended] DRIFT: apm-usage/dependencies.md still says nested-group shorthand requires object form; PR makes it work under PROXY_REGISTRY_ONLY at
packages/apm-guide/.apm/skills/apm-usage/dependencies.md:136 - [recommended] DRIFT: manifest-schema.md (formal spec) does not document the // boundary marker or env-gated nested-group behavior at
docs/src/content/docs/reference/manifest-schema.md:307 - [recommended] registry-proxy.md boundary-probe outcome table omits INCONCLUSIVE / transport-error fail-closed mode at
docs/src/content/docs/enterprise/registry-proxy.md:226 - [nit]
//opt-out not cross-referenced from outcome table atdocs/src/content/docs/enterprise/registry-proxy.md - [nit] Positive ack: CHANGELOG rework correctly distinguishes explicit-FQDN vs bare-shorthand parse-time defaults
Test Coverage Expert
- [recommended] No integration-with-fixtures test exercises
apm installthrough Artifactory boundary probe end-to-end attests/integration/test_install_artifactory_boundary.py
Install pipeline is critical-promise per tier-floor matrix. Unit tests cover resolver in isolation; no integration test wires parse->probe->rebuild->lockfile.
Proof (missing at):tests/integration/test_install_artifactory_boundary.py::test_install_nested_group_dep_through_boundary_probe-- proves: apm install through registry proxy resolves nested-group repos and writes correct lockfile entry [devx,portability-by-manifest] - [nit] No test asserts Mode 2 boundary probe uses http:// scheme when PROXY_REGISTRY_ALLOW_HTTP=1 at
tests/unit/install/test_artifactory_resolver.py
Proof (missing at):tests/unit/install/test_artifactory_resolver.py::test_mode_2_probe_uses_http_scheme_when_allow_http_set-- proves: Boundary probe over http:// isolated networks uses correct transport scheme [devx]
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
… + cite // bypass on inconclusive Folds two panel follow-ups on PR microsoft#1472: - Verbose probe output now flows through CommandLogger.verbose_detail() when a logger is available, so it honors the shared rich/no-rich console path and respects -v/--verbose self-gating like every other install-time detail emitter. Falls back to print() for callers that pass verbose=True without threading a logger (legacy unit tests). - The INCONCLUSIVE error message (transport error on every candidate URL) now cites the // boundary marker, matching the wording already present on the UNRESOLVED message. Users who hit a flaky proxy now learn about the explicit-boundary escape hatch from the message they actually saw, not just the parallel one. addresses CEO follow-ups FU-3 and FU-4 Co-authored-by: chkp-roniz <35386615+chkp-roniz@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Folds the doc-drift follow-up from the apm-review-panel CEO on PR microsoft#1472. Both pages claimed that nested-group GitLab repos require the object form because shorthand is ambiguous on >2-segment paths. That claim is still true for direct (non-proxy) GitLab but is no longer true for deps that route through a registry proxy: this PR adds an install-time boundary probe that HEAD-walks candidate splits against the proxy and deterministically resolves the repo/virtual-path boundary, so nested shorthand works without the object form. - packages/apm-guide/.apm/skills/apm-usage/dependencies.md: add the registry-proxy exception sentence to the existing nested-group note. - docs/src/content/docs/reference/manifest-schema.md: scope the object-form REQUIRED clause to direct nested-group access and link the registry-proxy guide for the proxy-routed exception. addresses CEO follow-up FU-2 Co-authored-by: chkp-roniz <35386615+chkp-roniz@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Folds the CEO follow-up to make the Fixed entry release-notes-ready. The previous wording named internal symbols (parse_artifactory_path, build_artifactory_archive_url, _detect_virtual_package, ArtifactoryOrchestrator ._split_owner_repo) -- accurate but unreadable for a release-notes reader. New wording: leads with what the user can now do (apm install against a proxy works for nested-group repos), names the concrete invocation shapes (explicit FQDN and bare shorthand under PROXY_REGISTRY_ONLY), explains the auth-vs-missing distinction and the no-redirect token-leak guard at a level release-notes readers can act on, and surfaces the // escape hatch. addresses CEO follow-up FU-5 Co-authored-by: chkp-roniz <35386615+chkp-roniz@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…yaml entry Folds the CEO follow-up calling for a full-pipeline regression trap on PR microsoft#1472. The existing unit tests cover the resolver in isolation; this file traps the cross-module chain that ships into apm.yml / lockfile: resolve_parsed_dependency_reference -> _resolve_artifactory_boundary (HEAD probe, mocked) -> _rebuild_dep_ref (proxy-verified boundary) -> dependency_reference_to_yaml_entry (structured git+path entry) Five scenarios: - Mode 1 (explicit FQDN), nested-path: probe locks in owner+repo split, pipeline marks the dep for structured-entry persistence, the yaml entry embeds the proxy host + artifactory/<key> prefix + virtual path. - Mode 1, unambiguous two-segment path: single-candidate short-circuit is preserved (no HEAD-probe traffic, apm.yml stays in original shape). - Mode 2 (bare shorthand under PROXY_REGISTRY_ONLY), nested-path: probe locks in the boundary but rebuilt ref stays bare (no artifactory_prefix embedded; host stays github.com), so lockfile shape stays env-portable. - All-404 across candidates: raises UNRESOLVED with the // bypass hint. - All-transport-error across candidates: raises INCONCLUSIVE with the // bypass hint AND surfaces 'could not reach the proxy' so the user knows the failure mode is reachability, not a missing repo. Mutation-break gate (run locally; not in the test file): removing the new // hint from the INCONCLUSIVE message makes the 'test_transport_errors_raise_inconclusive_with_bypass_hint' assertion fail (Regex pattern did not match), confirming the test is a real trap and not a logic-replay assertion. addresses CEO follow-up FU-1 Co-authored-by: chkp-roniz <35386615+chkp-roniz@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CodeQL flagged the substring assertions on entry['git'] (py/incomplete-url-substring-sanitization, high severity, alert microsoft#161 on PR microsoft#1472) and they also violated the repo's test convention: URL assertions in tests/** must use urllib.parse, never substring (.github/instructions/ tests.instructions.md). Switch to urlparse() + exact-match on hostname and ordered path segments. Same coverage (proxy host + artifactory/<key> prefix + probed owner/repo + virtual path), stricter assertion (a spoofed host containing 'art.example.com' as a substring would no longer pass), and CodeQL goes quiet. Co-authored-by: chkp-roniz <35386615+chkp-roniz@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shepherd: ready to mergeThis PR has been shepherded to convergence. All wave-1 CEO follow-ups folded into this branch; CI is fully green on the latest push. Folded follow-ups (5 / 5)
Mutation-break gateConfirmed on the new INCONCLUSIVE-hint trap: removing the Lint evidenceCI-mirror chain silent locally on
CI evidenceAll required checks pass on CodeQL recovery (1 iteration): wave-1 added substring URL assertions in the new integration test, which tripped Copilot signals0 actionable inline comments from Copilot since the last shepherd checkpoint ( DeferredNone. All wave-1 follow-ups were in-scope quality-bar items (tests, docs, CHANGELOG, ergonomics) and folded into this branch. AuthorshipEach new commit carries dual |
… mergeability table Refactor the batch-bug-shepherd skill into a single shepherd-driver convergence loop that closes four production gaps surfaced by the in-flight bug-queue sweep: 1. Recommendation-fold loop. Every panel CEO follow-up and Copilot inline review item is run through assets/fold-vs-defer-rubric.md and folded unless it crosses the PR's stated scope. Default is fold; defer is the scope-creep exception with a one-line scope_boundary_crossed note. 2. Copilot PR review address loop. Phase X.0 fetches copilot-pull-request-reviewer[bot] review per assets/copilot-classification-prompt.md, classifies each item LEGIT/NOT-LEGIT, and folds LEGIT into the same iteration. 2-round cap on Copilot fetches. 3. Post-push CI verification loop. gh pr checks --watch after every push, with assets/ci-recovery-checklist.md bucketing failures (lint / test / infra / unknown) under a 3-iteration cap. 4. Orchestrator ownership signal. Assigns the shepherd actor and applies status/shepherding on pickup; the label is cleared on terminal. New asset assets/shepherd-driver-prompt.md replaces the old shepherd-prompt / completion-prompt split. New supporting assets: fold-vs-defer-rubric.md, copilot-classification-prompt.md, ci-recovery-checklist.md, strategic-alignment-prompt.md, conflict-resolution-prompt.md, progress-diagram.md. New references/ directory with mergeability-gate.md and strategic-alignment-gate.md. Genesis design record in design.md. Mergeability status table (new in this commit). Shepherd-driver step X.8 captures a per-PR mergeability snapshot via gh pr view <n> --json mergeable,mergeStateStatus,statusCheckRollup immediately after the last push. The snapshot lands as a one-row table in the PR advisory comment (final-report-template.md PR ADVISORY COMMENT block) and is aggregated by the orchestrator at saga-end into a Mergeability status table in the FINAL REPORT block (PR, head SHA, CEO stance, outer iterations, folds, deferrals, Copilot rounds, CI status, mergeable, mergeStateStatus, notes). verdict-schema.json grows four optional completion-return fields: head_sha, mergeable, merge_state_status, ci_status. Validated on the wave-2 shepherd run that drove PRs #1472, #1512, #1513, #1514, #1515, #1516 to advisory-terminal. PR #1514 hit 4 outer iterations with 11 folds + 1 deferral, exercising the fold-by-default discipline at the cap. CHANGELOG entry under [Unreleased] / Added. Lint notes: this commit touches NO Python (.agents/ skill files are markdown + JSON + CHANGELOG markdown). The only applicable lint gates are the ASCII guard and bash scripts/lint-auth-signals.sh, both silent. ruff / pylint / ruff format are skipped per .apm/instructions/linting.instructions.md scope (src/ tests/ only). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… + mergeability table (#1518) * feat(batch-bug-shepherd): recommendation-fold loop, Copilot+CI gates, mergeability table Refactor the batch-bug-shepherd skill into a single shepherd-driver convergence loop that closes four production gaps surfaced by the in-flight bug-queue sweep: 1. Recommendation-fold loop. Every panel CEO follow-up and Copilot inline review item is run through assets/fold-vs-defer-rubric.md and folded unless it crosses the PR's stated scope. Default is fold; defer is the scope-creep exception with a one-line scope_boundary_crossed note. 2. Copilot PR review address loop. Phase X.0 fetches copilot-pull-request-reviewer[bot] review per assets/copilot-classification-prompt.md, classifies each item LEGIT/NOT-LEGIT, and folds LEGIT into the same iteration. 2-round cap on Copilot fetches. 3. Post-push CI verification loop. gh pr checks --watch after every push, with assets/ci-recovery-checklist.md bucketing failures (lint / test / infra / unknown) under a 3-iteration cap. 4. Orchestrator ownership signal. Assigns the shepherd actor and applies status/shepherding on pickup; the label is cleared on terminal. New asset assets/shepherd-driver-prompt.md replaces the old shepherd-prompt / completion-prompt split. New supporting assets: fold-vs-defer-rubric.md, copilot-classification-prompt.md, ci-recovery-checklist.md, strategic-alignment-prompt.md, conflict-resolution-prompt.md, progress-diagram.md. New references/ directory with mergeability-gate.md and strategic-alignment-gate.md. Genesis design record in design.md. Mergeability status table (new in this commit). Shepherd-driver step X.8 captures a per-PR mergeability snapshot via gh pr view <n> --json mergeable,mergeStateStatus,statusCheckRollup immediately after the last push. The snapshot lands as a one-row table in the PR advisory comment (final-report-template.md PR ADVISORY COMMENT block) and is aggregated by the orchestrator at saga-end into a Mergeability status table in the FINAL REPORT block (PR, head SHA, CEO stance, outer iterations, folds, deferrals, Copilot rounds, CI status, mergeable, mergeStateStatus, notes). verdict-schema.json grows four optional completion-return fields: head_sha, mergeable, merge_state_status, ci_status. Validated on the wave-2 shepherd run that drove PRs #1472, #1512, #1513, #1514, #1515, #1516 to advisory-terminal. PR #1514 hit 4 outer iterations with 11 folds + 1 deferral, exercising the fold-by-default discipline at the cap. CHANGELOG entry under [Unreleased] / Added. Lint notes: this commit touches NO Python (.agents/ skill files are markdown + JSON + CHANGELOG markdown). The only applicable lint gates are the ASCII guard and bash scripts/lint-auth-signals.sh, both silent. ruff / pylint / ruff format are skipped per .apm/instructions/linting.instructions.md scope (src/ tests/ only). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(skills/bbs): backfill genesis evals plan + real-task refinement evidence Backfills the genesis Step 6 EVALS PLAN and Step 8 EVALS GATE that the structural refactor (PR #1518) shipped without. Per genesis the EVALS GATE blocks any future shipping of skill-body changes. Adds three structured-input content evals that exercise the load- bearing decision policies introduced by the v2 refactor: * fold-vs-defer-panel -- Phase X.2 rubric application on a panel CEO ship_with_followups return (3 fold, 1 defer with scope_boundary_crossed). * copilot-classification-and-fold -- Phase X.0 LEGIT/NOT-LEGIT classification on 5 inline comments (4 LEGIT folded, 1 NOT-LEGIT dismissed with rationale). * ci-recovery-lint-bucket -- Phase X.6 bucket-1 lint recovery via ruff format + push + watch re-entry, cap 3. Each scenario ships with_skill and without_skill fixtures and a regex rubric scored by the existing scripts/run_evals.py runner. All five content scenarios + the trigger val split pass: triggers val: 1.0 should-fire, 1.0 should-not-fire content delta_anchors: 5, 7, 7, 7, 8 (gate: >=1) Adds evals/real-task-refinement.md capturing the wave-1 (v1 SKILL.md) vs wave-2 (v2 SKILL.md) comparison that drove the default-fold + Copilot-first-class + CI-recovery-first-class edits, plus the iter-4 #1513 CI-infra rollback as a positive trace of the new bucket-3 recovery path. Adds an Evals section near the bottom of SKILL.md pointing at evals/ and stating the EVALS GATE for future skill-body changes. ASCII guard clean. auth-signals lint clean. Scope: skill-only; install.ps1 and tests/unit/install/test_windows_shim_template.py remain untracked (they belong to PR #1512, not here). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Deps routed through an Artifactory VCS proxy can sit at arbitrary subgroup depth when the upstream is GitLab (e.g.
group/subgroup/project). Parse-time can't tell where the repo path ends and the in-repo virtual sub-path begins -- the previous code disambiguated by hard-coded directory markers (skills/,prompts/, ...). This PR replaces the heuristic with a deterministic install-time probe.host/artifactory/key/owner/repo/...).owner/repo/...) underPROXY_REGISTRY_URL+PROXY_REGISTRY_ONLY=1._resolve_artifactory_boundarywalks candidate(owner, repo, virtual_path)splits shallow-first and HEAD-probes each candidate's archive URL on the proxy; the first split that responds 2xx-3xx wins. Mirrors the native GitLab resolver pattern (_try_resolve_gitlab_direct_shorthand) but uses the archive URL itself as the existence signal, so no separate metadata API is needed.Key invariants:
allow_redirects=Falseon the HEAD probe means the Bearer token can't follow a redirect off the proxy host.PROXY_REGISTRY_TOKENfromRegistryConfig(the dep's logical host isgithub.combut the request goes to the proxy).Changes
apm_cli/install/artifactory_resolver.py— the deterministic probe.apm_cli/utils/github_host.py—iter_artifactory_boundary_candidates(mirrorsiter_gitlab_direct_shorthand_boundary_candidates);parse_artifactory_pathsimplified to a structural rule;build_artifactory_archive_urluses the basename for nested GitLab archive filenames.apm_cli/models/dependency/reference.py— removes_VIRTUAL_PATH_ROOT_SEGMENTS; addsfrom_artifactory_boundary_probe; simplifies_bare_shorthand_repo_segment_countto default-deep + structural file-extension rule;_validate_url_repo_pathnow strips theartifactory/<key>prefix from URL-form deps so round-trip doesn't double-prefix download URLs.apm_cli/install/package_resolution.py— wires the resolver alongside the GitLab probe; renames the second tuple slot todirect_virtual_resolvedsince it now covers both probes.apm_cli/commands/install.py— imports the resolver.apm_cli/deps/artifactory_orchestrator.py—_split_owner_repofolds subgroup segments into the repo slug instead of dropping them.tests/unit/test_artifactory_support.py— new tests for the iterator, the resolver (mocked HEAD), Mode 2 path, and the 401-vs-404 distinction; existing tests updated to reflect the parse-time-shallow + install-time-authoritative model.docs/src/content/docs/enterprise/registry-proxy.md— new "Nested-group repos" section + "Explicit Artifactory FQDN: deterministic boundary probe" subsection.Test plan
pytest tests/unit/test_artifactory_support.py(Artifactory unit suite).pytest tests/unit/install/ tests/unit/deps/ tests/unit/marketplace/ tests/unit/commands/test_install_resolve_refs.py(resolver + install + marketplace tests).ruff check src/apm_cli/install/artifactory_resolver.py src/apm_cli/install/package_resolution.py src/apm_cli/utils/github_host.py src/apm_cli/models/dependency/reference.py src/apm_cli/commands/install.py tests/unit/test_artifactory_support.py.apm install <host>/artifactory/<key>/<group>/<subgroup>/<repo>#<ref>.PROXY_REGISTRY_URL+PROXY_REGISTRY_ONLY=1:apm install <group>/<subgroup>/<repo>#<ref>.apm install <plugin>@<marketplace>resolves and integrates without falling back to github.com.🤖 Generated with Claude Code