Sven Luijten

My branching/tagging strategy for packages

Published on 5 minutes to read

I've never had a very good grasp on how I should branch my open source libraries. There are a lot of resources out there detailing how to branch and tag projects (git-flow, GitHub flow, Trunk based development, ...), but these don't map very well to distributed libraries.

This is the blog post I wish I had when I was searching for a good workflow for branching and tagging open source packages/libraries.

My strategy

This workflow is similar to what I've observed other packages/libraries/frameworks follow. It is not meant to be a definitive guide to how you should do it, but serves mainly as a reminder and guide for myself. Ok, let's dive in.

Initial development

First off, every repository has a main branch. This is always the next major version of the library, and therefore also where all the development happens before a version is tagged.

Tagging the first version

When you're ready to tag your first major version (v1.0.0), first create a branch called 1.x. This is where you'll then tag your first version from.

$ git switch main
$ git pull origin main
$ git switch -c 1.x
$ git push origin 1.x
$ git tag -s v1.0.0 -m 'Version 1.0.0'
$ git push origin v1.0.0

Be sure to also make 1.x the default branch on GitHub. From this point on, main is for v2.0.0 development. I would now also stop directly committing to any of these branches and work via pull requests instead. These pull requests most often target 1.x, and are ported over to main.

For instance, say you have a new feature without breaking changes in the works. The pull request for this feature will target 1.x so it lands in v1.1.0. Once approved and merged into 1.x, you'd merge that change into main:

$ git switch 1.x
$ git pull origin 1.x
$ git switch main
$ git pull origin main
$ git merge 1.x --ff-only
$ git push origin main

You could also facilitate this merge from the GitHub UI by using the "compare" page. Append /compare/main...1.x to your repository's URL, and you'll see a button to create a pull request to merge the changes from 1.x into main.

Tagging new minor and patch versions

Once v1.1.0 is ready to be released, tag it from the 1.x branch:

$ git switch 1.x
$ git pull origin 1.x
$ git tag -s v1.1.0 -m 'Version 1.1.0'
$ git push origin v1.1.0

Two point oh and beyond

When you introduce a breaking change in one of your pull requests, you should target it to the main branch. The moment you're ready to tag v2.0.0, you create a new 2.x branch and tag the next major version from there. This is pretty much the same as tagging the first version.

$ git switch main
$ git pull origin main
$ git switch -c 2.x
$ git tag -s v2.0.0 -m 'Version 2.0.0'
$ git push origin v2.0.0

Remember to also update the default branch to 2.x at this point.

Bug fixes for previous versions

If a bug fix comes in that should be applied to 2.x and 1.x (or other previous versions), it should be targeted to the latest branch where the bug is present. I will assume 2.x in this case. Once the bug fix is merged into 2.x, you can cherry-pick the commit(s) into the other affected version(s):

$ git switch 2.x
$ git pull origin 2.x
$ git switch 1.x
$ git cherry-pick <commit sha>
# Or, if you want to cherry-pick multiple commits:
# git cherry-pick <first commit>^..<last commit>
$ git push origin 1.x
$ git tag -s v1.x.y -m 'Version 1.x.y' # Where 'y' is incremented
$ git push origin v1.x.y
$ git switch main

The alternative

I considered a strategy where the main branch is always the current latest version, but that resulted in more forgettable administrative work. Besides, what I outlined above is actually what Laravel does (as far as I could infer), and it's similar to what Symfony follows. So we're in good company.

Conclusion

I hope now that I've written this down, I can actually follow this in my own projects. This should ease the process of contributing to my packages, and help me in being less confused when I need to tag a new release.