Skip to content

CI/CD Integration

Rocky’s ci command combines compilation and testing into a single step that runs entirely locally using DuckDB. No warehouse credentials, no external services. This makes it ideal for PR checks, branch protection rules, and automated pipelines.

Terminal window
rocky ci --models models --contracts contracts

This runs two phases in sequence:

  1. Compile: Type-check all models, resolve the DAG, validate contracts
  2. Test: Execute each model’s SQL against DuckDB in dependency order
Rocky CI Pipeline
Compile: PASS (12 models)
Test: PASS (12 passed, 0 failed)
Exit code: 0

Exit codes:

  • 0 — all checks passed
  • 1 — compilation or test failures detected

The command detects both compile-time issues (type mismatches, missing dependencies, contract violations) and runtime issues (SQL syntax errors, division by zero, invalid casts).

For PR review, rocky ci-diff is a companion to rocky ci. It compares model files between a base git ref and HEAD, compiles both sides, and reports added / modified / removed columns per model. The output is both JSON (for pipelines) and Markdown (for PR comments):

Terminal window
rocky ci-diff # defaults to main
rocky ci-diff release/2026-04 --models src/models

In GitHub Actions, post the pre-rendered Markdown block to the PR directly:

- name: Post diff to PR
run: |
rocky ci-diff --output json | jq -r .markdown | \
gh pr comment "$PR_NUMBER" --body-file -
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Semantic breaking-change findings and the promote gate

Section titled “Semantic breaking-change findings and the promote gate”

rocky ci-diff --semantic runs the typed-IR breaking-change classifier on top of the structural diff and surfaces classified findings under breaking_findings in the JSON output:

Terminal window
rocky ci-diff --semantic --output json | jq '.breaking_findings'

Each finding has a tagged change.kind (e.g. column_dropped, column_type_changed, target_renamed) and a severity (breaking / warning / info). ci-diff --semantic is informational — even a breaking finding does not change ci-diff’s exit code. Use it on every PR to make breaking changes visible to reviewers before promotion.

The hard gate lives on rocky plan promote + rocky apply. When promoting a branch to production, Rocky runs the same classifier against --base-ref (default main); any finding with severity == "breaking" blocks the promote at plan time and the apply step refuses to execute the plan. To override (e.g. a planned breaking release with downstream consumers already migrated), pass --allow-breaking at plan time. The override emits a breaking_changes_allowed audit event so the bypass leaves a paper trail.

Terminal window
# PR-time: detect (informational)
rocky ci-diff --semantic
# Promote-time: gate (blocks on `breaking` findings)
plan_id=$(rocky plan promote fix-price --base main --output json | jq -r .plan_id)
rocky apply "$plan_id"
# Promote-time override (audited)
plan_id=$(rocky plan promote fix-price --base main --allow-breaking --output json | jq -r .plan_id)
rocky apply "$plan_id"

The bare rocky branch promote <name> form continues to work as an alias for the two-step flow above. See rocky branch promote for the full flag list and the audit-event reference under rocky branch promote in the JSON output reference.

name: Rocky CI
on:
pull_request:
paths:
- "models/**"
- "contracts/**"
- "rocky.toml"
- "tests/**"
jobs:
rocky:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rocky
run: |
curl -fsSL https://raw.githubusercontent.com/rocky-data/rocky/main/engine/install.sh | bash
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Compile and Test
run: rocky ci --models models --contracts contracts

For richer CI reporting, output JSON and upload it as an artifact:

name: Rocky CI
on:
pull_request:
paths:
- "models/**"
- "contracts/**"
- "rocky.toml"
jobs:
rocky:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rocky
run: |
curl -fsSL https://raw.githubusercontent.com/rocky-data/rocky/main/engine/install.sh | bash
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Compile
run: rocky compile --models models --contracts contracts -o json > compile-report.json
- name: Test
run: rocky test --models models --contracts contracts -o json > test-report.json
- name: CI Check
run: rocky ci --models models --contracts contracts -o json > ci-report.json
- name: Upload Reports
if: always()
uses: actions/upload-artifact@v4
with:
name: rocky-reports
path: |
compile-report.json
test-report.json
ci-report.json

Parse the JSON output to post a summary comment on the PR:

