The kapi loop in CI
Source content changes on every merge, and everything derived from it (its translations, its voice and terminology conformance) falls behind. That drift is the normal state of a shipping project. 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, with content quality treated like any other test. This covers the source language too: the ship gate includes the source gates (voice, terminology) alongside translation coverage and the rule-based checks.
CI catches up and delivers; the gate alone decides what blocks a merge.
Inside a pass, for every language behind its gate, the default flow does three things in order: reuse, where exact content-memory matches fill first, for free, and an edited source carries its previously approved translation into the prompt as reference; translate, where the configured AI provider fills what remains, with the project's terminology and voice context; check, where deterministic checks run over what was produced (placeholder integrity, inline tags, do-not-translate terms). A unit with a failing finding still counts as translated, and the finding holds its locale out of shipping until it is fixed, 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 unit-state record, 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 |
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. 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 | none needed (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.
- 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, kapi-action, and a delivery
step that opens the pull request. Used directly, the action only reports:
has-changes and changed-files feed a delivery step you own (plain git,
git-auto-commit-action, create-pull-request), so the token, the authorship
and the review policy are stated in your workflow. One GitHub behavior to
know: pushes made with the workflow's own GITHUB_TOKEN trigger no
workflows, so give the delivery step a fine-grained PAT or a GitHub App token
when CI or deploys should react to delivered translations.
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 (voice, terminology,
rule-based checks, staleness) 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 makes the pipeline find them before a reader does.
- 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 unit-state record 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, with 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 that declares a server venue) 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. -
Unit state travels with the repo: a
reviewedthreshold reads the committed unit-state record under.kapi/state/; commit it in the PR or the runner sees no approvals. A job that makes decisions runskapi commitand commits the shards it produces, alongside the translated files. It never commits.kapi/work/store.db. -
The parse cache is the part to carry between runs:
.kapi/work/cache/docsis gitignored, rebuilt on a fresh runner, and keyed by each file's path and content, the parse configuration, the recipe and the kapi build, so a restored entry changes no result.setup-kapicarries it by default, and a job that uses the action needs no cache step of its own:- uses: neokapi/setup-kapi@v1with:version: "1.2.0"# cache-tm: false turns the parse cache offWithout the action, cache that one directory:
- uses: actions/cache@v6with:path: .kapi/work/cache/docskey: kapi-parse-${{ runner.os }}-${{ hashFiles('kapi.yaml', 'src/locales/en/**') }}restore-keys: kapi-parse-${{ runner.os }}-Cache nothing else under
.kapi/work.store.dbholds the unit state staged since the last commit, and a restored copy changes what a run reports: on a fixture project it turned a failingcheck --shipinto a passing one and a locale's coverage from 0% to 100%. The other entries undercache/, such as the extraction batches, the redaction record and the sync state, belong to the checkout that wrote them. Server-connected projects skip the cache entirely; the project's context lives on the server.
Everything on this page runs the loop inside your own CI. A project whose recipe declares a bowrain: 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 statsandkapi up --planbefore commissioning it. - Review & approve: the human half of a parked run.