Skip to content

feat(oci): plan immutable release tags - #10

Merged
jmgilman merged 2 commits into
mainfrom
feat/release-cli-slice3
Aug 19, 2026
Merged

jmgilman merged 2 commits into
mainfrom
feat/release-cli-slice3

Conversation

@jmgilman

Copy link
Copy Markdown
Contributor

Implements PR 3 of the release-cli program: the pure release model, the registry read port and planning engine, the oras-go v2 read adapter, and the plan tags command. It lands policy ahead of the workflow cutover so it can be reviewed and tested independently.

No workflow change. The actions/github-script planner in publish-oci-image.yml (OP-10/OP-11/OP-12) stays authoritative until the complete two-phase publication path replaces it, which avoids a mixed native-plan/ORAS-tag protocol.

What lands

  • internal/rel — pure release model: Version (canonical stable triple), Digest, Tag, Scope, Channel/ChannelsFor, TagState, ChannelState, Action, Decision, TagPlan, and PlanTags, with sentinels ErrImmutableTag, ErrChannelCorrupt, and ErrStateIncomplete, plus a Secret that redacts through String, GoString, MarshalText, and MarshalJSON.
  • internal/stage/puboci — Image/Reference, the consumer-owned StateReader port (port 2 of the closed budget of 13), classified ErrTagAbsent/ErrRetryable/ErrCorruptState, CollectState (fresh reads), and PlanTags.
  • internal/adapter/reg — read-only oras-go v2 (v2.6.2, the version proven by the GHCR parity spike) adapter. It never pushes, tags, or deletes. Credentials are revealed once and held only inside the auth closure. A nil HTTP client selects oras-go's bounded retry.DefaultClient.
  • internal/cli — plan tags [--image] [--version] --digest [--json] with derived defaults from GITHUB_REPOSITORY and GITHUB_REF_NAME, and the generated StateReader mock under internal/adapter/reg/mocks/.
  • docs/reference/release-cli-contract.md, docs/reference/oci-image-contract.md — tag policy, the JSON result, and the direct-CLI single-writer limitation.

Behavior preserved

Exact tag: absent creates, same digest accepts, any other digest fails. Channels minor -> major -> latest: absent creates, same digest accepts, otherwise the org.opencontainers.image.version annotation is read (only when present and differing, exactly as the workflow does), the release line is enforced for minor and major, and then newer creates, older retains, equal-version-with-different-digest fails.

Three deliberate, documented divergences: version components must fit uint64 (the JS used BigInt), a corrupt annotation surfaces a precise error rather than an absent version, and the CLI collects all state before deciding, so it may report a channel failure where the workflow would report the exact-tag conflict first. All three fail closed.

Verification

  • mise exec -- moon run root:check green: format, lint, build, test, protocol stamp, mock freshness.
  • Layer 1: version/digest/tag grammar and the PlanTags decision table, including release-line violations, equal-version corruption, and deterministic ordering. Layer 2: engine against the generated mock (including proof that the annotation is not fetched when a channel already matches), and the adapter against an in-process OCI registry over httptest covering absent tags, corrupt annotations, 429/503 retryable classification, 401, and transport failures.
  • Live read-only smoke against the published ghcr.io/meigma/release package (0.1.0 is sha256:bb696ae3...): candidate 0.1.0 at that digest planned four accept decisions and zero tags; the same version at another digest failed with ErrImmutableTag and exit 1; candidate 0.2.0 planned create for every tag after reading the 0.1.0 annotation; candidate 0.0.1 planned create for exact and minor while retaining 0 and latest; derived defaults from GITHUB_REPOSITORY/GITHUB_REF_NAME resolved correctly; an unreachable registry reported a retryable transport failure with no URL in the message.

Review follow-ups recorded, not fixed here

  • The Mockery testify template emits no per-method Godoc for generated mocks. Same shape as the mock merged in PR 2; changing it means changing the template for every mock, which belongs in its own change.
  • Collect-then-plan issues three channel reads even when the exact tag already conflicts. Documented; worth revisiting when the planner becomes authoritative.

Add internal/rel, the puboci StateReader port and planning engine, the
oras-go v2 read adapter, and the plan tags command. No workflow change:
the github-script planner stays authoritative until the two-phase path
lands.
Round-1 review and conformance fixes: default oras retry transport for
production reads, transport failures classified retryable, registry
credentials held only in the auth closure, CLI Godoc and reference docs
corrected, and added coverage for JSON configuration failures.
@jmgilman
jmgilman merged commit de75a92 into main Aug 19, 2026
2 checks passed
@jmgilman
jmgilman deleted the feat/release-cli-slice3 branch August 19, 2026 14:36
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