Integration Branches¶
An integration branch parks an epic's pull requests until the whole epic is
done, then brings it to main in one pull request. It is optional. Most work
merges straight to main, and an epic whose children can ship one at a time
needs no integration branch at all.
Reach for one when an epic spans many pull requests that must ship together,
while unrelated fixes keep landing on main. Without it, the release model
in the releasing skill treats an open atomic epic as a dirty trunk: a
release waits, or needs a release/X.Y branch cut from before the epic
started. With the epic parked on integration/<epic>, trunk stays releasable
throughout.
What the template provides¶
integration/*branches are protected: child pull requests need a greenCI Success, and force pushes are blocked. Checks a project adds throughextra_required_checksare required only on the final pull request tomain. See Repository Protection.ci.yml,docs.yml,codeql.ymland the automatic review run on pull requests that target anintegration/*branch, and CI also runs on every push to one.- The local structural and template-conformance gates measure a child branch
against the integration branch it was cut from, not against
main. - The release workflows ignore integration branches. Nothing is released from one, and merging a child pull request releases nothing.
Running an epic on an integration branch¶
-
Create the branch from
mainand push it. Status checks are not enforced on branch creation.git switch -c integration/<epic> origin/main git push -u origin integration/<epic> -
Open the final pull request early, as a draft from
integration/<epic>tomain. Its CI runs againstmainon every push, so a conflict or a broken build shows up while it is small, and its diff is the epic's running total. -
Target each child pull request at the integration branch and squash merge it, as you would into
main. The pull request title still has to be a conventional commit: the squash commit carries it tomainlater, where the changelog reads it.Write
Part of #<epic>orRefs #Nin a child's body, notCloses #N. GitHub closes linked issues only when a pull request merges into the default branch, so a closing keyword in a child does nothing. Collect theCloseslines in the final pull request instead. -
Merge
mainin; never rebase. Bringmaininto the integration branch after each release and whenever a conflict appears:git switch integration/<epic> git pull --ff-only git merge origin/main git pushA rebase rewrites every child commit already on the branch, so each open child pull request would suddenly show its siblings' changes as its own, and the reviewed commits would no longer exist. A merge keeps them, and it records any conflict resolution where a reviewer can find it. Pushing the merge directly relies on the ruleset bypass that the repository admin role holds; anyone else opens a pull request from
maininto the integration branch and merges it with a merge commit. -
Finish with a merge commit. Before merging the final pull request, check that no child pull request is still open: once the base branch is deleted, GitHub retargets open children to
main. Then merge with Create a merge commit, never with squash.knope computes the next version and the changelog from every commit reachable from
mainsince the last release, including the ones a merge commit brings in, and counts each commit once. With a merge commit, the changelog keeps one entry per child, and any child's!still decides the version bump. A squash would collapse the whole epic into the final pull request's single title. It also keeps the project's rule that roughly every issue and pull request maps to one commit onmain. -
Delete the branch through GitHub, or let automatic head-branch deletion do it. Deleting it with
git push --deletewhile a child pull request is still open closes that pull request instead of retargeting it.
Reviewing the final pull request¶
Every child was reviewed when it merged into the integration branch. The final review covers what those reviews could not see:
-
Conflict resolutions. Each merge of
maininto the branch may have resolved conflicts by hand. Git 2.36 and later shows exactly those resolutions, and nothing else:git log --remerge-diff --merges origin/main..origin/integration/<epic> -
Semantic conflicts. Changes on
mainthat merged cleanly but break an assumption the epic relies on. The final pull request's CI is the mechanical check; the review is the other. - The epic as a whole. Whether the children fit together, and whether docs, configuration and migration notes describe the finished result rather than an intermediate step.
- Release impact. The breaking markers among the children, and the
Closeslines in the final pull request's body.
The automatic review, where enabled, sees the whole diff against main. Its
findings on code a child review already accepted can be dismissed with a
pointer to that child pull request.
If the branch was rebased after all, the reviewed commits no longer exist and GitHub's "changes since last review" view cannot be relied on. Compare the old series with the new one instead, using the branch tip from before the rebase:
git range-diff <old-base>..<old-tip> origin/main..origin/integration/<epic>
It pairs each old commit with its rewritten form and shows only the ones whose content changed, which is what needs a fresh look.