Skip to main content

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.

changea merge or a schedule tickcatch upkapi up: memory first, then AI, then checkseach pass re-derivescoverage and repeatscaught up / waiting for reviewdelivera 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 runs these steps in order:

  1. 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.
  2. Translate. The configured AI provider fills what remains, with the project's terminology and voice as context.
  3. 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:

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

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, 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/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. 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.

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. A gate that did not run (exit 4) fails the job with a "did not run" annotation that names the cause.

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:

.github/workflows/settle.yml
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:

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

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 --ship and up --plan read 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/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 is derived from this checkout's working tree, 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. 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​