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.
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.
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.
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.
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.” 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. 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 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. A broken commit gets reverted in seconds, not untangled after three weeks of divergence.
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.
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. 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
- Developer’s Guide to an AI Subscription Stack That Works in 2026
- Why Deleting More Code Makes You a Better Developer
- System Design Interview Prep: The Only Resource Ladder You Need
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.