From 072fd7f46696781145705216156b68bec48ff82f Mon Sep 17 00:00:00 2001 From: Syco21 Date: Sat, 16 May 2026 17:13:12 +0200 Subject: [PATCH] Add ghcr.io support to Docker update checker --- server/routes/updates.ts | 57 +++++++++++++------ .../widgets/DockerUpdatesWidget.tsx | 4 +- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/server/routes/updates.ts b/server/routes/updates.ts index ce72534..d961bbe 100644 --- a/server/routes/updates.ts +++ b/server/routes/updates.ts @@ -55,7 +55,13 @@ function parseImage(fullImage: string): { registry: string; name: string; tag: s return { registry, name, tag } } -async function getRegistryToken(image: string): Promise { +const MANIFEST_ACCEPT = [ + 'application/vnd.docker.distribution.manifest.v2+json', + 'application/vnd.docker.distribution.manifest.list.v2+json', + 'application/vnd.oci.image.index.v1+json', +].join(', ') + +async function getDockerHubToken(image: string): Promise { const res = await axios.get( `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image}:pull`, { timeout: 8000 } @@ -69,27 +75,42 @@ async function getLatestDigest(image: string, tag: string): Promise { + const cacheKey = `ghcr:${image}:${tag}` + const cached = fromCache(cacheKey, HUB_TTL) + if (cached) return cached + + try { + const ghToken = process.env.GITHUB_TOKEN + const authHeaders: Record = ghToken + ? { Authorization: `Bearer ${ghToken}` } + : {} + + const tokenRes = await axios.get( + `https://ghcr.io/token?service=ghcr.io&scope=repository:${image}:pull`, + { headers: authHeaders, timeout: 8000 } + ) + const token = tokenRes.data.token as string + + const res = await axios.head( + `https://ghcr.io/v2/${image}/manifests/${tag}`, + { headers: { Authorization: `Bearer ${token}`, Accept: MANIFEST_ACCEPT }, timeout: 10000 } + ) + const digest = res.headers['docker-content-digest'] as string | undefined + if (digest) toCache(cacheKey, digest) + return digest ?? null + } catch { return null } } router.get('/docker', async (_req, res) => { @@ -136,11 +157,15 @@ router.get('/docker', async (_req, res) => { const { registry, name, tag } = parseImage(rawImage) const isDockerHub = registry === 'docker.io' + const isGhcr = registry === 'ghcr.io' let upToDate: boolean | null = null if (isDockerHub && repoDigest) { const latest = await getLatestDigest(name, tag) if (latest) upToDate = latest === repoDigest + } else if (isGhcr && repoDigest) { + const latest = await getGhcrLatestDigest(name, tag) + if (latest) upToDate = latest === repoDigest } containers.push({ diff --git a/src/components/widgets/DockerUpdatesWidget.tsx b/src/components/widgets/DockerUpdatesWidget.tsx index b2c30d1..3398597 100644 --- a/src/components/widgets/DockerUpdatesWidget.tsx +++ b/src/components/widgets/DockerUpdatesWidget.tsx @@ -26,7 +26,7 @@ export function DockerUpdatesWidget() { }, []) const outdated = containers.filter(c => c.upToDate === false).length - const unknown = containers.filter(c => c.upToDate === null).length + const unknown = containers.filter(c => c.upToDate === null && c.registry !== 'docker.io' && c.registry !== 'ghcr.io').length return (
@@ -52,7 +52,7 @@ export function DockerUpdatesWidget() { {c.image}:{c.tag} · {c.endpoint}
- {c.registry !== 'docker.io' ? ( + {c.upToDate === null && c.registry !== 'docker.io' && c.registry !== 'ghcr.io' ? ( ext ) : c.upToDate === true ? (