skip to content
cd/

branches without touching production: the CMS feat, dev, main flow

I knew how a branch flow worked in theory. in the CMS I finally did it in practice, with PRs into dev, a release into main and migrations before code

3 minview on github

tl;dr#

in the CMS of the blog I work on, the flow changed to feat/* | fix/* → dev → main. every new thing starts on its own branch, goes into dev through a PR, and main (which is production) only gets a release, which is a PR of the whole dev. I already knew how this worked, but I had never really done it. now I did, and what got me wasn't git: it was the order of things. migrations before code, new features off by default, and merge commits instead of squash.

before#

I used to push everything straight to main. it worked, but anything half done went live. there was no place to gather a few features and look at them together before shipping.

the flow#

feat/assistant ──┐
feat/bell ───────┼──> dev ──(release)──> main
fix/api-404 ─────┘
  • feature branch: feat/something or fix/something, always branching off dev.
  • PR into dev: this is where the feature gets reviewed. the PR template has a checkbox right at the top: "base is dev". sounds silly, but GitHub opens the PR against main because it's the default branch, so I have to switch the base by hand. the checkbox is there so I don't forget.
  • release into main: when dev is good, I open a PR from dev to main listing everything that went in (#6, #8, #9, #10). that's the only PR that targets main.

what I didn't know mattered#

1. migrations go before the code#

there's a single production database, no staging one. so before merging the release, bun run db:migrate runs in production. the migrations are additive only: new tables, new columns that are nullable or have a default, nothing dropped or renamed. that way the old code on main keeps working with the migrated database, and only then does the new code land.

the other way around (code first), /painel would break looking for a table that doesn't exist yet.

2. new features ship turned off#

the writing assistant only shows up if the GEMINI_API_KEY variable exists. without it, the panel looks the same as before. so the code can go to production without turning the feature on for everyone, and it gets turned on only where it makes sense.

3. merge commit, not squash#

on the release I use Create a merge commit. if I squashed, main would get one single commit, different from everything on dev, and on the next release git would think the two branches diverged. with a merge commit, dev is always just ahead of main, and the release goes in with no conflicts.

what I still don't know#

honestly, I don't have my own way of deciding what's right in all of this yet. a lot of what's here I followed from the way Luan does it, and I understood the why while doing it. but doing it in practice is what made the theory stop being theory.