Skip to content

Instantly share code, notes, and snippets.

@Ocramius
Created October 7, 2022 08:38
Show Gist options
  • Save Ocramius/bc5c76313ddfea340e3dcc31c991a682 to your computer and use it in GitHub Desktop.
Save Ocramius/bc5c76313ddfea340e3dcc31c991a682 to your computer and use it in GitHub Desktop.
Git-flow vs GitHub-flow

Git-flow vs GitHub-flow

What we want

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

Recap on git-flow and GitHub-flow

git-flow structure

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 (the master naming is no longer the default, in the GIT community at large)
  • release is our develop 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"
Loading

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"
Loading

Note how this requires main to be merged into develop after a release, or else the two histories will diverge.

GitHub-flow structure

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"
Loading

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).

Complexity and scope of git-flow

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.

Git-Flow and application vs library development

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.

Release report

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.

Reviews and QA

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.

Implications of switching to GitHub-flow

Deployment implications of switching to GitHub-flow

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.

Development implications of switching to GitHub-flow

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.

Release naming implication of switching to GitHub-flow

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:

  1. generate a report: GitHub and GitLab have such functionality, coupled with pull- and merge-requests
  2. 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.

@alamenai
Copy link

Nice one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment