feat(registry): introduce oci-layout backend + --registry flag (PR 1/6)#55
Merged
Conversation
Captures the approved brainstorm: full-pipeline rework where builds and tests run identically local and in CI via docker-wrapped matrices, a single phpup binary with subcommands, and a pluggable --registry that defaults to GHCR but accepts an oci-layout filesystem path. Six PR rollout, each independently shippable.
New internal/registry package introduces a backend-agnostic Store interface (Fetch, Push, Has, ResolveDigest) and a URI-dispatching Open() function. Concrete layout and remote backends arrive in the next two tasks; this commit lands the public surface and scheme parsing only, gated behind clear "implemented in Task N" stubs.
- Correct Open's godoc to match distinct "empty URI" vs "scheme" error branches. - Add table-driven Open dispatch test covering looksLikeRemote edge cases (dotless host, uppercase host, multi-segment path, leading slash, host-only). - Rename remoteStore.root to remoteStore.base so the field doesn't falsely mirror layoutStore.root (which is a filesystem path). - Document *Meta nil semantics on Store.Fetch / Store.Push. - Rename TestRefString_Remote -> TestRefString_WithDigest for symmetry. No behaviour changes; all existing call sites unaffected.
layoutStore reads/writes bundles as two-layer OCI images (layer 0 =
bundle bytes, layer 1 = meta.json) under an on-disk layout directory.
Push/Has/Fetch round-trip verified; legacy bundles without a meta
sidecar return Meta{SchemaVersion:1} to match runtime expectations.
Has/Fetch also accept a digest-only match so layouts populated by
oras copy (which doesn't set our annotation) are interoperable.
- Has/Fetch digest-only fallback now only accepts manifests that have
no annotationBundleName set; manifests with a mismatching annotation
are an affirmative negative. Prevents cross-linking a Ref{Name:"X"}
probe to a manifest whose annotation says it is Y.
- Switch Fetch to layers[i].Compressed() so returned bytes match the
remote-backed Store path (ghcr bundles are tar+zstd and callers
decompress). Document the Store.Fetch contract.
- Add three digest-fallback tests covering exact-match preference,
oras-style no-annotation acceptance, and wrong-annotation rejection.
- Strengthen TestLayoutStore_FetchMissingRef_Errors to also exercise
the populated-layout "not-found" branch with a valid-form digest.
- Drop a dead-branch predicate in Fetch's match loop.
remoteStore wraps go-containerregistry/pkg/v1/remote for Fetch / Has / ResolveDigest; Push returns ErrUnsupported (remote push lands with the phpup build subcommand in PR 2). Fetch uses layers[i].Compressed() to match the layout backend's raw-blob contract. Auth resolves from INPUT_GITHUB-TOKEN / GITHUB_TOKEN or falls through to anonymous. Tests use the in-process pkg/registry server via httptest, so no network or docker is needed. looksLikeRemote now accepts ':' in the host segment so host:port forms (used by the test registry and by private self-hosted registries on explicit ports) dispatch correctly.
…patch - remoteStore.Has now returns (false, nil) for empty-digest probes so both backends behave identically when asked "do you have this yet?" The Fetch path still rejects empty digest as an error. - Add TestRemoteStore_HasEmptyDigest_FalseNoError to lock the contract. - Add host:port row to TestOpen_URIFormDispatch so the looksLikeRemote scope expansion is covered at the dispatch layer, not implicitly. - Use errors.Is for the ErrUnsupported assertion in the Push test. - Comment the 401-as-not-present gotcha for when private-repo probes become relevant in Task 6/7.
internal/oci.Client now holds a registry.Store; its public surface (NewClient, FetchAll, Fetch, Exists, ResolveDigest, ResolvedBundle, FetchResult, Metadata) is unchanged so existing call sites in cmd/phpup, cmd/planner and cmd/lockfile-update compile without modification. NewClient now accepts "oci-layout:<path>" URIs in addition to remote hosts, enabling hermetic local fetches without touching GHCR. The facade itself is slated for deletion by end of PR 3 of the local+CI unification rollout.
- Fetch now errors with "unknown bundle kind %q" before calling ociName, restoring the pre-refactor diagnostic that returned a clear error instead of silently constructing a wrong remote reference. - The token env-var write in NewClient is now gated by sync.Once, making the "first non-empty token wins" semantics explicit and race-free. Godoc spells out the contract. - Add regression tests for both paths.
phpup install now accepts an explicit --registry URI and falls back through INPUT_REGISTRY (GitHub Actions input convention) and PHPUP_REGISTRY (local/CLI convention) before the hardcoded ghcr.io/buildrush default. Combined with internal/registry's oci-layout backend, end users can run phpup against a local filesystem layout — e.g. PHPUP_REGISTRY=oci-layout:./out/oci-layout — for hermetic dev loops. Action input "registry" surfaces the same knob to Action consumers; GitHub Actions forwards it as INPUT_REGISTRY and src/index.js already passes the full env to phpup at exec time, so no shim changes are needed.
test/registry/e2e_layout.sh seeds a local OCI layout from GHCR via oras, then runs phpup with PHPUP_REGISTRY pointing at the layout and asserts the resolved php binary runs. Provides a manual gate for PR 1 of the local+CI unification rollout and a fixture that PR 4's CI rework will reuse. The script exercises the Task 2 digest-only fallback in the layout backend: oras cp does not emit the io.buildrush.bundle.name annotation setup-php's pusher writes, so the layout's Has/Fetch must match by digest alone when no annotation is present. Pre-reqs documented in the script header (oras, jq, go); GHCR access via GITHUB_TOKEN/GHCR_TOKEN when private or rate-limited. Fails fast on non-linux hosts since the published php-core bundles are linux-only (run on CI or wrap in a linux container). Verified end-to-end on linux/arm64 by running the script inside an ubuntu:22.04 container: seeds the layout, phpup install completes without GHCR access, and the composed PHP 8.4.20 binary reports "PHP 8.4.20 (cli) (NTS)".
phramz
enabled auto-merge (squash)
April 23, 2026 11:47
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.
Summary
PR 1 of 6 in the local+CI unification rollout (spec).
internal/registrypackage withStoreinterface + two backends:layout(filesystem OCI) andremote(HTTPS OCI over GHCR/etc.).phpup installaccepts--registry <uri>with env fallbacks (INPUT_REGISTRY,PHPUP_REGISTRY), default unchanged atghcr.io/buildrush.internal/oci.Clientrefactored to delegate toregistry.Store— public surface byte-identical, socmd/planner/cmd/lockfile-update/cmd/phpupcompile unchanged. The facade is scheduled for deletion by end of PR 3 of the rollout.action.ymlgains a newregistryinput surfaced via GitHub Actions' env forwarding (nosrc/index.jsshim changes needed).Backward compatibility: Existing consumers (
buildrush/setup-php@v1with no new inputs) observe zero behavioral change — the default path still resolves toghcr.io/buildrushand fetches from GHCR as before.Non-goals for this PR (arrive in follow-ups):
phpup buildsubcommand — PR 2.phpup testsubcommand +make ci-cell— PR 3.ci.ymlwith thin OS×ARCH×PHP matrix — PR 4.planner,lockfile-update, etc. →phpupsubcommands) — PR 6.Test plan
go test ./internal/registry/... ./internal/oci/... ./cmd/phpup/...— unit tests for both backends (round-trip, digest-only fallback, 404→false, compression contract, flag precedence).make checkgreen end-to-end locally — including the docker-backedlocal-cismoke (jammy + noble × amd64 + arm64 × hard4 extensions,ALL GREEN).test/registry/e2e_layout.sh— seeds a real GHCRphp-core@sha256into a local OCI layout viaoras, runsphpup install --registry oci-layout:<dir>, asserts PHP 8.4.20 loaded offline. Executed end-to-end insideubuntu:22.04underdocker.compat-harness.yml/integration-test.yml— they exercise the default GHCR path, which is unchanged.Notes for reviewers
featcommits introduce capabilities behind clear stubs;fixcommits address review findings from a two-stage (spec + code-quality) review on each task.97a4d2b) is the design spec itself — committed to the docs tree so it lands alongside the implementation. Plans live underdocs/superpowers/plans/but that directory is gitignored per repo convention, so only the spec is tracked.e2e_layout.shharness is deliberately not wired intomake checkor any workflow in this PR — that wiring belongs to PR 4's newci.yml.