Kwisple
Browse the docs

CLI

CI and automation

A CI token with read and schema scopes, the GitHub Actions workflow that checks the content model on pull requests and pushes it on main, and how that relates to deploys and publishing.

Your content model lives in cms.config.ts, in the site repository. CI makes sure the platform follows the code: on a pull request it checks what the model change would do, and on main it applies it, so a deploy of new code never meets an old model. This page sets that up with GitHub Actions, using the RANSON.N repository as the reference.

What CI needs

Two values, stored as repository secrets:

SecretValue
KWISPLE_URLThe platform's delivery API.
KWISPLE_TOKENA site API token with the scopes read + schema.

That is all kwisple push needs. CI does not need the webhook secret, the dashboard origin or a preview scope: it does not render pages, it only sends the model.

Getting the token with the CLI

On a machine where you ran kwisple login, in the linked project:

npx kwisple ci-token --gh      # creates the token and stores KWISPLE_URL + KWISPLE_TOKEN as GitHub secrets
npx kwisple ci-token           # or print both once and add them yourself

--gh uses the GitHub CLI (gh secret set) for the repository of the current folder and sends the values over stdin, never as command arguments. Without --gh, add them under the repository's Settings → Secrets and variables → Actions. Use --site <id|slug> when the project is not linked or you need a token for another site. See kwisple ci-token.

Getting the token in the dashboard

For a site whose repository is connected under Website → Instellingen → Ontwikkeling → Hosting & deploys, open Geavanceerd (advanced) in that card: the block Contentmodel in CI has the button CI-token maken (create CI token). It shows KWISPLE_URL and KWISPLE_TOKEN once, each with a copy button (Kopieer het token nu: je ziet het maar één keer.). This is for the agency team: it needs the deploy right on the site.

Without a connected repository, use the CLI, or create a token by hand on the same page under API-tokens with the rights Lezen (gepubliceerd) and Contentmodel pushen.

One CI token per person per site

The CLI and the dashboard button create the same token: scopes read + schema, named CI (GitHub Actions). Creating a new one revokes the previous CI token you created for that site. That keeps old tokens from piling up, but it also means: if one site's model is pushed from two repositories, give each its own token from the API-tokens list instead of running ci-token twice. A CI token made by a colleague is not affected by yours.

The workflow

RANSON.N's .github/workflows/ci.yml is the reference: typecheck every push and pull request, check the content model on pull requests, push it on main.

# Checks every push and pull request, then applies the content model (cms.config.ts) to the
# platform on main. Pull requests only check the model (dry run); destructive changes are
# blocked (exit 2) until someone runs `kwisple push --force` deliberately.
#
# Secrets (Settings → Secrets and variables → Actions), easiest with `npx kwisple ci-token --gh`:
#   KWISPLE_URL     delivery API of the platform
#   KWISPLE_TOKEN   site token with "read + schema"
name: CI

on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: false

