Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-lastversion for openSUSE:Factory checked in at 2026-08-22 21:35:46 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-lastversion (Old) and /work/SRC/openSUSE:Factory/.python-lastversion.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-lastversion" Sat Aug 22 21:35:46 2026 rev:3 rq:1373041 version:3.6.17 Changes: -------- --- /work/SRC/openSUSE:Factory/python-lastversion/python-lastversion.changes 2026-07-06 12:37:27.162081978 +0200 +++ /work/SRC/openSUSE:Factory/.python-lastversion.new.1258/python-lastversion.changes 2026-08-22 21:37:57.338106982 +0200 @@ -1,0 +2,83 @@ +Sat Aug 22 09:35:52 UTC 2026 - Dirk Müller <[email protected]> + +- update to 3.6.17: + * **Fixes RPM spec auto-bumps that were silently failing.** + * Every `lastversion .spec` update against a spec genuinely + behind upstream died with: + * ``` + * TypeError: '<' not supported between instances of 'str' and + 'Version' + * ``` + * That is the only case the auto-bump exists for, so packaging + cron chains failed on exactly the releases they were meant to + catch. An already-current spec took the "nothing to do" + branch and looked healthy, which hid it. + * **Root cause:** `current_version` is read off the spec's + `Version:` tag — the state of the file on disk — but it was + stored in, and served back from, the release cache. The cache + round-trips through JSON, so a cache hit returned a plain + `str` while `version` was a `Version`. + * **Fixed:** + * Local project metadata is now parsed *before* the cache + lookup and re-applied over a cache hit, mirroring the fetch + path. This also fixes staleness: a cache entry written when + the spec said 6.0.16 no longer dictates `current_version` + after the spec moves on. + * `update_spec()` coerces `current_version` and `version` to + `Version` before comparing, as defence in depth. +- update to 3.6.16: + * Page-scan holders now return the artifact URL instead of + `[None]`. + * `lastversion --assets` crashed with `TypeError: sequence + item 0: expected str instance, NoneType found`. Holders that + scrape a links page have no `RELEASE_URL_FORMAT`, so + `release_download_url()` returned `None` and `get_assets()` + appended it verbatim. A falsy download URL is now simply not + an asset. + * The feed holder can do better: the hyperlink it matched on + *is* the artifact location, so it is kept (absolute) and + returned as the download URL. + * A releases page typically links both the tarball and its + release notes for the same version (`varnish-6.0.18.tgz` next + to `rel6.0.18.html`). Both sanitize to the same version, so + ties now break towards the archive rather than document + order. + * Pre-existing since the page link-scan landed in 3.6.13. +- update to 3.6.15: + * Resolve Varnish/Vinyl Cache 6.0 LTS via vinyl-cache.org. + * The Varnish Cache FOSS project renamed to **Vinyl Cache** in + the 9.0 release (2026-03-16) after a trademark dispute with + Varnish Software. The `varnishcache/*` GitHub org is + archived; development moved to code.vinyl-cache.org. Varnish + Software kept the trademark and now runs + github.com/varnish/varnish as a downstream fork (a different + codebase). + * The 6.0 and 8.0 LTS branches stayed with Vinyl Cache, so the + known-repo entries now scrape `vinyl-cache.org/releases/`. + * This also fixes resolution on EL7: `varnish-cache.org` is + TLS-1.3-only and EL7's OpenSSL 1.0.2k cannot negotiate it, so + lookups died on the handshake. `vinyl-cache.org` serves TLS + 1.2. + * All historic spellings keep working (`varnish-cache`, + `varnishcache/varnish-cache`, URL form); new `vinyl-*` + spellings and bare hostnames resolve too. +- update to 3.6.14: + * Resolve `varnishcache/varnish-cache` (GitHub owner/repo form) + from the varnish-cache.org releases page as well, not just + the bare name and site URL. The 6.0 LTS branch is no longer + tagged on GitHub, so that form returned a stale 6.0.16. +- update to 3.6.13: + * ## What's new + * Feed holder: known-repo entries can carry a `page` URL whose + hyperlinks are scanned for versioned artifact names — used + when a project's homepage feed lags or omits maintenance + releases. + * `varnish-cache` now resolves from varnish-cache.org/releases/ + (by name and by URL): Varnish 6.0 LTS releases after 6.0.16 + are dist-tarball-only (no GitHub tags), so `lastversion + varnish-cache --major 6.0` previously stalled at 6.0.16. It + now returns 6.0.18. + * Fixed a crash (`NoneType` concatenation) when invoking `--at + website-feed example.org` with a bare hostname. + +------------------------------------------------------------------- Old: ---- lastversion-3.6.12.tar.gz New: ---- lastversion-3.6.17.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-lastversion.spec ++++++ --- /var/tmp/diff_new_pack.9EmArQ/_old 2026-08-22 21:37:58.239139255 +0200 +++ /var/tmp/diff_new_pack.9EmArQ/_new 2026-08-22 21:37:58.241139326 +0200 @@ -17,7 +17,7 @@ Name: python-lastversion -Version: 3.6.12 +Version: 3.6.17 Release: 0 Summary: Find the latest stable release version of an arbitrary project License: BSD-2-Clause ++++++ lastversion-3.6.12.tar.gz -> lastversion-3.6.17.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/.readthedocs.yml new/lastversion-3.6.17/.readthedocs.yml --- old/lastversion-3.6.12/.readthedocs.yml 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/.readthedocs.yml 2026-08-12 08:44:57.000000000 +0200 @@ -5,7 +5,7 @@ # Set the version of Python and other tools you might need build: - os: ubuntu-20.04 + os: ubuntu-24.04 tools: python: "3.10" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/README-ZH-CN.md new/lastversion-3.6.17/README-ZH-CN.md --- old/lastversion-3.6.12/README-ZH-CN.md 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/README-ZH-CN.md 2026-08-12 08:44:57.000000000 +0200 @@ -2,7 +2,7 @@ [](https://github.com/dvershinin/lastversion/actions/workflows/pythonpackage.yml) [](https://badge.fury.io/py/lastversion) -[](https://lastversion.getpagespeed.com/en/latest/?badge=latest) +[](https://lastversion.getpagespeed.com/) [](https://www.codacy.com/manual/GetPageSpeed/lastversion?utm_source=github.com&utm_medium=referral&utm_content=dvershinin/lastversion&utm_campaign=Badge_Grade) [](https://www.codacy.com/gh/dvershinin/lastversion/dashboard?utm_source=github.com&utm_medium=referral&utm_content=dvershinin/lastversion&utm_campaign=Badge_Coverage) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/README.md new/lastversion-3.6.17/README.md --- old/lastversion-3.6.12/README.md 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/README.md 2026-08-12 08:44:57.000000000 +0200 @@ -3,7 +3,7 @@ [](https://github.com/dvershinin/lastversion/actions/workflows/pythonpackage.yml) [](https://badge.fury.io/py/lastversion) [](https://github.com/dvershinin/lastversion/releases) -[](https://lastversion.getpagespeed.com/en/latest/?badge=latest) +[](https://lastversion.getpagespeed.com/) [](https://www.codacy.com/manual/GetPageSpeed/lastversion?utm_source=github.com&utm_medium=referral&utm_content=dvershinin/lastversion&utm_campaign=Badge_Grade) [](https://www.codacy.com/gh/dvershinin/lastversion/dashboard?utm_source=github.com&utm_medium=referral&utm_content=dvershinin/lastversion&utm_campaign=Badge_Coverage) [](https://www.buymeacoffee.com/dvershinin) @@ -78,7 +78,7 @@ #> 5.6.51 ``` -> **Prefer a hosted solution?** Use the [lastversion API](https://rapidapi.com/ciapnz/api/lastversion) — no installation needed, works from any environment. [Learn more](https://lastversion.getpagespeed.com/en/latest/api/). +> **Prefer a hosted solution?** Use the [lastversion API](https://rapidapi.com/ciapnz/api/lastversion) — no installation needed, works from any environment. [Learn more](https://lastversion.getpagespeed.com/api/). ## Installation for RPM-based systems @@ -600,6 +600,20 @@ Exit status code `3` is returned when filtering assets of last release yields empty URL set (no match) +Exit status code `4` is returned when the API refuses the request and `lastversion` gives up. +This covers: + +- **GitHub/Gitea rate limit exceeded.** When the rate-limit reset window is under 5 minutes + away, `lastversion` automatically sleeps until the reset and retries (up to 2 sleeps). + If the reset is more than 5 minutes away, or retries are exhausted, it exits `4` rather + than blocking your script for a long time. The unauthenticated GitHub quota is 60 + req/hour, so a fresh quota exhaustion almost always falls into the "no wait, exit 4" + branch. Set `GITHUB_API_TOKEN` (see Tips below) to raise the ceiling to 5000 req/hour. +- Invalid or missing API token (HTTP 401). +- Semver constraint (`--major`, `--only`, etc.) filtered every release out. + +The error message goes to stderr; stdout is empty. + ## Tips Getting the latest version is heavy on the API, because GitHub does not allow to fetch tags in diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/docs/requirements.txt new/lastversion-3.6.17/docs/requirements.txt --- old/lastversion-3.6.12/docs/requirements.txt 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/docs/requirements.txt 2026-08-12 08:44:57.000000000 +0200 @@ -1,6 +1,7 @@ # docs/requirements.txt mkdocs mkdocs-material +mkdocs-redirects mkdocstrings[crystal,python] markdown-include pymdown-extensions diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/mkdocs.yml new/lastversion-3.6.17/mkdocs.yml --- old/lastversion-3.6.12/mkdocs.yml 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/mkdocs.yml 2026-08-12 08:44:57.000000000 +0200 @@ -17,6 +17,12 @@ python: paths: - src + # Redirect the old ReadTheDocs URL layout (/en/latest/...) to the current + # GitHub Pages root. The site moved off RTD; /en/latest/ was a stale 404 + # in Google Search Console (2026-07). + - redirects: + redirect_maps: + en/latest/index.md: index.md watch: - src/lastversion nav: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/setup.py new/lastversion-3.6.17/setup.py --- old/lastversion-3.6.12/setup.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/setup.py 2026-08-12 08:44:57.000000000 +0200 @@ -25,6 +25,9 @@ 'cachecontrol[filecache]>=0.14.0; python_version>="3.10"', # urllib3 2.x requires Python 3.9+ 'urllib3<2; python_version<"3.9"', + # CVE-2025-50181 (Moderate) fixed in 2.5.0; pip users on Py3.9+ get the safe floor. + # RPM builds sed-strip these urllib3 lines and use distro python3-urllib3 instead. + 'urllib3>=2.5.0; python_version>="3.9"', "appdirs", "feedparser", "python-dateutil", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/src/lastversion/__about__.py new/lastversion-3.6.17/src/lastversion/__about__.py --- old/lastversion-3.6.12/src/lastversion/__about__.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/src/lastversion/__about__.py 2026-08-12 08:44:57.000000000 +0200 @@ -1,4 +1,4 @@ """Package metadata""" -__version__ = "3.6.12" +__version__ = "3.6.17" __self__ = "dvershinin/lastversion" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/src/lastversion/holder_factory.py new/lastversion-3.6.17/src/lastversion/holder_factory.py --- old/lastversion-3.6.12/src/lastversion/holder_factory.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/src/lastversion/holder_factory.py 2026-08-12 08:44:57.000000000 +0200 @@ -97,6 +97,9 @@ if "release_url_format" in known_repo: holder.RELEASE_URL_FORMAT = known_repo["release_url_format"] + + if "page" in known_repo and hasattr(holder, "set_page"): + holder.set_page(known_repo["page"]) return holder @staticmethod diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/src/lastversion/lastversion.py new/lastversion-3.6.17/src/lastversion/lastversion.py --- old/lastversion-3.6.12/src/lastversion/lastversion.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/src/lastversion/lastversion.py 2026-08-12 08:44:57.000000000 +0200 @@ -254,6 +254,23 @@ """ repo_data = {} + # Parse local project metadata BEFORE consulting the release cache. Some of + # it (notably `current_version`, read off the spec's Version: tag) is state + # of the file on disk right now, not of the upstream release, so it must + # never be served from a cache entry written when the spec said something + # else -- and it must be re-applied over any cache hit, exactly as the + # fetch path does with `release.update(repo_data)` further down. + # noinspection HttpUrlsUsage + if repo.endswith(".yml") and not repo.startswith(("http://", "https://")): + repo_data = get_repo_data_from_yml(repo) + + # noinspection HttpUrlsUsage + if repo.startswith(("http://", "https://")) and repo.endswith("Chart.yaml"): + at = "helm_chart" + + if repo.endswith(".spec"): + repo_data = get_repo_data_from_spec(rpmspec_filename=repo) + # Check release data cache first (when enabled and using json/dict output) release_cache = get_release_cache() cache_key_params = { @@ -288,6 +305,12 @@ cached_data["version"] = Version(cached_data.get("version", "")) except InvalidVersion: pass + # Re-apply freshly parsed local metadata over the cached release, + # mirroring `release.update(repo_data)` on the fetch path. Without + # this, `current_version` stays whatever JSON round-tripped out of + # the cache -- a plain str, which then blows up comparing against + # a Version in update_spec(). + cached_data.update(repo_data) return cached_data else: # json return cached_data @@ -301,17 +324,6 @@ return result # Fall through to fetch fresh data if cache parsing failed - # noinspection HttpUrlsUsage - if repo.endswith(".yml") and not repo.startswith(("http://", "https://")): - repo_data = get_repo_data_from_yml(repo) - - # noinspection HttpUrlsUsage - if repo.startswith(("http://", "https://")) and repo.endswith("Chart.yaml"): - at = "helm_chart" - - if repo.endswith(".spec"): - repo_data = get_repo_data_from_spec(rpmspec_filename=repo) - # Define network error types that should trigger cache fallback network_errors = ( requests.exceptions.RequestException, # All requests errors (connection, timeout, etc.) @@ -568,6 +580,17 @@ def update_spec(repo, res, sem="minor", changelog: bool = False): log.info("Latest version: %s", res["version"]) + # Both are compared and have .release read off them below, so neither may + # arrive as a plain str (a JSON-round-tripped cache entry used to do + # exactly that, raising TypeError: '<' not supported between instances of + # 'str' and 'Version'). + for key in ("current_version", "version"): + if key in res and not isinstance(res[key], Version): + try: + res[key] = Version(str(res[key])) + except InvalidVersion: + log.critical("Failed to parse %s %r from release data", key, res[key]) + sys.exit(1) if "current_version" not in res or res["current_version"] < res["version"]: log.info("Updating spec %s with semantic %s", repo, sem) if "current_version" in res and len(res["version"].release) >= 3: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/src/lastversion/repo_holders/base.py new/lastversion-3.6.17/src/lastversion/repo_holders/base.py --- old/lastversion-3.6.12/src/lastversion/repo_holders/base.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/src/lastversion/repo_holders/base.py 2026-08-12 08:44:57.000000000 +0200 @@ -798,7 +798,10 @@ urls.append(asset["browser_download_url"]) else: download_url = self.release_download_url(release, short_urls) - if not assets_filter or re.search(assets_filter, download_url): + # Holders without a RELEASE_URL_FORMAT return None here; appending + # it yields [None], which blows up as a TypeError in any caller + # that joins or inspects the URLs. + if download_url and (not assets_filter or re.search(assets_filter, download_url)): urls.append(download_url) return urls diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/src/lastversion/repo_holders/feed.py new/lastversion-3.6.17/src/lastversion/repo_holders/feed.py --- old/lastversion-3.6.12/src/lastversion/repo_holders/feed.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/src/lastversion/repo_holders/feed.py 2026-08-12 08:44:57.000000000 +0200 @@ -10,16 +10,69 @@ log = logging.getLogger(__name__) +# The Varnish Cache FOSS project was renamed to Vinyl Cache in the 9.0 release +# (2026-03-16) after a trademark dispute with Varnish Software. The +# varnishcache/* GitHub org is archived; development moved to +# code.vinyl-cache.org. Varnish Software kept the trademark and now runs +# github.com/varnish/varnish as a *downstream fork* -- a different codebase, +# not what these entries should resolve to. +# +# The 6.0 and 8.0 LTS branches stayed with Vinyl Cache, so every historic +# spelling must point at vinyl-cache.org. That host also serves TLS 1.2, which +# varnish-cache.org (now Varnish Software's) does not -- and EL7 builders +# running packaging auto-bump crons cannot negotiate TLS 1.3. +# +# "repo" stays "varnish-cache": it is the display/cache label, and the tarballs +# are still named varnish-X.Y.Z.tgz. +_VINYL_CACHE = { + "repo": "varnish-cache", + "hostname": "vinyl-cache.org", + "page": "https://vinyl-cache.org/releases/", +} + class FeedRepoSession(BaseProjectHolder): """Feed repo session.""" + # Suffixes that mark a hyperlink as a release artifact rather than a + # release-notes page, when both carry the same version. + ARCHIVE_EXTENSIONS = ( + ".tgz", + ".tar.gz", + ".tar.bz2", + ".tar.xz", + ".tar.lz", + ".tbz2", + ".txz", + ".zip", + ) + + KNOWN_REPO_URLS = { + # URL-form lookups (e.g. update-spec deriving from a spec's URL:) + # must resolve to the releases page too, not the stale homepage feed. + "varnish-cache.org": _VINYL_CACHE, + "vinyl-cache.org": _VINYL_CACHE, + } KNOWN_REPOS_BY_NAME = { "filezilla": { "repo": "filezilla", "hostname": "filezilla-project.org", "only": "FileZilla Client", - } + }, + # 6.0 LTS releases exist only as dist tarballs linked from the + # releases page; the news feed announces them late or not at all. + "varnish-cache": _VINYL_CACHE, + "vinyl-cache": _VINYL_CACHE, + # The GitHub owner/repo form must resolve here too: the org is + # archived, so asking GitHub yields a stale 6.0.16 -- and packaging + # cascades call it in exactly this form to stamp the version their + # vmod builds pin against. + "varnishcache/varnish-cache": _VINYL_CACHE, + "vinyl-cache/vinyl-cache": _VINYL_CACHE, + # Bare-word host invocations (e.g. `--at website-feed vinyl-cache.org`) + # arrive as a repo name, not a hostname, so KNOWN_REPO_URLS misses. + "varnish-cache.org": _VINYL_CACHE, + "vinyl-cache.org": _VINYL_CACHE, } CAN_BE_SELF_HOSTED = True # Unlimited number of components (URI as is) @@ -63,8 +116,17 @@ return result def __init__(self, repo, hostname): + # A bare-word invocation like `--at website-feed varnish-cache.org` + # arrives with hostname=None and the site in `repo`. + if not hostname: + hostname = repo super().__init__(repo, hostname) self.home_soup = None + # Optional page whose hyperlinks carry versioned artifact names + # (e.g. a releases/downloads listing); takes precedence over the + # discovered feed because homepage feeds routinely lag or omit + # maintenance releases. Set via a known-repo "page" entry. + self.page_url = None feeds = self.find_feed("https://" + hostname + "/") if not feeds: return @@ -72,11 +134,61 @@ log.info("Using feed URL: %s", feeds[0]) self.feed_url = feeds[0] + def set_page(self, page_url): + """Use a link-listing page as the version source.""" + self.page_url = page_url + def is_instance(self): - return self.feed_url + return self.feed_url or self.page_url + + def get_latest_from_page_links(self, pre_ok=False, major=None): + """Latest version among hyperlink targets/texts of the page.""" + from urllib.parse import unquote + + from bs4 import BeautifulSoup as bs4 + + html = bs4(self.get(self.page_url).text, "html.parser") + ret = {} + for a in html.findAll("a"): + href = a.get("href", None) + if not href: + continue + candidate = unquote(href.rstrip("/").rsplit("/", 1)[-1]) + version = self.sanitize_version(candidate, pre_ok, major) + if not version and a.text: + version = self.sanitize_version(a.text.strip(), pre_ok, major) + if not version: + continue + # A releases page usually links both the tarball and its release + # notes for the same version (e.g. varnish-6.0.18.tgz and + # rel6.0.18.html). Both sanitize to the same version, so on a tie + # prefer the archive -- the notes page is not a download URL. + is_archive = candidate.endswith(self.ARCHIVE_EXTENSIONS) + if ( + not ret + or version > ret["version"] + or (version == ret["version"] and is_archive and not ret["is_archive"]) + ): + # The hyperlink we matched on IS the artifact location, so keep + # it absolute -- it is the only download URL this holder can + # offer (there is no RELEASE_URL_FORMAT to synthesize one from). + ret = { + "tag_name": candidate, + "version": version, + "download_url": urljoin(self.page_url, href), + "is_archive": is_archive, + } + ret.pop("is_archive", None) + return ret or None + + def release_download_url(self, release, shorter=False): + """URL of the artifact the page link-scan matched on.""" + return release.get("download_url") or super().release_download_url(release, shorter) def get_latest(self, pre_ok=False, major=None): """Get the latest release.""" + if self.page_url: + return self.get_latest_from_page_links(pre_ok=pre_ok, major=major) ret = {} # To leverage `cachecontrol`, we fetch the feed using requests as # usual, then feed the feed to feedparser as a raw string e.g. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/src/lastversion/repo_holders/sourceforge.py new/lastversion-3.6.17/src/lastversion/repo_holders/sourceforge.py --- old/lastversion-3.6.12/src/lastversion/repo_holders/sourceforge.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/src/lastversion/repo_holders/sourceforge.py 2026-08-12 08:44:57.000000000 +0200 @@ -14,10 +14,6 @@ # a URI does not start with a repo name, skip '/projects/' REPO_URL_PROJECT_OFFSET = 1 - def __init__(self, repo, hostname): - super().__init__(repo, hostname) - self.hostname = hostname - @staticmethod def get_normalized_url(download_url): """Get normalized URL for a download URL, without /download suffix.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/tests/test_lastversion.py new/lastversion-3.6.17/tests/test_lastversion.py --- old/lastversion-3.6.12/tests/test_lastversion.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/tests/test_lastversion.py 2026-08-12 08:44:57.000000000 +0200 @@ -2,12 +2,16 @@ import os import subprocess +from types import SimpleNamespace import pytest from packaging import version +from lastversion import lastversion as lastversion_mod from lastversion.exceptions import BadProjectError from lastversion.lastversion import latest +from lastversion.repo_holders.github import GitHubRepoSession +from lastversion.repo_holders.helmchat import HelmChartRepoSession from lastversion.repo_holders.test import TestProjectHolder from lastversion.version import Version @@ -209,8 +213,11 @@ assert v["license"]["path"] == "LICENSE" -def test_yml_input(): +def test_yml_input(monkeypatch): """Test passing a yml file as repo argument.""" + monkeypatch.setattr(GitHubRepoSession, "repo_license", lambda *_args: {"path": "LICENSE"}) + monkeypatch.setattr(GitHubRepoSession, "repo_readme", lambda *_args: {"path": "README.md"}) + repo = os.path.dirname(os.path.abspath(__file__)) + "/geoip2.yml" v = latest(repo, output_format="json") @@ -412,18 +419,49 @@ assert not v.is_prerelease -def test_char_yml_direct(): [email protected] +def helm_chart_urls(monkeypatch): + """Capture Helm chart requests while returning deterministic metadata. + + Args: + monkeypatch (pytest.MonkeyPatch): Pytest monkeypatch fixture. + + Returns: + list: URLs requested by the Helm chart adapter. + """ + requested_urls = [] + + def fake_get(_session, url): + """Return a minimal Helm chart response. + + Args: + _session (HelmChartRepoSession): Helm chart adapter instance. + url (str): URL requested by the adapter. + + Returns: + types.SimpleNamespace: Response containing valid Chart YAML. + """ + requested_urls.append(url) + return SimpleNamespace(text="version: 1.2.3") + + monkeypatch.setattr(HelmChartRepoSession, "get", fake_get) + return requested_urls + + +def test_char_yml_direct(helm_chart_urls): """Test URL with Chart.yaml.""" repo = "https://github.com/bitnami/charts/blob/master/bitnami/aspnet-core/Chart.yaml" v = latest(repo) assert v >= version.parse("1.0.0") + assert helm_chart_urls == ["https://raw.githubusercontent.com/bitnami/charts/master/bitnami/aspnet-core/Chart.yaml"] -def test_char_yml_indirect_hint(): - """Test URL with Chart.yaml""" +def test_char_yml_indirect_hint(helm_chart_urls): + """Test URL with Chart.yaml.""" repo = "https://github.com/bitnami/charts/blob/master/bitnami/aspnet-core" v = latest(repo, at="helm_chart") assert v >= version.parse("1.0.0") + assert helm_chart_urls == ["https://raw.githubusercontent.com/bitnami/charts/master/bitnami/aspnet-core/Chart.yaml"] def test_at_with_url_github(): @@ -475,3 +513,101 @@ repo = "https://github.com/lastversion-test-repos/nginx_ajp_module" release = latest(repo, output_format="dict") assert release["version"] == version.parse("0.3.2") + + +def test_varnish_cache_lts_from_releases_page(): + """Varnish 6.0 LTS releases exist only as tarballs on the releases page + (the homepage feed lags), so the known-repo `page` link-scan must surface + them for the whole vmod packaging chain.""" + v = latest("varnish-cache", major="6.0") + + assert v >= version.parse("6.0.18") + + +def test_varnish_cache_lts_by_github_owner_repo(): + """Packaging cascades call `lastversion varnishcache/varnish-cache + --major 6.0` to stamp the version their vmod builds pin against. That + GitHub org is archived (the project renamed to Vinyl Cache), so asking + GitHub returns a stale 6.0.16 and every vmod would pin a daemon ABI that + no longer exists.""" + v = latest("varnishcache/varnish-cache", major="6.0") + + assert v >= version.parse("6.0.18") + + +def test_varnish_cache_lts_by_url(): + """update-spec derives the repo from a spec's URL: field, so the URL + form must resolve through the same releases-page scan.""" + v = latest("https://varnish-cache.org", major="6.0") + + assert v >= version.parse("6.0.18") + + [email protected]( + "repo", + ["vinyl-cache", "vinyl-cache/vinyl-cache", "vinyl-cache.org", "https://vinyl-cache.org"], +) +def test_vinyl_cache_lts_new_names(repo): + """The FOSS project renamed to Vinyl Cache in 9.0 (2026-03-16) after the + Varnish Software trademark dispute, and kept stewardship of the 6.0 LTS + branch. The new spellings must resolve to the same releases page as the + legacy `varnish-cache` ones -- and must NOT fall through to + github.com/varnish/varnish, which is Varnish Software's downstream fork + and a different codebase.""" + v = latest(repo, major="6.0") + + assert v >= version.parse("6.0.18") + + +def test_page_scan_assets_is_the_tarball_not_the_notes_page(): + """A releases page links both the tarball and its release notes for the + same version. Both sanitize to the same version, so the scan must prefer + the archive -- otherwise `--assets` hands back an .html notes page as if + it were a download. Holders with no RELEASE_URL_FORMAT used to return + [None] here, which crashed the CLI with a TypeError on join.""" + assets = latest("varnish-cache", major="6.0", output_format="assets") + + assert assets, "page-scan holder must yield a download URL" + assert all(a for a in assets), f"no None entries allowed: {assets}" + assert assets[0].endswith(".tgz"), f"expected the tarball, got {assets[0]}" + + +def test_update_spec_coerces_str_versions_from_cache(tmp_path, monkeypatch): + """The release cache round-trips through JSON, so a cache hit hands + update_spec() `current_version` as a plain str while `version` is a + Version. Comparing them raised + + TypeError: '<' not supported between instances of 'str' and 'Version' + + which broke every EL7 packaging auto-bump the moment the spec was + genuinely behind upstream -- the one case that has to work.""" + spec = tmp_path / "varnish.spec" + spec.write_text("Name: varnish\nVersion: 6.0.16\nRelease: 1%{?dist}\n\n%changelog\n") + + monkeypatch.setattr(lastversion_mod, "get_rpm_packager", lambda: "Tester <[email protected]>") + + res = { + # Exactly what a JSON cache entry yields: both are strings. + "current_version": "6.0.16", + "version": Version("6.0.18"), + "tag_name": "varnish-6.0.18", + "spec_tag": "varnish-%{version}", + "spec_tag_no_prefix": "%{version}", + "spec_name": "%{name}", + "version_macro": "version", + } + + lastversion_mod.update_spec(str(spec), res, sem="any") + + assert "Version: 6.0.18" in spec.read_text() + + +def test_update_spec_no_op_when_cached_current_version_is_str(): + """The same str/Version mismatch on an up-to-date spec must still take the + 'nothing to do' path (exit 2) rather than crashing.""" + res = {"current_version": "6.0.18", "version": Version("6.0.18")} + + with pytest.raises(SystemExit) as exc: + lastversion_mod.update_spec("unused.spec", res, sem="any") + + assert exc.value.code == 2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lastversion-3.6.12/tests/test_sf.py new/lastversion-3.6.17/tests/test_sf.py --- old/lastversion-3.6.12/tests/test_sf.py 2026-05-15 15:42:18.000000000 +0200 +++ new/lastversion-3.6.17/tests/test_sf.py 2026-08-12 08:44:57.000000000 +0200 @@ -3,11 +3,20 @@ from packaging import version from lastversion.lastversion import latest +from lastversion.repo_holders.sourceforge import SourceForgeRepoSession # change dir to tests directory to make relative paths possible os.chdir(os.path.dirname(os.path.realpath(__file__))) +def test_sf_default_hostname_with_short_name(): + """Regression for #243: `--at sf <name>` must default hostname, not None.""" + holder = SourceForgeRepoSession("sevenzip", None) + assert holder.hostname == "sourceforge.net" + # URL used by get_latest() must not contain 'None' + assert holder.repo == "sevenzip" + + def test_sf_keepass(): """Test a SourceForge project.""" repo = "https://sourceforge.net/projects/keepass"
