The kapi loop in CI
Source content changes on every merge, and everything derived from it — its translations, its brand and terminology conformance — falls behind. That drift is the normal state of a shipping project, not an error. Running the kapi loop in CI keeps every language caught up from the pipeline that produces the drift: close what a machine can close, deliver it as a reviewable pull request, and enforce the release bar on merges — content quality treated like any other test. This covers the source language too: the ship gate includes the source gates (brand, terminology) alongside translation coverage and QA.
CI catches up and delivers; the gate — not drift — decides what blocks a merge.
Inside a pass, for every language behind its gate, the default flow does three things in order: reuse — exact translation-memory matches fill first, for free; translate — the configured AI provider fills what remains, with the project's terminology and brand context; check — deterministic checks run over what was produced (placeholder integrity, inline tags, do-not-translate terms), and a unit with a failing finding counts as drafted, not translated, so bad output can never lift a language over its gate. What a machine cannot decide parks for review; an approval is recorded in the committed state store, and the next run sees it.
Three commands carry the whole model, and they divide the work cleanly:
| Command | Role | Exit behavior |
|---|---|---|
kapi status | Observe — per-locale coverage and gate standing | Always 0: behind is pending work, not an error |
kapi up | Produce — runs the kapi loop | 0 whether the run caught everything up or parked work for review; non-zero only when the run broke |
kapi check --ship | Enforce — the release bar | 0 pass, 3 gate unmet, 1 operational error |
An ordinary build never fails because a target language is behind. The gate is the explicit, opt-in enforcement point — wire it into merge-request pipelines and a quality regression cannot merge, while day-to-day drift flows through review instead of breaking builds.
Source first: settle before you fan out
The loop is source-first: it settles the source
and holds at the source gate before translating any language, because a term left
unresolved or a sentence left off-brand in the source is paid for once per
language and again on the fix. In CI this shows up two ways. kapi check --ship
runs the project's source_gate alongside the target ship gates, so a merge whose
source is off-brand or below its terminology bar fails the gate the same way a
target-coverage regression does. And under source-first convergence, source that
has not cleared its source_gate holds — reported as source_not_ready,
routed to a source review — rather than spending on a machine translation of
content that is about to change. Settling the source first is the cheap, honest
outcome; see Source first for the model.
The surfaces
| CI | Install | Run |
|---|---|---|
| GitHub Actions | setup-kapi (CLI + plugins, cached) | kapi-action, or the one-line reusable workflows |
| GitLab CI | — (the components run on the image) | kapi-components from the CI/CD Catalog |
| Anything container-based | ghcr.io/neokapi/kapi (cosign-signed; a variant with the server-sync plugin preinstalled is published alongside) | The CLI directly — the exit codes above are the contract |
Catch up on a schedule, deliver a pull request
The produced translations land as a branch and a pull request carrying the run report — outcome, passes, parked locales. A run that parks (work remains that needs a person) still delivers what it did catch up; the parked locales are the review queue, not a failure.
- GitHub Actions
- GitLab CI
name: Translations
on:
schedule:
- cron: "0 6 * * 1-5"
workflow_dispatch:
jobs:
up:
uses: neokapi/kapi-workflows/.github/workflows/up.yml@v1
permissions:
contents: write
pull-requests: write
secrets:
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
The reusable workflow composes setup-kapi and kapi-action; use the action
directly (create-pull-request: "true") when the job needs a custom shape.
include:
- component: gitlab.com/neokapi/kapi-components/up@0.1.0
The component's job runs on scheduled and web-triggered pipelines by default,
opens the merge request through the API (GITLAB_API_TOKEN, api scope), and
exposes KAPI_OUTCOME, KAPI_PASSES, and KAPI_PARKED_LOCALES to downstream
jobs via dotenv.
Gate merges on content quality
kapi check --ship runs the project's bound quality gates — brand,
terminology, QA — plus its ship/source coverage gates, and exits 3 when any
gate is unmet. Machine-produced translations fail quietly: a dropped
placeholder, a competitor's term, wording that drifts off the approved
vocabulary. The gate turns those from something a reader finds into something
a pipeline finds.
- GitHub Actions
- GitLab CI
name: Ship gate
on:
pull_request:
paths: ["content/**", "src/locales/**"]
jobs:
ship-gate:
uses: neokapi/kapi-workflows/.github/workflows/gate.yml@v1
permissions:
contents: read
pull-requests: write
An unmet gate fails the job with a distinct "gate unmet" annotation and posts one sticky report comment that re-runs update in place.
include:
- component: gitlab.com/neokapi/kapi-components/check@0.1.0
The job runs on merge-request pipelines, fails on exit 3, and posts one
threaded MR note with the failing gates and findings; the full
kapi.check/v1 report is kept as a job artifact.
The Ship gates & CI recipe walks the
gate end to end, including the committed state store that lets a reviewed
threshold pass on a runner.
Report the cost of a change on its pull request
kapi up --plan dry-runs the loop: pending units, content-memory leverage, and a
token estimate — no writes, no provider calls, no keys. Posted on the pull request
that created the work, it answers "what does this change cost in translation?"
before anyone merges:
- GitHub Actions
- GitLab CI
- uses: neokapi/kapi-action@v1
with:
plan: "true"
pr-comment: "true"
include:
- component: gitlab.com/neokapi/kapi-components/up@0.1.0
inputs:
plan: true
deliver: "none"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
This change leaves 42 unit(s) of pending translation work: 30 recoverable from content memory, 12 for AI (~450 tokens estimated).
Credentials and state on a runner
-
A producing job uses an API key — set a provider key (for example
ANTHROPIC_API_KEY) as a CI secret. A subscription sign-in is bound to your user and its usage window; see Let kapi run Claude. In a server-connected project (a recipe with aserver:block) the server holds the keys instead, and the job authenticates with a server token. -
A gate-only job needs no keys at all —
check --shipandup --planread the working tree. -
Review decisions travel with the repo — a
reviewedthreshold reads the committed state store (.kapi-state.json); commit it in the PR or the runner sees no approvals. -
The cache is rebuildable, and cacheable —
.kapi/cache(block store, extractions) is gitignored and rebuilt on a fresh runner. Restore it across runs to skip re-extraction:- uses: actions/cache@v5with:path: .kapi/cachekey: kapi-cache-${{ hashFiles('kapi.yaml', 'src/locales/en/**') }}restore-keys: kapi-cache-Server-connected projects skip this — the project state lives on the server.
Everything on this page runs the loop inside your own CI. A project whose recipe declares a server: block runs the same loop on a Bowrain server instead — kapi up pushes, the server catches every locale up against the workspace's shared memory and terminology, and the run streams back — with review handled in the Bowrain web app rather than in your checkout. See the Bowrain documentation.
Next
- The kapi loop — the loop and the derived-state model behind
status,up, and the gates. - Ship gates & CI — the enforcement recipe, end to end.
- Estimate a run's cost — sizing a job with
kapi statsbefore commissioning it. - Review & approve — the human half of a parked run.