Skip to content

feat(registry): introduce oci-layout backend + --registry flag (PR 1/6)#55

Merged
phramz merged 11 commits into
mainfrom
feat/local-ci-pr1-registry
Apr 23, 2026
Merged

feat(registry): introduce oci-layout backend + --registry flag (PR 1/6)#55
phramz merged 11 commits into
mainfrom
feat/local-ci-pr1-registry

Conversation

@phramz

@phramz phramz commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 1 of 6 in the local+CI unification rollout (spec).

  • New internal/registry package with Store interface + two backends: layout (filesystem OCI) and remote (HTTPS OCI over GHCR/etc.).
  • phpup install accepts --registry <uri> with env fallbacks (INPUT_REGISTRY, PHPUP_REGISTRY), default unchanged at ghcr.io/buildrush.
  • internal/oci.Client refactored to delegate to registry.Store — public surface byte-identical, so cmd/planner / cmd/lockfile-update / cmd/phpup compile unchanged. The facade is scheduled for deletion by end of PR 3 of the rollout.
  • action.yml gains a new registry input surfaced via GitHub Actions' env forwarding (no src/index.js shim changes needed).

Backward compatibility: Existing consumers (buildrush/setup-php@v1 with no new inputs) observe zero behavioral change — the default path still resolves to ghcr.io/buildrush and fetches from GHCR as before.

Non-goals for this PR (arrive in follow-ups):

  • phpup build subcommand — PR 2.
  • phpup test subcommand + make ci-cell — PR 3.
  • New unified ci.yml with thin OS×ARCH×PHP matrix — PR 4.
  • Cutover / old-workflow deletion — PR 5.
  • CLI binary consolidation (planner, lockfile-update, etc. → phpup subcommands) — 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 check green end-to-end locally — including the docker-backed local-ci smoke (jammy + noble × amd64 + arm64 × hard4 extensions, ALL GREEN).
  • test/registry/e2e_layout.sh — seeds a real GHCR php-core@sha256 into a local OCI layout via oras, runs phpup install --registry oci-layout:<dir>, asserts PHP 8.4.20 loaded offline. Executed end-to-end inside ubuntu:22.04 under docker.
  • CI: full matrix green on this PR.
  • No regression in compat-harness.yml / integration-test.yml — they exercise the default GHCR path, which is unchanged.

Notes for reviewers

  • The 10 code commits are atomic and independently revertible. feat commits introduce capabilities behind clear stubs; fix commits address review findings from a two-stage (spec + code-quality) review on each task.
  • The first commit on this branch (97a4d2b) is the design spec itself — committed to the docs tree so it lands alongside the implementation. Plans live under docs/superpowers/plans/ but that directory is gitignored per repo convention, so only the spec is tracked.
  • The e2e_layout.sh harness is deliberately not wired into make check or any workflow in this PR — that wiring belongs to PR 4's new ci.yml.

phramz added 11 commits April 23, 2026 12:09
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
phramz enabled auto-merge (squash) April 23, 2026 11:47
@phramz
phramz merged commit a853877 into main Apr 23, 2026
143 checks passed
@phramz
phramz deleted the feat/local-ci-pr1-registry branch April 23, 2026 11:47
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.

1 participant