- name: CI Check
id: ci
run: |
rocky ci --models models --contracts contracts -o json > ci-report.json
echo "models=$(jq '.models_compiled' ci-report.json)" >> $GITHUB_OUTPUT
echo "passed=$(jq '.tests_passed' ci-report.json)" >> $GITHUB_OUTPUT
echo "failed=$(jq '.tests_failed' ci-report.json)" >> $GITHUB_OUTPUT
- name: Comment PR
if: always()
uses: actions/github-script@v7
with:
script: |
const models = '${{ steps.ci.outputs.models }}';
const passed = '${{ steps.ci.outputs.passed }}';
const failed = '${{ steps.ci.outputs.failed }}';
const status = failed === '0' ? 'PASS' : 'FAIL';
const body = `### Rocky CI: ${status}\n| Models | Tests Passed | Tests Failed |\n|---|---|---|\n| ${models} | ${passed} | ${failed} |`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
rocky-ci:
image: python:3.13-slim
before_script:
- curl -fsSL https://raw.githubusercontent.com/rocky-data/rocky/main/engine/install.sh | bash
- export PATH="$HOME/.local/bin:$PATH"
script:
- rocky ci --models models --contracts contracts
rules:
- changes:
- models/**
- contracts/**
- rocky.toml

Split compilation and testing into separate stages for faster feedback:

stages:
- compile
- test
rocky-compile:
stage: compile
image: python:3.13-slim
before_script:
- curl -fsSL https://raw.githubusercontent.com/rocky-data/rocky/main/engine/install.sh | bash
- export PATH="$HOME/.local/bin:$PATH"
script:
- rocky compile --models models --contracts contracts
rules:
- changes:
- models/**
- contracts/**
rocky-test:
stage: test
image: python:3.13-slim
needs: [rocky-compile]
before_script:
- curl -fsSL https://raw.githubusercontent.com/rocky-data/rocky/main/engine/install.sh | bash
- export PATH="$HOME/.local/bin:$PATH"
script:
- rocky test --models models --contracts contracts
artifacts:
reports:
junit: test-report.xml
rules:
- changes:
- models/**
- contracts/**

rocky compile is faster than rocky ci because it skips test execution. Use it as a lightweight required check on PRs:

Terminal window
rocky compile --models models --contracts contracts

This catches at compile time:

  • Type mismatches: A column used as Int64 in one model but String in another
  • Missing dependencies: depends_on references a model that does not exist
  • Contract violations: Required columns missing, wrong types, or protected columns removed
  • DAG cycles: Model A depends on B, B depends on A
  • Unresolved references: SQL references a table or column that cannot be found

For a single model check during development:

Terminal window
rocky compile --models models --model revenue_summary

Use rocky ai-test to automatically generate test assertions from your models. This requires ANTHROPIC_API_KEY to be set.

Terminal window
export ANTHROPIC_API_KEY="sk-ant-..."
# Add intent descriptions to all models (one-time setup)
rocky ai-explain --all --save --models models
# Generate test assertions from intent
rocky ai-test --all --save --models models

This creates test files in the tests/ directory. Commit them to your repository — they run as part of rocky ci and rocky test on every PR.

Terminal window
rocky ai-test --model revenue_summary --save --models models

Run AI test generation as a scheduled job to keep coverage up to date:

name: Update AI Tests
on:
schedule:
- cron: "0 6 * * 1" # Every Monday at 6am
jobs:
update-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rocky
run: |
curl -fsSL https://raw.githubusercontent.com/rocky-data/rocky/main/engine/install.sh | bash
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Generate Tests
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
rocky ai-explain --all --save --models models
rocky ai-test --all --save --models models
- name: Create PR
uses: peter-evans/create-pull-request@v6
with:
title: "test: update AI-generated test assertions"
body: "Auto-generated test updates from `rocky ai-test`"
branch: update-ai-tests

If you use Dagster to orchestrate Rocky, add both checks to your CI pipeline:

name: Data Pipeline CI
on:
pull_request:
paths:
- "models/**"
- "contracts/**"
- "rocky.toml"
- "dagster/**"
jobs:
rocky:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rocky
run: |
curl -fsSL https://raw.githubusercontent.com/rocky-data/rocky/main/engine/install.sh | bash
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Rocky CI
run: rocky ci --models models --contracts contracts
dagster:
runs-on: ubuntu-latest
needs: [rocky]
steps:
- uses: actions/checkout@v4
- name: Install Python dependencies
run: uv add dagster dagster-rocky
- name: Validate Dagster definitions
run: uv run dg check defs

Rocky’s ci command validates the models independently. Dagster’s definitions validate ensures the orchestration layer can load and wire the models into assets.

All CI-related commands produce structured JSON for programmatic consumption.

{
"version": "1.6.0",
"command": "ci",
"compile_ok": true,
"tests_ok": true,
"models_compiled": 12,
"tests_passed": 12,
"tests_failed": 0,
"exit_code": 0,
"diagnostics": [],
"failures": []
}
{
"version": "1.6.0",
"command": "compile",
"models": 12,
"execution_layers": 4,
"has_errors": true,
"diagnostics": [
{
"severity": "error",
"code": "E001",
"model": "fct_revenue",
"message": "unknown column 'nonexistent'",
"span": { "file": "models/fct_revenue.sql", "line": 5, "column": 9 },
"suggestion": "did you mean 'revenue'?"
}
],
"compile_timings": { "load_ms": 5, "resolve_ms": 1, "typecheck_ms": 12 }
}
{
"version": "1.6.0",
"command": "test",
"total": 12,
"passed": 11,
"failed": 1,
"failures": [
{ "name": "fct_revenue", "error": "division by zero at line 8" }
]
}

Parse with jq for custom CI reporting:

Terminal window
# Check if any tests failed
rocky ci -o json | jq -e '.tests_failed == 0'
# Extract error messages
rocky compile -o json | jq '.diagnostics[] | select(.severity == "error") | .message'