---
title: 'CI/CD Integration'
description: 'How to validate, diff, and deploy monitors automatically with GitHub Actions or GitLab CI.'
section: 'Guides'
canonical_url: 'https://yorkermonitoring.com/docs/guides/ci-cd'
---

# CI/CD Integration

Wire `yorker validate`, `yorker diff`, and `yorker deploy` into your CI pipeline to get config validation on every push, change previews on pull requests, and automatic deploys on merge.

## Prerequisites

1. **API key** -- generate one from **Settings > API Keys** in the dashboard.
2. **Store as a secret** -- add it as `YORKER_API_KEY` in your CI provider's secret store.
3. **Config committed** -- your `yorker.config.yaml` and any `monitors/` script files must be in version control.

> **Do not run `yorker login` in CI, Docker images, or any non-TTY environment.** `yorker login` is the interactive flow for human workstations: it opens a browser, runs a localhost listener, and writes `~/.yorker/credentials`. None of that works in a headless runner. For automation, mint an API key from **Settings > API Keys** in the dashboard and pass it via `YORKER_API_KEY`. The CLI auth resolution order (`--api-key` flag, then `YORKER_API_KEY`, then `~/.yorker/credentials`) means CI containers without a credentials file simply pick up the env var.

---

## GitHub Actions

Create `.github/workflows/yorker.yml` in your repository:

```yaml
name: Yorker Monitoring as Code

on:
  push:
    paths:
      - "yorker.config.yaml"
      - "monitors/**"
  pull_request:
    paths:
      - "yorker.config.yaml"
      - "monitors/**"

env:
  YORKER_API_KEY: ${{ secrets.YORKER_API_KEY }}
  # Add YORKER_SECRET_* vars here if your config uses {{secrets.*}} interpolation

jobs:
  validate:
    name: Validate config
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm install -g @yorker/cli
      - run: yorker validate

  diff:
    name: Preview changes
    if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
    needs: validate
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      issues: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm install -g @yorker/cli
      - name: Run diff
        id: diff
        run: |
          set +e
          OUTPUT=$(yorker diff --json 2>/dev/null)
          EXIT_CODE=$?
          set -e
          EOF_MARKER=$(dd if=/dev/urandom bs=15 count=1 2>/dev/null | base64)
          echo "json<<$EOF_MARKER" >> "$GITHUB_OUTPUT"
          echo "$OUTPUT" >> "$GITHUB_OUTPUT"
          echo "$EOF_MARKER" >> "$GITHUB_OUTPUT"
          echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"
      - name: Comment on PR
        if: always()
        uses: actions/github-script@v7
        env:
          DIFF_JSON: ${{ steps.diff.outputs.json }}
        with:
          script: |
            const esc = (s) => s.replace(/[|\\`*_{}[\]<>()#+\-!~@\n\r]/g, (ch) => ch === '\n' || ch === '\r' ? ' ' : ch === '@' ? '&#64;' : `\\${ch}`);
            const raw = process.env.DIFF_JSON ?? '';
            let body;
            try {
              const result = JSON.parse(raw);
              if (!result.ok) {
                body = `### Yorker Diff\n\n:x: Error: ${result.error?.message ?? 'Unknown error'}`;
              } else {
                const changes = result.data?.changes ?? [];
                const actionable = changes.filter(c => c.type !== 'unchanged');
                if (actionable.length === 0) {
                  body = '### Yorker Diff\n\n:white_check_mark: No changes. Remote state matches local config.';
                } else {
                  const symbols = { create: '+', update: '~', delete: '-' };
                  const rows = actionable
                    .map(c => `| ${symbols[c.type] ?? '?'} ${c.type} | ${esc(c.kind)} | ${esc(c.name)} |`)
                    .join('\n');
                  body = `### Yorker Diff\n\n| Action | Type | Name |\n|---|---|---|\n${rows}\n\n${actionable.length} change(s) will be applied on merge.`;
                }
              }
            } catch {
              body = `### Yorker Diff\n\n:warning: Could not parse diff output.\n\n<details><summary>Raw output</summary>\n\n\`\`\`\n${raw}\n\`\`\`\n</details>`;
            }

            const comments = await github.paginate(github.rest.issues.listComments, {
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            const existing = comments.find(c =>
              c.user?.type === 'Bot' && c.body?.startsWith('### Yorker Diff')
            );
            if (existing) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: existing.id,
                body,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body,
              });
            }
      - name: Fail on diff errors
        if: steps.diff.outputs.exit_code != '0'
        run: exit ${{ steps.diff.outputs.exit_code }}

  deploy:
    name: Deploy monitors
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: validate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm install -g @yorker/cli
      - run: yorker deploy
