Skip to main content

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, run dbt deps before 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.yml where dbt looks for it: the folder DBT_PROFILES_DIR names when it is set, otherwise the project folder, then ~/.dbt/. Runners usually have no profile, so commit one for CI and point DBT_PROFILES_DIR at 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.