fix(install): lowercase host in fallback-port-warned dedup key#815
Conversation
``_fallback_port_warned`` keys on ``(dep_host, repo_url_base, dep_port)``. DNS hostnames are case-insensitive per RFC 4343, so two deps that differ only in host casing should collapse to one cross-protocol fallback warning, not two. Normalise the host axis on insertion, matching the ``host.lower() if host else host`` idiom already used by ``AuthResolver._cache`` at ``core/auth.py:224``. The inline NOTE at the construction site was a placeholder for this follow-up and is removed with the fix. Trigger surface is narrower than the issue's manifest-based repro suggests: ``DependencyReference.parse`` already lowercases the host on URL-string inputs, so manifest-driven deps are shielded by the parser. Code paths that build ``DependencyReference`` via the dataclass constructor (bypassing the parser) still leak casing into the dedup key, which is the surface the fix defends. The new regression test builds its deps that way to exercise the fix. Closes microsoft#800
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 fixes protocol-fallback warning deduplication by normalizing dependency hostnames to lowercase (DNS hostnames are case-insensitive), and adds regression/boundary tests to prevent the warning from duplicating when host casing differs.
Changes:
- Lowercase
dep_hostwhen forming_fallback_port_warneddedup keys in the fallback warning path. - Remove the old inline NOTE (placeholder) and codify the behavior via unit tests.
- Add a boundary test ensuring repo-path differences still produce distinct warnings.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_protocol_fallback_warning.py | Adds regression + boundary tests for host-casing normalization in fallback warning deduping. |
| src/apm_cli/deps/github_downloader.py | Normalizes host casing in _fallback_port_warned keys to prevent duplicate warnings. |
Copilot's findings
- Files reviewed: 2/2 changed files
- Comments generated: 3
PR #815 (from edenfunf/apm fork) had the panel-review label applied but every job skipped because gh-aw's default fork policy on pull_request blocks fork-originated runs entirely (condition: github.event.pull_request.head.repo.id == github.repository_id). Add forks: ["*"] to remove the repo-id gate. The trust gate is the panel-review label itself -- only write-access maintainers can apply labels, so a malicious fork PR cannot self-trigger the workflow. gh-aw's safe-outputs isolation handles the fork-token problem (the write-scoped publisher job runs in base-repo context, separate from the read-only agent job). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Rs (#836) The panel review workflow was running on every push to a labelled PR because the activation gate only enforced the label check on the 'labeled' action; 'synchronize' bypassed it entirely. This burned agent quota and produced redundant verdicts. Changes: 1. Drop 'synchronize' from pull_request types. The panel runs once when a maintainer applies 'panel-review'. To re-run after addressing findings, remove + re-apply the label. 2. Add workflow_dispatch with a pr_number input. This is the GitHub.com- and GHES-compatible path for reviewing PRs from forks, where 'pull_request' does NOT pass repository secrets (COPILOT_GITHUB_TOKEN etc.) and gh-aw blocks 'pull_request_target' on public repos. workflow_dispatch always runs in the base/trusted context with full secrets, regardless of where the PR head lives. 3. Wire ${{ inputs.pr_number }} as the fallback for github.event.pull_request.number throughout the prompt body so both event paths target the same PR. Refs https://github.com/microsoft/apm/actions/runs/24752294360 (fork PR #815 lost secrets and failed). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #836 only dropped synchronize and added workflow_dispatch; the pull_request trigger still skips fork PRs because GitHub does not pass secrets to fork-PR workflows. Result: labelling a fork PR (e.g. #815) silently failed at the secret validation step. Switch to pull_request_target, which runs in the base repo context with full secrets regardless of where the PR head lives. Combined with roles: [admin, maintainer, write] (auto-injected by gh-aw), the trust gate becomes 'only repo maintainers can trigger' -- which matches the existing model that applying the panel-review label already requires write access. Why pull_request_target is safe for this workflow despite the well-known pwn-request pattern: - permissions are read-only (no write to contents / actions) - no actions/checkout of PR head; only gh pr view / gh pr diff (returns inert text) - imports pinned to microsoft/apm#main (panel skill + persona definitions are trusted, not from the PR) - single write surface is safe-outputs.add-comment (max 1) gh-aw does not expose names: on pull_request_target, so label-name filtering moves into the prompt body (Step 0) -- a one-line bash guard that exits cleanly when any non-panel-review label fires. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#837) PR #836 only dropped synchronize and added workflow_dispatch; the pull_request trigger still skips fork PRs because GitHub does not pass secrets to fork-PR workflows. Result: labelling a fork PR (e.g. #815) silently failed at the secret validation step. Switch to pull_request_target, which runs in the base repo context with full secrets regardless of where the PR head lives. Combined with roles: [admin, maintainer, write] (auto-injected by gh-aw), the trust gate becomes 'only repo maintainers can trigger' -- which matches the existing model that applying the panel-review label already requires write access. Why pull_request_target is safe for this workflow despite the well-known pwn-request pattern: - permissions are read-only (no write to contents / actions) - no actions/checkout of PR head; only gh pr view / gh pr diff (returns inert text) - imports pinned to microsoft/apm#main (panel skill + persona definitions are trusted, not from the PR) - single write surface is safe-outputs.add-comment (max 1) gh-aw does not expose names: on pull_request_target, so label-name filtering moves into the prompt body (Step 0) -- a one-line bash guard that exits cleanly when any non-panel-review label fires. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel VerdictDisposition: APPROVE (with one required action before merge) Per-persona findingsPython Architect: Clean, minimal fix -- 5 lines of production code, 114 lines of tests. The ternary CLI Logging Expert: No changes to output routing, CommandLogger lifecycle, or DiagnosticCollector. The fix correctly reduces duplicate DevX UX Expert: No command surface, flag, or help-text changes. Users on Bitbucket Datacenter setups (the primary real-world trigger) will now see exactly one port-mismatch warning when the same server appears with mixed casing across deps -- less noise, same information. No docs update needed; this is internal dedup behavior not surfaced in the CLI reference. The PR description is thorough and explains the parser-path shield clearly -- if someone files a doc issue, the existing fallback-warning docs are accurate. Supply Chain Security Expert: The normalization is applied ONLY to the dedup key used for warning emission. It has no effect on auth resolution ( Auth Expert: The PR description correctly observes the alignment with OSS Growth Hacker: No conversion-surface impact. This PR is a positive signal: it demonstrates the review-panel loop working end-to-end (#789 panel raised it, #815 closes it). That story angle is worth a one-liner in the release note when 0.9.x ships ("Responsive follow-up to community review: ..."). CEO arbitrationThis is a textbook follow-up PR: small, well-tested, clearly motivated by a documented prior finding, and zero risk of regression. All seven personas are aligned -- no disagreements to arbitrate. The one gap is a missing Required actions before merge
Optional follow-ups
|
danielmeppiel
left a comment
There was a problem hiding this comment.
Per agentic panel review
Description
Follow-up A from the PR #789 review panel: the
_fallback_port_warneddedup set keys on(dep_host, repo_url_base, dep_port), but DNS hostnames are case-insensitive per RFC 4343. Two deps that differ only in host casing should collapse to one cross-protocol fallback warning, not two.Normalise the host axis on insertion, matching the
host.lower() if host else hostidiom already used byAuthResolver._cacheatcore/auth.py:224. This keeps cache-identity semantics uniform across the module.The inline
NOTEat the construction site was a placeholder for this follow-up and is removed with the fix.Trigger surface note
While writing the regression test I noticed the issue's manifest-based repro is actually shielded by the URL parser:
DependencyReference.parselowercases the host on URL-string inputs, so twoapm.ymlentries that differ only in host casing collapse to one identity before they ever reach the dedup key. The casing leak only survives when aDependencyReferenceis built via the dataclass constructor (bypassing the parser), which is the surface the new test exercises by constructing its deps that way.The fix is still worth landing as defensive alignment with the
AuthResolver._cacheconvention -- and it becomes the only line of defence if any future refactor loosens the parser-side normalisation or adds a non-parser construction path.Fixes #800
Type of change
Testing
Two regression tests in
tests/unit/test_protocol_fallback_warning.py:test_warning_dedup_normalises_hostname_casing-- two deps withbitbucket.example.com/Bitbucket.Example.com, same port + repo, built via the dataclass constructor. Asserts exactly one[!]warning. Temporarily reverting the fix turns this red withgot 2:naming both casings verbatim, then green once reapplied -- the test bites on the regression it is named after.test_host_normalisation_does_not_collapse_distinct_repos-- guards against over-normalisation: same host+port but different repo paths still warn twice even when host casing differs. Stays green in both directions -- it is a boundary test, not a regression test.pytest tests/unit -q-> 4518 passed, zero new failures.Manual verification with the same two-casings scenario via
_clone_with_fallbackin a REPL: without the fix, two[!] Custom port ...lines; with the fix, one.