Add ghcr.io support to Docker update checker
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-16 17:13:12 +02:00
parent cea1a05af7
commit 072fd7f466
2 changed files with 43 additions and 18 deletions
+40 -15
View File
@@ -55,7 +55,13 @@ function parseImage(fullImage: string): { registry: string; name: string; tag: s
return { registry, name, tag } return { registry, name, tag }
} }
async function getRegistryToken(image: string): Promise<string> { 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<string> {
const res = await axios.get( const res = await axios.get(
`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image}:pull`, `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image}:pull`,
{ timeout: 8000 } { timeout: 8000 }
@@ -69,27 +75,42 @@ async function getLatestDigest(image: string, tag: string): Promise<string | nul
if (cached) return cached if (cached) return cached
try { try {
const token = await getRegistryToken(image) const token = await getDockerHubToken(image)
const res = await axios.head( const res = await axios.head(
`https://registry-1.docker.io/v2/${image}/manifests/${tag}`, `https://registry-1.docker.io/v2/${image}/manifests/${tag}`,
{ { headers: { Authorization: `Bearer ${token}`, Accept: MANIFEST_ACCEPT }, timeout: 10000 }
headers: {
Authorization: `Bearer ${token}`,
Accept: [
'application/vnd.docker.distribution.manifest.v2+json',
'application/vnd.docker.distribution.manifest.list.v2+json',
'application/vnd.oci.image.index.v1+json',
].join(', '),
},
timeout: 10000,
}
) )
const digest = res.headers['docker-content-digest'] as string | undefined const digest = res.headers['docker-content-digest'] as string | undefined
if (digest) toCache(cacheKey, digest) if (digest) toCache(cacheKey, digest)
return digest ?? null return digest ?? null
} catch { } catch { return null }
return null
} }
async function getGhcrLatestDigest(image: string, tag: string): Promise<string | null> {
const cacheKey = `ghcr:${image}:${tag}`
const cached = fromCache<string>(cacheKey, HUB_TTL)
if (cached) return cached
try {
const ghToken = process.env.GITHUB_TOKEN
const authHeaders: Record<string, string> = 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) => { router.get('/docker', async (_req, res) => {
@@ -136,11 +157,15 @@ router.get('/docker', async (_req, res) => {
const { registry, name, tag } = parseImage(rawImage) const { registry, name, tag } = parseImage(rawImage)
const isDockerHub = registry === 'docker.io' const isDockerHub = registry === 'docker.io'
const isGhcr = registry === 'ghcr.io'
let upToDate: boolean | null = null let upToDate: boolean | null = null
if (isDockerHub && repoDigest) { if (isDockerHub && repoDigest) {
const latest = await getLatestDigest(name, tag) const latest = await getLatestDigest(name, tag)
if (latest) upToDate = latest === repoDigest if (latest) upToDate = latest === repoDigest
} else if (isGhcr && repoDigest) {
const latest = await getGhcrLatestDigest(name, tag)
if (latest) upToDate = latest === repoDigest
} }
containers.push({ containers.push({
@@ -26,7 +26,7 @@ export function DockerUpdatesWidget() {
}, []) }, [])
const outdated = containers.filter(c => c.upToDate === false).length 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 ( return (
<div className="card"> <div className="card">
@@ -52,7 +52,7 @@ export function DockerUpdatesWidget() {
{c.image}:{c.tag} · {c.endpoint} {c.image}:{c.tag} · {c.endpoint}
</span> </span>
</div> </div>
{c.registry !== 'docker.io' ? ( {c.upToDate === null && c.registry !== 'docker.io' && c.registry !== 'ghcr.io' ? (
<span className="pill pill-blue">ext</span> <span className="pill pill-blue">ext</span>
) : c.upToDate === true ? ( ) : c.upToDate === true ? (
<span className="pill pill-green"></span> <span className="pill pill-green"></span>