Skip to content

feat(cli): verify Actions artifact handoffs - #7

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

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

Conversation

@jmgilman

Copy link
Copy Markdown
Contributor

Summary

Second slice of the release-cli program (plan §3 PR 2). Replaces the three actions/github-script artifact-metadata blocks with release-cli verify handoff, and introduces the program's first genuine port, adapter, and generated mock.

Replaced, about 36 lines each: OB-06 in go-oci-build.yml, GR-05 in publish-github-release.yml, OP-03 in publish-oci-image.yml.

  • pubgh.ArtifactMeta is declared in the consuming internal/stage/pubgh package and implemented by internal/adapter/ghact over go-github/v82, with a Mockery-generated mock under the adapter.
  • Mockery 3.7.3 is pinned; scripts/check-mocks.sh proves the checked-in mocks are byte-identical to the pinned generator's output. Both run in moon run root:check.
  • Each affected job order is now: existing gates and setup, then setup CLI, then verify handoff, then the unchanged actions/download-artifact step.
  • A dispatchable verify-setup-installed.yml exercises the composite's installed acquisition path. It cannot pass until a release exists, so it is intentionally not a merge gate.

Behavior preserved

The three replaced blocks validated that the artifact exists, belongs to the current run, is unexpired, and matches the expected digest, with retries: 3. All of that is preserved.

Retry lives in the engine with an injected sleep function, matching the @octokit/plugin-retry semantics it replaces: four attempts total, waits of 1s, 2s, and 4s, retrying only rate-limit and 5xx responses, never absent, authentication, or malformed responses, and returning immediately on context cancellation.

The three-owner handoff split is stated explicitly in the docs and enforced in code: the CLI verifies the API metadata tuple, the SHA-pinned download-artifact step owns the artifact ZIP transport digest, and later CLI commands verify extracted content. The CLI never claims to recompute the transport digest.

Contract

  • Missing or malformed configuration exits 2 before any network call. A tuple mismatch exits 1. No exit 3.
  • GITHUB_API_URL and GITHUB_SERVER_URL are honored, so Enterprise Server consumers work and the composed command is testable against a local stub. An explicitly empty value is rejected rather than silently retargeting to github.com.
  • Tokens never enter domain values, step outputs, or wrapped errors.
  • Each workflow gains only an optional cli-path input. Every existing input, output, secret, permission, and concurrency group is unchanged. There is still no cli-version input.

Verification

  • mise exec -- moon run root:check green across all seven tasks: format, lint, build, test, protocol stamp, mocks freshness, check.
  • End-to-end against a stub GitHub API: valid tuple exits 0 with the artifact envelope; wrong run, expired, digest mismatch, and missing workflow-run metadata each exit 1 with handoff mismatch plus a specific detail; 404 exits 1 after exactly one request; permanent 503 exits 1 after exactly four requests over about 7s; a flaky endpoint recovers within the budget; a missing token exits 2 with zero requests. The token string appears in no stdout or stderr in any case.
  • Independently confirmed that the three replaced blocks emitted no step outputs and that nothing downstream referenced them, so verify handoff correctly writes no GITHUB_OUTPUT names.
  • Independently confirmed the following download-artifact steps are untouched; the only added use is the dogfood binary download, gated on cli-path.

Review

Two review rounds (verdict: go) and one AGENTS.md conformance audit.

Blocking defects found and fixed:

  • The three verify handoff steps passed no token. Actions does not export GITHUB_TOKEN into run: environments, so the command would have exited 2 on every real run.
  • The bounded retry the replaced scripts carried had been dropped, leaving a single request. ErrRetryable existed with no consumer.

Also fixed: the client hardcoded api.github.com and ignored GITHUB_API_URL, which broke Enterprise Server portability and made the composed command untestable off CI; the mocks freshness check invoked an ambient Mockery 3.7.1 instead of the pinned 3.7.3 and used git status, so it failed on any newly added mock; attestations: read was missing from the oci-image job; composite outputs were interpolated into shell rather than passed through env:; an explicitly empty GITHUB_API_URL silently selected github.com; and the cancellation path returned a raw *url.Error containing the request URL.

Scope removals accepted during review, both for lack of a production caller: the cli.Actions port with internal/adapter/actenv and its generated mock, and the error-sentinel set reduced from eight to the two with real consumers.

Follow-ups recorded in the journal

  • verify-setup-installed.yml should be dispatched immediately after the first release exists, and its run retained as evidence.
  • Retry lives in pubgh, not ghact, so a future direct caller of ghact.Client.Get would get no retry. Noted for PR 3.

Human acceptance requested. I have not merged.

Replace the three actions/github-script artifact-metadata blocks (OB-06,
GR-05, OP-03) with `release-cli verify handoff`. Each caller now validates the
metadata tuple through the GitHub API before downloading: the artifact must
exist, belong to the current run, be unexpired, and report the digest the
caller expects.

This introduces the program's first genuine port and adapter. `ArtifactMeta`
is declared in the consuming pubgh package and implemented by ghact over
go-github v82, with a Mockery-generated mock under the adapter. A pinned
Mockery 3.7.3 plus scripts/check-mocks.sh keeps checked-in mocks byte-identical
to the generator, and both are wired into `moon run root:check`.

Bounded retry lives in the engine with an injected sleep, deliberately matching
the octokit `retries: 3` semantics it replaces: four attempts total with 1s, 2s,
and 4s waits, retrying only rate-limit and 5xx responses, never absent, auth, or
malformed ones, and returning immediately on cancellation.

The three-owner handoff split is preserved and stated explicitly: the CLI checks
the API tuple, the SHA-pinned download-artifact step still owns the ZIP
transport digest, and later commands verify extracted content. The CLI never
claims to recompute the transport digest.

GITHUB_API_URL and GITHUB_SERVER_URL are honored so Enterprise Server consumers
work and the composed command is testable against a local stub. Missing or
malformed configuration exits 2 before any network call; a tuple mismatch exits
1. Tokens never enter domain values, outputs, or wrapped errors.

Reviewed over two rounds and audited against AGENTS.md. The Actions output port
was dropped from this slice for lack of a production caller, and the sentinel
set was reduced to the two with real consumers.
@jmgilman
jmgilman merged commit 35ed2f9 into main Aug 19, 2026
2 checks passed
@jmgilman
jmgilman deleted the feat/release-cli-handoff branch August 19, 2026 03:12
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