Skip to content

Validating Boards in CI

Every board is YAML in git, which means every change to a board, and every change to the dbt models underneath it, can be checked on the pull request that makes it. This guide takes you from zero to a CI gate in three tiers, each catching a class of breakage the previous one cannot:

Tier Setup Catches Needs
1. Structural dct init ci Malformed YAML, unknown fields, broken references between charts, queries, and variables Nothing: no credentials, no dbt
2. dbt manifest add dbt parse A ref() to a renamed or deleted model; a query reading a column the model's SQL no longer produces profiles.yml for dbt parse (no warehouse queries run)
3. Warehouse add --warehouse Queries that no longer bind against the live warehouse: tables, columns, SQL validity Warehouse credentials on the runner

Most teams stop at tier 2: it catches the dominant failure (a dbt change breaking boards) without putting warehouse credentials in CI.

Tier 1: scaffold the workflow

From anywhere inside your project:

pip install dbt-charts
dct init ci

This writes a GitHub Actions workflow at the repository root (.github/workflows/dbt-charts.yml, or dbt-charts-<project-path>.yml for a project nested in a monorepo) that runs dct validate on every pull request touching your boards. Abridged (the real file also triggers on merge to main and manual dispatch, and watches its own path):

on:
  pull_request:
    paths:
      - 'charts/**'
      - 'dbt_charts.yml'

jobs:
  validate:
    name: Validate boards
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v6
        with:
          python-version: '3.13'
      - name: Install dbt Charts
        run: pip install dbt-charts
      - name: Validate boards
        run: dct validate charts

If your dbt project lives in a subdirectory, the scaffolded paths: filters and working-directory are qualified with the project's repo-relative path automatically: GitHub resolves paths: against the repository root, so a hand-written charts/** filter on a nested project silently never fires. See dct init ci for the details and options.

Commit the workflow, open a pull request that breaks a board on purpose (point a chart at a query that doesn't exist) and confirm the check goes red. A gate you have never seen fail is a gate you can't trust.

One operational gotcha: because of the paths: filters, the job doesn't start at all on a pull request that touches no boards. If you make it a required status check, such a PR pins at "Expected, waiting for status"; either leave it optional or drop the paths: filters so the job always runs (it is fast).

Tier 2: check against your dbt models

Structural validation cannot know that {{ ref('orders') }} points at a model you renamed last week. That knowledge lives in dbt's target/manifest.json, which a bare dbt parse writes without building anything or querying your warehouse. Add a parse step before the validate step:

      - name: Parse the dbt project
        run: |
          dbt deps
          dbt parse

      - name: Validate boards
        run: dct validate charts

With a manifest present, dct validate additionally checks:

  • ref() / source() targets exist. A board referencing a renamed or deleted model fails with a did-you-mean hint. Without a manifest this is reported as a WARN-DBT-MANIFEST-MISSING warning rather than checked.
  • Model column drift. Each model's output columns are derived statically from its SQL in the manifest, and every board query reaching that model through ref() is checked against them. Rename customer_id to user_id in models/orders.sql, and a board selecting customer_id from {{ ref('orders') }} fails validation with ERR-DBT-MODEL-COLUMN-MISSING on the same pull request, before dbt run has rebuilt the warehouse, which is exactly the window where warehouse-level validation still passes against the old table.

The drift check is honest about its limits rather than guessing: a model whose columns cannot be derived (a SELECT *, a macro in projection position, a seed or snapshot) warns WARN-DBT-MODEL-COLUMNS-UNRESOLVED, and a board query it cannot analyze (a SELECT * over a ref()) warns WARN-DBT-QUERY-COLUMNS-INDETERMINATE instead of passing in silence. Those warnings don't fail the build by default; dct validate --strict exits 1 on any warning if you want them to, knowing that a project which deliberately ships SELECT * queries will then need to make those columns explicit to get green.

dbt parse needs a profiles.yml to run, but it never queries the warehouse; a profile with placeholder credentials is enough for this tier.

Tier 3: warehouse validation

For the failures only a live warehouse can reveal (a table dropped outside dbt, a column that exists in the model but not yet in the warehouse, dialect-specific SQL errors) add --warehouse:

dct validate charts --warehouse

This uses the cheapest per-adapter mechanism available (DuckDB DESCRIBE, BigQuery dry-run, EXPLAIN on Postgres/Redshift/Snowflake) and never runs your queries at full cost; adapters with no safe primitive are reported as unchecked, not as passing. It requires resolvable warehouse credentials on the CI runner, so most teams run it on a schedule or on merge to main rather than on every pull request. The mechanism table and its guarantees are in dct validate.

Before the rename: dct impact

CI tells you a change broke boards after you push it. To get the answer before touching a model, ask the reverse index:

dct impact customer_id --table orders

dct impact lists every board whose compiled SQL reads that column, resolved through CTEs, aliases, and ref() calls, no warehouse connection needed. The renaming workflow becomes: dct impact to see the blast radius, update the boards and the model in one pull request, and let the tier-2 gate confirm nothing was missed.

Validating locally

The same command CI runs works at your desk and in a pre-commit hook: it is sub-second without --warehouse:

dbt parse && dct validate

Run it after editing models, before pushing, and you'll rarely see the CI gate fire at all.