feat(oci): prepare digest publication - #11
Merged
Conversation
Add the OCI layout reader, the prepare engine with the ContentPusher and Signer ports, the oras push half of the reg adapter, the cosign exec adapter, and publish oci prepare with --dry-run. No workflow change: the github-script publisher stays authoritative until finalize lands.
Round-1 review and conformance fixes: reject a 409 blob push instead of treating it as success, guard nil content, fail on an unparsable RELEASE_DRY_RUN rather than publishing, fence --plain-http to loopback and drop its environment variable, retry transient pushes with a reopened stream, bound cosign's wait after cancellation, require a platform on every index descriptor, and cover the JSON size bound and symlink escapes.
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.
Implements PR 4 of the
release-cliprogram: digest-addressed OCI publication, recursive Cosign signing, the versionedOCIPrepareResult, andpublish oci prepare [--dry-run]. Builds on PR 3 (plan tags).No workflow change. The
actions/github-scriptsteps inpublish-oci-image.yml(OP-13/OP-14/OP-15) keep publishing, signing, attesting, and tagging until the finalize slice lands, so invariant 14 (trust metadata strictly before public tags) is never temporarily weakened.What lands
internal/stage/puboci—Descriptor,DigestRef/Image.Pin, theContentPusherandSignerports (3 and 5 of the closed budget of 13),ReadLayoutoverfs.FSwith exactindex.jsonbyte retention and deduplicated blob ordering, therelease.dev/oci-prepare/v1result, andPrepare.internal/adapter/reg—PushBlob,PushManifest,Verifyon the existing oras-go v2 client. Content is streamed; layers are never buffered.internal/adapter/cosign— exec adapter running exactlycosign sign --yes --recursive <image>@<digest>.internal/cli—publish oci prepare --layout PATH [--image] [--version] --digest D [--dry-run] [--plain-http] [--json], plusRegistryConfigand themain.gowiring.Order of operations
Read and validate the layout → require the computed index digest to equal
--digest→ collect fresh registry state and plan tags, so an immutable-tag conflict refuses before any write → push every unique blob, each platform manifest, then the index, by digest → verify the index and every platform manifest resolve → sign recursively.--dry-runstops after planning, writes nothing, and marks the result"authoritative": false. The command never creates or moves a tag.Verification
mise exec -- moon run root:checkgreen: format, lint, build, test, protocol stamp, mock freshness.go-containerregistryregistry with a two-platform layout fixture (7 blobs, one layer shared by both platforms) and a recordingcosignstub: dry run left the catalog empty and invoked no signer; the authoritative run pushed content, lefttags: null, made the index resolve by digest, and invoked exactlysign --yes --recursive …@sha256:4ef3cfc6…; a rerun converged; a wrong--digestfailed before any write; a planted conflicting1.4.0tag refused withimmutable tag conflictbefore any push or signature.--plain-httpagainstghcr.ioexits 2;RELEASE_PLAIN_HTTPno longer exists;RELEASE_DRY_RUN=yesexits 2 instead of publishing;RELEASE_DRY_RUN=trueyieldsauthoritative:false.Defects the smoke and review caught, fixed here
*os.Filewas closed twice. The adapter now shields the reader andTestPushBlobLeavesReaderOpenpins the ownership rule. Found by the live smoke, not by the mock tests.authoritative:trueresult for an image with a missing layer, because verification only resolves manifests. HTTP 409 is now an error.RELEASE_DRY_RUN=yesparsed asfalseand performed a real publication plus signature. Unparsable booleans are now exit 2.--plain-httpwas environment-activatable and host-agnostic while carrying the registry token. It is now flag-only and refused for non-loopback hosts.GetBody, so oras-go's retry transport could not replay it. The engine now retriesErrRetryablepushes and verifications four times (1s/2s/4s) with a reopened stream and an injected sleep.cmd.WaitDelayso a cancelledcosigncannot hang the CLI, a requiredplatformon every index descriptor (no more"/"attestation subjects), and tests for the 4 MiB JSON bound, symlink escape, and pushed media types.Deliberate deviations from the plan's indicative signatures
The plan's
Repositoryis the already-shippedImage;Verify(ctx, ref DigestRef)drops the redundantexpectedargument because a digest-pinned reference carries it; the result exposes an orderedobserved[]projection becauserel.ChannelStateis a struct-keyed map and cannot be JSON; attestation subjects carry platform and digest only, leaving SBOM paths to the workflow.