dbt CI/CD Pipeline

A CI/CD pipeline for dbt automatically tests every code change before it reaches production and deploys passing changes without manual steps. CI stands for Continuous Integration — catching errors early. CD stands for Continuous Deployment — shipping verified changes automatically. Together they mean no bad code reaches your dashboards undetected.

The Problem Without CI/CD

Without CI/CD:
  Developer changes fct_orders.sql
  Runs dbt run locally, looks fine
  Merges to main branch
  Production job runs that night
  Broken model fails at 02:00
  CEO opens dashboard at 08:00 -- no data
  On-call engineer debugged the issue for 2 hours

With CI/CD:
  Developer changes fct_orders.sql
  Opens pull request
  CI pipeline runs automatically on the PR branch
  fct_orders test fails in CI
  PR is blocked, developer fixes the issue
  Production never sees the broken model

CI/CD Pipeline Overview

Developer writes code
        |
        v
Push to feature branch
        |
        v
Open Pull Request (PR)
        |
        v
[CI job triggers automatically]
  dbt deps
  dbt build --select state:modified+
        |
   Pass ---> PR can be reviewed and merged
   Fail ---> PR blocked, developer fixes and pushes again
        |
        v
Merge to main branch
        |
        v
[CD job triggers automatically]
  dbt deps
  dbt build --target prod
        |
        v
Production warehouse updated
        |
        v
Dashboard refreshed with new data

CI Pipeline with GitHub Actions

# .github/workflows/dbt_ci.yml
name: dbt CI

on:
  pull_request:
    branches: [main]

jobs:
  dbt-ci:
    runs-on: ubuntu-latest

    env:
      SNOWFLAKE_ACCOUNT:  ${{ secrets.SNOWFLAKE_ACCOUNT }}
      SNOWFLAKE_USER:     ${{ secrets.SNOWFLAKE_USER }}
      SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
      DBT_SCHEMA:         dbt_ci_${{ github.run_id }}

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dbt
        run: pip install dbt-snowflake

      - name: Install packages
        run: dbt deps

      - name: Download production artifacts
        run: |
          dbt run-operation download_artifacts
          # or copy from S3 / dbt Cloud API

      - name: Run changed models and tests
        run: |
          dbt build \
            --select state:modified+ \
            --defer \
            --state ./prod-artifacts \
            --target ci

Slim CI with --defer and --state

Running the entire project on every pull request wastes time and money. Slim CI runs only the models that changed and their downstream children:

dbt build --select state:modified+ --defer --state ./prod-artifacts

state:modified+   run changed models AND their downstream dependencies
--defer           for models NOT in the selection, read from production
--state           folder containing production manifest.json for comparison
Example with 200 models, only 3 changed:

Without slim CI: rebuild all 200 models (45 minutes)
With slim CI:    rebuild 3 changed + 8 downstream models (4 minutes)

Obtaining Production Artifacts

The --state flag needs the production manifest.json to compare against. Get it from:

Option 1: dbt Cloud API

curl -H "Authorization: Token $DBT_API_TOKEN" \
  "https://cloud.getdbt.com/api/v2/accounts/$ACCOUNT_ID/runs/latest/artifacts/manifest.json" \
  -o prod-artifacts/manifest.json

Option 2: S3 Bucket

aws s3 cp s3://my-bucket/dbt-artifacts/manifest.json prod-artifacts/

CD Pipeline: Deploy to Production

# .github/workflows/dbt_deploy.yml
name: dbt Deploy

on:
  push:
    branches: [main]

jobs:
  dbt-deploy:
    runs-on: ubuntu-latest

    env:
      SNOWFLAKE_ACCOUNT:  ${{ secrets.SNOWFLAKE_ACCOUNT_PROD }}
      SNOWFLAKE_USER:     ${{ secrets.SNOWFLAKE_USER_PROD }}
      SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD_PROD }}

    steps:
      - uses: actions/checkout@v4

      - name: Install dbt
        run: pip install dbt-snowflake

      - name: Install packages
        run: dbt deps

      - name: Run full production build
        run: dbt build --target prod

      - name: Generate docs
        run: dbt docs generate

      - name: Upload artifacts to S3
        run: |
          aws s3 cp target/manifest.json s3://my-bucket/dbt-artifacts/
          aws s3 cp target/catalog.json  s3://my-bucket/dbt-artifacts/
          aws s3 sync target/ s3://my-bucket/dbt-docs/

CI/CD with dbt Cloud (Simpler Setup)

dbt Cloud handles CI/CD with minimal configuration:

Step 1: Create a CI job in dbt Cloud
  Commands: dbt build --select state:modified+ --defer --state ./prod-artifacts
  Trigger: on pull request open

Step 2: Create a Production deploy job
  Commands: dbt deps, dbt build
  Trigger: after merge to main (via webhook or schedule)

Step 3: Connect dbt Cloud to GitHub
  dbt Cloud posts CI results directly to the GitHub PR
  No YAML files needed

Branch Strategy

main         production-ready code, auto-deploys to prod
feature/*    developer branches, CI tests on PR
hotfix/*     urgent fixes, tested in CI before merging to main

What to Test in CI

Always test in CI:
  dbt build --select state:modified+   (changed models and their children)

Also consider testing:
  +exposure:*   (all models feeding exposures)
  tag:critical  (models tagged as business-critical)

CI/CD Checklist

[ ] GitHub Actions (or GitLab CI) workflow file created
[ ] Warehouse credentials stored as encrypted secrets
[ ] Production manifest.json accessible to CI runner
[ ] Slim CI uses --select state:modified+ --defer --state
[ ] CD job runs dbt build --target prod on merge to main
[ ] dbt docs generate runs after production build
[ ] Artifacts uploaded to storage for next CI comparison
[ ] Failure notifications sent to Slack or email

Leave a Comment

Your email address will not be published. Required fields are marked *