Repository navigation
Integrate open fixes, anonymous pulls, immutable tags, append-only mode and conformance CI - #1
Merged
Merged
Conversation
The OCI distribution spec v1.1 allows a manifest with a subject to be pushed before the subject exists. regsync/regctl push the child manifests of an index in parallel, so buildx attestation manifests regularly arrive before the image manifest they refer to and the copy failed with BLOB_UNKNOWN "unknown subject".
ANONYMOUS_PULL_REPOSITORIES is a comma separated list of repository
names ("*" matches anything, including "/"). GET/HEAD requests
without an Authorization header may read manifests, blobs, tags and
referrers of those repositories. /v2/ still answers 401 with a Basic
challenge so docker keeps sending credentials for pushes; _catalog,
uploads, deletes and gc always need credentials, and requests with
wrong credentials are still rejected. Anonymous requests never trigger
the pull fallback (REGISTRIES_JSON), so they can not make us copy
arbitrary upstream images into R2.
Also stop writing per-request state into env: env is shared by all
concurrent requests of an isolate, so REGISTRY_CLIENT could be replaced
by a concurrent request. Every request now gets its
own copy.
(cherry picked from commit 48e16fc;
the regional read cache it was written on top of is not included)
When only some platforms of an image are mirrored (regsync "platforms"), the upstream index is pushed unchanged so its digest stays the same, and the manifests of the other platforms do not exist in this registry. Stop rejecting such indexes; pulling a missing platform fails with MANIFEST_UNKNOWN. Image manifests are still checked for their layers. (cherry picked from commit a8f8077)
IMMUTABLE_TAG_PATTERN is a regular expression that must match the whole tag. Matching tags are create-only: the tag is written with a conditional R2 put, an identical re-push is accepted, and assigning a different manifest returns 409 DENIED. While the policy is enabled, matching tags cannot be deleted, and deletes by manifest digest and blob deletes are refused because they could make a protected release unpullable. (cherry picked from commit d5e4c1a; the retention sweep, the /v2/_cleanup endpoint and the deployment configuration it was written alongside are not included)
Set Content-Encoding: identity so the runtime does not switch these responses to chunked transfer-encoding (which drops Content-Length); the distribution spec requires Content-Length on blob and manifest GET/HEAD. Adds regression tests for the blob/manifest GET and HEAD headers. (cherry picked from commit 1466f0b)
…ntent A reference containing ":" addresses a manifest by digest; the submitted content must hash to exactly that digest. Previously a mismatched or malformed digest reference was stored as a tag under the wrong key (the content checksum still matched), returning 201 instead of 400. Now returns 400 DIGEST_INVALID. Adds regression tests (mismatched digest, malformed digest, correct digest). (cherry picked from commit 15019ad) Integration note: this check replaces the equivalent, later check added by the immutable-tag commit; that commit's test now expects DIGEST_INVALID.
…tead of 500 (cherry picked from commit 5286a54)
(cherry picked from commit 4a07e8c)
(cherry picked from commit da8b561)
(cherry picked from commit beb506a)
(cherry picked from commit d895613)
(cherry picked from commit 4306bd6)
Clients may resume or probe a blob with the RFC 9110 suffix form 'Range: bytes=-<n>', asking for the last n bytes. That form was parsed as malformed and silently downgraded to a full 200 response. Model BlobRangeRequest as a union of an offset range and a suffix range, resolve suffixes against the object size in the R2 backend, and forward them upstream verbatim. A suffix longer than the object is satisfied by the whole object, while a zero-length suffix is unsatisfiable. Co-authored-by: Fang-Pen Lin <hello@fangpenlin.com> (cherry picked from commit 90d6a05)
When a blob is not cached in R2, the blob GET handler falls back to the configured upstream registries. Any error from an upstream was treated as a miss, so an unsatisfiable range reached the client as a synthetic 404 BLOB_UNKNOWN, telling it the blob does not exist and discarding the Content-Range that carries the real object size. Report a 416 from an upstream directly, matching how a 416 from the primary R2 store is already handled. Other failures keep falling through to the next registry. Co-authored-by: Fang-Pen Lin <hello@fangpenlin.com> (cherry picked from commit 5c161d4)
… uploads (cherry picked from commit 2898820) Integration notes: limit() keeps returning a FixedLengthStream, because R2 rejects streams of unknown length ("Provided readable stream must have a known length"); it takes this change's truncation of the final chunk and error propagation. A PATCH that carries neither Content-Length nor Content-Range is still buffered to learn its length, as before; the streaming path covers every request that does carry one. The test that expected limit() to pass a short stream through was dropped, since a short stream is an error for a fixed-length R2 write. Formatted with prettier.
(cherry picked from commit 889cc09)
The top-level mediaType is OPTIONAL in the OCI image-spec and Helm omits it, but manifestSchema required it, so `helm push` failed with a 400. Inferred into the parsed manifest only; stored bytes are untouched so they still hash to the pushed digest. (cherry picked from commit 3d4dcfd)
Union failures reported a bare "Invalid input" with no path. (cherry picked from commit 0a2f03f)
Blobs, manifests stored under their digest, mounted blobs and referrer entries always hold the same content for a given key, but every push wrote them again. Write them with a conditional create instead, and treat an object that already exists as success. A write refused for an object that is already there (for example by an R2 bucket lock or another retention rule) is success too. This lets the registry run on R2 buckets that protect their content prefixes with object lock or retention rules, and it stops re-pushes from rewriting large blobs. Tags and upload state are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015crRPdLa1DE7PcvFapd7Dk
When DISABLE_DELETE is "true", deleting manifests and blobs and running garbage collection answer 405 UNSUPPORTED, as the distribution spec allows for a registry that does not support deletion. Upload cancellation keeps working, since it only removes temporary state. The flag is off by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015crRPdLa1DE7PcvFapd7Dk
Another Worker can now depend on this package and hand registry requests to it while doing its own routing in front. package.json gains "exports" and "types" pointing at the TypeScript entry point, which wrangler bundles directly, and index.ts exports the handler as both the default and a named export, plus RegistryEnv (the bindings and variables the registry reads) and Env (RegistryEnv plus the state set for each request). ENVIRONMENT, which nothing reads, is optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015crRPdLa1DE7PcvFapd7Dk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015crRPdLa1DE7PcvFapd7Dk
A new workflow serves the registry with `wrangler dev` (the test configuration) and runs the distribution-spec conformance suite at v1.1.1 with the pull, push, content discovery and content management categories enabled. It needs no secrets and fails on any failing test. A second, non-blocking job runs the suite from distribution-spec's main branch, which is still changing, so its results are visible without gating merges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015crRPdLa1DE7PcvFapd7Dk
The non-blocking job showed as a failed check on every pull request while the suite on distribution-spec's main branch reports failures. Let only the suite step continue on error, and write its result table into the job summary instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015crRPdLa1DE7PcvFapd7Dk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch collects open fixes and features for the registry into one reviewable set, keeping each original author on their commits, and adds a few small generic changes on top.
What is included
From the WolfcastleDE fork (Nils van Lueck)
ANONYMOUS_PULL_REPOSITORIES, a comma- or space-separated list of globs. Requests with no credentials can read manifests, blobs, tags and referrers of matching repositories. Pushes, deletes, uploads,_catalogand gc still need credentials,/v2/still answers 401, and anonymous requests never trigger the pull fallback. The registry also stops writing per-request state into the sharedenv.From the roshanjonah fork (Roshan Jonnalagadda)
IMMUTABLE_TAG_PATTERN: tags that match the whole pattern are create-only. They are written with a conditional put. Re-pushing the identical manifest is accepted, and assigning a different manifest returns 409DENIED. While the policy is on, deleting a matching tag, deleting by digest and deleting a blob are refused.Open upstream pull requests (cherry-picked from
refs/pull/N/head)Content-Lengthon blob and manifest GET/HEAD.DIGEST_INVALID).mediaTypefromContent-Typewhen it is omitted.New in this branch
DISABLE_DELETE. When it is"true", manifest and blob deletes andPOST /v2/<name>/gcanswer 405UNSUPPORTED. It is off by default.package.jsongainsexportsandtypesthat point atindex.ts.index.tsexports the handler (default and namedhandler), plusRegistryEnv(the bindings and variables) andEnv. Another Worker can depend on a pinned commit and callregistry.fetch(request, env, ctx)after its own routing. The README documents this.wrangler devwith the test configuration and the distribution-spec conformance suite at v1.1.1 (pull, push, discovery, management). It needs no secrets and fails on any failure. A non-blocking job runs the suite from distribution-specmain.Left out
/v2/_cleanupendpoint (both delete content) and the deployment configuration.limit()that returned a stream of unknown length, because R2 rejects those. See the conflict notes.Conflict resolutions
envcopy is kept, and the regional cache tests are dropped.{ cause }to the thrown error so that the lint rule passes.DIGEST_INVALIDcheck replaces the equivalent later check from the immutable-tag commit. That commit's test now expectsDIGEST_INVALID.blobGetResponsekeeps fix: preserve Content-Length on blob and manifest GET/HEAD cloudflare/serverless-registry#136'sContent-Encoding: identity, and blob HEAD sends bothAccept-Rangesand identity. The last commit (prettier only) was empty here and is skipped.limit()stays aFixedLengthStream, because R2 rejects streams of unknown length, but it takes the PR's truncation of the final chunk and its error propagation. A PATCH with neitherContent-LengthnorContent-Rangeis still buffered to learn its length. The test that expected a short stream to pass throughlimit()is dropped. "Reduce back pressure recovery time" (b0a9601) is not included: after Fix #64, add range header forwarding cloudflare/serverless-registry#147 the pull-through path already starts the R2 upload before it returns the response.Verification
pnpm test: 125 tests pass.typecheck,lint,format:checkandgenerate-types:checkpass.main(OCI_VERSION=1.1, sha512 off as in the baseline): 570 passed, 7 failed, 4 skipped. Upstreammainscores 541 passed, 27 failed, 9 skipped. All 7 remaining failures are the non-distributable-layers data set.wrangler dev, all passing:oras pull --platformfor both platforms.oras attachandoras discoverlist the referrer, and the referrers API withartifactTypefiltering answers withOCI-Filters-Applied.DISABLE_DELETE, deletes and gc answer 405 and the content stays.github:<this fork>#<commit>typechecks and bundles with wrangler. It serves its own route, passes/v2/through, and push and anonymous pull work through it.🤖 Generated with Claude Code
https://claude.ai/code/session_015crRPdLa1DE7PcvFapd7Dk