TL;DR: Using GitFlow, which commit do you tag to identify the build of your next release? If you tag the merge commit on main, you'll need to rebuild and deploy something you didn't test on QA (and this contradicts "build-once, deploy anywhere"). If you tag the last commit on the release branch, your commits on the main branch won't represent a release (and this contradicts GitFlow).

The GitFlow model

This question is specific to the GitFlow workflow. In this workflow, you create release branches that are merged into the main branch. Supposing you have a QA (or "staging") environment, you would build the release branch and deploy it on that environment to do QA tests.

Once the release is ready, the process would then be this:

When the state of the release branch is ready to become a real release, some actions need to be carried out. First, the release branch is merged into master (since every commit on master is a new release by definition, remember). Next, that commit on master must be tagged for easy future reference to this historical version

So you're tagging the merge commit on the main branch (e.g. v1.0.1). In theory, you'll use that same commit to build your release (e.g. if you use docker, you would build an image that represents the state of the code on tag v1.0.1).

However, this means that you're building a different version of your code, that you did not test on the QA environment: the commit/tag v1.0.1 represents the merge commit on main, not the last commit on the release branch, so it was never deployed to the QA environment. It may sound like a small difference, but you're actually building your image at a different point in time, and this can introduce unexpected differences.

If you tag the last commit of the release branch, instead, you can deploy that exact build directly to production, knowing that you tested the exact same thing on QA. But then your commits on main won't represent a new release anymore (contradicting GitFlow).

Am I missing something here? How do you proceed to use GitFlow and still follow the "build-once, deploy-anywhere" idea?


