bakery ci publish: intermittent digest 'not found' from GHCR (eventual consistency, no retry in publish path)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 65/100
Research direction
Start in posit_bakery/plugins/builtin/oras/oras.py at OrasCommand.run and in posit_bakery/plugins/builtin/soci/soci.py at SociCommand.run, then trace publish() in posit_bakery/cli/ci.py. Add tests that simulate a transient not-found response followed by success. Done means registry-touching calls retry transient failures with configurable backoff while permanent errors still fail fast.
Written by the indexing model from the issue text.
Description
Summary
bakery ci publish (formerly bakery ci merge) intermittently fails with a digest-addressed manifest reported as not found by GHCR. The failures are ephemeral — re-running the merge job alone almost always succeeds — which points squarely at GHCR read-after-write (eventual consistency) on manifests that we push by digest, combined with the fact that there is no retry/backoff anywhere in the publish path.
Example failures
- SOCI convert:
SOCI convert failed for 'ImageTarget(image='workbench', version='2026.05.0+218.pro1', ...)': Error from source registry for "ghcr.io/posit-dev/workbench/tmp:workbench-...ba39112170": sha256:7e8001fdfc...: not found(run 27354249233) - oras fetch:
Error response from registry: could not find the manifest ghcr.io/posit-dev/workbench-session-init/tmp:latest@sha256:9a5a07a1...: not found(run 27024890914) - oras fetch:
... ghcr.io/posit-dev/workbench-session-init/tmp:latest@sha256:a734c599...: not found(run 27024930267)
Root cause analysis
The mechanism
-
Per-platform builds push by digest (untagged). In
posit_bakery/image/image_target.py(build()), when a temp registry is set, the build output is overridden to:output = {"type": "image", "push-by-digest": True, "name-canonical": True, "push": True}Each platform's manifest lands in
ghcr.io/posit-dev/<image>/tmpaddressable only by digest (no tag). The digest is recorded into the per-platform metadata artifact (image_metadata.py→<repo>/tmp@sha256:...). -
The platforms build on separate runners. Per
.github/workflows/bakery-build-native.yml,build-testis a matrix withlinux/amd64andlinux/arm64on different runners (different machine class/region). Each pushes its manifest + blobs to GHCR independently. -
The
mergejob references those digests immediately.bakery ci publish(posit_bakery/cli/ci.py) then, by digest:- Phase 1
oras manifest index create— builds the index fromget_merge_sources()(the per-platform...@sha256:refs). - Phase 2 SOCI convert —
oras cp <index> --to-oci-layoutpulls the index and every child manifest/blob down. - Phase 3/4 index-copy + verify.
- Phase 1
-
There is zero retry/backoff anywhere in this path. Every
subprocess.runinoras.py/soci.pyraises hard on the first non-zero exit.
Why it's intermittent — and why a re-run fixes it
All observed errors are the same shape: a digest-addressed manifest reported not found by GHCR. The decisive clue is that re-running the merge job alone succeeds — it re-references the exact same digests. If those manifests had been garbage-collected, the re-run would fail identically. It doesn't, so the manifests are durably present; they were merely transiently unreadable when the merge runner first asked for them.
That is the signature of GHCR eventual consistency: a manifest/blob pushed by digest from runner A isn't guaranteed to be immediately readable by digest from runner B. Pushing by digest with no tag is the worst case — there's no tag write to act as a consistency checkpoint, and oras does HEAD/GET-by-digest lookups that 404 until the backend replicates. By the time the job is re-run, propagation has completed.
(This is diagnosis-by-evidence, not a reproduced failure — GHCR's internal replication can't be reproduced on demand. But the evidence chain — always digest-not-found, always self-heals on plain re-run, separate-runner pushes, no retry — is consistent and rules out permanent GC/deletion.)
Proposed solutions
Ranked. The clean fix lives in bakery, not the workflow.
-
Retry-with-backoff around the registry-touching oras/soci calls (recommended core fix). Wrap
.run()ofOrasManifestIndexCreate, the SOCIoras cp --to-oci-layoutpull,OrasCopy, andOrasManifestFetchso that transient registry errors (not found/manifest unknown/ 5xx / timeouts) retry a handful of times with exponential backoff (e.g. 5 attempts, 2s→32s). Non-transient errors still fail fast. This directly absorbs propagation lag. Retry count/backoff should be configurable. -
Explicit pre-flight wait in
publish(complements #1, better diagnostics). Before Phase 1, poll each source digest withoras manifest fetch --descriptoruntil all resolve (or timeout). Turns "hope it's propagated" into condition-based waiting and logs which digest was lagging. -
Workflow-level retry (stopgap only). Wrap the Publish step with
nick-fields/retry. Cheap, but re-runs the whole publish and masks the real fix. -
Push per-platform images to real tags (
tmp:<uid>-<platform>) instead of by-digest. Would also improve consistency, but a bigger change with its own GC/cleanup implications. Not the lead recommendation.
Recommendation: implement #1 as the core fix (with retry/backoff configurable), optionally #2 for diagnostics. Both should be developed with tests that simulate a transient not found followed by success.
Affected files
posit-bakery/posit_bakery/plugins/builtin/oras/oras.py—OrasCommand.run, index-create/copy/fetch workflowsposit-bakery/posit_bakery/plugins/builtin/soci/soci.py—SociCommand.run,SociConvertWorkflowposit-bakery/posit_bakery/cli/ci.py—publish()orchestration (pre-flight wait would go here)
- Dominant language
- Python
- Stars
- 2
- Forks
- 0
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 20
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from posit-dev/images-shared
-
cvp:0 docker tdp:1
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
posit-dev/images-shared#757 · 2 comments ·
Maintainers usually reply within 1 day
-
bug cvp:0 docker priority/medium python tdp:1
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
posit-dev/images-shared#685 ·
Maintainers usually reply within 1 day
-
slack-build-notify: no persisted alert state — causes suppressed repeat-failure and recovery alertsOpenbug cicd cvp:0 docker needs discussion observability tdp:2
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
posit-dev/images-shared#677 ·
Maintainers usually reply within 1 day
-
cvp:0 docker tdp:2 tech debt
Difficulty 4/5 3-5 days Newbie friendliness 48/100
posit-dev/images-shared#795 ·
Maintainers usually reply within 1 day
-
cicd cvp:0 docker enhancement tdp:2
Difficulty 5/5 Over a week Newbie friendliness 35/100
posit-dev/images-shared#773 ·
Maintainers usually reply within 1 day
All issues in posit-dev/images-shared
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
bug
Difficulty 1/5 Under an hour Newbie friendliness 88/100
qgis/QGIS-Plugins-Website#459 ·
-
bug severity:medium
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
bot-found bug priority: P3
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
madenvel/KalinkaPlayer#179 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
ls1intum/edutelligence#1098 ·
Maintainers usually reply within 1 day