Un-f*ck your CICD and Branching Strategy

Let’s talk about a CI/CD anti-pattern that needs to die: environment branches. You know the ones - dev
, qa
, stage
, prod
. It seems logical at first, but over time it leads to pain, confusion, and chaos.
Modern teams should instead rely on short-lived feature branches and artifact-based promotion - a strategy that dramatically improves clarity, agility, and stability in your pipeline.
Why Short-Lived Branches Matter
Short-lived branches reduce complexity. The longer a branch lives, the more it drifts from the mainline, increasing the chance of merge conflicts, integration bugs, and confusion about what’s actually deployed.
By keeping branches short-lived:
Faster Feedback: Changes are merged and tested quickly, reducing integration risk.
Less Merge Hell: Avoid tangled PRs and back-and-forth reviews.
Better CI Hygiene: Every commit triggers a fresh, clean build.
Tighter Control: You know exactly what feature or fix each branch introduces - no bundling or guessing.
Supports Immutable Builds: When every branch produces a build artifact, you have a traceable lineage of what was built, tested, and released.
The Core Problem
When we test something in a lower environment (say QA), then rebuild it before pushing to production, we're introducing risk. A fresh build is not the same build. Even with the same code, dependencies, timestamps, environment variables - anything can introduce variance.
So why do we keep doing this?
The Better Way: Promote Artifacts, Not Branches
Instead of branching for environments, build an immutable, deployable artifact for every commit on every branch. Whether it's a .jar
, .tar.gz
, or a Docker image, the idea is the same - treat your build as the unit of promotion.
Tag it. Store it. Deploy it.
Why This Works (and What It Solves)
✅ Hotfixes: You can patch main
, build a new artifact, and promote just that. Or maybe create a branch from an existing tag, patch it, and promote it.
✅ Parallel Features: You can release features in any order because builds are isolated. If you need 2 features that are in different branches, simply merge them together and build! There's no need to figure out how to get them into some long-lived branch that may have other half ready features integrated.
✅ Rollback: Want to roll back? Just redeploy a previously tested image. No rebuild required.
✅ Auditability: You know exactly what’s running in each environment - and it was tested.
Implementation Tips
1. Tag your images
Use meaningful tags like:
myapp:feature-login
myapp:v1.2.3
2. Release Candidate Flow
Use a simple script or CI job that:
Ensures a clean Git state (no untracked or modified files)
Reads a VERSION
file with a -dev
suffix during development
Finalizes the version (removes -dev
, updates metadata like pom.xml
, package.json
, etc.)
Tags the commit (e.g. v1.2.3
)
Pushes the tag and updated VERSION (e.g. bumps to 1.2.4-dev
)
Builds and pushes the final artifact
3. CI Automation
Wrap this in a CI workflow so it's consistent, repeatable, and foolproof.
Final Thoughts
Don’t contort your release process around a branching model that fights against modern CI/CD practices. Design your app to use runtime environment config. Focus on traceable, reproducible builds. And promote artifacts, not code.
Your future self (and your team) will thank you.