feat(oci): finalize trusted image tags - #12
Merged
Conversation
Add fresh-state finalize with drift refusal and serial verified tag commits, the reg TagCommitter, publish oci finalize --result -, and the two-phase workflow cutover: prepare, three actions/attest steps, then finalize. Tags are now the last thing that happens in a publication.
Round-1 review and conformance fixes: accept the now-at-candidate-digest transition only for tags the prepared plan would have created, bound the finalize stdin decode, report an accurate applied count when a tag write lands but its verification fails, make the credential scrub unable to fail the job or leave credentials behind, and add the ambiguous-write, verification-retry, drift-branch, and prepare-to-finalize registry tests.
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 5 of the
release-cliprogram and performs the two-phase cutover. The publisher workflow no longer publishes, signs, or tags by itself:release-cli publish oci preparepushes content by digest and signs it, the threeactions/atteststeps run, andrelease-cli publish oci finalizeapplies tags last.Invariant 14 — trust metadata strictly before public tags — is the reason for the split, and it is now enforced by construction: no code path in the CLI can write a tag during
prepare, andfinalizeruns after the attestations.What lands
internal/stage/puboci/finalize.go— theTagCommitterport (4 of the closed budget of 13),Finalize,release.dev/oci-finalize/v1,ErrNotAuthoritative,ErrStateDrift.internal/adapter/reg/tag.go—Commit: resolveimage@digestonce, then apply each tag strictly serially, verifying each write before the next.internal/cli—publish oci finalize --result -, which accepts only stdin and only the exact--jsonenvelope emitted bypublish oci prepare..github/workflows/publish-oci-image.yml— ORAS and the fouractions/github-scriptpublication blocks are gone;aqua:oras-project/orasis dropped from the toolchain. Inputs, the six outputs and their meanings, permissions, the repository-wide concurrency group, timeouts, and every SHA pin are unchanged.docs/explanation/two-phase-oci-publication.mdplus reference and how-to updates.Finalize semantics
Refuse a non-authoritative result → collect fresh state → refuse drift → re-plan from what is actually there (never replay the serialized plan) → commit the ordered tags serially → independently verify through
StateReaderthat the exact tag and every applied tag resolve to the index digest.The drift check has one deliberate exception: a tag now sitting on the candidate digest is this publication's own partially completed work. Review found that exception was too broad, so it is now scoped to tags the prepared plan would have created. A channel the plan chose to retain (it sat on a newer release) that later moves onto our digest is drift, not convergence — otherwise a channel regression performed by someone else would be reported as a successful publication.
Verification
mise exec -- moon run root:checkgreen.cosignstub:preparepushed content and lefttags: null;finalizeapplied0.0.1,0.0,0,latest, and every tag resolved to the index digest on real GHCR;0.0.2moved every channel; republishing the older0.0.1reportedaccepted: [0.0.1],retained: [0.0, 0, latest]and left the tag map pointing at0.0.2— invariants 11 and 12 proven against the real registry;channel 0.0 points outside its minor release line, which is the planner catching a genuine inconsistency.accepted, nothing applied), drift refusal after retagginglatestto a foreign digest,authoritative:falserefused, and the stdin usage errors.permissionschange, no references to the deletedsteps.push/steps.tagsoutputs anywhere in.githuborexamples/.Review fixes in this PR
resultwas buffered asjson.RawMessagefirst.Commitnow counts a tag as applied when the write lands but its verification read fails, so the operator-facing count is right exactly when the outcome is ambiguous.prepare→finalizepass over the in-memory registry.Outstanding before the first real release
The live rehearsal covered digest publication and tagging against real GHCR, but keyless Cosign signing and the three
actions/atteststeps only run inside Actions. Spike B proved those against real GHCR previously; the first tag after this merge is the end-to-end proof.publish-image: falseremains the rollback: it exercisesprepare --dry-run, applies no tags, and keepsimage-referenceempty.