```

### How it works

| Trigger | Job | What it does |
|---|---|---|
| Push or PR touching config files | **validate** | Validates YAML syntax and schema. Blocks the pipeline on errors. |
| Same-repo pull request | **diff** | Runs `yorker diff --json`, parses the output, and posts a summary comment on the PR. Updates the same comment on subsequent pushes. Skipped for fork PRs. |
| Push to main touching config files | **deploy** | Applies changes to the remote state. Runs after validation passes. |

**Note:** The diff job's `if:` condition skips fork PRs, where `GITHUB_TOKEN` is read-only and repository secrets are not exposed. The validate job still runs on fork PRs, but will fail if your config uses `{{secrets.*}}` placeholders (since the corresponding environment variables won't be set). If you accept fork contributions, either avoid secret placeholders in validation-critical fields or add the same `full_name == github.repository` guard to the validate job.

### Secrets

The workflow uses workflow-level `env:` so all jobs (including `validate`) can resolve `{{secrets.*}}` and `{{env.*}}` placeholders. Add secrets referenced in your config as additional environment variables:

```yaml
env:
  YORKER_API_KEY: ${{ secrets.YORKER_API_KEY }}
  YORKER_SECRET_SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
  YORKER_SECRET_AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }}
```

---

## GitLab CI

Create `.gitlab-ci.yml` in your repository:

```yaml
stages:
  - validate
  - diff
  - deploy

.yorker:
  image: node:20-slim
  before_script:
    - npm install -g @yorker/cli

validate:
  extends: .yorker
  stage: validate
  rules:
    - changes:
        - yorker.config.yaml
        - monitors/**
  script:
    - yorker validate

diff:
  extends: .yorker
  stage: diff
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - yorker.config.yaml
        - monitors/**
  script:
    - yorker diff
  variables:
    YORKER_API_KEY: $YORKER_API_KEY

deploy:
  extends: .yorker
  stage: deploy
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      changes:
        - yorker.config.yaml
        - monitors/**
  script:
    - yorker deploy
  variables:
    YORKER_API_KEY: $YORKER_API_KEY
```

Add `YORKER_API_KEY` to your project's **Settings > CI/CD > Variables** as a masked variable. Only mark it as protected if you restrict it to protected branches — otherwise MR pipelines on unprotected branches won't have access.

---

## JSON output format

Most CLI commands support `--json` for machine-readable output and share a consistent envelope. The interactive `yorker dashboard` command does not emit this envelope. The other exception is `yorker results tail --json`, which emits one JSON object per result (newline-delimited) instead of a single envelope — this allows streaming consumption.

### Success

```json
{
  "ok": true,
  "data": { ... }
}
```

### Error

```json
{
  "ok": false,
  "error": {
    "code": "general_error",
    "message": "3 config error(s): ..."
  }
}
```

### Exit codes

These are the codes most relevant to CI pipelines (`validate`, `diff`, `deploy`). Other commands may use additional codes (e.g., `yorker status` exits `10` when monitors are unhealthy).

| Code | Meaning |
|---|---|
| `0` | Success |
| `1` | General error (validation failure, API error, missing config) |
| `2` | Authentication failure |
| `3` | Plan/quota limit exceeded |
| `4` | Partial failure (some deploy operations succeeded, others failed) |

### Key command outputs

**`yorker validate --json`**

```json
{
  "ok": true,
  "data": {
    "valid": true,
    "monitors": 5,
    "slos": 2
  }
}
```

**`yorker diff --json`**

```json
{
  "ok": true,
  "data": {
    "changes": [
      {
        "type": "create",
        "kind": "check",
        "name": "API Health",
        "fieldChanges": []
      },
      {
        "type": "update",
        "kind": "check",
        "name": "Homepage",
        "fieldChanges": [
          { "path": "configJson.timeoutMs", "oldValue": 30000, "newValue": 15000 }
        ]
      },
      {
        "type": "unchanged",
        "kind": "check",
        "name": "Orders API",
        "fieldChanges": []
      }
    ]
  }
}
```

Each change has a `type` (`create`, `update`, `delete`, `unchanged`), a `kind` (`check`, `alert`, `slo`, `channel`), and a `fieldChanges` array (empty when there are no field-level differences). Actual CLI output also includes metadata fields such as `remoteId`, `local`, and `remote` which are omitted here for brevity.

**`yorker deploy --json`**

Same as `diff`, plus an `applied` field with operation counts:

```json
{
  "ok": true,
  "data": {
    "changes": [ ... ],
    "applied": {
      "created": 1,
      "updated": 1,
      "deleted": 0,
      "errors": []
    }
  }
}
```

If `applied.errors` is non-empty, the exit code is `4` (partial failure).

---

## Tips

### Pin the CLI version

The CLI install takes 2-3 seconds. To lock a specific version:

```bash
npm install -g @yorker/cli@0.4.0
```

### Deploy with pruning

To keep remote state exactly in sync (deleting monitors removed from config):

```bash
yorker deploy --prune
```

Only use this if your config is the single source of truth. Monitors created through the web UI will be deleted.

### Gate deploys on diff

To require an explicit approval step before deploying, separate the diff and deploy jobs and add a manual gate:

```yaml
# GitLab CI
deploy:
  stage: deploy
  when: manual
  script:
    - yorker deploy
```

### Multiple environments

Use environment variables to deploy different configs to different environments:

```yaml
# GitHub Actions
deploy-staging:
  env:
    YORKER_API_KEY: ${{ secrets.YORKER_API_KEY_STAGING }}
  steps:
    - run: yorker deploy

deploy-production:
  env:
    YORKER_API_KEY: ${{ secrets.YORKER_API_KEY_PRODUCTION }}
  needs: [deploy-staging]
  steps:
    - run: yorker deploy
```

Each API key is scoped to a team, so the same config deploys to different teams.
