Version Control: Monorepo & CODEOWNERS
Elin puts the dbt project under version control: branching, pull requests, monorepo vs. many repos, and CODEOWNERS for approvals.
Part 3 of 5— GitOps with Snowflake & dbt
- Why DevOps for Data?
- Environment Setup: dbt + Snowflake Basics
- Version Control: Monorepo & CODEOWNERS
- Automating with CI: GitHub Actions for dbt
- Full GitOps Flow: Deploy, Promote, Rollback
Six weeks after Elin joined, Nordicvinter’s data team had grown from three people to six: Elin, Mikael, and Johan — back from the fjords and now the office’s loudest convert to version control — plus new hires covering finance, sales, and marketing analytics. It took exactly one week for the first conflict to surface.
The marketing analyst pushed a change to a shared customer model — the one everyone’s reports pulled from — without anyone from finance reviewing it. Finance’s month-end numbers shifted overnight. Nobody had done anything malicious. There just wasn’t a process yet for “who needs to sign off on changes to what.”
Elin, Johan, Mikael, and Nordicvinter Retail are fictional — the story continues from Parts 1 and 2. The lessons are real.
This is the point where “put it in Git” stops being enough on its own, and two more decisions become unavoidable: how you structure the repository, and who’s allowed to approve what.
First: Git Basics for the dbt Project
Before any of that, the project from Part 2 needs to actually become a
Git repo. From the nordicvinter-data folder created there:
cd nordicvinter_project
git init
A .gitignore file matters immediately for dbt projects — there are folders you never want tracked. dbt init
scaffolded one for you (you can spot it in Part 2’s screenshot); make sure it covers at least:
target/
logs/
dbt_packages/
.env
target/ and logs/ are generated output every time dbt runs — tracking them just creates noise and merge conflicts
over files nobody wrote by hand. dbt_packages/ is downloaded dependencies, not your code. .env (or wherever
credentials live) should never be committed, full stop — that’s the Git equivalent of the profiles.yml warning from
Part 2.
Branching, Kept Simple
Full “Gitflow” — with develop branches, release branches, hotfix branches — is overkill for a six-person data team. A simpler model covers nearly everything a data team needs:
main— always reflects what’s actually in production.feature-branches— one per change, named descriptively (add-customer-ltv-model,fix-order-status-mapping), branched offmain, merged back via a Pull Request.
git checkout -b add-customer-ltv-model
# make changes
git add .
git commit -m "Add customer lifetime value model"
git push origin add-customer-ltv-model
Then open a Pull Request on GitHub, comparing the branch against main.
Why Review Matters for SQL Specifically
Code review isn’t just a software-engineering ritual imported for its own sake. For data models specifically, a second
pair of eyes catches things that are easy to miss when you’ve been staring at your own query for an hour: a join that
silently duplicates rows, a WHERE clause that quietly excludes a valid segment of customers, a warehouse-sizing choice
that will make a nightly job needlessly expensive. The marketing analyst’s mistake from the opening story — an
unreviewed change to a shared model — is precisely what a PR requirement exists to prevent.
The Bigger Question: One Repo, or Several?
With six people now touching the project, Mikael asked the obvious question: should finance, sales, and marketing each get their own repository, or should everything live in one?
What “Monorepo” Means Here
- Monorepo — one Git repository containing the entire dbt project: every domain’s models, in folders, side by side.
- Multi-repo — each domain (or team) owns a separate repository —
dbt-finance,dbt-sales,dbt-marketing— often stitched together later via cross-project references.
Why Most Teams Start Monorepo
- Lineage stays whole. dbt’s dependency graph (
dbt docs generate) is genuinely most useful when it can see every model at once. Split it across repos and you lose the full picture of what depends on what. - Shared models live in one place. A
dim_dateorstg_customerstable used by all three domains exists once — not copy-pasted three times, drifting out of sync. - Changes are atomic. Renaming a column that seven downstream models depend on is one Pull Request, not seven coordinated ones across seven repos.
- One CI/CD setup, one standard, applied everywhere — instead of maintaining (and forgetting to update) the same pipeline five times over.
Why Some Enterprises Still Split
- Team autonomy — at real scale (hundreds of engineers), one team may not want to compete for review attention inside a single repo with everyone else.
- Access control granularity — Git repo permissions are coarse; multi-repo lets you restrict visibility of a domain’s models entirely, not just approval.
- CI/build time — thousands of models in one repo can mean every CI run touches a sprawling dependency graph, unless it’s scoped carefully (more on that in Part 4).
- Blast radius — a bad merge can, in theory, ripple into unrelated domains if dependencies aren’t managed carefully.
The Middle Ground: dbt Mesh
Worth knowing this exists, even if it’s beyond what Nordicvinter needs today: dbt Mesh allows multiple dbt projects,
each in its own repository, to reference each other in a governed way ({{ ref('other_project', 'model_name') }}). It’s
the natural next step once a monorepo genuinely outgrows itself — domain teams keep autonomy, lineage still connects
across projects.
The rule of thumb I promote: start monorepo. Split only when a specific, real pain shows up — review bottlenecks, access control needs, CI time — not preemptively because “that’s what big companies do.”
Solving “Who Approves What” Without Splitting Repos
Here’s where the second problem — from the opening story — gets solved directly, and it’s the mechanism that makes monorepo-at-scale actually workable: CODEOWNERS.
A CODEOWNERS file (placed at .github/CODEOWNERS) maps folders to the teams responsible for reviewing changes within
them. GitHub automatically requires their approval before a PR touching that folder can merge.
# Finance domain models
/models/finance/ @nordicvinter/finance-data-team
# Sales domain models
/models/sales/ @nordicvinter/sales-data-team
# Marketing domain models
/models/marketing/ @nordicvinter/marketing-data-team
# Shared staging/core models — platform team must sign off
/models/staging/ @nordicvinter/data-platform-team
/models/marts/core/ @nordicvinter/data-platform-team
# Macros affect everyone — extra scrutiny
/macros/ @nordicvinter/data-platform-team
# CI/CD workflows — platform team only
/.github/workflows/ @nordicvinter/data-platform-team
A few things worth knowing before you write your own:
- Last match wins. The file is read top to bottom, and when two patterns match the same file, the last one
applies. So order your rules from broad to specific — and be careful with catch-alls: a
* @some-teamline added at the bottom silently overrides everything above it. - It does nothing on its own. CODEOWNERS only becomes enforced once the repository’s branch protection settings have “Require review from Code Owners” turned on. Teams write the file, forget this step, and wonder why unreviewed PRs still merge.
- Prefer teams over individuals.
@nordicvinter/finance-data-teamscales;@karin-lindqvistdoesn’t — someone has to remember to update the file every time staffing changes. - A path can require multiple owners. List more than one team on a line to require sign-off from both, e.g., a shared core model requiring both a domain team and the platform team.
Connecting It Back
CODEOWNERS directly answers two of the “why split repos” concerns from earlier: it gives domain teams review-level autonomy over their own folders, and it gives folder-level approval gating — though it’s worth being precise that CODEOWNERS controls who must approve, not who can see. True visibility restriction still requires repo-level permissions, or, on GitHub Enterprise, more advanced path-based access controls.
Had CODEOWNERS been in place before the opening story, the marketing analyst’s PR touching a shared customer model would have been blocked from merging until someone from the platform team — the model’s actual owner — signed off.
What’s Next
The following Friday, Elin approved her tenth pull request of the week. Every change now had a diff, a reviewer, and an
owner — even Johan’s fixes arrived as branches, with commit messages bordering on penitent. But scrolling the merged
list that afternoon, she noticed something that made her pause: on at least two of those PRs, nobody — including her —
had remembered to run dbt test before clicking merge. The process no longer depended on anyone’s memory. Except, she
realized, where it still did.
The repo now has structure, branches, review requirements, and folder-level ownership. What it doesn’t have yet is
automation — a human still has to remember to run dbt test before merging, and humans forget. In
Part 4, we wire up GitHub Actions so tests run automatically
on every Pull Request, and a broken model gets caught before it ever has the chance to reach main — including
previewing how CI can be scoped to only the folders a given PR actually touched.