Skip to main content

CI/CD pipelines

Everything cotctl does on your laptop, it can do unattended in a pipeline. Running it in CI/CD is what turns "a partner deploys changes by hand" into "changes are validated and deployed automatically on every merge" — repeatable, reviewable, and not dependent on anyone remembering the steps. This page shows the recommended shape and, importantly, how to handle credentials safely.

A good cotctl pipeline mirrors the manual workflow: validate on every change, apply on merge.

  1. On a pull request — run cotctl validate --dir (offline, no credentials needed). This catches schema and cross-reference errors before review.
  2. On merge to your main branch — run cotctl apply --dir -c <profile> against the target environment, optionally preceded by a --dry-run.

Because every apply is idempotent, re-running the deploy is always safe.

Handling credentials safely

This is the part to get right. In a pipeline there's no browser to log in with, so you authenticate non-interactively — but you never put a token in your repository.

The rule: secrets live in your CI provider, never in code. Store the credentials as encrypted CI secrets (GitHub Actions secrets, GitLab CI variables, etc.) and read them from environment variables at runtime. Never commit a token, and never paste one into a YAML or script that's checked in.

Both cotctl login (browser) and cotctl login --no-browser (email/password) are interactive — they open a browser or prompt for credentials — so they aren't suitable for an unattended job on their own. The reliable way to authenticate in CI is with a pre-generated API token.

1. Generate the token once. An administrator issues an API token from the Cotalker admin panel (or the Partner Platform) and you store its value as an encrypted CI secret — for example COTCTL_API_TOKEN.

2. Authenticate non-interactively with --paste-token. cotctl login --paste-token creates a profile from a pre-generated token instead of prompting for credentials. In CI, pipe the secret into it:

echo "$COTCTL_API_TOKEN" | cotctl login \
--url https://web.cotalker.com \
--subdomain acme \
--profile acme \
--paste-token

Nothing is hardcoded. The command does show a paste prompt, but it reads the piped value from stdin, so the job never hangs.

A worked example (GitHub Actions)

This workflow validates on pull requests and deploys on pushes to main. The credentials come entirely from repository secrets:

name: Deploy Cotalker config

on:
pull_request:
push:
branches: [main]

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm install -g @cotctl/cli
# Offline — no credentials required
- run: cotctl validate --dir config/

deploy:
if: github.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
env:
COTCTL_API_TOKEN: ${{ secrets.COTCTL_API_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm install -g @cotctl/cli
- run: echo "$COTCTL_API_TOKEN" | cotctl login --url https://web.cotalker.com --subdomain acme --profile acme --paste-token
- run: cotctl apply --dir config/ -c acme -y

Note cotctl apply ... -y — the -y flag skips the interactive confirmation prompts, which is exactly what you want in an unattended job.

The CI-oriented flags live on the scoped applies

This is the detail that catches people wiring up their first pipeline. The flags that make an apply pipeline-friendly — machine-readable output, quiet mode, diff control, and the destructive-change gate — are not options of the unified cotctl apply (or apply --dir). They live only on the entity-scoped applies:

FlagOnWhat it does
--jsonsurveys apply, properties apply, workflows applyEmits results as JSON, one object per line, to stdout
--quiet / -qthose three, plus bots/routines/schedules applySuppresses the would-create/would-update chatter; errors still surface
--diff <off\|compact\|verbose>surveys apply, properties apply, workflows applyControls how much per-field diff detail is printed (default compact)
--fail-on-destructivesurveys apply, properties apply, workflows applyExits 2 when a --dry-run detects a destructive change

So a strict per-resource gate uses the scoped form:

# Fail the job if deploying this workflow would destroy anything
cotctl workflows apply -f workflow.yaml -c acme --dry-run --fail-on-destructive

cotctl apply --dir remains the right tool for deploying a mixed directory in dependency order — it just doesn't carry those four flags. A common pattern is: gate each sensitive kind with a scoped --dry-run --fail-on-destructive check, then deploy the whole set with apply --dir.

Exit codes

cotctl maps outcomes to three exit codes, and a good pipeline branches on them:

CodeMeaning
0Success — including a clean --dry-run and a user-cancelled prompt
1Runtime error — network failure, an API 4xx/5xx, a missing file or profile
2Validation failure (the YAML was rejected before anything was sent) — or, on a scoped apply with --fail-on-destructive, a destructive change was detected

Because 2 is distinct from 1, you can treat "destructive change / invalid YAML" differently from "the API was down". The unified apply uses 0/1/2 for success/runtime/validation; the 2-for-destructive meaning is specific to the scoped applies with --fail-on-destructive.

stdout vs. stderr

cotctl keeps the two streams disciplined so your pipeline can parse output reliably:

  • stdout carries the result — the human table, or, under --json, the JSON-Lines payload and nothing else. When you pass --json, the human banner is suppressed so stdout stays machine-parseable.
  • stderr carries warnings, progress notes, and prompts.

So the safe pattern in CI is to capture stdout for parsing and let stderr flow to the log:

cotctl workflows apply -f workflow.yaml -c acme --dry-run --json > result.jsonl
# parse result.jsonl; warnings and progress already went to the job log via stderr

Token lifetime in CI

Tokens expire after 7 days of inactivity. For pipelines that run regularly this is rarely an issue, but for infrequent deploys, prefer a service account and re-authenticate at the start of each run rather than caching a token between runs.

Use --continue-on-error deliberately

By default, a directory apply stops at the first failure — usually what you want, so a broken deploy halts loudly. Add --continue-on-error only when you intentionally want the remaining entities to apply despite one failing.

See also