From b96f225c3cb2dabf93eaa285fc367cd7b3f5c74d Mon Sep 17 00:00:00 2001 From: Daniel Maksymilian Syrnicki Date: Thu, 16 Apr 2026 16:29:11 +0200 Subject: [PATCH] fix(updater): /releases?limit=1 instead of /releases/latest Forgejo's /releases/latest silently skips pre-releases (any release with a -alpha / -beta / -rc suffix) and 404s when there's no stable release. During Furtka's alpha stage every tag is a pre-release, so the Check-for-updates button always 404'd against a perfectly-valid releases page. Switch check_update() to GET /releases?limit=1 and take the first entry. Forgejo returns releases newest-first regardless of kind, so this works whether the top of the list is pre-release or stable. Empty list (no releases published yet) now returns a clean "no releases" UpdateError instead of a raw 404. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 9 ++++++++- furtka/updater.py | 16 +++++++++++++--- tests/test_updater.py | 42 +++++++++++++++++++++++++++--------------- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb12a3..6aa9cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r ## [Unreleased] +## [26.2-alpha] - 2026-04-16 + +### Fixed + +- **Updater "Check for updates" no longer 404s when every release is a pre-release.** `check_update()` queried Forgejo's `/releases/latest`, which silently excludes pre-releases (anything tagged `-alpha`/`-beta`/`-rc`) and returns 404 when there is no stable release. Switched to `/releases?limit=1`, which Forgejo sorts newest-first across all release kinds. During the alpha stage where every tag is a pre-release this is the only thing that works; once we tag a stable release, the same query still picks it up. + ## [26.1-alpha] - 2026-04-16 ### Added @@ -59,6 +65,7 @@ First tagged snapshot. Pre-alpha — the installer does not yet boot, but the de - **Containers:** Docker + Compose - **License:** AGPL-3.0 -[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.1-alpha...HEAD +[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.2-alpha...HEAD +[26.2-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.2-alpha [26.1-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.1-alpha [26.0-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.0-alpha diff --git a/furtka/updater.py b/furtka/updater.py index 6530211..5ebc5a7 100644 --- a/furtka/updater.py +++ b/furtka/updater.py @@ -124,12 +124,22 @@ def _version_tuple(v: str) -> tuple: def check_update() -> UpdateCheck: - """Return current + latest versions and whether an update is available.""" + """Return current + latest versions and whether an update is available. + + Forgejo's /releases/latest endpoint skips anything marked as a + pre-release, so during the CalVer alpha/beta stage where every tag + carries a suffix, that endpoint always 404s. Query the paginated + /releases list instead and take the first entry — Forgejo returns + them newest-first, including pre-releases. + """ current = read_current_version() - release = _forgejo_api("/releases/latest") + releases = _forgejo_api("/releases?limit=1") + if not isinstance(releases, list) or not releases: + raise UpdateError("no releases published yet") + release = releases[0] latest = str(release.get("tag_name") or "").strip() if not latest: - raise UpdateError("no latest release (empty tag_name)") + raise UpdateError("latest release has empty tag_name") tarball_url = None sha256_url = None for asset in release.get("assets") or []: diff --git a/tests/test_updater.py b/tests/test_updater.py index bc16d62..5a1839e 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -308,24 +308,27 @@ def test_rollback_flips_to_previous_slot(tmp_path, updater, monkeypatch): def test_check_update_queries_forgejo_and_compares(updater, monkeypatch): - # Stub the API and the current-version read. + # Stub the API and the current-version read. Forgejo's /releases list + # returns most-recent first, including pre-releases — we take [0]. monkeypatch.setattr(updater, "read_current_version", lambda: "26.0-alpha") monkeypatch.setattr( updater, "_forgejo_api", - lambda path: { - "tag_name": "26.1-alpha", - "assets": [ - { - "name": "furtka-26.1-alpha.tar.gz", - "browser_download_url": "https://x/t.tar.gz", - }, - { - "name": "furtka-26.1-alpha.tar.gz.sha256", - "browser_download_url": "https://x/t.tar.gz.sha256", - }, - ], - }, + lambda path: [ + { + "tag_name": "26.1-alpha", + "assets": [ + { + "name": "furtka-26.1-alpha.tar.gz", + "browser_download_url": "https://x/t.tar.gz", + }, + { + "name": "furtka-26.1-alpha.tar.gz.sha256", + "browser_download_url": "https://x/t.tar.gz.sha256", + }, + ], + } + ], ) check = updater.check_update() assert check.current == "26.0-alpha" @@ -340,7 +343,16 @@ def test_check_update_reports_up_to_date_when_same_version(updater, monkeypatch) monkeypatch.setattr( updater, "_forgejo_api", - lambda path: {"tag_name": "26.1-alpha", "assets": []}, + lambda path: [{"tag_name": "26.1-alpha", "assets": []}], ) check = updater.check_update() assert check.update_available is False + + +def test_check_update_raises_when_no_releases_published(updater, monkeypatch): + # Newly-created repo with zero releases: don't crash, surface a clean + # error the UI can show instead of "HTTP 404 Not Found". + monkeypatch.setattr(updater, "read_current_version", lambda: "26.0-alpha") + monkeypatch.setattr(updater, "_forgejo_api", lambda path: []) + with pytest.raises(updater.UpdateError, match="no releases"): + updater.check_update()