Summary
The About page shows Latest Version: "unable to check" and the "Update available" button never appears, even when a newer image (e.g. 0.22.3) has been published to Docker Hub. The version check is silently broken for every deployed hub.
Log line confirming the failure:
WARN CheckLatestVersion: no config digest in manifest
Root cause
web/update.go::CheckLatestVersion() fetches the manifest for boltcard/card:latest and reads manifest.Config.Digest (update.go:70-83). That assumes a single-platform Docker v2 image manifest, which has a top-level config object.
Docker Hub no longer serves that. The tag now resolves to an OCI image index (multi-manifest), which has a manifests[] array and no top-level config, so manifest.Config.Digest == "" and the function bails out with "no config digest in manifest", returning "" → the UI renders "unable to check".
Verified directly against the registry (same calls the code makes):
GET https://registry-1.docker.io/v2/boltcard/card/manifests/latest
Accept: application/vnd.docker.distribution.manifest.v2+json
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{ "mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:d9ddd6...", "platform": {"architecture":"amd64","os":"linux"} },
{ "mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:7ea8e9...",
"annotations": {"vnd.docker.reference.type":"attestation-manifest"},
"platform": {"architecture":"unknown","os":"unknown"} }
]
}
The version label is still correct — it's just one level deeper. Following the amd64 entry → its image manifest → its config blob yields:
org.opencontainers.image.version: 0.22.3
When this regressed
Commit 37712c6 ("CI: cache Docker layers and parallelize image builds", PR #43, v0.22.2) switched the image build/push from plain docker build + docker push to docker/build-push-action@v6 (buildx). buildx enables provenance attestations by default, so the pushed tag becomes an OCI image index containing the real image manifest plus an attestation-manifest (the platform: unknown/unknown entry above). Before that commit, docker push produced a single-platform v2 manifest with the top-level config the code expects.
Fix options
-
(Recommended) Handle the OCI image index in CheckLatestVersion. Detect when the manifest is an index/list (mediaType is application/vnd.oci.image.index.v1+json or ...manifest.list.v2+json, or the JSON has a manifests[] array), pick the linux/amd64 entry (skip attestation-manifest / platform: unknown), then fetch that child manifest to obtain config.digest and continue as today. Also broaden the Accept header to advertise the index/list media types. This is forward-compatible and keeps provenance attestations.
-
(Workaround) Disable provenance in CI by adding provenance: false to the docker/build-push-action@v6 step for the card image, restoring a plain single-platform manifest. Simpler but loses attestations and breaks again if multi-arch is ever added.
I recommend option 1 (with option 2 as a stopgap if needed). Happy to put up a PR.
Affected code
docker/card/web/update.go — CheckLatestVersion() (manifest parsing at lines ~49-83)
.github/workflows/ci.yml — "Build and push card image" step (docker/build-push-action@v6)
Summary
The About page shows Latest Version: "unable to check" and the "Update available" button never appears, even when a newer image (e.g.
0.22.3) has been published to Docker Hub. The version check is silently broken for every deployed hub.Log line confirming the failure:
Root cause
web/update.go::CheckLatestVersion()fetches the manifest forboltcard/card:latestand readsmanifest.Config.Digest(update.go:70-83). That assumes a single-platform Docker v2 image manifest, which has a top-levelconfigobject.Docker Hub no longer serves that. The tag now resolves to an OCI image index (multi-manifest), which has a
manifests[]array and no top-levelconfig, somanifest.Config.Digest == ""and the function bails out with "no config digest in manifest", returning""→ the UI renders "unable to check".Verified directly against the registry (same calls the code makes):
The version label is still correct — it's just one level deeper. Following the
amd64entry → its image manifest → its config blob yields:When this regressed
Commit
37712c6("CI: cache Docker layers and parallelize image builds", PR #43, v0.22.2) switched the image build/push from plaindocker build+docker pushtodocker/build-push-action@v6(buildx). buildx enables provenance attestations by default, so the pushed tag becomes an OCI image index containing the real image manifest plus anattestation-manifest(theplatform: unknown/unknownentry above). Before that commit,docker pushproduced a single-platform v2 manifest with the top-levelconfigthe code expects.Fix options
(Recommended) Handle the OCI image index in
CheckLatestVersion. Detect when the manifest is an index/list (mediaTypeisapplication/vnd.oci.image.index.v1+jsonor...manifest.list.v2+json, or the JSON has amanifests[]array), pick thelinux/amd64entry (skipattestation-manifest/platform: unknown), then fetch that child manifest to obtainconfig.digestand continue as today. Also broaden theAcceptheader to advertise the index/list media types. This is forward-compatible and keeps provenance attestations.(Workaround) Disable provenance in CI by adding
provenance: falseto thedocker/build-push-action@v6step for the card image, restoring a plain single-platform manifest. Simpler but loses attestations and breaks again if multi-arch is ever added.I recommend option 1 (with option 2 as a stopgap if needed). Happy to put up a PR.
Affected code
docker/card/web/update.go—CheckLatestVersion()(manifest parsing at lines ~49-83).github/workflows/ci.yml— "Build and push card image" step (docker/build-push-action@v6)