Catch unexpected data changes before a dbt PR is reviewed.
Documentation · Quickstart · GitHub Actions · Contributing · Issues
Fortify validates dbt pull requests locally against Snowflake, Databricks, and BigQuery. It builds changed models in temporary schemas, compares them with production, and reports what needs a fix before review. Written in Rust.
- Validate before review. Build changed models and critical downstream paths in temporary schemas, then clean them up.
- Compare with production. Run dbt tests and compare schema, row counts, null rates, cardinality, ranges, and primary keys.
- See downstream impact. Show affected models and columns.
- Connect directly. Use your warehouse credentials, with no Embrasure account or data sent to Embrasure.
- Check and fix with your agent. Use the
verifyskill to run the check-and-fix loop.
fortify auth login uses Snowflake OAuth. Databricks uses a token supplied through the configured environment variable. BigQuery uses Google Application Default Credentials. The warehouse identity needs production read access and permission to create and remove temporary schemas or datasets.
Fortify is the new name for Embrasure CLI. The embrasure command,
embrasure-check.yml, and EMBRASURE_* environment variables remain supported
through v0.x. New projects use fortify-check.yml; --config takes precedence,
then the Fortify file, then the legacy file. FORTIFY_* variables take precedence
over their legacy equivalents. Existing credentials and warehouse cleanup markers
stay in place.
Update GitHub workflows to uses: EmbrasureAI/fortify@v1. GitHub does not
redirect Actions after a repository rename. Existing pinned CLI versions remain
available. If an older standalone updater only installs embrasure, rerun the
installer below to add fortify.
For Homebrew, run brew update && brew upgrade embrasureai/tap/embrasure to
migrate the installed formula. Scoop users can keep updating embrasure; to
switch package names, uninstall embrasure before installing fortify. Do the
same for WinGet package IDs to avoid competing command aliases.
From your existing Snowflake dbt Core project directory, create or activate its Python environment:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "dbt-core>=1.5,<2" "dbt-snowflake>=1.5,<2" "sqlglot>=30,<31"Then install Fortify on macOS or Linux with Homebrew:
brew install embrasureai/tap/fortify
fortify init
fortify auth login
fortify doctor
fortify check --dry-run
fortify checkBy default, check compares your branch with origin/main.
If dbt is managed by your project
Fortify uses the dbt Core and Snowflake adapter versions already installed by your project. Activate the project's normal environment, or set dbt.command in fortify-check.yml to the wrapper your project uses.
Install the Databricks dbt adapter instead of dbt-snowflake:
python -m pip install "dbt-core>=1.5,<2" "dbt-databricks>=1.5,<2" "sqlglot>=30,<31"Use a version 2 configuration with a typed provider block:
version: 2
dbt:
project_dir: .
profile: analytics
accounts:
- name: primary
provider:
type: databricks
host: https://your-workspace.cloud.databricks.com
http_path: /sql/1.0/warehouses/your-warehouse-id
catalog: analytics
production_schema: prod
auth:
type: token
token_env: DATABRICKS_TOKENThe integration uses a Databricks SQL warehouse and Unity Catalog. Set DATABRICKS_TOKEN, then run fortify doctor and fortify check. Incremental baselines currently require managed Delta tables and use Unity Catalog shallow clones; doctor verifies that a suitable table and grants are available.
Install the BigQuery dbt adapter:
python -m pip install "dbt-core>=1.5,<2" "dbt-bigquery>=1.5,<2" "sqlglot>=30,<31"For local development, create Google Application Default Credentials, then initialize Fortify from the dbt project root:
gcloud auth application-default login
fortify init
fortify doctor
fortify check --dry-run
fortify checkinit detects an active BigQuery dbt profile and writes the version 2 provider configuration. The equivalent manual configuration is:
version: 2
dbt:
project_dir: .
profile: analytics
accounts:
- name: primary
provider:
type: bigquery
project: analytics-prod
location: US
production_schema: prod
maximum_bytes_billed: 10737418240
auth:
type: application_defaultApplication Default Credentials also support GOOGLE_APPLICATION_CREDENTIALS and an attached Google Cloud service account. maximum_bytes_billed applies the same per-query cap to dbt builds and Fortify comparison queries. Incremental baselines use BigQuery table clones; clone mode seeds candidates with table copies. The source and temporary datasets must be in the same location, and table-clone restrictions still apply.
If you do not use Homebrew, use the installer on macOS or Linux:
curl -fsSL https://raw.githubusercontent.com/EmbrasureAI/fortify/main/install.sh | shThe installer writes to /usr/local/bin when writable, otherwise ~/.local/bin. Set FORTIFY_INSTALL_DIR to choose another directory.
Installing on Windows (Windows 11 or Windows Server 2022+)
Download the PowerShell installer from GitHub Releases, inspect it, then run it:
$installer = Join-Path $env:TEMP 'fortify-install.ps1'
Invoke-WebRequest https://github.com/EmbrasureAI/fortify/releases/latest/download/install.ps1 -OutFile $installer
Get-Content $installer
Unblock-File $installer
& $installerThe installer verifies the release checksum, installs Fortify under %LOCALAPPDATA%\Programs\Fortify, and adds its bin directory to your user PATH. It does not need administrator access. Open a new terminal when it finishes.
Unblock-File removes the internet-zone marker after you inspect the script. It does not change PowerShell's execution policy or verify the publisher. If your organization blocks the script, use the portable ZIP from the same release.
Installer options:
- Pin a release:
& $installer -Version 0.6.0 - Run without prompts:
& $installer -Quiet - Uninstall:
& $installer -Uninstall
Uninstalling removes Fortify and its PATH entry, but keeps your configuration, credentials, reports, and logs.
Scoop users can install from Fortify's official bucket:
scoop bucket add fortify https://github.com/EmbrasureAI/scoop-bucket
scoop install fortify/fortifyWinGet manifests are generated with each release. After the first WinGet listing is accepted:
winget install --id EmbrasureAI.Fortify --exactUse Fortify from an existing Snowflake, Databricks, or BigQuery dbt project whose unchanged production models are already materialized. Fortify uses those existing relations as the comparison baseline.
For Snowflake and BigQuery, init reads the active dbt profile and asks only for missing values. Databricks uses the version 2 configuration shown above. Use --config <path> before or after any subcommand to choose another config file.
Continue only when fortify doctor reports READY. Fortify generates a temporary dbt profile for its own runs; it does not modify your existing profile or production models.
If dbt is installed in .venv, run source .venv/bin/activate in each new shell before using Fortify.
✓ Safe to continue
The change passed across every selected dbt model.
Scope
5 affected models
2 selected for validation
Evidence
2 / 2 models built · 2 compared with production
151,615,312 candidate rows evaluated
Schema, row counts, nulls, cardinality, and distributions checked
1 primary key checked
0 findings · 3 unvalidated models
Temporary warehouse schema removed
Lineage impact
fct_orders
└─ finance_daily
├─ executive_revenue
├─ regional_margin
└─ revenue_forecast_input
Completed in 2m11s
In an interactive terminal, check shows live progress while it runs. Redirected output, CI, and --json stay plain.
The default validates changed models and every path to a critical model. Critical targets are tagged critical, configured with critical: true, or used directly by a dbt exposure. Use --downstream all for every downstream model or --downstream none for changed models only. Impact is always computed from the full changed set.
Intersect the changed set with one or more explicit models:
fortify check --select orders --select order_items
fortify check --select orders --downstream noneAn unknown, ambiguous, unchanged, or out-of-scope selection fails instead of returning a misleading pass.
Preview the plan without creating schemas or querying warehouse data:
fortify check --dry-run
fortify check --dry-run --jsonDry runs use local dbt parsing but do not resolve credentials, create warehouse schemas, or query warehouse data.
JSON output is versioned and stably ordered. Progress goes to stderr, so stdout contains one JSON document.
fortify check --json
fortify check --json --markdown fortify-check.md
fortify check --json --report-version 1Published contracts: v1, v2, v3, and v4. V4 adds column lineage and bounded warehouse execution links and is the default; older versions remain available with --report-version.
| Exit code | Meaning |
|---|---|
0 |
The check passed |
1 |
The data or code needs a fix |
2 |
A requested check could not be completed |
3 |
Setup, execution, or cleanup failed |
Agent loop:
Run `fortify check --base origin/main --json`.
Exit 1: fix every finding and rerun.
Exit 2: resolve or explain every coverage gap.
Exit 3: fix the setup or execution failure.
Request review only after exit 0.
For a faster first pass on large tables, add --mode quick. Quick mode estimates cardinality and skips percentiles. Deep mode is the default. Primary-key integrity stays exact in both modes.
Query-diff checks compare any two read-only query results exactly. production_sql defaults to sql, and each dbt ref() is rendered against the candidate or production-state manifest. Checks with no refs, or definitions changed since --base, run even when no model changed.
checks:
- type: query_diff
name: paid order totals
sql: |
select customer_id, sum(amount) as paid_amount
from {{ ref('orders') }}
where status = 'paid'
group by customer_id
primary_key: [customer_id]With a primary key, Fortify reports added, removed, and changed rows plus per-column mismatch counts. Null or duplicate keys block the value join. Without a key, grouped rows and their multiplicities preserve duplicate-only differences. Query examples are bounded by the configured sample, column, and value limits.
Only persisted dbt models are supported in ref(). Query checks accept one SELECT, WITH, or VALUES expression; other Jinja and multi-statement SQL are rejected. Removed checks are reported as incomplete coverage instead of silently passing.
The composite action installs Fortify. Install dbt with your project's normal locked setup; this example uses requirements.txt. Keep the check in a visible run step so exit codes and secrets remain explicit.
In fortify-check.yml, configure the account to read the CI secret:
auth:
type: programmatic_access_token
token_env: SNOWFLAKE_PROGRAMMATIC_ACCESS_TOKENjobs:
fortify:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: python3 -m pip install -r requirements.txt
- uses: EmbrasureAI/fortify@v1
- run: fortify check --base origin/main --json
env:
SNOWFLAKE_PROGRAMMATIC_ACCESS_TOKEN: ${{ secrets.SNOWFLAKE_PROGRAMMATIC_ACCESS_TOKEN }}fetch-depth: 0 is required because selection compares the working tree with the base revision.
List managed temporary schemas older than six hours:
fortify clean
fortify clean --older-than 24 --yesclean searches only configured account databases and verifies the prefix and ownership marker before removal.
Check for or install an update:
fortify update --check
fortify updateGenerate shell completion scripts:
fortify completion bash
fortify completion zsh
fortify completion fish
fortify completion powershellYour generate_schema_name macro must preserve the complete target schema. Make custom schemas children of target.schema; do not replace it.
Every existing incremental model needs a stable baseline copy, including in full-refresh mode. Snowflake supports tables that can be zero-copy cloned; Databricks currently supports managed Delta tables that can be shallow cloned; BigQuery supports base tables, table clones, and table snapshots accepted by CREATE TABLE CLONE. Use another materialization or exclude an unsupported relation from this validation path.
The validation role needs SELECT on the production source and CREATE TABLE in the target schema. Run fortify doctor. If the relation should not use clone mode, rerun with --incremental-mode full-refresh.
Use --config <path> before or after any subcommand to choose another config file.
See the example configuration and enterprise setup guide for service credentials, multiple accounts, model policies, filters, thresholds, concurrency, external changes, cross-account dependencies, Metabase, and grants.
Every temporary schema has a unique name and ownership marker. Query results are materialized in a dedicated run-owned schema so they cannot collide with dbt model aliases. Fortify checks ownership before removal and treats cleanup failures as execution failures. Use a dedicated identity that can read and clone only the production tables under test and create temporary schemas in the required databases or catalogs. SQL validation is not a side-effect sandbox, so the identity must not be able to call unsafe procedures, functions, or external integrations.
Security and data flow documents network connections, local files, returned data, credentials, cleanup, updates, and release verification.
- Databricks support requires a Unity Catalog SQL warehouse and environment-supplied token authentication.
- Native Windows support requires 64-bit Windows 11 or Windows Server 2022+. Windows 10, Windows on Arm, and machine-wide installation are not supported.
- Column lineage covers compiled dbt SQL that SQLGlot can resolve. Wildcards without an input schema and dynamic SQL are reported as unresolved.
- Dashboard column lineage is not inferred from model lineage.
- Metabase matching covers native SQL cards that reference fully qualified production relations. Unsupported or inaccessible metadata becomes a coverage gap.
| Guide | What you will find |
|---|---|
| Snowflake quickstart | Install, authenticate, preview, and run your first check |
| Databricks and BigQuery | Provider setup, authentication, and baseline requirements |
| Example configuration | Accounts, model policies, checks, and thresholds |
| Enterprise setup | Service credentials, multiple accounts, grants, and integrations |
| Reports and exit codes | JSON contracts, coverage gaps, and the agent loop |
| Security and data flow | Network connections, credentials, cleanup, and release verification |
| Upgrading from Embrasure | Command, configuration, and package migration |
Rust 1.88 or newer is required when building from source.
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --lockedThe opt-in Snowflake suite uses FORTIFY_RUN_SNOWFLAKE_TESTS=1 and the FORTIFY_TEST_SNOWFLAKE_* account, user, role, database, warehouse, and token variables. It covers exact keyed passes and changes, duplicate-only unkeyed differences, incremental cloning, cleanup, and 100,000 synthetic rows.
Bug reports, documentation improvements, and code contributions are welcome. Read Contributing for development and review expectations. Open an issue to report a bug or discuss a substantial change.
Fortify is licensed under Apache-2.0. See NOTICE for attribution.