CI/CD integration
Run the dcupl CLI inside your pipeline to push workspace changes automatically on every merge. The pattern is the same on every platform: install the CLI, expose your project ID and API key from your CI secrets as environment variables, then push. This page shows the core pattern, what the push changes in the cloud, and ready-to-copy configs for the major platforms.
The pattern
Run the job from the workspace root, the folder that holds dcupl.config.json: the CLI reads that file, and the files under its baseFolder (default dcupl/), from the current directory. In a folder without a dcupl.config.json the push uses the defaults (baseFolder dcupl, include **/*.json, **/*.csv and **/*.js) and says so on stderr.
# DCUPL_PROJECT_ID and DCUPL_API_KEY come from your CI secrets as environment variables
dcupl files push --force --yesThe files commands read DCUPL_PROJECT_ID and DCUPL_API_KEY straight from the environment, so the job needs no dcupl config set step. Use the project's CLI key (type CLI read/write, see API keys): files status, push and pull reject every other key type. --yes keeps the job non-interactive.
What --force overwrites
A CI job starts from a fresh checkout, so it has no sync baseline: the CLI records the last-synced state in .dcupl/sync-state/, which belongs to one checkout and is never committed. Without a baseline, push can't tell your changes from edits made in the Console. Every file whose content differs from the server copy counts as a conflict, and a push without --force aborts with PUSH_CONFLICTS_NO_BASELINE (exit 1) before it uploads anything. --force resolves each conflict by uploading the repository copy.
On a fresh checkout, dcupl files push --force does this to the target version (draft, unless you pass --version <id>):
| File | What happens |
|---|---|
| Only in the repository | Uploaded |
| In both, with different content | Uploaded: the repository copy replaces the server copy, including Console edits since the last push |
| In both, with the same content | Skipped |
| Only on the server | Kept. Nothing is deleted, with or without --strict |
Push never changes your files, and it never touches production: it rejects that version (promote with dcupl files versions copy). Add --path <folder> to limit the push, and what it can overwrite, to part of the workspace.
The repository wins. Every run overwrites the target version's files with the repository copy, so Console edits to those files are lost on the next push. Edit that version only in the repository, or pull Console edits into it with dcupl files pull (--force in a checkout that has never synced) and commit them before the next run.
Deletions are not mirrored: --strict deletes a server file only when the baseline shows it was deleted locally, and a CI checkout has no baseline. Remove a file from the cloud version with dcupl files delete --path <path> --yes, or in the Console.
Never commit dcupl.secrets.json or .env. Inject the API key from your CI provider's secrets at runtime as the DCUPL_API_KEY environment variable. Make sure .gitignore lists dcupl.secrets.json and .env. dcupl init <folder> writes a .gitignore that does, but dcupl init --here keeps a .gitignore that is already in the folder and adds neither. The CLI keeps .dcupl/ out of Git itself.
If you'd rather have a credentials step, dcupl config set --project-id "$DCUPL_PROJECT_ID" --api-key "$DCUPL_API_KEY" --yes writes the project ID to dcupl.config.json and the key to .env (a fresh checkout has no dcupl.secrets.json). Run it only where dcupl.config.json is committed: without one, it creates a config whose filesUpload.include lacks **/*.js, so scripts, operators and transformers are not pushed.
For a self-hosted Console, commit consoleApiUrl in dcupl.config.json (dcupl config set --console-api-url <url> writes it). There is no environment variable for it: unlike DCUPL_API_KEY and DCUPL_PROJECT_ID, consoleApiUrl is read from the config file only.
Gate the push with validation
Optionally run dcupl validate first and fail the job before any files are uploaded. Remember that validate exits 0 whenever it produces a report, even one with errors, and that the report's top-level ok is always true — check summary.critical and load.ok instead. It exits 1 only when it cannot produce a report (for example, a malformed loader config), so keep the exit-code check too (see Validating a project):
report=$(dcupl validate --json) || { echo "$report"; exit 1; }
loaded=$(echo "$report" | jq -r '.load.ok')
critical=$(echo "$report" | jq -r '.summary.critical')
if [ "$loaded" != "true" ] || [ "$critical" -gt 0 ]; then
echo "Validation failed — aborting push" && exit 1
fi
dcupl files push --force --yesPlatform examples
name: Deploy to dcupl
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v7
with:
node-version: '24'
- run: npm install -g @dcupl/cli
- name: Push to dcupl
run: dcupl files push --force --yes
env:
DCUPL_PROJECT_ID: ${{ secrets.DCUPL_PROJECT_ID }}
DCUPL_API_KEY: ${{ secrets.DCUPL_API_KEY }}Store DCUPL_PROJECT_ID and DCUPL_API_KEY as repository secrets under Settings → Secrets and variables → Actions.
deploy:
image: node:24
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- npm install -g @dcupl/cli
- dcupl files push --force --yesDefine DCUPL_PROJECT_ID and DCUPL_API_KEY as masked CI/CD variables under Settings → CI/CD → Variables. GitLab passes them to the job as environment variables.
version: 2.1
jobs:
deploy:
docker:
- image: cimg/node:24.21
steps:
- checkout
- run: npm install -g @dcupl/cli
- run:
name: Push to dcupl
command: dcupl files push --force --yes
workflows:
deploy-on-main:
jobs:
- deploy:
filters:
branches:
only: mainSet DCUPL_PROJECT_ID and DCUPL_API_KEY as project environment variables in the CircleCI dashboard. CircleCI passes them to the job as environment variables.