From 8adcef8e4744a16e08a0841b6c5b8e775e92f36f Mon Sep 17 00:00:00 2001 From: Anso Date: Thu, 9 Apr 2026 20:10:55 -0400 Subject: [PATCH] fix(fleet): add Docker Hub fallback for version detection on private repos (#463) * fix(fleet): add Docker Hub fallback for version detection on private repos The GitHub Releases API returns 404 for private repos, causing the latest version fetch to silently fail and fall back to the gateway's own version (defeating the update detection fix from PR #454). Now tries GitHub first, then falls back to Docker Hub tags API which is always public. Adds console.warn logging on fetch failures per Directive 7. * ci: trigger CI re-run --- CHANGELOG.md | 1 + backend/src/index.ts | 45 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b4f2c35..6d75b299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **fleet:** fix false "Update available" on remote nodes whose `api_url` has a trailing slash, causing `fetchRemoteMeta` to construct a double-slash URL that fails silently * **fleet:** detect updates via GitHub Releases API instead of comparing against the gateway's own version. Previously, the local node could never appear outdated because it compared its version to itself. The Recheck button now invalidates the 30-minute version cache and fetches the actual latest release. +* **fleet:** add Docker Hub tags API as fallback for version detection when the GitHub repo is private. The GitHub Releases API returns 404 for private repos, causing version detection to silently fail and fall back to the gateway's own version. ## [0.41.1](https://github.com/AnsoCode/Sencho/compare/v0.41.0...v0.41.1) (2026-04-08) diff --git a/backend/src/index.ts b/backend/src/index.ts index 71be8117..ccc7310c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1241,17 +1241,44 @@ let latestVersionCache: { version: string; fetchedAt: number } | null = null; let latestVersionInflight: Promise | null = null; const LATEST_VERSION_CACHE_TTL = 30 * 60 * 1000; // 30 minutes +async function fetchFromGitHub(): Promise { + const res = await fetch('https://api.github.com/repos/AnsoCode/Sencho/releases/latest', { + headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'Sencho' }, + signal: AbortSignal.timeout(10000), + }); + if (!res.ok) return null; + const data = await res.json() as { tag_name?: string }; + const tag = data.tag_name?.replace(/^v/, '') ?? null; + return tag && semver.valid(tag) ? tag : null; +} + +async function fetchFromDockerHub(): Promise { + const res = await fetch( + 'https://hub.docker.com/v2/repositories/saelix/sencho/tags/?page_size=50&ordering=last_updated', + { headers: { 'User-Agent': 'Sencho' }, signal: AbortSignal.timeout(10000) }, + ); + if (!res.ok) return null; + const data = await res.json() as { results?: { name: string }[] }; + const tags = (data.results ?? []) + .map(t => t.name) + .filter(n => semver.valid(n)); + if (tags.length === 0) return null; + tags.sort(semver.rcompare); + return tags[0]; +} + async function fetchLatestSenchoVersion(): Promise { try { - const res = await fetch('https://api.github.com/repos/AnsoCode/Sencho/releases/latest', { - headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'Sencho' }, - signal: AbortSignal.timeout(10000), - }); - if (!res.ok) return null; - const data = await res.json() as { tag_name?: string }; - const tag = data.tag_name?.replace(/^v/, '') ?? null; - return tag && semver.valid(tag) ? tag : null; - } catch { + const gh = await fetchFromGitHub(); + if (gh) return gh; + } catch (err) { + // GitHub API fails for private repos or rate limits; try Docker Hub + console.warn('[VersionCheck] GitHub fetch failed:', (err as Error).message); + } + try { + return await fetchFromDockerHub(); + } catch (err) { + console.warn('[VersionCheck] Docker Hub fetch failed:', (err as Error).message); return null; } }