The kapi loop in CI
Run the kapi loop in CI, and every language stays caught up with the source without anyone starting a run. A scheduled job translates what changed and opens a pull request, and a merge check holds back a change that breaks your content rules, as a failing test would.
The check covers the source language too. The ship gate includes the source gates (voice, terminology) beside 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 runs these steps in order:
- Reuse. Exact content-memory matches fill first, at no cost. An edited source carries the approved translation of its earlier wording into the prompt as reference.
- Translate. The configured AI provider fills what remains, with the project's terminology and voice as context.
- Check. Deterministic checks run over what was produced: placeholder integrity, inline tags, do-not-translate terms.
A block with a failing finding still counts as translated, and the finding keeps its locale from shipping until it is fixed. What a machine cannot decide waits for review. An approval is recorded in the project store, and the next run sees it.
Three commands divide the work:
| 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 when the run caught everything up and when it left work waiting for review; non-zero only when the run broke |
kapi check --ship | Enforce: the release bar | 0 pass, 3 gate unmet, 4 the check did not run, 1 operational error |
An ordinary build never fails because a target language is behind. You opt in to enforcement with the gate. Wire it into merge-request pipelines, and a quality regression cannot merge, while day-to-day drift goes to review and the build stays green.
Source first: settle before you fan out
The loop is source-first. It settles the
source and holds each block until its source reaches translate_after, then
translates. 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 below the project's
defaults.translate_after level holds (reported as source_not_ready,
routed to a source review), and kapi spends nothing on machine-translating
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, with the server-sync plugin preinstalled) | 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, languages waiting for review. A run that leaves work waiting for review (work a person has to decide) still delivers what it did catch up; the languages waiting for review 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. On GitHub, 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.
stages: [build, test, kapi, deploy]
include:
- component: gitlab.com/neokapi/kapi-components/up@0.2.0
inputs:
stage: kapi
deliver: mr
The component's job runs on scheduled and web-triggered pipelines and on
pushes to the default branch by default. Without a deliver input it only
reports what changed, for a delivery job of your own; deliver: mr opens the
merge request through the API (GITLAB_API_TOKEN, api scope). The job exposes
KAPI_OUTCOME, KAPI_PASSES, KAPI_PARKED_LOCALES and KAPI_HAS_CHANGES to
downstream jobs via dotenv, and lists the changed paths in the
kapi-changed-files.txt artifact.
The pipeline's stages: list must declare the stage the stage input names,
or GitLab refuses to create the pipeline. The job writes its report files
(kapi.env, kapi-up.ndjson and kapi-changed-files.txt) into the checkout,
so list them in .gitignore; otherwise the job counts them as changes and a
delivery commits them.
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. A gate that did not run
(exit 4) fails the job with a "did not run" annotation that names the cause.
include:
- component: gitlab.com/neokapi/kapi-components/check@0.2.0
The job runs on merge-request pipelines and on pushes to the default branch,
fails on exit 3, and posts one threaded MR note with the failing gates and
findings; the full --json report is kept as a job artifact. Exit 4, a gate
that did not run, fails the job too, and the report names the cause. To leave
pushes to the default branch ungated, set the rules input to merge-request
pipelines alone.
The Ship gates & CI recipe walks the
gate end to end, including the import step that lets an established
threshold pass on a runner.
Settle suggestions after a merge
A suggestion becomes a rule once a person's work backs it, and a change
merged to the default branch is such a signal: a person reviewed and merged it
(how a suggestion becomes a rule).
kapi context sync --merged <range> reads the change a range of commits made,
counts it as evidence for every suggestion whose preferred wording it added or
whose avoided wording it removed, and makes each suggestion the evidence backs
a rule when nothing contradicts it. A change counts only in the files where the
suggestion holds, so a merge never applies a rule more widely than where it was
seen. It names the pull request and the merger,
read from the last commit when you pass neither --pr nor --merger. The same
range records the same evidence, so a rerun changes nothing.
The same command syncs the context, so the rules the merge settled reach the rest of the team at once. A project that shares its context nowhere settles the merge on the machine that runs it. On a developer machine, run it after pulling the default branch:
git pull
kapi context sync --merged ORIG_HEAD..HEAD
On a runner, run it after a push to the default branch. With a git backend
the job needs the full history and permission to push the context ref:
name: Settle context
on:
push:
branches: [main]
permissions:
contents: write # push the context ref
jobs:
settle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: neokapi/setup-kapi@v1
- run: >-
kapi context sync
--merged "${{ github.event.before }}..${{ github.sha }}"
--merger "${{ github.actor }}"
A project with no backend keeps what the merge settled in the runner's
workspace, so keep the kapi data directory between runs (KAPI_DATA_DIR and a
cache step) or the evidence is lost with the runner.
Report the cost of a change on its pull request
kapi up --plan dry-runs the loop: pending blocks, 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.2.0
inputs:
stage: kapi
plan: true
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
As in the catch-up job, the pipeline's stages: list must declare the kapi
stage.
This change leaves 42 block(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. -
A runner starts with an empty context store, because a project's terms, voice profiles, content memory and approvals live in a workspace on the machine that holds them. Sync the project's context before the job and again after it; see below. A job 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.dbis derived from this checkout's working tree, 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. Review happens in the Bowrain web app. See the Bowrain documentation.
Sync context around a job
A project that declares a context backend
brings its context to a runner with kapi context sync --no-push before the
job, which reads the shared context and shares nothing, and shares what the job
recorded with kapi context sync after it. With the git
backend the context is kept on a ref of the repository itself, so the job
needs nothing but the checkout and permission to push:
permissions:
contents: write # push the context ref
steps:
- uses: actions/checkout@v5
- uses: neokapi/setup-kapi@v1
- run: kapi context sync --no-push
- run: kapi up
- run: kapi context sync
- run: kapi check --ship
A runner that skips the sync holds the translations the checkout carries and
no record of the source each was made from, so a source edited since a
translation was made is not graded stale there. kapi up and kapi status
say so in one line naming kapi context sync.
kapi context sync fetches the context ref itself (refs/kapi/context),
which a checkout does not. A pull request from a fork can read the shared
context and cannot share back to it, so its jobs run kapi context sync --no-push and leave sharing to a job that runs in the repository. A sync that
cannot reach the backend exits with status 5 and changes nothing, so a job can
retry it.
A job whose gates depend on terms or a voice the repository carries runs
kapi store import before the gate, which reads them into the project's store.
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 person's half of a run that waits for review.