Skip to content

Cut the release tag in CI under OIDC — it is the only step in the chain still requiring a human's laptop #22

Description

@bdelanghe

The observation

mint is already a CI action almost everywhere:

step where it runs identity
mint plan (preview) version.yml, workflow_call
mint attest + cosign signing release-provenance.yml, workflow_call keyless OIDC, no key material
JSR publish consumer's publish-jsr.yml, on v* tag OIDC
cut + push the v* tag a human's laptop a person's git credential

Everything downstream of the tag is already keyless and attested. The tag cut is the only step that isn't — and it's the step that starts the whole chain.

Why that's backwards

release.mjs's in-toto Statement binds tag → version plan → commit and is signed in CI by the workflow's OIDC identity. But the tag itself — the subject of that statement — is created by whoever happened to run the CLI. The provenance attests the record, not the act.

Moving the cut into a pinned workflow_dispatch job improves that rather than merely automating it. Per infra/docs/cloud-sessions.md, job_workflow_ref is load-bearing because it names reviewed, deterministic code on protected main — program attestation. A tag cut inside a pinned workflow is attested by what ran; a laptop tag is attested by who ran it.

It's also the shape the org already settled on for exactly this reason:

The correct shape is the one already in use: the session dispatches; a reviewed, pinned workflow holds the credential and does the work.

And it fits #1's direction (OIDC trusted publishing, no tokens) rather than cutting against it.

It subsumes #18's second item

#18 documents the symptom — under a ruleset restricting tag creation, git push origin v<version> is rejected even for an admin — and proposes documenting a bypass actor plus a --no-push recipe.

That's a workaround for a problem that disappears if the tag is cut by a workflow: the workflow's github.token is an installation token that can be granted tag-creation directly, and there's no standing human credential to bypass anything with. #18's item 1 (bootstrapping a first release) is orthogonal and still worth documenting.

Fresh evidence, 2026-07-31

Hit this cutting drift-gate v0.3.0. mint version and the PR merged fine; mint release did this:

mint: created signed tag v0.3.0
error: RPC failed; HTTP 403
fatal: the remote end hung up unexpectedly

Isolated rather than assumed: a branch push to the same remote succeeded seconds later, and the tag push failed identically on retry. So the refusal is specific to refs outside refs/heads/*.

Different mechanism from #18 — that's an org ruleset, this is a Claude Code session's git proxy — but the same shape: the release stalls at exactly the one step that needs a credential no automated context has. drift-gate 0.3.0 is merged, version-bumped, changelog-written, jsr publish --dry-run green, and unreleasable without someone opening a laptop.

That also blocks a downstream security fix: conformance can't clear its brace-expansion advisory until drift-gate 0.3.0 reaches JSR (ci-workflows#8).

Shape worth considering

A reusable release-cut.yml (workflow_call + consumer workflow_dispatch) that:

  1. verifies the manifest version has no existing tag (fail closed — never overwrite a published version)
  2. verifies .release/ is empty, i.e. mint version has already been applied and merged
  3. re-derives the statement and checks it matches the merged commit
  4. creates and pushes the annotated tag with github.token
  5. lets the existing publish-jsr.yml / release-provenance.yml fire as they do today

Steps 1–3 are things the CLI checks locally today; in CI they'd be enforced rather than trusted.

Open questions

  • Trigger: workflow_dispatch (a human or a session dispatches, workflow does the work) vs. automatic on merge when .release/ has intents. Dispatch keeps a decision point in front of an immutable JSR publish, which argues for it.
  • Confirmation input: require the expected version as a dispatch input and fail if it doesn't match the manifest? Cheap guard against dispatching the wrong thing.
  • Tag signing: the CLI signs the tag when git signing is configured. A workflow has no signing key — but it has OIDC and cosign, and the release Statement is already the stronger attestation. Worth deciding explicitly whether the tag itself still needs a signature or whether the signed Statement supersedes it.
  • Does the CLI path stay? Probably yes for local/bootstrap use (#18 item 1), but it should stop being the expected path.

One thing this must not become

cloud-sessions.md argues against giving an agent Actions OIDC, because "a workflow whose job is 'run an agent' presents an identically-shaped claim while its actual behaviour is whatever the conversation directs."

This is the opposite of that. A release-cut workflow runs pinned, deterministic code against repo state — it's the reviewed-program side of that distinction, not the agent side. Worth stating in the implementation so the two don't get conflated later.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions