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.
1. The rocky ci Command
Section titled “1. The rocky ci Command”rocky ci --models models --contracts contractsThis runs two phases in sequence:
- Compile: Type-check all models, resolve the DAG, validate contracts
- 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: 0Exit 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).
Structural diff against a base ref
Section titled “Structural diff against a base ref”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):
rocky ci-diff # defaults to mainrocky ci-diff release/2026-04 --models src/modelsIn 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:
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.
# 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.
2. GitHub Actions
Section titled “2. GitHub Actions”Basic setup
Section titled “Basic setup”name: Rocky CIon: 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 contractsWith JSON output and artifact upload
Section titled “With JSON output and artifact upload”For richer CI reporting, output JSON and upload it as an artifact:
name: Rocky CIon: 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.jsonPR comment with results
Section titled “PR comment with results”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 });3. GitLab CI
Section titled “3. GitLab CI”Basic setup
Section titled “Basic setup”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.tomlSeparate compile and test stages
Section titled “Separate compile and test stages”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/**4. Using rocky compile for PR Checks
Section titled “4. Using rocky compile for PR Checks”rocky compile is faster than rocky ci because it skips test execution. Use it as a lightweight required check on PRs:
rocky compile --models models --contracts contractsThis catches at compile time:
- Type mismatches: A column used as
Int64in one model butStringin another - Missing dependencies:
depends_onreferences 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:
rocky compile --models models --model revenue_summary5. AI-Powered Test Coverage
Section titled “5. AI-Powered Test Coverage”Use rocky ai-test to automatically generate test assertions from your models. This requires ANTHROPIC_API_KEY to be set.
Generate tests locally
Section titled “Generate tests locally”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 intentrocky ai-test --all --save --models modelsThis 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.
Generate tests for a single model
Section titled “Generate tests for a single model”rocky ai-test --model revenue_summary --save --models modelsCI workflow with AI test generation
Section titled “CI workflow with AI test generation”Run AI test generation as a scheduled job to keep coverage up to date:
name: Update AI Testson: 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-tests6. Integration with Dagster CI
Section titled “6. Integration with Dagster CI”If you use Dagster to orchestrate Rocky, add both checks to your CI pipeline:
name: Data Pipeline CIon: 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 defsRocky’s ci command validates the models independently. Dagster’s definitions validate ensures the orchestration layer can load and wire the models into assets.
7. JSON Output Schema
Section titled “7. JSON Output Schema”All CI-related commands produce structured JSON for programmatic consumption.
rocky ci
Section titled “rocky ci”{ "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": []}rocky compile
Section titled “rocky compile”{ "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 }}rocky test
Section titled “rocky test”{ "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:
# Check if any tests failedrocky ci -o json | jq -e '.tests_failed == 0'
# Extract error messagesrocky compile -o json | jq '.diagnostics[] | select(.severity == "error") | .message'