diff --git a/src/generate_repo_overview/collector/reference_integration.py b/src/generate_repo_overview/collector/reference_integration.py index 5be5b86..9a3e45b 100644 --- a/src/generate_repo_overview/collector/reference_integration.py +++ b/src/generate_repo_overview/collector/reference_integration.py @@ -4,7 +4,7 @@ import re from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Protocol, cast from urllib.parse import urlsplit from repo_cache import default_cache_directory @@ -42,6 +42,14 @@ class GitRefComparison: left_ahead_by: int +class _CompletableCommit(Protocol): + """Minimal PyGithub commit interface needed to validate a lazy ref.""" + + sha: str + + def complete(self) -> object: ... + + def fetch_reference_integration_repository_names( *, reference_integration_repository: object | None, @@ -216,10 +224,14 @@ def resolve_reference_integration_pin( version_refs.append(f"v{pin.version}") for version_ref in version_refs: try: - commit = get_commit(version_ref) + commit = cast("_CompletableCommit", get_commit(version_ref)) + # The collector uses PyGithub's lazy mode. An incomplete + # Commit can expose the requested ref as ``sha`` before + # GitHub has confirmed that the ref exists. + commit.complete() + resolved_sha = commit.sha except Exception: continue - resolved_sha = getattr(commit, "sha", None) if isinstance(resolved_sha, str) and resolved_sha: pin_ref = resolved_sha break diff --git a/tests/test_repo_overview.py b/tests/test_repo_overview.py index eda87b5..d9475f5 100644 --- a/tests/test_repo_overview.py +++ b/tests/test_repo_overview.py @@ -1154,6 +1154,52 @@ def test_known_good_pin_parser_ignores_malformed_json() -> None: ) +def test_version_pin_retries_v_prefixed_tag_after_plain_version_is_rejected() -> None: + """Resolve a module version through a real tag before comparing Git commits. + + ``known_good.json`` stores ``8.3.0``, while this repository's Git tag is + ``v8.3.0``. PyGithub's lazy commit initially echoes either requested ref as + ``sha``; completion must reject the plain version and return the tag's real + commit SHA before the resolver accepts it. + """ + module_version = "8.3.0" + github_tag = "v8.3.0" + verified_commit_sha = "0123456789abcdef0123456789abcdef01234567" + attempted_refs: list[str] = [] + + class LazyCommit: + """Mirror PyGithub: before completion, sha still echoes the requested ref.""" + + def __init__(self, requested_ref: str) -> None: + self.sha = requested_ref + + def complete(self) -> None: + if self.sha != github_tag: + raise LookupError(f"No Git ref named {self.sha}") + self.sha = verified_commit_sha + + class Repository: + def get_commit(self, requested_ref: str) -> LazyCommit: + attempted_refs.append(requested_ref) + return LazyCommit(requested_ref) + + version_pin = reference_integration.KnownGoodPin( + module="score_docs_as_code", + group="Core", + branch="main", + version=module_version, + commit_hash=None, + ) + + assert ( + reference_integration.resolve_reference_integration_pin( + Repository(), version_pin + ) + == verified_commit_sha + ) + assert attempted_refs == [module_version, github_tag] + + def test_latest_release_details_keep_divergence_from_single_comparison( monkeypatch: pytest.MonkeyPatch, ) -> None: