Kaiten

Guides

Deploy with CI/CD

Drive the release pillar from your pipeline — register the component version on build, cut the release, and record each landing zone by zone, so the deployment journal is written by the system that ships.

Kaiten does not build, store, or run your artifacts. It keeps the authoritative record of what a release contains and where it landed, when, by whom — and the pipeline that does the shipping is the one to write that record. Three calls, at three points of a pipeline run: register the component version when the build succeeds, cut the release when the bundle is final, record the landing when the runner reports success in a zone.

The worked example is Kaiten Sushi Shop's next release. 2026.8.0 runs in shared-eu and shared-apac; sakura-dedicated still runs 2026.7.0. The pipeline ships API 2026.9.0 bundled with the components already in release 2026.8.0, cuts release 2026.9.0, and lands it zone by zone. Examples use $KAITEN_API as in the quickstart; the token is the pipeline's own, created in step 1. Responses are illustrative — ids, actors, and timestamps depend on your stack.

Create a service account for the pipeline

Give the pipeline its own identity, so every journal entry it writes says so, and its token can be rotated without touching anyone else's.

  1. Open Integrations → Service Accounts, click Create Service Account, and name it ci.
  2. On the service account, click Generate Token. On the New Token page, name the token github-actions and, under Access, set Components, Releases and Deployment Zones to Read & write, leaving every other resource on No access. The summary then lists exactly the scopes the three calls need — write:components, write:releases, write:deployment_zones, each with its read:. The Control plane preset covers them too, and grants more than a pipeline needs.
  3. Click Create Token, then Copy token — the token is shown once — and store it as a CI secret (KAITEN_TOKEN), together with the API base URL (KAITEN_API).

On build — register the component version

When the build of the API component succeeds, register the new version as a component chained to its predecessor. Look up the predecessor's id by slug, then create:

API_280_ID=$(curl -s "$KAITEN_API/components/api-2026-8-0" \
  -H "Authorization: Bearer $KAITEN_TOKEN" | jq -r '.id')

curl -s -X POST "$KAITEN_API/components" \
  -H "Authorization: Bearer $KAITEN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API",
    "version": "2026.9.0",
    "slug": "api-2026-9-0",
    "description": "Order batching for multi-location groups.",
    "previousComponentId": "'"$API_280_ID"'"
  }'
{
  "id": "4f5a6b7c-8d9e-4f0a-9b1c-2d3e4f5a6b7c",
  "name": "API",
  "version": "2026.9.0",
  "slug": "api-2026-9-0",
  "description": "Order batching for multi-location groups.",
  "previousComponentId": "…",
  "createdBy": {"id": "…", "name": "ci"},
  "createdAt": "2026-09-08T11:00:00Z"
}

201 Created, and COMPONENT_CREATED is emitted with the pipeline's service account as createdBy. Re-runs are safe: (name, version) is unique per organization, so registering API 2026.9.0 a second time returns 409 Conflict — treat it as "already registered" and read the existing component by slug instead of failing the job.

Derive version and slug from the same source your build uses — the tag, or the version file — so the record and the artifact cannot disagree.

When the bundle is final — cut the release

A release bundles component ids, fixed at creation. Collect the ids of the components that ship together — the new API, and the components already bundled in release 2026.8.0 — Web App 2026.8.0, Kitchen Display 1.2.0, Delivery Service 1.0.0 — and post them:

ids() { curl -s "$KAITEN_API/components/$1" -H "Authorization: Bearer $KAITEN_TOKEN" | jq -r '.id'; }
API_290_ID=$(ids api-2026-9-0)
WEB_280_ID=$(ids web-app-2026-8-0)
KDS_120_ID=$(ids kitchen-display-1-2-0)
DELIVERY_100_ID=$(ids delivery-service-1-0-0)

curl -s -X POST "$KAITEN_API/releases" \
  -H "Authorization: Bearer $KAITEN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "2026.9.0",
    "slug": "r-2026-9-0",
    "description": "Order batching for multi-location groups.",
    "componentIds": ["'"$API_290_ID"'", "'"$WEB_280_ID"'", "'"$KDS_120_ID"'", "'"$DELIVERY_100_ID"'"]
  }'
{
  "id": "8a9b0c1d-2e3f-4a4b-8c5d-6e7f8a9b0c1d",
  "version": "2026.9.0",
  "slug": "r-2026-9-0",
  "description": "Order batching for multi-location groups.",
  "components": [
    {"id": "4f5a6b7c-8d9e-4f0a-9b1c-2d3e4f5a6b7c", "name": "API", "version": "2026.9.0", "slug": "api-2026-9-0", "…": "…"},
    {"id": "…", "name": "Web App", "version": "2026.8.0", "slug": "web-app-2026-8-0", "…": "…"},
    {"id": "…", "name": "Kitchen Display", "version": "1.2.0", "slug": "kitchen-display-1-2-0", "…": "…"},
    {"id": "…", "name": "Delivery Service", "version": "1.0.0", "slug": "delivery-service-1-0-0", "…": "…"}
  ],
  "createdBy": {"id": "…", "name": "ci"},
  "createdAt": "2026-09-08T11:05:00Z"
}

201 Created; RELEASE_CREATED is emitted. A release is immutable — no update endpoint, and the component list cannot change. If a bundle was wrong, delete the release — allowed only while it was never deployed, 409 Conflict afterwards — and cut a new version. A second POST with the same version returns 409 Conflict, so the same re-run rule applies: read GET /releases/r-2026-9-0 and continue. Keep the release id — the landing step needs it.

After the runner succeeds — record the landing

Your runner deploys the artifacts to a zone. When it reports success, point the zone at the release. PUT /deployment-zones/{slug} replaces the zone's details, so read the zone first and resend its fields with the new releaseId:

RELEASE_290_ID=$(curl -s "$KAITEN_API/releases/r-2026-9-0" \
  -H "Authorization: Bearer $KAITEN_TOKEN" | jq -r '.id')

ZONE=$(curl -s "$KAITEN_API/deployment-zones/shared-eu" -H "Authorization: Bearer $KAITEN_TOKEN")

curl -i -X PUT "$KAITEN_API/deployment-zones/shared-eu" \
  -H "Authorization: Bearer $KAITEN_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(echo "$ZONE" | jq --arg r "$RELEASE_290_ID" \
        '{name, type, description, metadata: (.metadata // {}), releaseId: $r}')"

HTTP/1.1 204 No Content. One entry is appended to the journal — 2026.9.0 in shared-eu, now, by ci — and RELEASE_DEPLOYED is emitted with that entry as payload. The zone's releaseId now points at 2026.9.0; every instance in shared-eu is known to run it.

Repeat per zone as your pipeline promotes the release: shared-apac next, sakura-dedicated after its own approval gate. The journal then reads as the rollout it was — three entries, three timestamps, one actor. A PUT that echoes the zone's current releaseId appends nothing to the journal, so re-running a landing step is safe too.

In the console, the zone's page shows where it stands now: open Releases → Deployment Zones, then Shared EU — Current release reads 2026.9.0. The page keeps no history of its own; on the zones list, the zone's Releases count opens the releases it has run. The dated entries of the journal are the Release deployed to a zone rows of the organization's Audit trail page.

Shared EU's overview after the pipeline's deploy: the Production and Deployed badges, 2026.9.0 as the current release, updated by ciShared EU's overview after the pipeline's deploy: the Production and Deployed badges, 2026.9.0 as the current release, updated by ci

A rollback is a deployment. If 2026.9.0 misbehaves in shared-eu, the same PUT with the 2026.8.0 release id appends another entry — the zone ran 2026.8.0, then 2026.9.0, then 2026.8.0, and the journal says so in that order. Nothing is rewritten. The seeded 2026.8.0 is addressable as r-2026-8-0; looking it up by version works on any stack:

RELEASE_280_ID=$(curl -s "$KAITEN_API/releases" -H "Authorization: Bearer $KAITEN_TOKEN" \
  | jq -r '.items[] | select(.version == "2026.8.0") | .id')

Put it in the pipeline

The three calls, as GitHub Actions steps. KAITEN_API and KAITEN_TOKEN are repository secrets; the version comes from the tag that triggered the run; the landing step runs once per zone through an environment gate.

name: Ship API
on:
  push:
    tags: ['v*']

env:
  KAITEN_API: ${{ secrets.KAITEN_API }}
  KAITEN_TOKEN: ${{ secrets.KAITEN_TOKEN }}
  VERSION: ${{ github.ref_name }}          # v2026.9.0

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # ... build and push the API artifact ...

      - name: Register the component version in Kaiten
        run: |
          SLUG="api-$(echo "${VERSION#v}" | tr . -)"
          PREV_ID=$(curl -sf "$KAITEN_API/components/api-2026-8-0" \
            -H "Authorization: Bearer $KAITEN_TOKEN" | jq -r '.id')
          STATUS=$(curl -s -o /tmp/component.json -w '%{http_code}' -X POST "$KAITEN_API/components" \
            -H "Authorization: Bearer $KAITEN_TOKEN" -H "Content-Type: application/json" \
            -d "{\"name\":\"API\",\"version\":\"${VERSION#v}\",\"slug\":\"$SLUG\",\"previousComponentId\":\"$PREV_ID\"}")
          # 201 = registered now, 409 = already registered by an earlier run
          [ "$STATUS" = "201" ] || [ "$STATUS" = "409" ] || { cat /tmp/component.json; exit 1; }

      - name: Cut the release in Kaiten
        run: |
          id() { curl -sf "$KAITEN_API/components/$1" -H "Authorization: Bearer $KAITEN_TOKEN" | jq -r '.id'; }
          IDS=$(jq -cn --arg a "$(id "api-$(echo "${VERSION#v}" | tr . -)")" \
                        --arg b "$(id web-app-2026-8-0)" \
                        --arg c "$(id kitchen-display-1-2-0)" \
                        --arg d "$(id delivery-service-1-0-0)" '[$a,$b,$c,$d]')
          STATUS=$(curl -s -o /tmp/release.json -w '%{http_code}' -X POST "$KAITEN_API/releases" \
            -H "Authorization: Bearer $KAITEN_TOKEN" -H "Content-Type: application/json" \
            -d "{\"version\":\"${VERSION#v}\",\"slug\":\"r-$(echo "${VERSION#v}" | tr . -)\",\"componentIds\":$IDS}")
          [ "$STATUS" = "201" ] || [ "$STATUS" = "409" ] || { cat /tmp/release.json; exit 1; }

  deploy:
    needs: build
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 1
      matrix:
        zone: [shared-eu, shared-apac, sakura-dedicated]
    environment: ${{ matrix.zone }}         # approval gate per zone
    steps:
      # ... your runner deploys the artifacts to ${{ matrix.zone }} ...

      - name: Record the landing in Kaiten
        run: |
          RELEASE_ID=$(curl -sf "$KAITEN_API/releases/r-$(echo "${VERSION#v}" | tr . -)" \
            -H "Authorization: Bearer $KAITEN_TOKEN" | jq -r '.id')
          ZONE=$(curl -sf "$KAITEN_API/deployment-zones/${{ matrix.zone }}" -H "Authorization: Bearer $KAITEN_TOKEN")
          curl -sf -X PUT "$KAITEN_API/deployment-zones/${{ matrix.zone }}" \
            -H "Authorization: Bearer $KAITEN_TOKEN" -H "Content-Type: application/json" \
            -d "$(echo "$ZONE" | jq --arg r "$RELEASE_ID" '{name, type, description, metadata: (.metadata // {}), releaseId: $r}')"

The shape transfers to any runner: register on build, cut when final, record after success, and accept 409 as "already done" on the two creates.

For pipelines that prefer a binary to curl, the CLI covers the same three calls. kaiten components create and kaiten releases create --component-id … are the two creates: a refused one exits with code 5 for a conflict and a validation error alike, and prints the API's status and error code on stderr, which is where a 409 is told apart from a validation error. The landing is a read-modify-write, because kaiten deployment-zones update replaces the zone as the PUT does:

kaiten deployment-zones get shared-eu --output json \
  | jq --arg r "$RELEASE_ID" '.releaseId = $r' \
  | kaiten deployment-zones update shared-eu --file -

Let the landing trigger the rest

Every recorded landing is published as RELEASE_DEPLOYED, with the journal entry — deploymentZoneId, releaseId, createdBy, createdAt — as payload; every cut release as RELEASE_CREATED. Subscribe the systems that act on a landing: the customer notification for the zone's instances, the status page, your own timeline. On Kaiten Cloud Kaiten Cloud that is a webhook endpoint; on a self-hosted stack, a consumer on the event stream. The event catalog has both payloads.

The journal sits next to the contract without being derived from it. Sakura Tokyo's Premium License grants delivery-tracking, and sakura-dedicated ran 2026.7.0 — the release before delivery tracking shipped — until the pipeline landed a newer one there. Keeping what a customer is entitled to and what their instance runs as two records is what lets you see that gap, and close it from the pipeline. Deployments & Lifecycle resolves it instance by instance.

Where to go next

On this page