A list of requirements:
- stakeholders expect a list of provided features, every few days, in a human-friendly report
- every change must have been reviewed, before being deployed
- every change must have passed our automated checks, before being deployed
- every change must have been verified by QA staff, before being deployed
See: https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow See: https://nvie.com/posts/a-successful-git-branching-model/ (original article)
Note that our wording is a bit confusing:
main
is our stable branch (themaster
naming is no longer the default, in the GIT community at large)release
is ourdevelop
branch (our naming is different here)
The idea of git-flow is to keep a main
branch always ready for release, while backwards-incompatible changes accumulate in a develop
branch, to allow for breaking change releases to not interfere with the stability of the library being maintained.
This is very critical for frameworks and libraries that are distributed to consumers with a backwards compatibility promise.
main
moves whenever a new public release is necessary:
- a security patch is needed (x.y.Z, usually)
- a bugfix patch is needed (x.y.Z)
- a new feature is completed and ready for publishing (x.Y.z)
- a new major release is needed (X.y.z)
develop
moves whenever a piece of development work is complete: it is not associated with releases.
The idea is that this eases following SemVer, while still allowing developers
to work on breaking changes internally, accumulating them, without having to hold back major pieces of work,
and without affecting downstream consumers.
gitGraph
commit tag:"v1.0.0"
branch develop
branch feature-1
commit id: "Added button"
commit id: "Made it red"
checkout develop
merge feature-1
branch feature-2
commit id: "Removed legacy login"
checkout develop
merge feature-2
checkout main
merge develop tag:"v2.0.0"
This independence of main
from develop
allows critical fixes to land in it regardless of how much develop
moved. A hotfix can be applied to main
, while work continues on develop
. This allows the library to stay
stable, receive fixes, while still being improved behind the scenes:
gitGraph
commit tag:"v1.0.0"
branch hotfix-1
branch develop
branch feature-1
commit id: "Added another button"
checkout main
checkout hotfix-1
commit id: "Fixed checkout"
checkout main
merge hotfix-1 tag:"v1.0.1"
checkout develop
merge main
checkout feature-1
commit id: "Bigger button"
commit id: "Make button green"
checkout develop
merge feature-1
checkout main
merge develop tag:"v2.0.0"
Note how this requires main
to be merged into develop
after a release, or else the two histories will diverge.
See: https://docs.github.com/en/get-started/quickstart/github-flow See: https://githubflow.github.io/
The GitHub-flow approach is more linear, and requires main
to be deployable at any point in time.
Hotfixes, features and breakages all go back to mainline, and breakages are deployed and communicated to consumers at the time of the changelog landing in mainline:
gitGraph
commit tag:"v1.0.0"
branch hotfix-1
checkout hotfix-1
commit id: "Fixed checkout"
checkout main
merge hotfix-1 tag:"v1.0.1"
branch feature-1
commit id: "Added another button"
commit id: "Bigger button"
commit id: "Make button green"
checkout main
merge feature-1 tag:"v2.0.0"
Only one main
branch that acts as "safe" reference, and everything else happens inside
pull requests ("merge requests" in GitLab).
In GitHub-flow, the project moves with the merged patches, without any "going back" (except for reverts).
Git-Flow was designed for maintaining libraries. Libraries are slow-moving, stable pieces of code, with lots of downstream consumers, and stability takes priority over velocity and continuous improvement. Supporting old-stable and next-gen is vital in the OSS world, which is where git-flow matches requirements.
Quoting Vincent Driessen:
Web apps are typically continuously delivered, not rolled back, and you don't have to support multiple versions of the software running in the wild.
This is not the class of software that I had in mind when I wrote the blog post 10 years ago. If your team is doing continuous delivery of software, I would suggest to adopt a much simpler workflow (like GitHub flow) instead of trying to shoehorn git-flow into your team.
While it is possible to do continuous delivery with git-flow (just merge to main
every time a new change
lands to develop
), it is just added overhead, and it is more error-prone.
GitHub-flow, on the other end, allows for more streamlined development, with fewer ceremonies around the
branches in use: less moving parts means less complexity and confusion.
Since the idea behind GitHub-flow is that main
is always deployable, every change must be validated on
its own feature or hotfix branch first.
In the scope of application development, the only consumer is (usually) customers or API clients talking to our system: we don't share internal code symbols with the outside world, and therefore our backward compatibility promise is limited to external observable behavior in the UI, for example:
- a button disappears
- an endpoint has new mandatory fields
- requirements for a web-form/endpoint become stricter
The foundation on which git-flow is built doesn't seem to apply to it, since there is only ever one "live" version of the application, not a "stable" and "unstable" version: your app either works or it doesn't, and one puts what works live.
On this front, GitHub-flow is a clear winner.
One of our requirements is to create a human-friendly report about which work made it into a release. Practically,
with git-flow, such report can be crafted at the time when develop
is merged into main
.
Within git-flow, the report is generated by looking at git log --merges main..release
.
Within GitHub-flow, such a report is to be generated with something like git log --merges --since="2022-07-18 13:14"
.
Alternatively, a tag can be attached to a specific main
commit, and then we can use git log --merges v1.2.3..main
.
Both approaches provide a way to generate a report, but the difference is that these features already went live with GitHub-flow, while they were held off with git-flow.
Holding off releases ultimately has a negative impact on software projects stability, as it grows the size of a release, therefore its complexity, therefore its failure modes. Smaller, iterative releases (and testing on top of them), is key to reducing large complex defects.
Both in git-flow and GitHub-flow, before landing a patch, changes are verified:
- by automated test (existing and new)
- quality assurance tooling
- peer review(s)
- QA staff performing exploratory testing
Once a patch passes all of the above, it is merged to develop
(git-flow) or main
(GitHub-flow or git-flow hotfix).
There is one major pitfall in the git-flow scenario: the verification becomes stale.
The verification performed by peer reviewers and QA staff gets invalidated by the fact that the patch remains
in develop
for a longer time, before making it into production (where we
"observe" and monitor if it works or not).
By introducing this artificial delay between when a piece of work is ready for production or not, we effectively increase the risk of compound defects in a growing system, since multiple individually verified features may not work well once assembled together.
This is one of the main arguments of proponents of continuous delivery, which is a further away target.
Assuming we get to switch to GitHub-flow, one of the implications is that a patch should go live when we merge it. This means that our deployment procedures will be stressed more, especially with humans involved.
It is endorsed to mitigate this via automation, but for that to happen reliably, the entire deployment should be automated, rather than just parts of it.
DB migrations that may have been executed only at the time of a release, are now to be checked (and performed, if any new were added) at every deployment.
Switching to a previous stable release (rollback) in case of a failed deployment must also be automatic, to avoid affecting production uptime in case of defects. If rollbacks are not viable, then some mitigation procedure (change reverting + re-deploy) is needed.
Because changes land directly in main
in GitHub-Flow, the main advantage is that changes go live quicker: this
allows developers to get faster feedback on whether their changes have negatively impacted production reliability,
and therefore it is easier to mitigate them.
All the people that worked on a specific feature or patch still focus on said feature: this reduced context switching allows for better understanding of regressions, and reduce the number of compound defects that would accumulate in bigger releases.
Creating correlation between a production defect and a coding change also becomes easier, since the time at which
a defect is first detected, and the time at which a patch has landed in production, is much smaller. Instead of
looking for the reason for a defect inside a release with a large git diff
, we get a more precise idea of which
changes broke some behavior.
Because GitHub-flow encourages rolling releases, introducing a process that assigns artificial names to such releases is not necessary: the current git HEAD SHA1 usually suffices, in order to pinpoint a release.
Past releases lose relevance too, as "what is currently live" becomes more important (as it should).
In order to satisfy the requirement for release management from stakeholders, it is possible to:
- generate a report: GitHub and GitLab have such functionality, coupled with pull- and merge-requests
- generate a release report in the issue tracker: could be as easy as a live query that shows which features landed, and in which timeframe
Having this part automated away would also free some humans from doing busywork that certainly pays the bills, but which could be provided to stakeholders via other manners.
Nice one.