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 @@
 
 [![Python 
package](https://github.com/dvershinin/lastversion/actions/workflows/pythonpackage.yml/badge.svg)](https://github.com/dvershinin/lastversion/actions/workflows/pythonpackage.yml)
 [![PyPI 
version](https://badge.fury.io/py/lastversion.svg)](https://badge.fury.io/py/lastversion)
-[![Documentation 
Status](https://readthedocs.org/projects/lastversion/badge/?version=latest)](https://lastversion.getpagespeed.com/en/latest/?badge=latest)
+[![Documentation 
Status](https://readthedocs.org/projects/lastversion/badge/?version=latest)](https://lastversion.getpagespeed.com/)
 [![Codacy 
Badge](https://api.codacy.com/project/badge/Grade/380e3a38dc524112b4dcfc0492d5b816)](https://www.codacy.com/manual/GetPageSpeed/lastversion?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=dvershinin/lastversion&amp;utm_campaign=Badge_Grade)
 [![Codacy 
Badge](https://app.codacy.com/project/badge/Coverage/380e3a38dc524112b4dcfc0492d5b816)](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 @@
 [![Python 
package](https://github.com/dvershinin/lastversion/actions/workflows/pythonpackage.yml/badge.svg)](https://github.com/dvershinin/lastversion/actions/workflows/pythonpackage.yml)
 [![PyPI 
version](https://badge.fury.io/py/lastversion.svg)](https://badge.fury.io/py/lastversion)
 [![GitHub 
Release](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Flastversion-api.getpagespeed.com%2Fdvershinin%2Flastversion&query=version&label=Release)](https://github.com/dvershinin/lastversion/releases)
-[![Documentation 
Status](https://readthedocs.org/projects/lastversion/badge/?version=latest)](https://lastversion.getpagespeed.com/en/latest/?badge=latest)
+[![Documentation 
Status](https://readthedocs.org/projects/lastversion/badge/?version=latest)](https://lastversion.getpagespeed.com/)
 [![Codacy 
Badge](https://api.codacy.com/project/badge/Grade/380e3a38dc524112b4dcfc0492d5b816)](https://www.codacy.com/manual/GetPageSpeed/lastversion?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=dvershinin/lastversion&amp;utm_campaign=Badge_Grade)
 [![Codacy 
Badge](https://app.codacy.com/project/badge/Coverage/380e3a38dc524112b4dcfc0492d5b816)](https://www.codacy.com/gh/dvershinin/lastversion/dashboard?utm_source=github.com&utm_medium=referral&utm_content=dvershinin/lastversion&utm_campaign=Badge_Coverage)
 [![Buy Me a 
Coffee](https://img.shields.io/badge/dynamic/json?color=blue&label=Buy%20me%20a%20Coffee&prefix=%23&query=next_time_total&url=https%3A%2F%2Fwww.getpagespeed.com%2Fbuymeacoffee.json&logo=buymeacoffee)](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";

Reply via email to