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:
| Secret | Value |
|---|---|
KWISPLE_URL | The platform's delivery API. |
KWISPLE_TOKEN | A 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 pushWhat each part does:
- Triggers: every pull request, and pushes to
main. Feature branches are checked through their pull request. concurrencywithout cancelling: two pushes tomainin 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_URLandKWISPLE_TOKENfrom the environment when there is no.env(there is none in CI;.envis git-ignored). pnpm typecheck:tsc --noEmit. Becausecms.config.tsis 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-runon pull requests: prints the changes the merge would make. Read them in the job log during review.pnpm kwisple pushonmain: 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
| Situation | push exit code | The job |
|---|---|---|
Applied, unchanged, or a --dry-run without destructive changes | 0 | passes |
Destructive changes without --force (also on a --dry-run) | 2 | fails; nothing was applied |
| Invalid model, revoked token, unreachable platform | 1 | fails |
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:
- Make sure the code that no longer reads the field is ready (merged, about to deploy).
- From a linked machine, run
npx kwisple push --forceon themainversion of the config. Yourkwisple linktoken has theschemascope. - 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/nullKeep 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:
| Where | Scopes | Why |
|---|---|---|
Your machine (kwisple link) | read + preview + schema | You render drafts and push models. |
| CI | read + schema | Only pushes the model. |
| Production host | read + preview | Renders 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:
| Event | What happens | CI involved? |
|---|---|---|
Code merged to main | CI pushes the model; your host builds the new code. | yes |
| Content published in the dashboard | Depending 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 exitTroubleshooting
- KWISPLE_URL en KWISPLE_TOKEN ontbreken.: the secrets are missing or not passed as environment to the step.
- A
401from 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.