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:
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 aWARN-DBT-MANIFEST-MISSINGwarning 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. Renamecustomer_idtouser_idinmodels/orders.sql, and a board selectingcustomer_idfrom{{ ref('orders') }}fails validation withERR-DBT-MODEL-COLUMN-MISSINGon the same pull request, beforedbt runhas 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:
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 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:
Run it after editing models, before pushing, and you'll rarely see the CI gate fire at all.
Related¶
dct init ci: scaffold options, nested dbt roots, monoreposdct validate: everything each validation tier checksdct impact: which boards break if this column changes- Error reference and warning reference