CI Integration
dfmt check is the CI command. It writes nothing, exits 0 when every file is formatted, and exits 1
when any file would change or could not be checked.
Before you start: commit a dlab.json
A CI runner has nothing on it but your repository, so the repository has to say which dialect the
files are in. Run dfmt or dfmt setup once in a terminal (or dfmt setup --dialect <d> for a
project with one dialect) and commit the dlab.json it writes. Without it, dfmt in CI stops
instead of guessing. See Getting Started.
A dbt project needs two more things in CI, because its models are templates dfmt has to run:
- Its packages. If the project has a
packages.yml, rundbt depsbefore dfmt. dfmt does not need dbt to format, but a model can call macros from those packages, and dfmt refuses to format the project while they are missing. - A profile. dfmt reads the adapter from
profiles.ymlwhere dbt looks for it: the folderDBT_PROFILES_DIRnames when it is set, otherwise the project folder, then~/.dbt/. Runners usually have no profile, so commit one for CI and pointDBT_PROFILES_DIRat it, or write one in the job (below). The values only have to be present: dfmt never connects to the warehouse. The fields are the ones dbt itself requires for that adapter. For Snowflake:
my_project: # the "profile:" name in dbt_project.yml
target: ci
outputs:
ci:
type: snowflake
account: ci
user: ci
database: analytics
schema: ci
warehouse: ci
The adapter type must be the dialect your dlab.json names. dfmt refuses a model whose adapter
and dialect disagree.
GitHub Actions
Check formatting on pull requests
name: SQL format
on: [pull_request]
jobs:
dfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dfmt
env:
DLAB_VERSION: "0.1.0" # pin the version so a new release cannot change a green build
run: |
curl -fsSL https://dfmt.datoria.ai/install.sh | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Check SQL formatting
run: dfmt check .
ubuntu-latest is Linux x86_64 with glibc, which dfmt supports. The install writes to
~/.local/bin, and the GITHUB_PATH line puts that folder on the PATH of the later steps.
A dbt project
Add the profile and the packages before the check. With a profile committed at ci/profiles.yml,
setting DBT_PROFILES_DIR: ci in the job's env: is all the profile step there is. Otherwise
write one:
- name: Write a dbt profile for dfmt
run: |
mkdir -p ~/.dbt
cat > ~/.dbt/profiles.yml <<'EOF'
my_project:
target: ci
outputs:
ci:
type: snowflake
account: ci
user: ci
database: analytics
schema: ci
warehouse: ci
EOF
- name: Install dbt packages
run: |
pip install dbt-core dbt-snowflake
dbt deps
- name: Check SQL formatting
run: dfmt check .
The last two steps are only needed when the project has a packages.yml.
Check only changed files
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the base branch must be in the clone
- name: Check changed SQL files
run: dfmt check --changed-since origin/main
If git cannot find the ref, which in a shallow clone is the usual case, dfmt exits 2, not 0, so
the step cannot pass on files it never checked. fetch-depth: 0 fetches the history it needs.
Pre-commit
With the pre-commit framework, as a local hook. Each developer needs dfmt
installed (Install); in CI, install it as above before running pre-commit.
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: dfmt
name: dfmt
entry: dfmt check
language: system
types: [sql]
pre-commit passes the staged SQL files as arguments, so only those are checked. To have the hook
fix the files instead, use entry: dfmt write: pre-commit then fails the commit once, with the
files formatted and ready to stage again.
Or as a plain git hook, without the framework:
#!/bin/sh
# .git/hooks/pre-commit
dfmt check --changed-since HEAD
What check reports
check prints one line:
❌ 42 files checked: 3 would be reformatted, 36 already formatted, 2 partly formatted, 1 not formatted (1.2s)
A partly formatted or not formatted file is one dfmt could not fully parse, most often because the dialect is wrong: what it could not read is left as written. It is not a passing file, so it also exits 1. The CLI Reference has every exit code.
--changed-since
--changed-since takes any git ref and checks the files changed since it:
dfmt check --changed-since main # changed compared with main
dfmt check --changed-since origin/main # compared with the remote branch
dfmt check --changed-since HEAD~1 # changed in the last commit
The changed files are filtered through include and exclude in dlab.json, so only files that
belong to the project are checked.