Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles
Version Controlling Your dbt Project with Git: Branching Strategies, CI Checks, and Safe Deployments to Production

Version Controlling Your dbt Project with Git: Branching Strategies, CI Checks, and Safe Deployments to Production

Data Engineering🌱 Foundation17 min readJul 16, 2026Updated Jul 16, 2026
Table of Contents
  • Introduction
  • Prerequisites
  • Why dbt and Git Are a Natural Pair
  • The Core Git Concepts You Actually Need
  • Designing Your Branching Strategy
  • The Three Tiers Explained
  • Your First Day: Setting Up the Repository

On this page

  • Introduction
  • Prerequisites
  • Why dbt and Git Are a Natural Pair
  • The Core Git Concepts You Actually Need
  • Designing Your Branching Strategy
  • The Three Tiers Explained
  • Your First Day: Setting Up the Repository
  • Your Daily Development Workflow
  • Setting Up CI Checks That Actually Catch Problems
  • What Should Your CI Check Do?
  • Your Daily Development Workflow
  • Setting Up CI Checks That Actually Catch Problems
  • What Should Your CI Check Do?
  • A GitHub Actions Workflow for dbt CI
  • Deploying Safely to Production
  • The Deployment Branch Pattern
  • Rollbacks: When Production Goes Wrong
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps
  • Version Controlling Your dbt Project with Git: Branching Strategies, CI Checks, and Safe Deployments to Production

    Introduction

    Picture this: it's a Tuesday afternoon and your analytics engineer just pushed a change to the fct_revenue model. By Wednesday morning, the CFO is asking why last month's revenue numbers look completely different in the dashboard. Nobody touched the dashboard. Nobody changed the data source. But something changed — and nobody can easily say what, when, or why. Without version control, your dbt project is a black box where changes happen invisibly and problems are nearly impossible to trace.

    Version control solves this. More specifically, Git — the industry-standard version control system — gives your dbt project a complete, auditable history of every change ever made. It lets multiple people collaborate without overwriting each other's work. It lets you test changes in isolation before they affect production data. And it gives you a rollback button when something inevitably goes wrong. For a dbt project, which is essentially a collection of SQL files and configuration, Git is not optional — it's foundational infrastructure.

    By the end of this lesson, you'll understand how to structure a professional Git workflow for a dbt project, from daily development habits through automated checks to safe production deployments. You'll be able to work confidently on a team without fear of breaking things — and when things do break, you'll know how to find out why.

    What you'll learn:

    • How Git's core concepts (repositories, branches, commits, pull requests) apply specifically to dbt projects
    • A practical branching strategy that separates development, staging, and production work
    • How to configure CI (Continuous Integration) checks that automatically test your dbt models before they merge
    • How to promote changes to production safely using a deployment pipeline
    • The most common Git mistakes data engineers make and how to avoid them

    Prerequisites

    • A working dbt project (even a small one with a few models) — if you don't have one yet, the "Building Your First dbt Project" lesson in this learning path will get you there
    • Git installed on your machine (git --version in your terminal should return a version number)
    • A GitHub account (free) — the examples here use GitHub, though GitLab and Bitbucket follow nearly identical patterns
    • Basic comfort with a terminal or command line

    Why dbt and Git Are a Natural Pair

    A dbt project is, at its core, a folder of text files: SQL models, YAML configuration files, Jinja macros, and tests. There are no binary blobs, no compiled executables, nothing opaque. This is actually great news, because Git was designed for exactly this kind of content — plain text that humans can read and compare.

    When you run git diff on a dbt model, you see exactly which lines of SQL changed. When you look at git log, you see who changed a model, when, and with what explanation. This means the same audit trail your company requires for financial records exists for the logic that produces those records.

    Compare this to managing transformations in a BI tool or a stored procedure in a database. In those environments, changes often happen in a UI with no history, no review process, and no easy way to compare "old version" with "new version." dbt with Git gives you software engineering discipline applied to data work — and that discipline is what separates mature data teams from ones that are constantly firefighting.


    The Core Git Concepts You Actually Need

    Before jumping into strategy, let's make sure the fundamental vocabulary is solid. These four concepts underpin everything else.

    Repository (repo): The folder containing your dbt project, tracked by Git. When you run git init inside a folder or clone an existing project with git clone, you're creating or copying a repository. The repo stores every version of every file, forever, in a hidden .git folder.

    Commit: A snapshot of your project at a specific moment in time. Think of it like a save point in a video game. Each commit has a unique ID (a hash like a3f92c1), an author, a timestamp, and a message you write explaining what changed and why. Good commit messages are short descriptions of intent, not just action: "fix: correct revenue calculation to exclude refunds" is far more useful than "update revenue model".

    Branch: An independent line of development. When you create a branch, you're essentially making a copy of the project at that moment that you can modify freely without affecting anything else. Branches are cheap — creating one takes milliseconds. The main branch (often called main or master) represents the official, production-ready state of your project.

    Pull Request (PR): A formal request to merge changes from one branch into another. PRs are where code review happens — teammates can comment on specific lines, ask questions, and approve or reject changes. In dbt projects, PRs are also where automated checks (CI) run before anything touches production.


    Designing Your Branching Strategy

    A branching strategy is just an agreement about how your team uses branches. Without one, you get chaos: people committing directly to main, half-finished features accidentally deployed, impossible-to-trace bugs. With one, deployments become boring — and boring is good.

    For most dbt projects, a three-tier strategy works well: feature branches → main → production.

    The Three Tiers Explained

    Feature branches are where all actual work happens. Every task — fixing a broken test, adding a new model, refactoring a staging layer — gets its own branch. The naming convention matters more than you might think. Use a format like type/short-description:

    • feature/add-customer-lifetime-value-model
    • fix/revenue-excludes-internal-transfers
    • refactor/staging-layer-uses-sources

    This tells anyone looking at the branch list exactly what's in progress and whether it's new work, a bug fix, or cleanup.

    The main branch represents code that has been reviewed and tested. Merging to main should require a pull request and at least one approval. main is the source of truth for "what the team has agreed is correct." It should always be in a deployable state.

    The production branch (some teams call it prod or just deploy directly from main with tags) is what's actually running against your production data warehouse. The distinction between main and production gives you a buffer — you can have reviewed, approved code sitting in main that you choose to deploy on a schedule or after manual sign-off.

    Tip: Some teams skip the separate production branch entirely and deploy automatically whenever main is updated. This "continuous deployment" approach works fine for mature teams with strong CI. If you're just getting started, the explicit production branch gives you more control.

    Your First Day: Setting Up the Repository

    Here's the sequence for taking an existing dbt project and putting it under proper Git control:

    # Navigate to your dbt project folder
    cd /path/to/your/dbt_project
    
    # Initialize git if it isn't already
    git init
    
    # Create a .gitignore file to exclude sensitive and generated files
    touch .gitignore
    

    Your .gitignore file is critical. dbt generates files you never want committed — especially profiles.yml, which contains database credentials, and the target/ folder, which contains compiled SQL and run artifacts. A solid starting .gitignore for dbt looks like this:

    # dbt artifacts (generated, not source)
    target/
    dbt_packages/
    logs/
    
    # Sensitive configuration
    profiles.yml
    
    # OS junk
    .DS_Store
    Thumbs.db
    
    # Python virtual environments if you have them
    .venv/
    __pycache__/
    

    Now commit your initial state:

    git add .
    git commit -m "initial commit: dbt project structure"
    

    Then push to GitHub. In GitHub, create a new repository through the web interface (click the "+" in the top right, select "New repository," give it a name like analytics-dbt, set it to private, and don't initialize it with any files since you already have files locally). Then run:

    git remote add origin https://github.com/your-org/analytics-dbt.git
    git branch -M main
    git push -u origin main
    

    Your Daily Development Workflow

    Every piece of work, no matter how small, starts the same way:

    # Make sure you're up to date
    git checkout main
    git pull origin main
    
    # Create a new branch for your task
    git checkout -b feature/add-customer-lifetime-value-model
    

    Now you work. You modify SQL files, add YAML tests, maybe create a new macro. As you reach logical stopping points, you commit:

    # See what's changed
    git status
    
    # Stage specific files (don't just do git add . blindly)
    git add models/marts/fct_customer_lifetime_value.sql
    git add models/marts/schema.yml
    
    # Commit with a meaningful message
    git commit -m "feat: add customer lifetime value model with 12-month lookback"
    

    When you're ready for review, push your branch and open a PR:

    git push origin feature/add-customer-lifetime-value-model
    

    Then go to GitHub. Your branch will appear with a prompt to "Compare & pull request." Click it, write a description of what changed and why, and assign a reviewer.


    Setting Up CI Checks That Actually Catch Problems

    CI stands for Continuous Integration — the practice of automatically running tests every time code is proposed for merging. For dbt projects, CI checks are where you catch broken models, failing tests, and compilation errors before they ever reach production data.

    The most common CI platform for GitHub repositories is GitHub Actions. It's free for public repos and free up to a certain usage limit for private ones.

    What Should Your CI Check Do?

    A meaningful dbt CI check does at least these three things:

    1. dbt compile — Verifies that all your SQL is syntactically valid and all Jinja references resolve correctly. This catches typos in model names, broken ref() calls, and missing macros.

    2. dbt test — Runs your schema tests (not_null, unique, accepted_values, relationships). This verifies that the logic of your models is correct, not just that they compile.

    3. Slim CI with --select state:modified+ — Instead of running your entire project on every PR (which could take 30+ minutes on a large project), dbt Cloud and some open-source setups support "slim CI": only run models that have changed plus their downstream dependents. This makes CI fast enough to actually use.

    A GitHub Actions Workflow for dbt CI

    Create the file .github/workflows/dbt_ci.yml in your repository:

    name: dbt CI
    
    on:
      pull_request:
        branches:
          - main
    
    jobs:
      dbt-check:
        runs-on: ubuntu-latest
    
        env:
          DBT_PROFILES_DIR: .
    
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Set up Python
            uses: actions/setup-python@v5
            with:
              python-version: '3.11'
    
          - name: Install dbt
            run: |
              pip install dbt-snowflake==1.7.0
              # Replace dbt-snowflake with dbt-bigquery or dbt-postgres as needed
    
          - name: Install dbt packages
            run: dbt deps
    
          - name: Write CI profiles.yml
            run: |
              cat > profiles.yml <<EOF
              your_project:
                target: ci
                outputs:
                  ci:
                    type: snowflake
                    account: ${{ secrets.SNOWFLAKE_ACCOUNT }}
                    user: ${{ secrets.SNOWFLAKE_USER }}
                    password: ${{ secrets.SNOWFLAKE_PASSWORD }}
                    role: TRANSFORMER
                    database: ANALYTICS_CI
                    warehouse: COMPUTE_WH
                    schema: dbt_ci_${{ github.event.pull_request.number }}
              EOF
    
          - name: dbt compile
            run: dbt compile
    
          - name: dbt test
            run: dbt test
    

    A few things worth understanding here:

    The on: pull_request: branches: [main] block tells GitHub Actions to run this workflow whenever someone opens or updates a PR targeting main. The CI job won't run on pushes to feature branches directly — only when you're proposing to merge.

    The profiles.yml is written dynamically using GitHub Secrets — environment variables you configure in your GitHub repository settings (go to Settings → Secrets and variables → Actions, then add each secret). This way, credentials never appear in your code.

    Notice the schema is dbt_ci_${{ github.event.pull_request.number }}. This creates a unique schema in your CI database for each PR — dbt_ci_42, dbt_ci_43, etc. This means multiple PRs can run CI simultaneously without interfering with each other.

    Warning: Make sure your CI database user has permission to create schemas. A common CI failure that's confusing to debug is a permission error that looks like a dbt problem but is actually a database access issue.

    Once this file is committed and pushed, every future PR to main will automatically trigger the check. You'll see a status indicator on the PR in GitHub — a green checkmark if tests pass, a red X if they fail. You can (and should) configure GitHub to require this check to pass before merging is allowed, under Settings → Branches → Branch protection rules.


    Deploying Safely to Production

    CI tells you your changes are safe to merge. Deployment tells you how to actually apply those changes to your production data warehouse in a controlled way.

    The Deployment Branch Pattern

    If you're using a separate production branch, the workflow is:

    1. Changes accumulate in main through reviewed PRs
    2. On a schedule (or manually), a maintainer opens a PR from main → production
    3. That PR triggers a final CI run
    4. After merge to production, a deployment job runs dbt run and dbt test against your production warehouse

    Here's a second GitHub Actions workflow for the production deployment, saved as .github/workflows/dbt_prod.yml:

    name: dbt Production Deploy
    
    on:
      push:
        branches:
          - production
    
    jobs:
      dbt-production:
        runs-on: ubuntu-latest
    
        env:
          DBT_PROFILES_DIR: .
    
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Set up Python
            uses: actions/setup-python@v5
            with:
              python-version: '3.11'
    
          - name: Install dbt
            run: pip install dbt-snowflake==1.7.0
    
          - name: Install dbt packages
            run: dbt deps
    
          - name: Write production profiles.yml
            run: |
              cat > profiles.yml <<EOF
              your_project:
                target: prod
                outputs:
                  prod:
                    type: snowflake
                    account: ${{ secrets.SNOWFLAKE_ACCOUNT }}
                    user: ${{ secrets.SNOWFLAKE_USER_PROD }}
                    password: ${{ secrets.SNOWFLAKE_PASSWORD_PROD }}
                    role: TRANSFORMER_PROD
                    database: ANALYTICS
                    warehouse: COMPUTE_WH
                    schema: dbt_prod
              EOF
    
          - name: dbt run
            run: dbt run --target prod
    
          - name: dbt test
            run: dbt test --target prod
    

    Tip: Use a separate set of credentials for production — SNOWFLAKE_USER_PROD and SNOWFLAKE_PASSWORD_PROD — with read/write access only to what the transformer role actually needs. Never use a superuser account for production dbt runs.

    Rollbacks: When Production Goes Wrong

    Even with CI, sometimes a bad change makes it through. You discover at 7am that fct_orders is producing wrong numbers. Here's the immediate response:

    # Find the last known-good commit on production
    git log production --oneline
    
    # Output might look like:
    # a3f92c1 deploy: merge main into production 2024-01-15
    # 8b4d33e deploy: merge main into production 2024-01-08
    # ...
    
    # Create a rollback branch from the previous good commit
    git checkout -b hotfix/rollback-revenue-to-jan-8 8b4d33e
    
    # Push it and open a PR directly to production
    git push origin hotfix/rollback-revenue-to-jan-8
    

    This gets you back to a known state fast. Then, in parallel, you investigate what went wrong on main in a separate branch and fix it properly before re-deploying.


    Hands-On Exercise

    This exercise walks you through the complete workflow end-to-end using a real scenario.

    Scenario: You need to add a new model called fct_order_refunds that calculates the total refunded amount per customer per month.

    Step 1: Start from a clean main branch.

    git checkout main
    git pull origin main
    

    Step 2: Create your feature branch.

    git checkout -b feature/add-order-refunds-model
    

    Step 3: Create the model file at models/marts/fct_order_refunds.sql:

    with orders as (
        select * from {{ ref('stg_orders') }}
    ),
    
    refunds as (
        select * from {{ ref('stg_refunds') }}
    ),
    
    joined as (
        select
            o.customer_id,
            date_trunc('month', r.refunded_at) as refund_month,
            sum(r.refund_amount_usd)            as total_refunded_usd,
            count(r.refund_id)                  as refund_count
        from refunds r
        inner join orders o on r.order_id = o.order_id
        group by 1, 2
    )
    
    select * from joined
    

    Step 4: Add tests in models/marts/schema.yml:

    models:
      - name: fct_order_refunds
        description: "Monthly refund totals per customer"
        columns:
          - name: customer_id
            tests:
              - not_null
          - name: refund_month
            tests:
              - not_null
          - name: total_refunded_usd
            tests:
              - not_null
    

    Step 5: Commit your work.

    git add models/marts/fct_order_refunds.sql models/marts/schema.yml
    git commit -m "feat: add fct_order_refunds model with monthly aggregation"
    

    Step 6: Push and open a PR.

    git push origin feature/add-order-refunds-model
    

    Go to GitHub and open a pull request from feature/add-order-refunds-model into main. Watch the CI checks run. If they pass, request a review.

    Step 7: After approval and merge, verify your changes are in main:

    git checkout main
    git pull origin main
    git log --oneline -5
    

    Your commit should appear at the top.


    Common Mistakes & Troubleshooting

    Committing directly to main This is the most common mistake on small teams. It bypasses code review, bypasses CI, and can push untested changes straight toward production. Fix it by enabling branch protection rules on GitHub (Settings → Branches → Add rule, check "Require a pull request before merging" and "Require status checks to pass before merging").

    Committing profiles.yml with real credentials If you accidentally commit a profiles.yml with a real password, don't just delete it — the file's contents live in Git history. You need to rotate the credentials immediately (assume they're compromised), then use git filter-repo or contact your Git platform's support team to purge the history. Prevention: always have profiles.yml in .gitignore before your first commit.

    Merge conflicts in schema.yml When two people add new models to the same YAML file simultaneously, you'll get a merge conflict. Git will mark the conflict with <<<<<<, =======, and >>>>>>> markers in the file. Open the file, keep both sets of changes (since each person added different model definitions), remove the conflict markers, save, then git add schema.yml and git commit to complete the merge. This is less scary than it looks.

    CI passes but production run fails This usually means your CI environment doesn't have the same data as production. If CI runs against a subset of data or a different schema and a model assumption only breaks on production-scale data, CI won't catch it. The best mitigation is making your CI data as representative of production as possible — even a recent subset is much better than an empty database.

    Long-lived feature branches The longer a branch lives, the harder it becomes to merge. If you're working on something that takes more than a week, break it into smaller PRs that each add incremental value. A PR that changes 30 files will sit in review forever; a PR that changes 3 files will get merged in hours.


    Summary & Next Steps

    You've built a complete picture of how Git fits into a professional dbt workflow. Let's recap what you now know how to do:

    • Structure your work using feature branches that isolate changes until they're reviewed and ready
    • Write meaningful commit messages that create a useful audit trail
    • Configure a GitHub Actions CI workflow that automatically compiles and tests your dbt models on every pull request
    • Use branch protection rules to enforce code review and make CI a hard gate before merging
    • Deploy to production through a controlled process with a separate deployment branch and workflow
    • Respond to production incidents by identifying the last good commit and rolling back quickly

    These practices are the same ones used by mature data engineering teams at companies of every size. The overhead feels real at first — it's slower to open a PR than to just run SQL directly against production. But the first time CI catches a broken ref() before it causes a data incident, you'll understand why the discipline is worth it.

    Where to go from here:

    • Slim CI with dbt Cloud: dbt Cloud has built-in support for running only modified models in CI, with no custom configuration required. If your project grows large, this is worth exploring.
    • dbt Semantic Layer and deployment environments: Learn how dbt's environment concept maps to your Git branches — development, staging, and production environments with different targets.
    • Advanced Git: Topics like git rebase, interactive rebasing for clean commit history, and git bisect for tracking down exactly which commit introduced a bug are all worth learning once this foundation feels comfortable.
    • Observability and alerting: Once your deployments are automated, connect dbt's test results to a notification channel (Slack, PagerDuty) so failures surface immediately rather than waiting for someone to check a dashboard.

    The boring stuff — version control, testing, repeatable deployments — is what makes everything else possible.

    Learning Path: Modern Data Stack

    Previous

    Multi-Hop Data Lineage Tracking Across the Modern Data Stack: Connecting Ingestion, Transformation, and Consumption with OpenLineage and Marquez

    Related Articles

    Data Engineering🌱 Foundation

    Connecting to Databases, Flat Files, and Cloud Storage for Data Pipeline Ingestion

    17 min
    Data Engineering🔥 Expert

    Multi-Hop Data Lineage Tracking Across the Modern Data Stack: Connecting Ingestion, Transformation, and Consumption with OpenLineage and Marquez

    29 min
    Data Engineering🔥 Expert

    Multi-Tenant Data Pipeline Architecture: Isolating, Routing, and Scaling Pipelines Across Customers and Teams

    26 min
    A GitHub Actions Workflow for dbt CI
  • Deploying Safely to Production
  • The Deployment Branch Pattern
  • Rollbacks: When Production Goes Wrong
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps