Skip to content

Commit 33696e1

Browse files
danielmeppieljstar0Copilot
authored
fix(ado): preserve lock coordinates for outdated and update (#2226)
* fix(cli): warn when Codex agent tool scope is dropped Signed-off-by: King Star <mcxin.y@gmail.com> * test(cli): prove Codex scope loss is diagnosed Co-authored-by: King Star <54024410+jstar0@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b3ef1ddd-3e6e-4516-b1ef-147d6d3b7381 * fix(ado): preserve lock consumer coordinates Persist canonical Azure DevOps coordinates across lock serialization and reconstruction, route semver ref checks through the canonical ADO URL, and add real Consume lifecycle coverage for outdated, update, audit, and convergence.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 1117b9a1-68ed-4d08-8f91-04694a98ce5f * docs(changelog): link ADO lock fix PR Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 1117b9a1-68ed-4d08-8f91-04694a98ce5f * fix(cli): tighten Codex scope diagnostics Co-authored-by: King Star <54024410+jstar0@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b3ef1ddd-3e6e-4516-b1ef-147d6d3b7381 * fix(deps): honor SSH for semver ref resolution Route git-source semver tag enumeration through the canonical transport selector and preserve the chosen transport across install and update. Add a real-binary lifecycle contract with one-protocol rewrites, transport tracing, strict failure, lock provenance, and convergence evidence. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * docs(changelog): link semver transport PR Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * chore(ci): refresh spec conformance context Signed-off-by: King Star <mcxin.y@gmail.com> * fix: fold panel findings (parse regression, dead isinstance, hostname, error messages) - reference.py parse() Phase 3: restore _validate_final_repo_fields so non-ADO virtual-extension validation still runs; canonical_ado_coordinates is for consumers with already-validated input only (lockfile, outdated) - lockfile.py: remove dead isinstance(dep_ref, DependencyReference) guard; function signature already requires DependencyReference, and tests now provide proper Mocks with ado_* attributes set to None - ref_resolver.py: normalize hostname comparison to .lower() per RFC 3986 3.2.2 (urlparse always lowercases); OfflineMissError now uses cache_key instead of owner_repo to match the actual cache lookup key; error message now shows expected vs actual hostname with actionable repair hint - test_dev_dependencies.py: fix Mock to set ado_organization/project/repo=None so the test double correctly represents a non-ADO DependencyReference apm-spec-waiver: restores ADO lock coordinate fidelity lost in prior impl; no new spec semantics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(deps): guard resolver transport cache identity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * fix: trim reference.py comment to stay within 2100-line guardrail Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: update critical_suite.toml module count ratchet to 13 Two new modules were added across PRs (#2217 and this PR): - test_ado_lock_consumer_contract.py (this PR) - test_virtual_claude_skill_lock_convergence.py (#2217 merged to main) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(deps): preserve explicit HTTP ref transport Fold Copilot review by carrying the selected plain-HTTP scheme into semver enumeration, centralizing the URL builder, suppressing all auth channels on HTTP, and tightening cache-key typing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * spec(targets): require visible lossy-agent diagnostics Co-authored-by: King Star <54024410+jstar0@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b3ef1ddd-3e6e-4516-b1ef-147d6d3b7381 * chore: record OpenAPM spec waiver apm-spec-waiver: bug fix restores existing transport preference semantics; no new normative requirement Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * refactor(deps): keep semver transport fix SSH-scoped Remove the post-review explicit-HTTP expansion while retaining precise cache typing and concise changelog coverage. Issue #2184 remains bounded to explicit SSH/prefer-ssh behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * test(architecture): guard diagnostic ASCII ownership Co-authored-by: King Star <54024410+jstar0@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b3ef1ddd-3e6e-4516-b1ef-147d6d3b7381 * test(architecture): close sanitizer guard bypass Co-authored-by: King Star <54024410+jstar0@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b3ef1ddd-3e6e-4516-b1ef-147d6d3b7381 * test(architecture): reject raw diagnostic identity flow Co-authored-by: King Star <54024410+jstar0@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b3ef1ddd-3e6e-4516-b1ef-147d6d3b7381 * fix(architecture): guard OpenCode diagnostic attribution Co-authored-by: King Star <54024410+jstar0@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b3ef1ddd-3e6e-4516-b1ef-147d6d3b7381 * test(spec): reconcile stacked conformance totals Preserve both req-rs-016 and req-tg-006 after stacking current main with the accepted Codex and SSH predecessor heads.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 1117b9a1-68ed-4d08-8f91-04694a98ce5f * docs(spec): reconcile stacked diagnostic requirement Fold spec-guardian count, cross-reference, and canonical visibility wording after stacking req-tg-006 on req-rs-016. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * docs(spec): fold stacked guardian followups Clarify req-tg-006 without adding normative statements, align its section metadata at 8.5.1, and restore the complete governance enumeration.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 1117b9a1-68ed-4d08-8f91-04694a98ce5f * test(deps): close stacked SSH transport guardrails Align unauthenticated ref enumeration with the canonical empty-token sentinel and assert explicit SSH URL rendering for custom ports. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * fix(deps): preserve generic-host token contract Retain the established token=None call shape for unauthenticated HTTPS while keeping the new custom-port SSH regression guard. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * fix(ux): clarify stacked resolver diagnostics Use outcome-led agent conversion wording and make the Azure DevOps SSH repair hint name accepted repository coordinate shapes.\n\napm-spec-waiver: wording-only implementation fold; req-tg-006 semantics are already cited and unchanged\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 1117b9a1-68ed-4d08-8f91-04694a98ce5f * docs(changelog): drop merged predecessor duplicate Keep the post-#2186 PR diff scoped to the SSH semver fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * fix: route ADO ref resolution through canonical coordinates Addresses shepherd panel follow-ups by removing the ADO SSH owner_repo split, validating supplied ADO remote URLs against canonical dependency coordinates, and making corrupted ADO lock guidance actionable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(deps): fold SSH semver review follow-ups Tighten the SSH semver transport path by removing unrelated spec edits, polishing changelog attribution, typing the resolver cache boundary, scrubbing SSH askpass, and adding a regression trap for the ADO SSH bearer-retry guard. Addresses apm-review-panel follow-ups for PR #2229. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: keep dependency reference under line guard Shortens the ADO coordinate recovery message introduced by the shepherd fold so the CI file-length guard remains green without changing behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(deps): fence SSH semver lifecycle network Route frozen-binary HTTP metadata probes through a closed loopback proxy and disable persistent cache so the one-transport Git rewrite contract cannot reach shared or external state. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * docs(changelog): deduplicate SSH semver fix Keep the attributed #2229 entry as the single Unreleased record surfaced by the advisory panel. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * docs(changelog): align SSH fix entry format Keep the issue reference in prose and reserve the trailing parenthetical for PR #2229. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 324c57ea-aa78-4f0e-a656-d12f966cedff * test: harden SSH semver transport contract Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e86c834c-e4a2-4354-825f-2a51cfb99e0e * test: isolate SSH semver resolver tier Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e86c834c-e4a2-4354-825f-2a51cfb99e0e * docs(changelog): preserve single SSH fix entry Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e86c834c-e4a2-4354-825f-2a51cfb99e0e * refactor(lock): derive ADO transport coordinates Keep provider-specific Azure DevOps coordinates transient under DependencyReference and persist only generic host plus repository identity in the lockfile. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1117b9a1-68ed-4d08-8f91-04694a98ce5f * refactor(models): isolate provider coordinates Keep DependencyReference under the source length guard while retaining canonical provider-coordinate derivation behind its model interface. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1117b9a1-68ed-4d08-8f91-04694a98ce5f * fix(ado): name resolver failures Fold the final review findings by attaching repository identity to ADO resolver errors and removing the duplicate inherited SSH changelog entry. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1117b9a1-68ed-4d08-8f91-04694a98ce5f * test(ado): fold lock replay follow-ups Address panel follow-ups for PR #2226 by tightening the provider-coordinate mixin contract, making the mismatch recovery text command-shaped, documenting the RefResolver remote_url cache contract, and adding a regression trap for mismatched transient ADO coordinates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Signed-off-by: King Star <mcxin.y@gmail.com> Co-authored-by: King Star <mcxin.y@gmail.com> Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 796e229 commit 33696e1

19 files changed

Lines changed: 808 additions & 91 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020

2121
### Fixed
2222

23+
- Azure DevOps dependencies now report real latest versions in `apm outdated`
24+
and resolve correctly during bounded `apm update`, deriving transport
25+
coordinates from generic `host` and `repo_url` identity without
26+
provider-specific lock fields. (closes #2197; #2226)
2327
- Govern policy cache freshness now honors the effective policy's `cache.ttl`;
2428
bounded property coverage protects all 39 enforceable fields, cold/warm parity,
2529
canonical serialization, and last-good bytes after malformed refreshes. (#2235)

docs/src/content/docs/reference/lockfile-spec.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ Each item in `dependencies` describes one resolved package.
182182
Fields are emitted only when set. A minimal entry is just `repo_url` plus
183183
`resolved_commit`.
184184

185+
Azure DevOps uses the same generic `host` and `repo_url` fields as every other
186+
Git provider. APM derives its transient organization, project, and repository
187+
coordinates when reconstructing the dependency reference; no ADO-specific
188+
fields are persisted.
189+
185190
## Lockfile identity keys
186191

187192
Lockfile dependency keys keep `github.com` implicit for migration stability:

scripts/lint-architecture-boundaries.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,18 @@ if ! grep -q 'transport_plan = transport_selector.select(' "$semver_transport_ro
562562
violations=$((violations + 1))
563563
fi
564564

565+
echo "[*] AC14: ADO lock-coordinate authority"
566+
if ! grep -q 'with_derived_provider_coordinates' \
567+
src/apm_cli/deps/lockfile.py \
568+
|| grep -Eq 'ado_(organization|project|repo)' src/apm_cli/deps/lockfile.py \
569+
|| ! grep -q 'DependencyReference.canonical_ado_coordinates' \
570+
src/apm_cli/marketplace/ref_resolver.py \
571+
|| grep -Eq '(self\.)?repo_url\.split\(' src/apm_cli/deps/lockfile.py \
572+
|| grep -Eq 'owner_repo\.split\(' src/apm_cli/marketplace/ref_resolver.py; then
573+
echo "[x] ADO coordinates must be derived by DependencyReference, never persisted"
574+
violations=$((violations + 1))
575+
fi
576+
565577
if [ "$violations" -gt 0 ]; then
566578
echo "[x] $violations architecture boundary rule(s) failed"
567579
exit 1

src/apm_cli/deps/git_semver_resolver.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ def resolve(
219219
owner_repo: str,
220220
package_name: str,
221221
constraint: str,
222+
remote_url: str | None = None,
222223
tag_patterns: Sequence[str] = DEFAULT_TAG_PATTERNS,
223224
now_iso: str | None = None,
224225
) -> GitSemverResolution:
@@ -235,6 +236,8 @@ def resolve(
235236
constraint:
236237
Semver range (e.g. ``"^1.2.0"``, ``"~2.1"``, ``">=1.0 <2.0"``)
237238
or exact version (``"1.2.3"``).
239+
remote_url:
240+
Canonical ADO URL supplied by ``DependencyReference``.
238241
tag_patterns:
239242
Ordered tag patterns to try. Defaults to
240243
:data:`DEFAULT_TAG_PATTERNS`. A bare-version fallback
@@ -256,7 +259,10 @@ def resolve(
256259
When no tag on the remote satisfies the constraint after
257260
trying the default patterns and the bare-version fallback.
258261
"""
259-
refs = self._ref_resolver.list_remote_refs(owner_repo)
262+
if remote_url is None:
263+
refs = self._ref_resolver.list_remote_refs(owner_repo)
264+
else:
265+
refs = self._ref_resolver.list_remote_refs(owner_repo, remote_url=remote_url)
260266
primary_patterns = tuple(tag_patterns)
261267
# Two-pass: try the author-supplied patterns first; only if they
262268
# find zero candidates do we widen to the bare-version fallback.

src/apm_cli/deps/lockfile.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,11 @@ def from_dict(cls, data: dict[str, Any]) -> LockedDependency:
403403
# legacy migration key handled above
404404
"deployed_skills",
405405
}
406-
unknown_fields = {k: v for k, v in data.items() if k not in _known_keys}
406+
unknown_fields = {
407+
k: v
408+
for k, v in data.items()
409+
if k not in _known_keys and not DependencyReference.is_transient_provider_field(k)
410+
}
407411

408412
return cls(
409413
repo_url=data["repo_url"],
@@ -532,6 +536,7 @@ def from_dependency_ref(
532536
else:
533537
resolved_ref_val = dep_ref.reference
534538

539+
dep_ref.validate_provider_coordinates()
535540
if registry_resolution is not None:
536541
version_value = registry_resolution.version
537542
elif git_semver_resolution is not None:
@@ -617,7 +622,7 @@ def to_dependency_ref(self) -> DependencyReference:
617622
source=self.source,
618623
skill_subset=sorted(self.skill_subset) if self.skill_subset else None,
619624
target_subset=sorted(self.target_subset) if self.target_subset else None,
620-
)
625+
).with_derived_provider_coordinates()
621626

622627

623628
@dataclass

src/apm_cli/install/helpers/ref_reuse.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ def maybe_resolve_git_semver(
148148
owner_repo=owner_repo,
149149
package_name=package_name,
150150
constraint=constraint,
151+
remote_url=(
152+
dep_ref.to_github_url()
153+
if transport_scheme == "https" and dep_ref.is_azure_devops()
154+
else None
155+
),
151156
)
152157

153158

src/apm_cli/marketplace/ref_resolver.py

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import subprocess
2222
import threading
2323
import time
24+
import urllib.parse
2425
from dataclasses import dataclass
2526

2627
from ..utils.github_host import (
@@ -50,6 +51,49 @@
5051
_SHA_RE = re.compile(r"^[0-9a-f]{40}$")
5152

5253

54+
# Local import below avoids a dependency-model import cycle at module load time.
55+
def _ado_coordinates_from_owner_repo(
56+
*,
57+
host: str,
58+
owner_repo: str,
59+
) -> tuple[str, str, str]:
60+
"""Return validated ADO coordinates from the canonical dependency owner."""
61+
from apm_cli.models.dependency.reference import DependencyReference
62+
63+
try:
64+
return DependencyReference.canonical_ado_coordinates(host, owner_repo)
65+
except ValueError as exc:
66+
if "/_git/" in owner_repo:
67+
try:
68+
dep_ref = DependencyReference.parse(f"https://{host}/{owner_repo}")
69+
return DependencyReference.canonical_ado_coordinates(
70+
dep_ref.host,
71+
dep_ref.repo_url,
72+
)
73+
except ValueError:
74+
pass
75+
raise GitLsRemoteError(
76+
package=owner_repo,
77+
summary="Azure DevOps resolution requires org/project/repo coordinates.",
78+
hint=(
79+
"Re-add the dependency with the original Azure DevOps URL "
80+
"to regenerate the lock entry."
81+
),
82+
) from exc
83+
84+
85+
def _ado_remote_path_for_coordinates(
86+
organization: str,
87+
project: str,
88+
repo: str,
89+
) -> str:
90+
"""Return the canonical HTTPS path for ADO coordinates."""
91+
quoted_org = urllib.parse.quote(organization, safe="")
92+
quoted_project = urllib.parse.quote(project, safe="")
93+
quoted_repo = urllib.parse.quote(repo, safe="")
94+
return f"/{quoted_org}/{quoted_project}/_git/{quoted_repo}"
95+
96+
5397
@dataclass(frozen=True)
5498
class RemoteRef:
5599
"""A single ref returned by ``git ls-remote``."""
@@ -72,7 +116,7 @@ class _CacheEntry:
72116

73117

74118
class RefCache:
75-
"""In-memory cache keyed on ``owner/repo``.
119+
"""In-memory cache keyed on the effective remote identity.
76120
77121
TTL defaults to 5 minutes. Not thread-safe on its own; callers
78122
should use external synchronisation (``RefResolver`` does this via
@@ -201,7 +245,12 @@ def _remote_lock(self, owner_repo: str) -> threading.Lock:
201245
self._remote_locks[owner_repo] = threading.Lock()
202246
return self._remote_locks[owner_repo]
203247

204-
def _git_url_and_env(self, owner_repo: str) -> tuple[str, dict[str, str]]:
248+
def _git_url_and_env(
249+
self,
250+
owner_repo: str,
251+
*,
252+
remote_url: str | None = None,
253+
) -> tuple[str, dict[str, str]]:
205254
"""Build the remote URL and auth environment for one git operation."""
206255
use_ssh = self._transport_scheme == "ssh"
207256
requested_bearer = self._auth_scheme == "bearer"
@@ -215,20 +264,10 @@ def _git_url_and_env(self, owner_repo: str) -> tuple[str, dict[str, str]]:
215264
bearer = requested_bearer and ado_host and not use_ssh
216265
url_token = None if requested_bearer or use_ssh else self._token
217266
if use_ssh and ado_host:
218-
parts = owner_repo.split("/")
219-
if len(parts) == 4 and parts[2] == "_git":
220-
org, project, repo = parts[0], parts[1], parts[3]
221-
elif len(parts) == 3:
222-
org, project, repo = parts
223-
else:
224-
raise GitLsRemoteError(
225-
package=owner_repo,
226-
summary=(
227-
"Azure DevOps SSH resolution requires org/project/repo "
228-
f"coordinates; got '{owner_repo}'."
229-
),
230-
hint="Use a standard Azure DevOps dependency URL.",
231-
)
267+
org, project, repo = _ado_coordinates_from_owner_repo(
268+
host=self._host,
269+
owner_repo=owner_repo,
270+
)
232271
ssh_host = "ssh.dev.azure.com" if self._host == "dev.azure.com" else self._host
233272
url = build_ado_ssh_url(org, project, repo, host=ssh_host)
234273
elif use_ssh:
@@ -238,6 +277,40 @@ def _git_url_and_env(self, owner_repo: str) -> tuple[str, dict[str, str]]:
238277
port=self._port,
239278
user=self._ssh_user,
240279
)
280+
elif remote_url is not None:
281+
parsed_remote = urllib.parse.urlparse(remote_url)
282+
expected_ado_path = (
283+
_ado_remote_path_for_coordinates(
284+
*_ado_coordinates_from_owner_repo(host=self._host, owner_repo=owner_repo)
285+
)
286+
if ado_host
287+
else None
288+
)
289+
# urlparse lowercases hostname per RFC 3986 3.2.2; normalize both sides.
290+
if (
291+
not ado_host
292+
or parsed_remote.scheme != "https"
293+
or parsed_remote.hostname != self._host.lower()
294+
or parsed_remote.path != expected_ado_path
295+
or parsed_remote.username is not None
296+
or parsed_remote.password is not None
297+
or parsed_remote.query
298+
or parsed_remote.fragment
299+
):
300+
raise GitLsRemoteError(
301+
package=owner_repo,
302+
summary=(
303+
"The canonical remote URL does not match the configured host "
304+
"or Azure DevOps dependency coordinates."
305+
),
306+
hint=(
307+
"Re-add the dependency with the original Azure DevOps URL "
308+
"to regenerate the lock entry."
309+
),
310+
)
311+
# ADO HTTPS intentionally keeps credentials out of the URL; auth
312+
# is injected below through git http.extraheader.
313+
url = remote_url
241314
elif ado_host:
242315
url = f"https://{self._host}/{owner_repo}"
243316
else:
@@ -274,7 +347,12 @@ def _git_url_and_env(self, owner_repo: str) -> tuple[str, dict[str, str]]:
274347
env.update(build_authorization_header_git_env("Basic", credential))
275348
return url, env
276349

277-
def list_remote_refs(self, owner_repo: str) -> list[RemoteRef]:
350+
def list_remote_refs(
351+
self,
352+
owner_repo: str,
353+
*,
354+
remote_url: str | None = None,
355+
) -> list[RemoteRef]:
278356
"""Fetch all tags and heads from the configured Git host.
279357
280358
Results are cached; subsequent calls for the same remote return
@@ -284,6 +362,10 @@ def list_remote_refs(self, owner_repo: str) -> list[RemoteRef]:
284362
----------
285363
owner_repo:
286364
``"owner/repo"`` string (no host, no ``.git`` suffix).
365+
remote_url:
366+
Canonical ADO URL from ``DependencyReference.to_github_url``.
367+
Pass consistently for the same logical remote within one
368+
``RefResolver`` lifetime so cache identity stays aligned.
287369
288370
Returns
289371
-------
@@ -297,17 +379,18 @@ def list_remote_refs(self, owner_repo: str) -> list[RemoteRef]:
297379
GitLsRemoteError
298380
When the ``git ls-remote`` subprocess fails.
299381
"""
300-
lock = self._remote_lock(owner_repo)
382+
cache_key = remote_url or owner_repo
383+
lock = self._remote_lock(cache_key)
301384
with lock:
302385
# Check cache first
303-
cached = self._cache.get(owner_repo)
386+
cached = self._cache.get(cache_key)
304387
if cached is not None:
305388
return cached
306389

307390
if self._offline:
308-
raise OfflineMissError(package="", remote=owner_repo)
391+
raise OfflineMissError(package="", remote=cache_key)
309392

310-
url, env = self._git_url_and_env(owner_repo)
393+
url, env = self._git_url_and_env(owner_repo, remote_url=remote_url)
311394
try:
312395
result = subprocess.run(
313396
["git", "ls-remote", "--tags", "--heads", url],
@@ -329,9 +412,13 @@ def list_remote_refs(self, owner_repo: str) -> list[RemoteRef]:
329412
hint=f"Ensure git is installed and on PATH. Error: {exc}",
330413
)
331414

332-
fallback_refs = self._retry_rejected_ado_pat(result, owner_repo)
415+
fallback_refs = self._retry_rejected_ado_pat(
416+
result,
417+
owner_repo,
418+
remote_url=remote_url,
419+
)
333420
if fallback_refs is not None:
334-
self._cache.put(owner_repo, fallback_refs)
421+
self._cache.put(cache_key, fallback_refs)
335422
return fallback_refs
336423

337424
if result.returncode != 0:
@@ -355,13 +442,15 @@ def list_remote_refs(self, owner_repo: str) -> list[RemoteRef]:
355442
)
356443

357444
refs = _parse_ls_remote_output(result.stdout)
358-
self._cache.put(owner_repo, refs)
445+
self._cache.put(cache_key, refs)
359446
return refs
360447

361448
def _retry_rejected_ado_pat(
362449
self,
363450
result: subprocess.CompletedProcess,
364451
owner_repo: str,
452+
*,
453+
remote_url: str | None = None,
365454
) -> list[RemoteRef] | None:
366455
"""Retry one rejected ADO basic credential with an Azure CLI bearer."""
367456
eligible = (
@@ -396,7 +485,7 @@ def _bearer_op(bearer: str) -> list[RemoteRef]:
396485
git_env=bearer_env,
397486
)
398487
try:
399-
return resolver.list_remote_refs(owner_repo)
488+
return resolver.list_remote_refs(owner_repo, remote_url=remote_url)
400489
finally:
401490
resolver.close()
402491

0 commit comments

Comments
 (0)