Skip to content

CI/CD integration

View .md

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.

  1. 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 accepts expiresAt); rotate by creating a replacement, swapping the secret and deleting the old one. See API keys.

  2. 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 reads COMVI_API_KEY and COMVI_API_BASE_URL from the environment; both override the config file, and the config file has no variable substitution.

  3. Commit a .comvirc.json with 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"
    }
.github/workflows/ci.yml
- 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.

.github/workflows/sync-translations.yml
name: Sync translation files
on:
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 }}

A 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.

.github/workflows/promote.yml
name: Promote translations to production
on:
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.

Terminal
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.

Terminal
# PROJECT_ID: the id field of GET /v1/project
curl -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.

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

For 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.

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.

  • comvi push uploaded nothing. Push reads files, not code. Check the key is in a JSON file under translationsPath matching fileTemplate, 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 --ns or --locale on push exits 0 with “No translation files found”.
  • “requires an interactive terminal”. The run hit a conflict while --force-mode was ask. Pass override, keep or abort, or set push.forceMode in .comvirc.json.
  • Exit 4. Invalid configuration or failed validation: a .comvirc.json that is not valid JSON, an empty namespaces or locales array (omit the field instead), a --ns or --locale the project lacks on pull, an unrecognised --force-mode, or a --dotenv path that does not exist. A missing .comvirc.json exits 1 instead, 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.