diff --git a/dep_freshness/_tables.py b/dep_freshness/_tables.py index 79a75f0..9e0fea0 100644 --- a/dep_freshness/_tables.py +++ b/dep_freshness/_tables.py @@ -54,13 +54,15 @@ NODE_RELEASES: Final = "https://nodejs.org/dist/index.json" GRADLE_RELEASES: Final = "https://services.gradle.org/versions/current" # Asked in order; a 404 moves on. Google first because AndroidX and the # Android Gradle plugin exist nowhere else, and Central answers 404 fast. -# JitPack last: it serves tag metadata for `com.github.*` artifacts that -# were never published anywhere else (PhotoView, DirectionalViewPager, ...). +# JitPack is not on the list: since 2026-09 its maven-metadata answers +# HTTP 401 "No access token" (artifact downloads still work), so `com.github.*` +# artifacts published nowhere else (PhotoView, DirectionalViewPager, ...) +# are answered from the source repo's tags instead -- which is all JitPack +# ever built. MAVEN_REPOS: Final = ( "https://dl.google.com/dl/android/maven2/", "https://repo1.maven.org/maven2/", "https://plugins.gradle.org/m2/", - "https://jitpack.io/", ) UTILS_TAG_REMOTE: Final = "https://github.com/kuhyx/utils" # JitPack coordinates: `com.github.:` or diff --git a/dep_freshness/registries/maven.py b/dep_freshness/registries/maven.py index 5f4905f..bd1dbc6 100644 --- a/dep_freshness/registries/maven.py +++ b/dep_freshness/registries/maven.py @@ -1,12 +1,18 @@ """Maven repositories: newest stable of `group:artifact`, from maven-metadata. -Four repositories, asked in order until one knows the artifact: Google +Three repositories, asked in order until one knows the artifact: Google Maven (every `androidx.*` and `com.android.*` artifact lives ONLY there), -Maven Central, the Gradle plugin portal (plugin marker artifacts -`:.gradle.plugin`) and JitPack (tag metadata for `com.github.*` -artifacts published nowhere else). A 404 means "not here", not "not -anywhere", so it moves on; anything else surfaces as `Offline` from the HTTP -layer. +Maven Central and the Gradle plugin portal (plugin marker artifacts +`:.gradle.plugin`). A 404 means "not here", not "not anywhere", so +it moves on; anything else surfaces as `Offline` from the HTTP layer. + +JitPack used to be the fourth: it served tag metadata for `com.github.*` +artifacts published nowhere else (PhotoView, DirectionalViewPager). Since +2026-09 that endpoint answers HTTP 401 "No access token" while the artifacts +themselves still download, so a `com.github.:` name that no +repository knows is answered from the GitHub repo's tags over `git +ls-remote` (unmetered, no token). JitPack's version IS the tag name, `v` +prefix included, so the tags are compared as they are. `` and `` in the metadata are not trusted: AndroidX writes `1.2.0-alpha03` into `` routinely. The full `` list goes @@ -22,7 +28,9 @@ from __future__ import annotations from xml.etree import ElementTree -from dep_freshness._tables import MAVEN_REPOS +from dep_freshness._tables import GITHUB_REMOTE, MAVEN_REPOS +from dep_freshness.registries._git import ls_remote +from dep_freshness.registries.gitcommit import source_repo from dep_freshness.registries.http import get_text from dep_freshness.versions import newest_per_generation, newest_stable, reference @@ -48,6 +56,15 @@ def versions_in(xml: str) -> list[str]: ] +def github_tags(name: str) -> list[str]: + """Every tag of the GitHub repo behind a JitPack coordinate; [] otherwise.""" + repo = source_repo(name) + if repo is None: + return [] + lines = ls_remote(GITHUB_REMOTE.format(repo=repo), "--tags", "--refs") + return [line.rpartition("refs/tags/")[2] for line in lines if "refs/tags/" in line] + + def latest(name: str) -> str | None: seen: list[str] = [] for repo in MAVEN_REPOS: @@ -62,4 +79,6 @@ def latest(name: str) -> str | None: if stable is not None: return stable seen.extend(versions) + if not seen: + seen.extend(github_tags(name)) return reference(seen) or newest_per_generation(seen) diff --git a/dep_freshness/tests/test_gradle_registry.py b/dep_freshness/tests/test_gradle_registry.py index 90303a4..2ff7413 100644 --- a/dep_freshness/tests/test_gradle_registry.py +++ b/dep_freshness/tests/test_gradle_registry.py @@ -13,7 +13,7 @@ from dep_freshness.registries.http import Offline from dep_freshness.resolve import _toolchain_latest from dep_freshness.tests.conftest import write -GOOGLE, CENTRAL, PORTAL, JITPACK = maven.MAVEN_REPOS +GOOGLE, CENTRAL, PORTAL = maven.MAVEN_REPOS def metadata(*versions: str, latest: str | None = None) -> str: @@ -68,21 +68,74 @@ def test_a_repo_with_only_prereleases_does_not_stop_the_search(repos): assert maven.latest("g:a") == "0.9.0" -def test_jitpack_is_asked_last_and_reads_tag_metadata(repos): - """PhotoView was never published anywhere but JitPack; `v`-tags and bare - tags both appear and the newest wins.""" +def tags(monkeypatch, remote_tags: dict[str, list[str]]): + """Serve `git ls-remote --tags --refs` for the GitHub remotes in `remote_tags`.""" + calls: list[tuple[str, ...]] = [] + + def fake(remote, *args): + calls.append((remote, *args)) + repo = remote.removeprefix("https://github.com/") + if repo not in remote_tags: + raise Offline(remote) + return [f"{'0' * 40}\trefs/tags/{tag}" for tag in remote_tags[repo]] + + monkeypatch.setattr(maven, "ls_remote", fake) + return calls + + +def test_jitpack_artifact_is_answered_from_the_repo_tags(repos, monkeypatch): + """PhotoView was never published anywhere but JitPack, whose metadata + endpoint now answers 401; the repo's tags are the same list. `v`-tags and + bare tags both appear and the newest STABLE wins: a newer pre-release + tag is skipped exactly as it would be in maven-metadata.""" name = "com.github.chrisbanes:PhotoView" - repos[maven.metadata_url(JITPACK, name)] = metadata("v1.3.1", "2.2.0", "2.3.0") + calls = tags( + monkeypatch, + {"chrisbanes/PhotoView": ["v1.3.1", "2.2.0", "2.3.0", "v3.0.0-beta1"]}, + ) assert maven.latest(name) == "2.3.0" + assert calls == [("https://github.com/chrisbanes/PhotoView", "--tags", "--refs")] -def test_a_project_named_tag_is_read_as_its_version(repos): +def test_a_project_named_tag_is_read_as_its_version(repos, monkeypatch): """`java-nat-sort` is tagged `natural-comparator-1.1`, not `1.1`.""" name = "com.github.gpanther:java-nat-sort" - repos[maven.metadata_url(JITPACK, name)] = metadata("natural-comparator-1.1") + tags(monkeypatch, {"gpanther/java-nat-sort": ["natural-comparator-1.0", "natural-comparator-1.1"]}) assert maven.latest(name) == "natural-comparator-1.1" +def test_a_repository_answer_wins_over_the_tags(repos, monkeypatch): + """A `com.github.*` artifact that Central knows is not a JitPack build.""" + name = "com.github.ben-manes.caffeine:caffeine" + repos[maven.metadata_url(CENTRAL, name)] = metadata("3.1.8", "3.2.0") + calls = tags(monkeypatch, {}) + assert maven.latest(name) == "3.2.0" + assert calls == [] + + +def test_prereleases_from_a_repository_skip_the_tag_lookup(repos, monkeypatch): + """Something was found, just nothing stable: the repo answered, so the + pre-release fallback applies and the tags are never consulted.""" + name = "com.github.x:y" + repos[maven.metadata_url(CENTRAL, name)] = metadata("1.0.0-beta1") + calls = tags(monkeypatch, {}) + assert maven.latest(name) == "1.0.0-beta1" + assert calls == [] + + +def test_a_tagless_repo_and_a_non_jitpack_name_are_none(repos, monkeypatch): + calls = tags(monkeypatch, {"o/r": [], "o/s": ["HEAD-not-a-tag"]}) + assert maven.latest("com.github.o:r") is None + assert maven.github_tags("g:a") == [] + assert calls == [("https://github.com/o/r", "--tags", "--refs")] + + +def test_an_unreachable_repo_is_offline(repos, monkeypatch): + tags(monkeypatch, {}) + with pytest.raises(Offline): + maven.latest("com.github.o:gone") + + def test_a_package_with_no_stable_anywhere_falls_back_to_newest_prerelease(repos): """biometric-ktx has been alpha-only since 2021: the newest alpha is the reference, and a stable release in ANY repo would still win over it."""