Gå til hovedinnhold

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.

changea merge or a schedule tickcatch upkapi up: memory first, then AI, then checkseach pass re-derivescoverage and repeatscaught up / parkeddelivera pull request with the run reportexit 3 blocks the mergegatekapi check --ship on merge requests

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:

CommandRoleExit behavior
kapi statusObserve: per-locale coverage and gate standingAlways 0: behind is pending work
kapi upProduce: runs the kapi loop0 whether the run caught everything up or parked work for review; non-zero only when the run broke
kapi check --shipEnforce: the release bar0 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

CIInstallRun
GitHub Actionssetup-kapi (CLI + plugins, cached)kapi-action, or the one-line reusable workflows
GitLab CInone needed (the components run on the image)kapi-components from the CI/CD Catalog
Anything container-basedghcr.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/workflows/translations.yml
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.

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/workflows/ship-gate.yml
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.

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:

- uses: neokapi/kapi-action@v1
with:
plan: "true"
pr-comment: "true"

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 --ship and up --plan read the working tree.

  • Unit state travels with the repo: a reviewed threshold 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 runs kapi commit and 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/docs is 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-kapi carries it by default, and a job that uses the action needs no cache step of its own:

    - uses: neokapi/setup-kapi@v1
    with:
    version: "1.2.0"
    # cache-tm: false turns the parse cache off

    Without the action, cache that one directory:

    - uses: actions/cache@v6
    with:
    path: .kapi/work/cache/docs
    key: kapi-parse-${{ runner.os }}-${{ hashFiles('kapi.yaml', 'src/locales/en/**') }}
    restore-keys: kapi-parse-${{ runner.os }}-

    Cache nothing else under .kapi/work. store.db holds the unit state staged since the last commit, and a restored copy changes what a run reports: on a fixture project it turned a failing check --ship into a passing one and a locale's coverage from 0% to 100%. The other entries under cache/, 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.

Works with Bowrain

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