CI/CD integration
The CDN serves translations at runtime, so a pipeline only bundles them when the app reads local files. CI is for the checks around the edges: proving generated types still match the project, uploading translation files a developer changed, and promoting approved content on a release.
| Command | What it does | When to run it |
|---|---|---|
comvi typegen --check |
Compares the types file on disk with the project schema and exits non-zero when stale | Every build |
comvi typegen |
Regenerates the types file in place | When you regenerate instead of committing types |
comvi push |
Uploads the JSON files under translationsPath |
After a developer edits translation files |
comvi pull |
Downloads translations into local JSON files | Builds that bundle translations, and promotions |
@comvi/cli installs comvi and comvi-typegen; both point at the same program. There is
no source-code extractor: comvi push reads translation files, not t('...') calls.
Set up
Section titled “Set up”-
Create a CI key under Settings → API Keys with the matching preset: CLI pull (
translations:read), CLI push/import (translations:read,translations:write) or Type generation (schema:read). The dashboard sets no expiry (the REST API acceptsexpiresAt); rotate by creating a replacement, swapping the secret and deleting the old one. See API keys. -
Store the key as a CI secret named
COMVI_API_KEY: GitHub under Settings → Secrets and variables → Actions, GitLab under Settings → CI/CD → Variables (masked). The CLI readsCOMVI_API_KEYandCOMVI_API_BASE_URLfrom the environment; both override the config file, and the config file has no variable substitution. -
Commit a
.comvirc.jsonwith everything except the key:.comvirc.json {"apiBaseUrl": "https://api.comvi.io","translationsPath": "./src/locales","fileTemplate": "{namespace}/{languageTag}.json","format": "json","outputPath": "src/types/i18n.d.ts"}
Check generated types
Section titled “Check generated types”- name: Check translation types run: npx --package @comvi/cli comvi typegen --check env: COMVI_API_KEY: ${{ secrets.COMVI_API_KEY }}
- run: npm run build| Exit code | Meaning |
|---|---|
0 |
The types file matches the project |
1 |
The types file is outdated |
2 |
The check could not run: connection, auth, timeout or an unreadable response |
4 |
Invalid config, or a schema that cannot be emitted |
--check never writes. Exit 2 is separate from exit 1 so a flaky network reads differently
from real drift. To regenerate instead of committing the file, drop --check and run
comvi typegen before the build.
Push translation files on merge
Section titled “Push translation files on merge”name: Sync translation fileson: push: branches: [main]
jobs: push-files: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 24 } - run: npm ci - name: Preview the push run: npx --package @comvi/cli comvi push --dry-run env: COMVI_API_KEY: ${{ secrets.COMVI_API_KEY }} - name: Push run: npx --package @comvi/cli comvi push --force-mode keep env: COMVI_API_KEY: ${{ secrets.COMVI_API_KEY }}push-files: stage: post-merge only: - main script: - npm ci - npx --package @comvi/cli comvi push --force-mode keep variables: COMVI_API_KEY: $COMVI_API_KEYA local value that differs from the value in Comvi is a conflict, and --force-mode
decides: override uploads local values over remote ones, keep uploads only the values
that do not conflict, abort stops on the first conflict. The default, ask, prompts on
the terminal and throws in CI with --force-mode ask requires an interactive terminal.
--dry-run prints how many keys would be created, how many translations updated and how
many conflict, without writing.
Promote staging to production on release
Section titled “Promote staging to production on release”name: Promote translations to productionon: release: types: [published]
jobs: promote: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 24 }
- name: Pull from staging run: npx --package @comvi/cli comvi pull --path ./promote env: COMVI_API_KEY: ${{ secrets.COMVI_STAGING_KEY }}
- name: Push to production run: npx --package @comvi/cli comvi push --path ./promote --force-mode keep env: COMVI_API_KEY: ${{ secrets.COMVI_PROD_KEY }}The API key decides which project the CLI talks to. comvi pull has no status filter, so
an unreviewed draft in staging arrives in production as Translated: finish the review in
staging before the release runs, or promote by hand with Export filtered to Translated
and Import. See Staging to production.
comvi pull --path ./promote --locale de,fr --ns checkout--locale and --ns take comma-separated lists and fully replace the locales and
namespaces arrays in .comvirc.json. A pull exits 4 if the server does not return a
locale or namespace you asked for, so a typo fails the job instead of writing empty files.
Gate on coverage
Section titled “Gate on coverage”# PROJECT_ID: the id field of GET /v1/projectcurl -sf -H "Authorization: Bearer $COMVI_API_KEY" \ "https://api.comvi.io/v1/projects/$PROJECT_ID/locales/stats" \ | jq -e '.[] | select(.localeCode == "de") | .translatedPct >= 95'GET /v1/projects/:id/locales/stats returns one row per locale with counts and percentages
for translated, notReviewed and notTranslated; the key needs translations:read.
jq -e exits 1 when the expression is false. Percentages count only Translated, so a
language full of unreviewed drafts reads as low coverage. Locale codes in responses are
canonical BCP-47 (pt-BR, zh-Hans), so match on the hyphenated form.
Pull on every build
Section titled “Pull on every build”- run: npx --package @comvi/cli comvi pull env: COMVI_API_KEY: ${{ secrets.COMVI_API_KEY }}- run: npx --package @comvi/cli comvi typegen- run: npm run buildFor an app that bundles translation files instead of reading the CDN, pull before the build so every deploy carries the latest published values and the repository never stores them. See comvi pull.
Webhook or CLI on every build
Section titled “Webhook or CLI on every build”Running the CLI on every build is deterministic, but a fresh copy lands only on the next
build. A webhook on translation.updated pointed at
GitHub’s repository_dispatch API or a GitLab pipeline trigger token starts the build within
minutes of a change. Apps that read the CDN at runtime need neither.
Troubleshooting
Section titled “Troubleshooting”comvi pushuploaded nothing. Push reads files, not code. Check the key is in a JSON file undertranslationsPathmatchingfileTemplate, and that every locale you pushed exists in the project; the import endpoint skips a locale the project does not have and still reports success. An unknown--nsor--localeon push exits0with “No translation files found”.- “requires an interactive terminal”. The run hit a conflict while
--force-modewasask. Passoverride,keeporabort, or setpush.forceModein.comvirc.json. - Exit 4. Invalid configuration or failed validation: a
.comvirc.jsonthat is not valid JSON, an emptynamespacesorlocalesarray (omit the field instead), a--nsor--localethe project lacks on pull, an unrecognised--force-mode, or a--dotenvpath that does not exist. A missing.comvirc.jsonexits1instead, the same code as a stale types file. - CI sees older translations than translators. The CDN response is cached for 30 seconds and auto-deploy waits out a 5-second debounce; a CI cache in front of that can hold a much older copy.