jobs:
  check:
    runs-on: ubuntu-latest
    env:
      KWISPLE_URL: ${{ secrets.KWISPLE_URL }}
      KWISPLE_TOKEN: ${{ secrets.KWISPLE_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm typecheck
      - name: Check the content model (pull request)
        if: github.event_name == 'pull_request' && env.KWISPLE_TOKEN != ''
        run: pnpm kwisple push --dry-run
      - name: Push the content model
        if: github.event_name == 'push' && env.KWISPLE_TOKEN != ''
        run: pnpm kwisple push

What each part does:

  • Triggers: every pull request, and pushes to main. Feature branches are checked through their pull request.
  • concurrency without cancelling: two pushes to main in a row run one after the other. A model push is never cut off halfway by a newer run.
  • Secrets as job environment: the CLI reads KWISPLE_URL and KWISPLE_TOKEN from the environment when there is no .env (there is none in CI; .env is git-ignored).
  • pnpm typecheck: tsc --noEmit. Because cms.config.ts is TypeScript, this also catches a model that does not type-check, before the platform sees it.
  • env.KWISPLE_TOKEN != '': without the secret (a fork's pull request, a repository not set up yet) the model steps are skipped instead of failing.
  • pnpm kwisple push --dry-run on pull requests: prints the changes the merge would make. Read them in the job log during review.
  • pnpm kwisple push on main: applies the model. Unchanged models print Geen wijzigingen and pass.

pnpm kwisple works because the project's package.json has a script "kwisple": "kwisple". Without that script, use npx kwisple.

Exit codes in CI

Situationpush exit codeThe job
Applied, unchanged, or a --dry-run without destructive changes0passes
Destructive changes without --force (also on a --dry-run)2fails; nothing was applied
Invalid model, revoked token, unreachable platform1fails

A dry run exits 2 when a change is destructive, so the pull request already fails on what the push on main would refuse. The lines marked (destructief) say which changes. Release such a change deliberately, as described below.

Releasing a destructive change

A destructive change hides existing content: a field or section removed or renamed. The content itself is never deleted and returns when the field does. When the change is intended:

  1. Make sure the code that no longer reads the field is ready (merged, about to deploy).
  2. From a linked machine, run npx kwisple push --force on the main version of the config. Your kwisple link token has the schema scope.
  3. Re-run the failed CI job; it now finds nothing to change.

Do not add --force to the workflow. The block is there so a person decides; for renames, see Workflow: dev, push and changes.

The starter workflow

The Astro and Next.js starters ship a workflow named Kwisple under .github/workflows/. It does the same model steps as RANSON.N without the typecheck, and differs in two ways:

  • it installs with whatever lockfile the project has (pnpm, yarn or npm) and runs npx kwisple;
  • it supports an optional third secret, KWISPLE_DEPLOY_HOOK, for a strict "schema first, then build" order:
      - name: Start the production build
        if: github.event_name == 'push' && env.KWISPLE_DEPLOY_HOOK != ''
        run: curl -fsS -X POST "$KWISPLE_DEPLOY_HOOK" > /dev/null

Keep one of the two workflows, not both, or every push to main pushes the model twice.

Production tokens never have the schema scope

Each environment gets the narrowest token that works:

WhereScopesWhy
Your machine (kwisple link)read + preview + schemaYou render drafts and push models.
CIread + schemaOnly pushes the model.
Production hostread + previewRenders published pages and drafts for live preview. Never schema.

A token on a production server is the one most exposed: in the host's environment, readable by every build and by anyone who breaks into the running site. Without schema it cannot change the content model, so a leaked production token cannot hide content. A managed hosting connection creates its own read + preview token for the host. For your own host, create one under API-tokens with Lezen (gepubliceerd) and Preview (concepten). See Environment variables and tokens.

CI, deploys and publishing

Three things change a live site, and they are separate:

EventWhat happensCI involved?
Code merged to mainCI pushes the model; your host builds the new code.yes
Content published in the dashboardDepending on Als inhoud verandert: Alleen verversen sends the signed publish webhook so the site purges its cache, no build; Opnieuw bouwen queues a production build (at most one per site per minute).no
Model pushed by hand (kwisple push)The model changes; the platform's cached answers for the site are refreshed.no

Ordering on merge. Most hosts start building from the repository on the same push that starts CI, so the build and the model push run in parallel. Usually the push is done in seconds, long before the build. If a deploy must never run against the old model, turn off the host's automatic production builds, store the host's deploy hook as the KWISPLE_DEPLOY_HOOK secret, and use the starter's last step (or copy it into your workflow after the push step): the build then starts only after a successful push.

Publishing never needs CI. Editors publish content without any repository activity. A site set to Opnieuw bouwen rebuilds through its hosting connection, not through GitHub Actions. Details in Hosting and deploys.

Staging

A staging environment is a site of its own with its own token. Its branch's CI pushes the model to the staging site with a CI token made for the staging site (kwisple link → the (staging) entry, then kwisple ci-token), stored in the secrets the staging workflow reads. Give the staging run its own secret names or its own repository environment, so a staging push can never use production's token. See Staging.

Other CI systems

Nothing is GitHub-specific apart from ci-token --gh. On any CI:

export KWISPLE_URL=…         # from the CI's secret store
export KWISPLE_TOKEN=…
npm ci
npx kwisple push --dry-run   # for change requests
npx kwisple push             # on the main branch; fail the job on a non-zero exit

Troubleshooting

  • KWISPLE_URL en KWISPLE_TOKEN ontbreken.: the secrets are missing or not passed as environment to the step.
  • A 401 from the platform: the token was revoked, often because someone created a newer CI token for the site. Create a new one and update the secret.
  • Deze site is een gehoste site met het standaardschema; …: the token belongs to a legacy hosted site; only custom-code sites push models.
  • Exit code 2: see Releasing a destructive change.

Last updated 2026-10-30