Skip to content

Integrate open fixes, anonymous pulls, immutable tags, append-only mode and conformance CI - #1

Merged
EricAndrechek merged 24 commits into
mainfrom
integration-1
Oct 1, 2026
Merged

EricAndrechek merged 24 commits into
mainfrom
integration-1

Conversation

@EricAndrechek

Copy link
Copy Markdown
Member

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)

  • Accept referrers pushed before their subject (distribution-spec v1.1 allows it).
  • Anonymous pulls for configured repositories: 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, _catalog and 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 shared env.
  • Accept sparse image indexes, whose child manifests do not all exist.

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 409 DENIED. 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)

New in this branch

  • Idempotent content-addressed writes. Blobs, manifests by digest, mounted blobs and referrer entries are written with a conditional create. An object that already exists counts as success, and so does a write refused for an object that is already there. The registry therefore works on R2 buckets that protect those keys with bucket locks or retention rules, and re-pushes no longer rewrite large blobs.
  • DISABLE_DELETE. When it is "true", manifest and blob deletes and POST /v2/<name>/gc answer 405 UNSUPPORTED. It is off by default.
  • Importable entry point. package.json gains exports and types that point at index.ts. index.ts exports the handler (default and named handler), plus RegistryEnv (the bindings and variables) and Env. Another Worker can depend on a pinned commit and call registry.fetch(request, env, ctx) after its own routing. The README documents this.
  • Conformance workflow. On pull requests it runs wrangler dev with 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-spec main.

Left out

  • The WolfcastleDE regional read-through cache buckets, their write-through replication endpoint, and the bundle build for their Terraform deployment. They assume one deployment's layout: a primary bucket in one region plus fixed EU and US cache bindings chosen by continent.
  • From the roshanjonah fork: the scheduled retention sweep, the /v2/_cleanup endpoint (both delete content) and the deployment configuration.
  • From Fix: blocking PATCH requests and implement true streaming for chunked… cloudflare/serverless-registry#120: the change to limit() that returned a stream of unknown length, because R2 rejects those. See the conflict notes.

Conflict resolutions

Verification

  • pnpm test: 125 tests pass. typecheck, lint, format:check and generate-types:check pass.
  • Conformance v1.1.1: 74 of 74 specs pass, 5 skipped, 0 failed.
  • Conformance main (OCI_VERSION=1.1, sha512 off as in the baseline): 570 passed, 7 failed, 4 skipped. Upstream main scores 541 passed, 27 failed, 9 skipped. All 7 remaining failures are the non-distributable-layers data set.
  • Functional checks with oras v1.3.4 against wrangler dev, all passing:
    • Anonymous pull of a configured repository works. Anonymous push, upload and pull of an unlisted repository answer 401.
    • A 150 MiB blob uploaded as POST, four PATCHes of at most 50 MB and a PUT verifies.
    • A multi-platform index works with oras pull --platform for both platforms.
    • oras attach and oras discover list the referrer, and the referrers API with artifactType filtering answers with OCI-Filters-Applied.
    • An immutable tag refuses reassignment (409) and accepts an identical re-PUT (201).
    • With DISABLE_DELETE, deletes and gc answer 405 and the content stays.
  • A separate Worker that depends on 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

vanlueckn and others added 24 commits October 1, 2026 18:50
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.
(cherry picked from commit d895613)
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
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
@EricAndrechek
EricAndrechek merged commit 397a3ec into main Oct 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants