Integrations

GitLab CI

Publish Kubb Studio snapshots from GitLab CI and review generated-file changes.

Publish an installable Kubb Studio snapshot from GitLab CI so reviewers can compare generated files against the base branch and previous runs.

Prerequisites

  • A repository with a working kubb.config.ts. Follow Installation if you have not set up Kubb.
  • A Kubb Studio organization and an organization CI API key.
  • Commit your npm lockfile and add kubb to the project’s development dependencies so npm ci installs the CLI.

Store the CI API key as KUBB_TOKEN in a masked CI/CD variable. Snapshot installation uses a separate registry key. Only expose the CI secret to trusted pipelines.

Configure the workflow

Add this job:

.gitlab-ci.yml
snapshot:
  stage: build
  image: node:22
  resource_group: kubb-snapshot-$CI_COMMIT_REF_SLUG
  script:
    - npm ci
    - npx kubb studio snapshot
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

kubb ships the Studio runtime, so npm ci is the only setup. The rules run the job on merge request pipelines and on the default branch, so a merge request has a snapshot of its target branch to compare with.

resource_group keeps two pipelines on the same branch or merge request from running the job at once. Registering the agent again ends the other run's session.

Post the result as a note

Use --json to capture the snapshot result, then post it as a merge request note with this script. It updates the same note on each pipeline and uses only Node:

scripts/kubb-note.mjs
import { readFileSync } from 'node:fs'

const snapshot = JSON.parse(readFileSync('snapshot.json', 'utf8'))
const marker = '<!-- kubb-snapshot -->'
const counts = (c) => `${c.added.length} added · ${c.changed.length} changed · ${c.removed.length} removed`
const lines = [marker, `Kubb snapshot: \`npm i ${snapshot.url}\``]

const { branchChanges: branch, changes } = snapshot
if (branch) {
  if (branch.base) lines.push(`Changes against \`${branch.branch}\`: ${counts(branch)}`)
  else if (branch.baseFound) lines.push(`No snapshot of \`${branch.branch}\` for this package yet`)
  else lines.push(`No snapshot of \`${branch.branch}\` to compare with`)
}
if (changes?.base) lines.push(`Changes since ${changes.base.commit?.slice(0, 7) ?? changes.base.createdAt}: ${counts(changes)}`)

const { CI_API_V4_URL, CI_PROJECT_ID, CI_MERGE_REQUEST_IID, GITLAB_API_TOKEN } = process.env
const notes = `${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes`
const headers = { 'PRIVATE-TOKEN': GITLAB_API_TOKEN, 'Content-Type': 'application/json' }
const existing = (await (await fetch(`${notes}?per_page=100`, { headers })).json()).find((note) => note.body.startsWith(marker))
const response = await fetch(existing ? `${notes}/${existing.id}` : notes, {
  method: existing ? 'PUT' : 'POST',
  headers,
  body: JSON.stringify({ body: lines.join('\n\n') }),
})
if (!response.ok) throw new Error(`GitLab answered ${response.status}`)
.gitlab-ci.yml
  script:
    - npm ci
    - npx kubb studio snapshot --json > snapshot.json
    - '[ -z "$CI_MERGE_REQUEST_IID" ] || node scripts/kubb-note.mjs'

Writing a note needs a project access token with the api scope, stored as GITLAB_API_TOKEN. CI_JOB_TOKEN does not carry it.

Review and install the result

Open the snapshot in Studio to review generated changes. Snapshots expire after seven days; schedule a default-branch run if it can go a week without a push.

To consume the generated package, follow Install the snapshot.

See also