Note: there's a similar question on SE but it doesn't mention the part about building images for deployment, so I think my question is more specific and not a duplicate.

  • 2
    I'd like to highlight the notice at the top of the article which tries to point out that git-flow is often not a good fit. Git-flow is not intended for the Continuous Delivery (CD) mode of release that you seem to be after here; quite the opposite - it is designed for longer release cycles where multiple concurrent release versions need to be supported. With git-flow an artefact is fully tested after a release cut has been taken. Issues discovered in QA are "fixed forward" using hotfixes, applied to the release branch (subsequent hotfix releases are cut from the release branch). Commented Dec 17, 2023 at 14:13
  • 2
    I would take a step back and start by capturing what your needs are from a branching strategy - ideally starting out with something as minimal and light-weight as possible. The added complexity of git-flow brings a raft of disadvantages unless you really need it - georgestocker.com/2020/03/04/please-stop-recommending-git-flow Commented Dec 17, 2023 at 14:18
  • Note the update at the top of the nvie article: "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". You can usually drop the right half of that diagram. There are no release branches. Hotfixes are just prioritized features. The develop branch is always deployable. Tags can be useful for keeping track of what was actually deployed/released. Instead of explicit version numbers, you might just use Git commit IDs.
    – amon
    Commented Dec 17, 2023 at 21:28
  • Thank you @BenCottrell. I work in a regulated environment and we must follow compliance rules, so each release has to be well-defined. That is why GitFlow seems like a better candidate compared to, say, Github Flow. I'm not sure why you mention CD though. GitFlow states that each commit on master is a release. What I want to do is just reuse (in prod) the same build that has already been tested on the QA environment. Why does it involve CD? Could you elaborate? Commented Dec 18, 2023 at 14:02
  • 1
    @KurtBourbaki I mentioned it as Continuous Delivery was a relatively uncommon practice back in 2010 when the article about git-flow first appeared. A prerequisite for CD is that changes are fully tested before merging, so every merge into main/master should be production ready (Which I believe is what you're after here). The people who developed Git-flow did not practice CD at all -- their concept of a 'release' merely refers to the existence of a version tag from which an artefact could be published into QA ready for testing; it's not an expectation of production readiness. Commented Dec 19, 2023 at 15:02

2 Answers 2


I don't agree that tagging a merge commit means that you're building a different version of the code that was not tested.

If you have commit x on your release branch and then merge that into your main branch, you will have commit x and merge commit y on the main branch. The contents of your repository on commit x and merge commit y will be identical. If you build by commit hash, unless your build products contain timestamps or commit hashes, the result should be identical if you build commit x or merge commit y. If you are very concerned, you could always tag the commit before the merge commit, which should be the last commit in the release branch, but I would consider this to be an unnecessary overhead.

In practice, teams that I've worked with that are still following a model like gitflow have moved away from some of the specifics of gitflow, primarily in two main ways.

First, most teams have dropped the concept of a main or what gitflow calls the master branch. They have also dropped the notion of a hotfix branch. They only use develop, release branches per release, and feature branches. This greatly reduces the overhead of merging changes between branches.

Second, many of the teams have switched to containerized applications. The build process creates containers from the develop branch, release branches, and feature branches, and identifies them appropriately. The commit (and, if it exists, tag) from git is used in the container identification. Whenever a container passes all necessary (automated and manual) tests, it can be identified. The same container that was deployed to the test environment and passed can also be deployed to the production environment.

  • Thanks for the useful answer. You said: "The contents of your repository on commit x and merge commit y will be identical". This is true, but a build is not just the code. If you build the same code in two different moments, you can have different results (for example, the most recent build might break because some external dependencies introduced breaking changes between commit x and y). That's why I'd rather build and deploy the last commit on the release branch, rather than the merge commit from main. I would say that this applies to containerized applications as well. Commented Dec 18, 2023 at 14:45
  • A question regarding dropping the main branch: how would you then use the release branches? I guess you branch off develop to create a new feature branch and then you merge the feature branch back into develop (as you would do with the main branch in Github flow). When do the release branches come into play, and for what purpose? Commented Dec 18, 2023 at 14:50
  • @KurtBourbaki External dependencies are not a concern of the branching model. When teams need to ensure tighter control over external dependencies, they should mirror those external dependencies into an artifact repository that they control. This can be beneficial to protect against missing dependencies or supply chain attacks, as well. By pulling build dependencies from a more trusted source, you can be sure that your builds will be identical as time passes.
    – Thomas Owens
    Commented Dec 18, 2023 at 14:52
  • @KurtBourbaki Without main, the release branches are used exactly as you describe. They come into play when teams need to support multiple releases at once. Perhaps v1.2, v1.3, and v2.0 are all supported and may need patches. You would be able to appropriately patch all the versions for security fixes or critical defects until the version is no longer supported. Depending on the changes, you can branch from the release branch or cherry-pick into the release branch to product 1.2.x, 1.3.x, and 2.0.x versions as needed.
    – Thomas Owens
    Commented Dec 18, 2023 at 14:54
  • your suggestion regarding external dependencies is cool, however, I've never seen it applied before (doesn't mean it's not good!). Usually, you would have a requirements.txt file (or whatever you use) that will be used to manage dependencies of a docker image at build time. If you build twice, dependencies will be resolved twice (in two different moments). Your suggestion is interesting though: do you know some open source project that uses this approach? Thanks again! Commented Dec 18, 2023 at 15:06

You are correct. Hotfix branches screw GitFlow because the merge to master may not be the same as the merge to develop. Thus when you come to merge in your release branch, the post merge code may be different.

Here is an example.

master v1 has a bug in the email sender. I create a hotfix branch to fix it

Emailer.Send(Message m)
   smtpclient.Send(m); //fixed! this was commented out!!

This merges into master fine the only change was removing the commented out line. But in develop we have already refactored the email sender so when I come to merge I have to resolve the conflict manually

Emailer.Send(Message m)
   _client.Send(m, timeout); //this was commented out and renamed

I carry on with develop, branch to release v2 and finally come to merge release v2.2 into master.

I get a merge conflict, because the emailer class has been changed separately in both branches and I have to manually resolve it. When doing so I make a mistake

Emailer.Send(Message m)
   _client.send(m, timeout);

Now the build from release v2.2 which was tested and works fine and the build from master tag v2.2 which will fail are from different codebases.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.