What started as a tiny MVP suddenly became a full-blown product, and our old release process just couldn’t keep up. We needed a branching and release setup that wouldn’t slow us down — even when everything around us was changing every week. Here’s what we came up with.Our Branching and Release Strategy That Actually WorkedIn the first part, I wrote about our ticket workflow and how it helped reduce a bit of the chaos. From Chaos to Flow: How We Organized Our Dev–QA–PO Pipeline [Part 1]The next big step was organizing a proper release management system. Our project grew fast: we started with a small MVP that quickly turned into a large product, and the team kept expanding. We needed something flexible, but at the same time simple, transparent, and predictable for everyone.Why we needed a proper release systemWe never knew in advance which stories, tickets, or bugs would enter the release. Business priorities changed, integrations were delayed, designs shifted.We needed the ability to add extra tickets to a release at any moment.Hot-fixes were always possible, so we needed an easy and fast way to prepare production hotfix releases.Developers and QA must not be blocked while release preparation was happening.Branching strategyWe chose a branching strategy where each story (and each staging or production bug) had its own branch. We worked with three main permanent branches:main - the code running on productionintegration - connected to the test environment for QAdev - connected to the developer environment for early checksBranches were always created from main. This guaranteed a stable starting point based on the current production code. We followed a similar philosophy - stage always ran a clean release branch, not a random mix of features.Choosing the correct branchOur branch naming pattern helped a lot. Every branch had:ticket numberauthor initialsand if needed - the parent ticket numberThanks to this strict pattern, we always knew what the branch belonged to and whether it depended on another story. As the person assembling releases, this saved me many times.Working with dependent storiesIf a developer needed another story that was not yet merged into main, they could merge it locally into their own branch. In that case, the ticket had to include a clear note about this dependency.Backend and frontend developers always worked in the same branch. This prevented cross-branch confusion and helped keep the feature consistent.Development flow inside the branching systemDeveloper creates a branch from master (yes yes yes, we had master, old-school style).Implements backend and frontend work in that branch.Creates an MR into dev and merges it to test functionality on the dev environment.If everything works, the developer creates an MR into integration with the label “ready to test”.The MR is reviewed and merged.QA tests the feature on the integration environment.After QA approval, the ticket is ready for PO validation.If PO approves it, the story becomes “ready for release”.Syncing main and dev + cleanup routineTo keep all environments stable, we had a regular housekeeping cycle:After every release, changes from main were merged into both integration and dev.Every two releases, we cleaned up all branches that were already merged into main.Every three months, we fully reset the dev environment to avoid broken data, stale configs, and leftover test artifacts.This routine kept dev smooth and fast, prevented old branches from polluting Git, and ensured that developers always had predictable environments.How I assembled the releasesThis part required a bit of manual work, but it was manageable. Since I was responsible for both the team and the release cycles, I handled this task.We released every two sprints. When release time came, I followed this flow:I collected all “ready for release” tickets.I merged each of them manually into main.If conflicts appeared (usually Symfony migrations or Composer), I resolved them. (Later I will add a small guide about this.)After merging everything, the main branch contained the full release.Rollback was easy because main always matched production and each release was tagged.I created a new tag.Deployed the tag to the staging environment.PO validated the release on staging.After approval, we deployed it to production.Hot-fixes used the exact same structure - just with a dedicated small branch created directly from main.This system gave us the flexibility we needed while keeping everything predictable and stable.What worked well (and what didn't)No release system is perfect, and ours wasn’t either. But this approach solved a lot of real problems we faced before. Here’s what worked, and what still caused headaches.BenefitsOnly fully finished tickets went into releases. Nothing half-ready, nothing “almost done”. This kept releases clean and predictable.Developers always had stable code to work with. Because branches were created from main, devs never inherited chaos from unfinished fea…