ADR 0008: GitHub Actions first

Status: accepted, 2026-10-06

Context

Keep Shipping runs inside a CI system; it does not replace the runner. It is a job step, given a checkout and a trigger. So "supporting" a CI system means five specific things: a first-class action people can write in their workflow, the CI's triggers mapped onto the plan's on:, a run identity the cloud trusts, status the way the CI renders it, and a hand-off from the agent to a human that the CI itself enforces. Without all five, a run on that CI is a shell script that happens to print some lines.

M2 is where this is paid for: README.md puts "The same file runs locally and on GitHub Actions. An agent run waits for a human." in M2 Early-access alpha. The examples already assume GitHub's vocabulary — an image tag like ghcr.io/acme/api in docs/LANGUAGE.md and an on: push main trigger in docs/ERRORS.md — and the project lives on GitHub. This ADR decides which CI is native first, from #9.

The port that decides most of this already exists. RunContext (crates/core/src/ports/run_context.rs) is documented as "the one port that differes between a laptop and CI", and it holds the actor, the event, the capabilities and the workspace — everything that differs goes there and nowhere else. Its module docs are also explicit about identity: only an adapter that established Establishment::CiOidc may report ActorKind::Ci, and that is the only IdentityStrength::Strong establishment; a local run can never be CI, however its environment insists. Steps declare what they need as StepCapabilities (crates/core/src/step.rs) — ci_only, oidc, network — and what a run has is the RunContext port's Capabilities.

The GitHub Actions side of that port is already written: crates/cli/src/github_actions_run_context.rs answers from the variables GitHub Actions sets and mints the job's own ID token, reading iss and sub into Establishment::CiOidc. It is honest about its own limits: the token's signature is not verified there, so that establishment means "obtained from GitHub's dedicated OIDC endpoint", not "cryptographically verified"; verifying it is the consumer's job (#150).

Decision

Consequences