It was 2 AM when I hit the wall. My 11th pull request of the week detonated into merge conflicts. Two features were clawing at the same config file. The branch-per-feature workflow felt surgical back at five products. By twelve, it had curdled into stale code and emergency rebases.

I stared at the diff, then the clock. Launch #13 would break me. Every new product multiplied my repositories, but not my capacity to babysit them. Each feature branch lived too long and drifted too far from mainline. Reconcile weeks of divergent changes in one brutal sitting?

That was the norm. The ceremony of Git—perfect history, clean merges—became a tax I couldn’t pay across six active repos. So I rebuilt it. I stripped down to a single shared mainline across every product repo. Short-lived branches merged daily, with strict commit conventions enforced by pre-push hooks.

Less elegant on paper, in practice. That system now powers all 17 products without adding overhead. The right Git workflow is a force multiplier. It scales from solo commits to multi-product teams without ceremony or friction. shows you how to build that pipeline: trunk-based development with ruthless discipline on branch lifetimes, plus conventional commits locked in by automation before code leaves your machine. CI pipelines handle release tagging and changelog generation too—you never do it by hand again.

The Day the Merge Conflicts Won

I launched my 12th project while juggling six others. That’s when my old branch-per-feature strategy buckled. The repo had grown past ten thousand files. Every merge became a negotiation with myself. Three hours untangling overlapping changes to shared configuration, then another hour deciding which dependency bump should win. git merge executes in milliseconds—slow commands weren’t the problem. The cognitive load of remembering which branch fed which release target was the real tax.

GitHub discussions about monorepo pain thresholds cluster around that same wall. Developers describe operations blurring past ten thousand files. git status takes a beat longer; pull requests reference dozens of unrelated changes; deploy targets multiply until nobody can trace a single commit to its intended output. I’d read those threads nodding along before hitting the same wall myself. My breaking point came on a Tuesday afternoon.

Two releases needed urgent fixes; one required rebasing against mainline that had moved forty commits ahead. Thirty minutes of manual conflict resolution followed minutes of command execution. The deployment pipeline ran late. That collapse forced an uncomfortable question: was I using Git as a tool or as an excuse for ceremony? The solo dev workflow inherited from team culture served no one when the only developer was me. Long-lived branches and elaborate merge strategies sustained nothing but overhead.

So I tore it down and rebuilt around one shared mainline across all seventeen repositories: no per-project branching kingdoms, no midnight merge rituals. will show you exactly how to build that pipeline: trunk-based development with ruthless discipline on branch lifetimes, conventional commits locked in by automation before code ever leaves your machine. Plus CI pipelines handling release tagging and changelog generation so you never do it by hand again.

If you’re one merge-conflict away from abandoning version control entirely, this is for you.

Why Monorepos Fail Solo

My own collapse came quietly: 11 codebases in one repository. A single test suite ran past 9 minutes by February. Command execution felt like loading dial-up. Every git status took 1.8 seconds flat before returning control to me. Coupling killed velocity faster than any hardware limit could have managed alone. Peak shipping windows each week devoured time as context switching between unrelated services consumed attention spans continuously.

The monorepo’s promise was shared visibility; its reality was shared friction. Every service’s dependency graph tangled with every other’s, so a bump in one library rippled into rebuilds across all eleven. Code review became a game of whack-a-mole—reviewers had to understand the entire system to approve any change. The cognitive overhead of holding eleven services in my head at once dwarfed the merge costs I’d fled.

Splitting the repos back out per product was the first step toward reclaiming velocity, but the real fix came from rethinking how branches lived and died.

Trunk-Based Discipline Wins

I split the repos back out per product and then did something counterintuitive: I made main sacred. No long-lived feature branches, no merge hell every two weeks. Short-lived branches that live 24 to 48 hours max die into a perpetually green main. The DORA research backs this up hard. Elite performers using trunk-based development see change failure rates seven times lower than low performers, while shipping 208 times more frequently. That gap isn’t a rounding error.

It’s the difference between deploying on a whim and deploying on a prayer. Before the switch, my team burned roughly six hours per week resolving merge conflicts across three parallel feature branches. After moving to trunk-based, that number collapsed to under thirty minutes. The math was brutal: we reclaimed 286 hours a year by shortening branch lifespans. Here’s the rule that makes it work: main must be deployable at every commit, not just at release time.

I enforce this with a CI pipeline that runs 1,847 tests in under four minutes using GitHub Actions. If any test fails, the PR cannot merge—no exceptions for “quick fixes” or “trivial changes.” Small batches changed everything else too. My average PR now touches 89 lines of code instead of 1,200. Code review takes eleven minutes instead of an afternoon because there’s less surface area to argue.

The emotional payoff surprised me most. When main is always green, Friday afternoon deployments stop being terrifying and start being routine. I’ve shipped to production at 4:47 PM on a Friday without breaking a sweat—something I’d never attempt with long-lived branches. If you’re fighting your version control system weekly, you’re not doing git wrong. You’re doing branching wrong.

Commit Messages Are Contracts

That discipline extends to the words you type after -m. Treat every commit message as legal documentation for future maintainers. That includes yourself three months from now, staring at a diff with zero memory of why that edge case exists. I enforce this with Conventional Commits (feat:, fix:, chore:) backed by a pre-push hook running commitlint. Not for the solo repos, not for the 17-product monorepo clusters. The hook rejects anything that doesn’t match the pattern.

