Keep Shipping

Keep shipping.

Deploys you can read.

One typed workflow file per repo. It builds your images, plans and applies your infrastructure, waits for a human, and deploys. It's checked before anything runs, and it runs the same on your laptop as in CI.

Early access · in development

ship.ks api · 23 lines
# deploy the api: image → infra → prod
name:  api
on:    push main

steps:
  build:   oci.image
    from:  ./Dockerfile
    push:  ghcr.io/acme/api:{git.sha}
    sign:  true

  plan:    tofu.plan
    dir:   ./infra/prod

  review:  approval
    show:  plan.changes
    from:  @platform

  apply:   tofu.apply
    plan:  plan.file        # the exact plan you approved

  deploy:  k8s.rollout
    image: build.digest     # oci.Digest, never a tag
    to:    apply.out.cluster
    wait:  healthy, 5m
Example Keep Shipping workflow: build and sign an OCI image, plan OpenTofu, wait for approval, apply, and roll out to Kubernetes.

01 / Push and pray

You shouldn't need eleven commits to change a pipeline.

Hundreds of lines of YAML nobody fully understands. Terraform and Docker held together with shell scripts. Every repo drifting a little. And the only way to test any of it is to push and wait.

$ git log --oneline .ci/
e91a0c2 fix ci
b73fd14 fix ci again
40c2e8a try quoting it
8d1f3b7 Revert "try quoting it"
f02a9e1 indent??
3ce77b0 copy deploy step from billing repo
a5519d3 add deploy2.sh
71be0af ok now it runs terraform twice
cc0d6f8 pls
92e4a10 fix ci (final)
0b8f3c5 fix ci (final) (2)

02 / Typed and validated

Mistakes show up before the deploy, not halfway through it.

Every step has typed inputs and outputs. Wire an image tag where a digest belongs, misspell a step, or reference a plan that doesn't exist, and keepshipping check tells you in under a second. Your editor does too.

Fine print: the checker catches wiring and type mistakes. It can't tell you that your tests are wrong or that the registry is down.

$ keepshipping check ✗ ship.ks:21 deploy.image expected oci.Digest, got string 21 │ image: build.tag ^^^^^^^^^ hint: use build.digest so prod runs exactly the image you built 0 steps ran. Nothing was touched.

03 / Local = CI

Run the pipeline before you push it.

keepshipping run on your laptop uses the same engine, the same file and the same steps as the CI runner. If it works here, it works there.

you@laptop ~/apilocal
$ keepshipping run ▸ build sha256:9f2c…e1 signed 38s ▸ plan +2 ~1 -0 11s ▸ review approve 3 changes? y ▸ apply 3 resources 42s ▸ deploy 4/4 pods healthy 27s ✓ shipped api@a41c9e in 2m 18s
runner-07 · push maincloud
$ keepshipping run ▸ build sha256:9f2c…e1 signed 36s ▸ plan +2 ~1 -0 10s ▸ review approved by @platform ▸ apply 3 resources 41s ▸ deploy 4/4 pods healthy 27s ✓ shipped api@a41c9e in 2m 14s

Same digest. Same plan. Same order. The only difference is who pressed y.

Fine print: the engine and steps are identical. Hardware, network and secrets still differ. Steps that can only run in CI are marked, and check tells you before you start.

04 / Built in, not bolted on

Containers and infrastructure are steps, not shell scripts.

oci.image · oci.artifact

Build it, sign it, push it.

Use your Dockerfile. Push to any OCI registry. Signing is one line. The step returns a digest, so every later step deploys exactly what was built, never whatever latest points at today. Charts, SBOMs and other OCI artifacts work the same way.

build.digest sha256:9f2c4a…e1 build.ref ghcr.io/acme/api@sha256:9f2c… build.signed true

tofu.plan · terraform.apply

Read the plan. Then apply that plan.

Terraform and OpenTofu both work. The plan is saved, shown to a reviewer in plain terms, and apply runs that exact file. If the world changed since the plan, apply stops and asks again.

Fine print: a clean plan shows what will change, not whether you should change it. That part is still your call.

+ aws_lb.api ~ aws_ecs_service.api count 2 → 4 + aws_route53_record.api 2 to add · 1 to change · 0 to destroy
k8s.rollout Kubernetes Applies manifests or charts, waits for healthy pods, rolls back if they aren't.
vm.deploy Virtual machines Rolling deploys over SSH, a few hosts at a time, with health checks between.
fn.deploy Serverless Publishes a version, shifts traffic in steps, keeps the previous one warm.

05 / Reusable blocks

Write it once. Use it in forty repos.

Package steps as a typed, versioned block. Each repo pins a version and upgrades on its own schedule. No copy-pasted YAML, no quiet drift.

# acme/blocks · web-service@v3
block:  web-service
inputs:
  image:    oci.Digest
  domain:   string
  replicas: int = 2

# in any repo
  deploy:   acme/web-service@v3
    image:  build.digest
    domain: api.acme.dev

06 / Escape hatch

When a step needs real code, write real code.

Database migrations, odd APIs, that one legacy thing. Drop into a script with typed inputs and outputs, and it runs locally and in CI like every other step.

// ship/migrate.ts
export default step({
  inputs: { db: secret() },
  async run({ db, log }) {
    const n = await migrate(db)
    log(`applied ${n} migrations`)
    return { applied: n }
  },
})

07 / Safe for AI agents

Let agents ship. Keep a human on the button.

Coding agents can write, check and run your workflow. They can't guess their way into prod: a file that doesn't type-check never runs, and anything risky waits for a person.

ship.ks · policyyou decide the line
policy:
  agents:
    can:    check, build, plan, deploy staging
    ask:    @platform
    before: apply, deploy prod, destroy
    never:  read secrets
# humans follow the same file, minus the leash
agent · pr #412--as agent
$ keepshipping run --as agent ✓ check ship.ks is valid ▸ build sha256:4be1…07 signed ▸ plan +1 ~0 -1 1 destroy ⏸ review waiting on a human @platform notified, plan attached nothing applied. agent is free to wait.

Ask a human only when it matters.

A decide step sends the plan to Jev, a typed decision model from TypeSafe AI. It returns one of the answers you defined plus a confidence score, so the checker validates it like any other step. Low-risk, high-confidence changes go through on their own. Everything else waits for a reviewer, with the reason attached.

ship.ks · decidetyped answer + confidence
  risk:    decide
    model:   typesafe/jev
    ask:     "How risky is this plan?"
    input:   plan.changes
    returns: low | medium | high

  review:  approval
    auto:    risk is low ≥ 0.95
             and plan.destroys == 0
    else:    ask @platform, show risk.reason
two runs, same file--as agent
# pr #412 · bump replicas 2 → 4 ▸ risk low 0.97 1 change, 0 destroys ✓ review auto-approved by policy # pr #418 · rename database module ▸ risk high 0.91 replaces aws_db_instance.main ⏸ review waiting on @platform

Fine print: Jev answers in milliseconds and only returns the answers you defined, but it can still be wrong. A confidence score is a number, not a promise. You set the threshold, destroys always go to a human, and every decision is logged with its score.

Wrong files don't run Typed steps and exact error messages give agents something to fix, instead of a failed job to reinterpret.
Approval where it matters Gate apply, prod or destroy. The reviewer sees the plan and the diff, then approves from chat, CLI or web.
Every action signed The run log records who did what: which agent proposed it, which human approved it, which digest shipped.

08 / Honest comparison

What you're probably using now.

Including the rows where it wins.

YAML-based CI Shell scripts, glued Keep Shipping
Mistakes found When the job reaches that line When the script reaches that line Before anything runs
Test locally Mostly no. Push and wait. Yes, if your laptop matches the runner Yes. Same engine as CI.
Containers and IaC Community plugins of varying age Whatever you wrote in 2021 Built in: OCI, Terraform, OpenTofu
Review plan before apply Possible, with artifacts and manual jobs Someone reads the terminal One approval step
Sharing across repos Templates, or copy-paste Copy-paste Typed, versioned blocks
AI agents Push commits and read logs until green Can run anything the shell allows Checked first, scoped by policy, human approval for risky steps
Maturity Huge ecosystem, years of production use Runs anywhere bash does New. That's why it's early access.

Plan. Apply. Ship. Repeat.

Go home on time.

Early access opens in small batches. One email when your invite is ready, nothing else.

Early access · in development