Skip to content

Migrating to dbt-bouncer v4.0.0#

dbt-bouncer v4.0.0 removes the back-compat shims that v3 kept around, raises the minimum supported dbt version, and adds a few small, non-breaking improvements. This guide covers what changed and what you need to do to upgrade.

Who needs to do what#

You are... Action required
Running dbt manifests/catalogs generated by dbt-core below 1.10 Must upgrade — see dbt version floor
Importing from dbt_bouncer.check_base, check_context, check_decorator, check_patterns or checks.common Must update imports — see removed import paths
Writing custom checks by subclassing BaseCheck with an execute() method Must migrate — see class-based checks removed
Importing dbt_bouncer.main.run_bouncer or dbt_bouncer.cli.list_checks Must update imports — see removed re-exports
Importing dbt_bouncer.enums.OutputFormatCLI Must update imports — see renamed enum
Keeping custom checks in a custom_checks_dir Must verify that every file imports — see custom check load failures
Relying on a run that matches no resources exiting 0 Must review — see no checks run
Using check_model_description_contains_regex_pattern in config No immediate action — see deprecated check name
Scripting against dbt-bouncer's exit code Review — see new exit codes
Using the *_has_meta_keys checks No action needed — see new criteria parameter
Upgrading to dbt 2.0 Review how you generate catalog.json — see dbt 2.0 support
Using dbt-bouncer.yml / dbt-bouncer.toml config only, no custom checks or Python imports No action required
Using the dbt-bouncer GitHub Action No action required — new optional inputs are available

Minimum supported dbt version is now 1.10#

The minimum supported dbt-core version has been raised from 1.7 to 1.10. A manifest.json (or other artifact) generated with an older dbt version is no longer accepted:

The supplied `manifest.json` was generated with dbt version 1.9.0, this is below the minimum supported version of 1.10.0.

This is now a hard, fail-fast error raised at artifact-parsing time (exit code 3, see exit codes) rather than a warning.

Previously, running against a dbt 1.7, 1.8 or 1.9 manifest could silently skip checks that were internally gated on a minimum dbt version (for example, unit-test checks were only evaluated for manifests generated with dbt >= 1.8.0). Those checks would report success without ever running. With the 1.10 floor, every one of those gates is now unconditionally true, so this failure mode is gone — either your artifact is supported and every check runs, or parsing fails immediately and tells you why.

Forward compatibility with dbt 1.12+ / dbt Fusion (dbt-core 2.0) is unaffected by this change.

Removed import paths#

The five deprecation shim modules that have warned since v3.0.0 are gone. Importing from any of them now raises ImportError immediately — there is no more deprecation warning, because the modules themselves no longer exist.

Removed Replacement
dbt_bouncer.check_base dbt_bouncer.check_framework.base
dbt_bouncer.check_context dbt_bouncer.check_framework.context
dbt_bouncer.check_decorator dbt_bouncer.check_framework.decorator
dbt_bouncer.check_patterns dbt_bouncer.check_framework.patterns — also removed, no replacement (see below)
dbt_bouncer.checks.common dbt_bouncer.check_framework.exceptions
- from dbt_bouncer.check_decorator import check, fail
+ from dbt_bouncer.check_framework.decorator import check, fail

Note that dbt_bouncer.check_framework.patterns itself has also been removed as part of dropping the class-based check framework — see the next section. If you were importing pattern ABCs (BaseColumnsHaveTypesCheck, BaseDescriptionPopulatedCheck, BaseHasMetaKeysCheck, BaseHasTagsCheck, BaseHasUnitTestsCheck, BaseNamePatternCheck) from either the shim or the canonical module, there is no replacement — rewrite the check using the @check decorator.

Class-based custom checks are rejected#

The class-based check framework (subclassing BaseCheck directly and implementing execute()) has been dropped. The @check decorator is now the only way to define a check — it was already the recommended path throughout v3.

This is not an import error: BaseCheck still exists (it is the internal class the @check decorator builds subclasses from), so a subclass still imports and instantiates fine. Instead, check discovery rejects it. Loading a hand-written subclass raises DbtBouncerConfigError, so dbt-bouncer stops with exit code 2 and a message naming the offending class and the file it came from:

`CheckMyCustomCheck` in `my_checks/manifest/check_models.py` is a hand-written
class-based check. Class-based checks were removed in dbt-bouncer v4; define it
with the `@check` decorator instead.

The failure happens when your custom-checks module is loaded, not part-way through a run, so you find out immediately rather than watching a check quietly do nothing.

# v3.x — rejected at discovery in v4
from dbt_bouncer.check_framework.base import BaseCheck

class CheckModelAccess(BaseCheck):
    access: Literal["private", "protected", "public"]
    model: Any = Field(default=None)
    name: Literal["check_model_access"]

    def execute(self) -> None:
        assert self.model is not None
        if self.model.access.value != self.access:
            raise DbtBouncerFailedCheckError(
                f"Model `{self.model.name}` has access `{self.model.access.value}`, expected `{self.access}`."
            )
# v4.0.0 — the only supported way to define a check
from dbt_bouncer.check_framework.decorator import check, fail

@check
def check_model_access(model, *, access: str):
    """Each model should have the specified access level."""
    if model.access and model.access.value != access:
        fail(
            f"Model `{model.name}` has access `{model.access.value}`, expected `{access}`."
        )

If you have custom checks still written the class-based way, migrate each one to the decorator API — see Adding a new check for the full pattern, including how the resource iterated over is inferred from the function's first parameter.

Removed back-compat re-exports#

Two undocumented re-exports that existed purely to ease the v2 → v3 transition are gone:

- from dbt_bouncer.main import run_bouncer
+ from dbt_bouncer import run_bouncer
# or the canonical path:
+ from dbt_bouncer.cli.run.utils import run_bouncer
- from dbt_bouncer.cli import list_checks
+ from dbt_bouncer.cli.list import list_checks

Both were self-aliases/lazy re-exports rather than documented public API, so if you were importing the canonical paths already this is a no-op.

Renamed: OutputFormatCLI to ListOutputFormat#

dbt_bouncer.enums.OutputFormatCLI — the enum backing the list command's --output-format option — is renamed to ListOutputFormat to distinguish it from the separate OutputFormat enum used by the run command's --output-format option (which supports a different set of choices: csv, json, junit, sarif, tap).

- from dbt_bouncer.enums import OutputFormatCLI
+ from dbt_bouncer.enums import ListOutputFormat

The two enums remain deliberately separate — they are not being merged — this is a rename only.

Also corrected in the same change: the --output-format help text and docs previously claimed it applied "to the output file or stdout when no output file is specified". That was never accurate — structured output is only ever written when --output-file is set. Without an output file, the console always shows the rich table regardless of the chosen format. No behaviour changed; only the documentation was wrong.

A custom check that fails to import now stops the run#

In v3, a file in your custom_checks_dir that raised on import was logged as a warning and skipped. The run continued and could report "all checks passed" while the check never loaded once.

In v4 the same failure raises DbtBouncerConfigError and the run stops with exit code 2:

Failed to load custom check file `my_checks/models/check_naming.py`: No module
named 'yaml'. A custom check that cannot be imported must not be skipped
silently.

The import error is caught for AttributeError, ImportError, ModuleNotFoundError, OSError and SyntaxError. Run at -vvv to see the original traceback.

Before you upgrade, run your existing config once and confirm that the check count matches what you expect. If a custom check has been quietly failing to import since you wrote it, v4 is where you find out.

Two related messages remain warnings, not errors:

  • A custom_checks_dir that does not exist.
  • A Python file placed directly in the top level of custom_checks_dir. Custom checks are discovered by a */*.py glob, so a check file must sit in a subdirectory (for example my_checks/models/check_naming.py). v4 warns about top-level files instead of ignoring them in silence.

A run that matches no resources now exits 4#

In v3, a config that matched no resources printed a success summary and exited 0. A typo in package_name, a --check filter that matched nothing, or artifacts from the wrong project all produced a green run that checked nothing.

In v4 this exits 4 (NO_CHECKS_RUN) and logs:

No checks were run. A config that matches no resources exits without running
anything. Check the `package_name`, the config file, the dbt artifacts, and any
`--check` or `--only` filters.

--dry-run is exempt. It reports the empty plan instead.

If a pipeline of yours deliberately runs dbt-bouncer against a project with no matching resources, either narrow the step so it does not run, or accept exit code 4 explicitly.

Informational and additive changes#

The following changes do not require any action on your part, but are worth knowing about.

Distinct exit codes for config and artifact errors#

dbt-bouncer previously exited 1 for every failure mode, making it impossible for a CI pipeline to tell a genuine check failure apart from a misconfigured run. Exit codes are now:

  • 0 (SUCCESS) — all checks succeeded.
  • 1 (CHECK_ERRORS) — at least one check failed with error severity.
  • 2 (CONFIG_ERROR) — the config file is missing, unreadable, or invalid (for example, an invalid --only value, or a custom check that fails to import).
  • 3 (ARTIFACT_ERROR) — a required dbt artifact (manifest.json, catalog.json, run_results.json) is missing, or was generated with an unsupported dbt version.
  • 4 (NO_CHECKS_RUN) — the config matched no resources, so nothing ran. See a run that matches no resources now exits 4.

Codes 2, 3 and 4 are all new. A script that treats any non-zero exit as "checks failed" keeps working. A script that treats 1 as the only failure mode must be updated, because a misconfigured run no longer exits 1.

Two typed exceptions, DbtBouncerConfigError and DbtBouncerArtifactError (both RuntimeError subclasses), back the new codes and replace what were previously bare RuntimeError, FileNotFoundError and AssertionError raises. If you call run_bouncer() directly and catch RuntimeError around it, that code keeps working unchanged since both new exception types still subclass it.

Check renamed: check_model_description_contains_regex_pattern#

Renamed to check_model_description_contains_regexp_pattern, matching its regexp_pattern parameter and the naming of the other two pattern-matching checks. The rule code (MO020) and behaviour are unchanged.

  manifest_checks:
-   - name: check_model_description_contains_regex_pattern
+   - name: check_model_description_contains_regexp_pattern
      regexp_pattern: .*pattern_to_match.*

Unlike the changes above, this is a soft break: the old name still works for the whole v4 cycle via a deprecated-name alias that rewrites it before validation and logs a deprecation warning. This applies wherever a check name can be supplied — the config file, the --check CLI flag, and dbt-bouncer validate.

One thing to be aware of: because the alias is applied before validation, schema.json only contains the new name. If your editor validates config against the JSON Schema, it will flag a config still using the old name, even though dbt-bouncer itself accepts it. Treat that as a nudge to migrate rather than an error to work around.

New criteria parameter for the has_meta_keys checks#

The criteria parameter (all / any / one) — already available on the *_has_tags checks — is now also available on the seven *_has_meta_keys checks: check_exposure_has_meta_keys, check_macro_has_meta_keys, check_model_has_meta_keys, check_seed_has_meta_keys, check_snapshot_has_meta_keys, check_source_has_meta_keys and check_test_has_meta_keys.

manifest_checks:
  - name: check_model_has_meta_keys
    criteria: any # at least one of the listed keys must be present
    keys:
      - owner
      - team

The default is all, matching the previous, only, behaviour, so existing configs are unaffected — no action is required. check_model_columns_have_meta_keys and the *_has_labels_keys checks are unchanged by this release.

New GitHub Action inputs#

The action gained four optional inputs, so a workflow can reach the features added in v4 without falling back to environment variables:

Input Effect
baseline Path to a baseline file. Only new failures fail the run.
dry-run Print the checks that would run, without running them.
preset Run a bundled preset (minimal, standard, strict) instead of a config file.
state Directory of base artifacts. Only new failures fail the run.

Set either config-file or preset, not both. When preset is set, the action does not pass config-file, because dbt-bouncer ignores --preset whenever an explicit --config-file is supplied.

A config file resolves artifact paths relative to its own directory. A preset has no file, so it resolves them relative to the repository root and expects target/ there. If your dbt project is not at the repository root, set DBT_PROJECT_DIR in the workflow and the action passes it through. See the GitHub Actions setup for an example.

Every existing input keeps its name, default and meaning, so an unchanged workflow behaves exactly as it did in v3.

dbt 2.0 support#

dbt-bouncer v4.0.0 runs against dbt 2.0. No dbt-bouncer config change is needed. Every check that works against dbt-core 1.x works against dbt 2.0, because dbt 2.0 emits the same artifact schemas: manifest v12, catalog v1 and run results v6.

Three things changed on the dbt side. They change how you generate artifacts, not how you configure dbt-bouncer.

dbt 2.0 ships under new distribution names. The dbt-core distribution stays on the 1.x line. dbt 2.0 ships as dbt (the Fusion engine CLI) and as dbt-oss (the Apache-2.0 build).

You install You get
pip install dbt-core dbt 1.x
pip install dbt dbt 2.x, writes catalog.json
pip install dbt-oss dbt 2.x, writes no catalog.json

--write-catalog moved off dbt build. On dbt 2.0 the flag is available on dbt compile and on dbt docs generate. A plain dbt build writes no catalog.json. If your pipeline relied on dbt build --write-catalog, split it:

dbt compile --write-catalog # writes catalog.json
dbt build                   # writes manifest.json and a real run_results.json

Keep that order. dbt compile executes nothing, so it writes a run_results.json with zero results, and dbt build does not overwrite the catalog.

The dbt-oss distribution writes no JSON catalog. It writes the catalog as Parquet under target/private/ instead. If you run catalog checks, use the dbt distribution. If you pass a target directory with no catalog.json while catalog checks are configured, dbt-bouncer fails fast with exit code 3:

No catalog.json found at dbt_project/target/catalog.json.

Manifest checks and run results checks work on either distribution.

Getting help#

If you run into issues upgrading:

  1. Check that any manifest, catalog or run-results artifact you pass in was generated with dbt-core >= 1.10.
  2. Update imports for the removed shim modules and re-exports per the tables above.
  3. Migrate any class-based custom checks to the @check decorator.
  4. Open an issue at github.com/godatadriven/dbt-bouncer/issues