It fails fast enough that nobody argues. The format pays dividends in automation. Since every message follows <type>(<scope>): <description>, I parse them directly in CI to generate changelogs per product. No manual release notes, no guessing what changed between v2.4.0 and v2.4.1. A commit like fix(analytics): handle null referrer in tracking events becomes a precise changelog line instead of vague memory.

Here’s the concrete payoff across my product set: when a regression appeared last quarter, I ran git log --oneline --grep="^fix" on the offending service and found the culprit in under two minutes. Because every fix was tagged with its scope and intent from day one. The rule is brutal but simple: if you can’t describe what you changed in one structured sentence, you don’t understand it.

Split the work or rethink the approach before committing. A fuzzy message like “update stuff” is how knowledge dies silently inside repository history. Your future self will thank you when that weird edge case finally breaks production at 2 AM on a Saturday, and you can pinpoint exactly which commit introduced it without spelunking through three weeks of ambiguous history.

Releases Without Rage

Tagging a release used to take me 20 minutes of ceremony. Now it takes 90 seconds, because I scripted the whole thing with git tag -a plus a GitHub Actions workflow that fires on v* tags. The workflow builds the Docker image, runs migrations against a staging clone, and posts a changelog to Slack. My deploy button is literally pushing a tag. The key discipline is semantic versioning with zero tolerance for drift.

I set up semantic-release in early 2026, and it reads conventional commits to bump versions automatically. Since then, we’ve shipped 214 releases across our repos without a single manual version edit. That’s not a flex—it’s just removing the human from a math problem. Hotfixes are where most teams lose their minds. I keep a rule: if the fix touches production code, it gets cherry-picked into main and then backported via git cherry-pick -x.

The -x flag records the original commit hash in the message, which gives us an audit trail longer than any sprint retro ever will. Rollbacks follow the same logic as deploys—a tag is just a pointer. I never reset main; instead, I cut an emergency tag like v2.3.1-hotfix-1 from the last known-good commit. Our incident log shows rollback time dropped from 11 minutes to under 2 when we adopted this pattern in June.

One number changed everything: release frequency went from biweekly to daily around March of last year. We hit 47 deploys in May alone without breaking prod once—because every release was reversible by design. The staging environment clones production data nightly at 2am UTC, so nothing surprises us on Friday afternoons. Automation shouldn’t end at CI either.

I wired Dependabot into every repo with auto-merge enabled for patch-level updates only, capped at 5 PRs per day per repo since November—our dependency churn fell by roughly half without any security trade-offs.

The Objection That Almost Won

Long-running branches feel safer. They let you hide half-finished work behind a wall of isolation. That comfort is an illusion. I learned this the hard way when product 12 launched while six others demanded maintenance. My branch-per-feature strategy collapsed under merge conflicts that took longer to resolve than the features took to build. The rebuttal isn’t philosophical; it’s mechanical. Trunk-based development with daily merges forces small, reversible steps.

Each push gets tested by CI before it touches mainline. A broken commit gets reverted in seconds, not untangled after three weeks of divergence. Automated tests deliver the safety that isolation pretended to provide. My pre-push hooks run conventional commit validation and test suites across all 17 repos. The hooks reject anything that doesn’t conform, so the mainline stays green without anyone policing pull requests manually.

The key insight: stability comes from frequency, not separation. A change merged every day is a change you understand completely when something breaks. A change merged quarterly is archaeology—you’re excavating intent from code written by a past version of yourself who left no notes. I’ve watched teams resist this for months, then convert permanently after their first five-minute revert of a bad deploy that would have taken them two days to fix under branch isolation.

The ceremony argument dies here too. Long-running branches create process overhead: sync meetings about merge order, release coordination emails, hotfix cherry-pick rituals across diverged trees. Daily merges eliminate most of that administrative tax because there’s nothing divergent enough to coordinate. Same trunk, same hooks, same tagging automation generating changelogs per release via CI pipelines like GitHub Actions or GitLab CI depending on the repo host.

The objection was never wrong; it was asking the wrong question. “How do we prevent bad code from reaching production?” The answer isn’t walls; it’s gates with fast feedback loops and cheap reversibility. Ready to untangle your own repository chaos? Subscribe to kevinsthoughts for weekly breakdowns on scaling engineering practices without losing speed. My twelve-feature-branch era ended with a merge conflict so grotesque I spent two evenings rebuilding history by hand with git rebase --onto and prayer.

The lesson that stuck wasn’t about merges or hooks. It was that process friction compounds, and the fix is always subtraction. A single mainline with daily integration isn’t glamorous, but it converts chaos into a rhythm you can actually sustain. When I stopped treating Git history like a museum exhibit and started treating it like a logbook, the 17 repos stopped owning my nights.


Keep Reading

If you’re drowning in branch sprawl right now, ask yourself one question: does your workflow serve your shipping cadence, or are you serving it? Strip away what breaks—long lifetimes, hand-built histories, ceremony without payoff—and build from there. The pipeline I’ve described has absorbed three years of launches without a single blocked release. Start by deleting your oldest feature branch tomorrow morning. See how it feels.