Short answer: you’re close, but a few tweaks will save you a lot of pain. The biggest risks are (1) letting “system test” become a grab-bag of half-ready features and (2) not having a crisp, repeatable RC process with versioned release branches and back-merges.
Here’s a pragmatic way to refine what you have.
A dedicated pre-prod/RC branch separate from main.
A place to integrate features before RC.
A clear “promote, don’t rebuild” path: preprod → main.
System test as a playground
If it includes both current and future features, you’ll get “hidden dependencies” and late surprises when you try to cherry-pick only some changes into RC.
Rolling back is hard if future work bleeds in.
No versioned release branches
preprod
branch that mutates over time makes it hard to track exactly what’s in RC1 vs RC2, generate clean release notes, or hotfix a specific release.Hotfix path ambiguity
Keep your branch names, add a few guardrails.
Branches
main
→ production only.
preprod
→ acts as the current release candidate, but create versioned RC branches when you cut a release: release/1.8
(or release/2025.09
). You can keep preprod
as a pointer (or alias) to the active release branch if the name helps your team.
develop
(your system test) → integration of all features for next releases, but protected with feature flags for anything not planned for the current RC.
Short-lived feature/*
branches → merge into develop
via MR.
Flow
Cut RC
When you’re ready to stabilize, branch from develop
to release/x.y
.
Only allow bug-fix merges into release/x.y
(no new features). Tag candidates vX.Y.0-rc.1
, -rc.2
, etc.
Stabilize RC
Run full regression in pre-prod environment from release/x.y
.
Any fixes are merged into release/x.y
and back-merged into develop
(to avoid regressions next cycle).
Release
When green, fast-forward or merge release/x.y → main
, tag vX.Y.0
, deploy.
Optionally, merge main → develop
to ensure post-release parity (if your GitLab settings don’t auto-sync).
Hotfixes
Create hotfix/x.y.z
from main
, merge back to main
, tag vX.Y.Z
, deploy.
Then cherry-pick to any open release/x.y
(if applicable) and merge to develop
. Keep a checklist so hotfixes don’t get lost.
Why this helps
You still use your “system test” branch, but release hardening happens in a clean, versioned branch.
You prevent the “playground” effect from polluting RC by cutting RC from a known commit and controlling what gets cherry-picked.
main
is the only long-lived branch, features behind flags, and cut release/x.y
only during stabilization. This reduces long-lived divergence but requires strong CI + feature flag discipline.Protected branches & approvals
main
and all release/*
branches. Require MR approvals (e.g., code owner + QA). Disable direct pushes.Merge rules
“Merge when pipeline succeeds”, enable merge trains on develop
/main
to reduce flaky integration breaks.
Prefer squash merges for feature branches to keep history clean.
Pipelines by branch
feature/*
: unit + component tests, static analysis.
develop
: full integration + e2e on a Review App or shared “system test” env.
release/*
: full regression, perf/smoke, DB migration dry-run, security scans.
main
: deploy to prod, post-deploy smoke, rollback job.
Environments & tagging
Use GitLab Environments: system-test
for develop
, preprod
for release/*
, production
for main
.
Tag RCs (vX.Y.0-rc.N
) and releases (vX.Y.Z
) for traceability and release notes.
Feature flags
develop
but disabled by default. Only features planned for release/x.y
get their flags enabled in that branch/env.Back-merge automation
main
(hotfix), auto-open MRs to develop
and active release/*
branches (GitLab CI job or a small bot).MR templates
Database migrations
release/*
pipeline (dry-run). Include a down/rollback plan.Release freeze
release/*
before GA; only severity-rated fixes allowed.“System test includes current + later features”: OK if and only if those later features are behind flags and you cut RC from a known good commit (or cherry-pick only the features intended for the release). Otherwise, create a next
branch to park future features separately.
“Preprod as RC branch”: Better to make it versioned release/x.y
and map your Preprod environment to whichever release branch is active. You can keep a preprod
alias branch, but the versioned branch is what you merge and tag.
“Push the feature branch to RC”: always via MRs (no direct push) with approvals, and ideally cherry-pick or merge only the specific commits intended for the RC to avoid dragging unrelated changes.
Branches: feature/login-otp
, develop
, release/2025.09
, main
, hotfix/2025.09.1
Tags: v2025.09.0-rc.1
, v2025.09.0
, v2025.09.1
Envs: system-test
(develop), preprod
(release/2025.09), production
(main)
If you adopt the versioned release/*
branch + feature flags + protected merges, your current plan will work smoothly and remain auditable. Want me to write a short GitLab policy doc (branch protections, MR templates, CI “rules:” snippets) tailored to your repo?
Thanks