Skip to content

feat(cli): stage Go release artifacts - #5

Merged
jmgilman merged 1 commit into
mainfrom
feat/release-cli-stage
Aug 19, 2026
Merged

jmgilman merged 1 commit into
mainfrom
feat/release-cli-stage

Conversation

@jmgilman

@jmgilman jmgilman commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

First slice of the release-cli program (architecture rev 3, plan §3 PR 1). Four production packages replace the producer's bespoke bash validation, and this repository's released artifact becomes release-cli instead of the release-mvp exercise binary.

  • release-cli stage --profile go --dist PATH [--json] replaces the two validation steps formerly at go-pre-publish.yml:88-119 (inventory PP-06/PP-07/PP-08), about 32 lines of bash.
  • release-cli version [--json] reports the linker-injected version and commit plus the protocol constant.
  • .github/actions/setup-release-cli acquires the CLI from the stamped release or from a caller-supplied cli-path.
  • scripts/check-protocol-stamp.sh keeps the action's EXPECTED_PROTOCOL, the Go cli.Protocol constant, and the Release Please stamping markers in step; wired into moon run root:check.

Behavior preserved

PP-06 verifies every checksums.txt payload hash and requires a non-empty regular checksums.txt.sigstore.json. PP-07 requires exactly one Linux Binary record per amd64 and arm64. PP-08 requires each selected path to be confined under the dist root, a regular file, and executable.

Path confinement uses os.OpenRoot, so a symlink escaping the dist root is rejected — not only a lexical ... The workflow_call contract and the PP-09/PP-10 upload steps are unchanged.

Contract

  • Exit codes 0 success, 1 contract/verification failure, 2 usage or configuration error. No exit 3, no generic safe-rerun promise.
  • Under --json, stdout carries exactly one release.dev/result/v1 envelope, including failures with ok:false. If flag parsing itself fails there is deliberately no envelope: usage goes to stderr with exit 2.
  • version prints its data to stdout; stage prints nothing to stdout on success without --json.
  • One release unit: workflows, composite action, and CLI share one version and one consumer pin. There is deliberately no cli-version input. cli-path is an unsupported escape hatch that warns rather than fails on stamp mismatch; the installed path fails closed.
  • go-pre-publish.yml loads the composite through uses: $/.github/actions/setup-release-cli, because external consumers run that workflow against their own checkout.

Verification

  • mise exec -- moon run root:check green: format, lint (strict .golangci.yml), build, test, protocol stamp.
  • Direct binary exercise, not only tests: version and version --json; bare invocation and unknown flag both exit 2 with empty stdout; unknown profile exits 2; env-only resolution through RELEASE_PROFILE, RELEASE_DIST, RELEASE_JSON; flag-over-env precedence; a valid bundle under a non-dist directory name exits 0.
  • Six mutations each exit 1 with ok:false: bad checksum, missing architecture record, escaped path, cleared execute bit, symlink escaping the dist root, and missing signature bundle.
  • Marker guard proven by deleting a Release Please marker and observing exit 1.
  • Three architecture spikes passed beforehand and gate this design: self-repository ($/) pin immunity under an external SHA-pinned caller, oras-go v2.6.2 GHCR parity, and Release Please stamping plus the protocol guard.

Review

Two review rounds by a dedicated reviewer (verdict: go) and one AGENTS.md conformance audit.

Fixed from those rounds:

  • Fail closed when github.action_repository is empty. It previously fell back to the consumer's repository, which collapsed the download source and the attestation trust root onto untrusted ground.
  • Sentinel-based exit classification. A data-controlled asset name could previously flip a verification failure to exit 2.
  • Path confinement derived from the supplied dist directory rather than a hardcoded prefix.
  • --signer-workflow and --deny-self-hosted-runners added to the bootstrap attestation check, matching what the docs tell consumers to run.
  • A bare invocation no longer exits 0 with help on stdout.
  • Standard-library usage per R3, nil guards at exported boundaries, and Viper removed under L1.

Follow-ups recorded in the journal, not in this PR

  • The installed acquisition path cannot be exercised until this PR's first release exists; a dispatchable smoke job lands with PR 2.
  • cmd/release-cli/main.go has no CLI-level testscript coverage; testscript is deferred by the plan.
  • A nested or absolute dist value still fails closed with exit 1 and is documented as the basename rule.

Human acceptance requested. I have not merged.

Introduce release-cli and move the producer's bespoke bash validation into
it. `release-cli stage --profile go --dist PATH` preserves PP-06/PP-07/PP-08
exactly: every checksums.txt payload is streamed through SHA-256, a non-empty
signed checksum bundle is required, and exactly one Linux binary per amd64 and
arm64 is selected, path-confined, and proven to be a regular executable.

Path confinement uses os.OpenRoot so a symlink escaping the dist root is
rejected, not just a lexical `..`. Failures are exit 1 with a diagnostic naming
the offending artifact; usage and configuration errors are exit 2 through a
single ErrUsage sentinel. Under --json each command emits exactly one
`release.dev/result/v1` envelope on stdout.

This also makes release-cli the repository's own released artifact, replacing
the release-mvp exercise binary across GoReleaser, Melange, apko, Release
Please, and moon. The new setup-release-cli composite action acquires the CLI
either from the stamped release (verifying the archive checksum, then
`gh attestation verify` with a derived --signer-workflow, then failing closed
on a version or protocol mismatch) or from a caller-supplied cli-path, which
warns instead of failing because the caller owns that pairing. Workflows and
the action are one release unit with one consumer pin: there is deliberately no
cli-version input. scripts/check-protocol-stamp.sh keeps the action's expected
protocol, the Go constant, and the Release Please stamping markers in step.

Reviewed over two rounds and audited against AGENTS.md. Viper was dropped under
L1: six calls for three environment variables did not justify its dependency
tree in a supply-chain tool, so flags and RELEASE_* now resolve in the command
layer with flag-over-env precedence.
@jmgilman
jmgilman merged commit da64bb3 into main Aug 19, 2026
2 checks passed
@jmgilman
jmgilman deleted the feat/release-cli-stage branch August 19, 2026 00:52
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