0% прочитано

Release Branch as an Integration Unit, Not a Series of Separate Merges into Development

For a larger release, I do not merge related tasks into development one by one. They first come together in a dedicated release branch, are stabilized as a group, and only then move forward as one verified unit.

19 жовтня 2026 р. 10 хв читанняCI/CD

When a release contains one small task, a dedicated release branch may be unnecessary.

But when several tasks change one functional area, I no longer treat them as independent merges into development.

In that situation, the release branch becomes an integration boundary. It is the first place where related changes must work together as one future release.

This approach has been especially useful in larger releases where backend, frontend, permissions, realtime behavior, analytics, and tests changed together.

For me, a release branch is not just a temporary Git branch. It is where separate tasks stop behaving as separate tasks and start behaving as one release.

The Problem Is Not the Merge Itself, but the Partially Assembled Release

Separate merges do not show the boundary of a complete release
Task A  |  vdevelopment Task B  |  vdevelopment Task C  |  vdevelopment  

If Task A is already in development while Task B is still under review, the system may enter an intermediate state that nobody intended as a separate release.

There may be a new API without the matching frontend, a new permission without UI support, a new listener without the complete event flow, or analytics without all the required data sources.

Each task may be correct in isolation, but the release is still incomplete.

The release branch becomes the shared integration point
main  |  vrelease/feature-area  |  +<-- feature/task-a  |  +<-- feature/task-b  |  +<-- feature/task-c  |  +-- integration fixes  |  vstabilized release  |

I create the release branch from a stable baseline - the current main.

Related feature branches are then integrated into the release branch, where they are tested together. This is where I fix integration bugs that could not be detected in isolated tasks.

After combining the changes, stabilizing the release, and completing the required checks, the entire release is merged into development through a single merge request.

This way, development receives a complete functional unit rather than a sequence of separate tasks.

Real integration starts between tasks
Backend task:new notification state Frontend task:new unread notification indicator Realtime task:broadcast state changes Permission task:control access Analytics task:track reads and reactions Separately:

In larger releases, the most interesting bugs often live not inside one task but at the boundary between several tasks.

For example, the backend already returns a new state while the frontend updates it only after reload. A realtime event exists, but permissions prevent the channel subscription. Analytics is ready, but one flow does not produce the required event.

A release branch provides a dedicated place for exactly this class of problem.

The Release Branch Should Start from a Stable Baseline

The release starting point should be explicit
git fetch origin git switch \  -c release/feature-area \  origin/main

It matters to me that a release branch does not inherit the accidental local state of another feature branch.

It should start from a clear stable baseline. Then the entire release diff can be reviewed as one future system change.

Before integrating feature branches, I also check their baseline and commit history. This is important to avoid accidentally bringing unrelated changes from development into the release.

A Release Branch Does Not Replace Feature Branches

I still keep a dedicated feature branch for each logical task.

That preserves a small review scope, focused tests, and understandable change history.

The difference is the integration target: a feature branch merges into the release branch rather than directly into development. Only after shared stabilization does the entire release move into development as one integration unit.

I Run the Full Test Suite Once the Release Is Assembled

Each task should have focused tests during development.

But an expensive full test suite provides the most value once the release branch already contains all planned changes and the main integration fixes.

Otherwise a team may spend a lot of time running the entire suite after every merge even though the next task will modify the same area again.

Checks have different cost and belong at different stages
For each task: targeted testslintstatic analysis of changed code areas  On the assembled release branch: full test suitefull static analysislintintegration checksbuildconfiguration and migration checks 

I Keep Integration Fixes Inside the Release Branch

After several tasks are merged, small fixes often appear that do not fully belong to one original feature branch.

For example, the backend and frontend may need a shared field name adjusted, or a common layout may need a fix after a new state is introduced.

These fixes belong to the release branch because they arise specifically during release integration.

Depending on their complexity, I may apply them as separate commits or through small fix branches with code review. After making the fixes, I rerun the necessary tests.

I Do Not Use development as a Temporary Space for Unfinished Integration

For me, development is a shared branch that should receive already assembled and verified functional units.

I do not use it to assemble a larger release piece by piece when its individual parts cannot yet work together properly.

If the functionality is not yet complete as a single release, I keep it in the release branch until integration is finished.

This approach makes development easier to reason about while still allowing parallel work on several related tasks.

A Release Branch Gives One Diff for Final Review

The whole release can be reviewed as one change
git diff \  origin/main...release/feature-area

This is very useful before the final merge.

Individual merge requests explain specific tasks, while the final release branch diff shows the changes included in the release relative to the common ancestor with main.

At this level it becomes easier to spot an accidental file, leftover debug code, a configuration change, or a dependency between tasks.

I also review the expected merge into development separately. Because that branch may already contain other changes, successful tests on the release branch do not guarantee that the integration result will work without issues.

If One Task Is Problematic, the Release Can Still Be Reworked Before Merging into Development

A release branch leaves room to make a decision: fix the task, replace its implementation, or, when safe, postpone it from the release.

As long as the release has not yet been merged into development, these decisions remain within the release branch. If the release scope changes, I recheck dependencies between tasks and run the necessary tests again.

This is simpler than first merging several dependent changes into development and then trying to revert just one without affecting the others.

I Define Readiness at the Release Level, Not at the Last Task

The final feature branch may be completely finished, but that does not automatically mean the release is ready.

For me, release readiness includes shared tests, checks for behavioral conflicts, the build, configuration, migrations, access permissions, readiness of required runtime processes, and the deployment procedure.

Only after that does the release branch stop being an active integration workspace and become a ready candidate for a merge request into development.

A Release Branch Is Not Needed for Every Change

I do not want to turn this approach into a rule for every bugfix.

If a change is small, independent, and safe to release on its own, a dedicated release branch only adds unnecessary steps.

It becomes useful when several tasks together form one product or technical unit and share one readiness point.

When I Use a Release Branch as an Integration Boundary

  • The release contains several related tasks.

  • Related tasks should not enter development independently of one another.

  • Some integration bugs can only be detected once all parts are assembled.

  • The full test suite is expensive and is most useful on the assembled release.

  • One final diff from the stable baseline is valuable.

  • We want room to stabilize or adjust release scope before the final merge.

  • Readiness is defined for the whole release rather than the last task.

For a Larger Release, the Integration Boundary Matters More Than Individual Merges

A release branch does not make individual feature branches less important. It adds one level that larger changes need: release-level integration.

Individual tasks can be developed, tested, and reviewed independently. But before merging into development, they must demonstrate that they work together as one release.

That is the main role of a release branch for me: not merely storing code between merges, but giving the whole release its own readiness boundary.

A feature branch answers "is this task ready?" A release branch answers a different question: "are all these tasks ready to work together as one release?" In my workflow, a release branch is created from main, brings related tasks together, goes through stabilization, and is integrated into development through a single merge request. Further promotion of changes to production is a separate stage with its own checks and release readiness decision.