Gitlab Flow with SemVer - How To?

Background

I’m investigating using gitlab flow as a new branching strategy with semver for automated versioning in a project, and have been reading this for inspiration What are GitLab Flow best practices? | GitLab.

Feature branches would merge to master, and once ready for release would merge master into release branch. To trigger a automated release pipeline (build, package, test etc.) we’d create a semver tag.

Problem

Now this feels like a great workflow, but I can’t figure out how to handle versions on master if the release version is purely based on tags that exists on release branch. We could easily add some metadata that the dev builds have, like major.minor.patch-dev e.g. But I still don’t understand a good way of setting the major.minor.patch versions for master without having a checked in version file, which feels like the thing you want to avoid when doing tag based releases.

Developers will often use master builds during development, and need some way of distinguishing between builds. Maybe I need to rethink how versioning on master should work, so interested in hearing some opinions on my problem!

Hi there!

Welcome to the club! :slight_smile:

I would be interested in hearing other stories as well.

I believe the only reason why this is not standardized yet, is because every project/company has different needs about development/release cycle… so we do come to a question what makes release - a release? (in terms of “project snapshot” with a version). Is it an added tag? Or is it a merge to a specific branch… and also (as you asked) - what is a pre-release? Dev/master/beta versions…? And how we add version numbers into those builds.

What I’ve done so far are four things (just to mention: my release is a merge to a branch, so git tag is automatically created by the pipeline):

  1. Simple version.txt file. Yes, needs to be manually adjusted, but allows control over the version (some devs prefer that). In the pipeline builds (e.g. on develop/main branches) we add a CI_PIPELINE_IID to distinguish between different builds during a Sprint, but before official next release. You could also add a “-dev” or “-beta” to your version, depends again on the need.
  2. As we have a few .NET projects, devs like to specify this version manually in the properties file. Again, the same logic as No1, just different source.
  3. Use an automatic tool to detect version (e.g. Semantic Release). This one you can also configure to act differently on your master branch. However, this has its quirks as well… and developers MUST write their commits in the corresponding format, as that is how the tool recognizes which version it should be.
  4. Don’t use SemVer, but CalVer. TBH, the more I use it, the more I like it. If you don’t need to deal with dependencies (e.g. your project is not a library and this “breaking changes” definition of SemVer does not really matter), I would recommend this. Super easy to setup and calculate (I can share my own one-liners if needed), and devs don’t need to do anything. However, yes, it’s a bit unpredictable, but hey - a lot of other problems easily disappear with this approach.

Now, if you “read” your versions from git tags and this is how you create a release… I would say you can do the same way on your master branch. The only “problem” is that it’s not automated… so every time you want a master build with a specific version, you would need to tag it. Perhaps an idea would be to make a mini script that runs on your master branch that will read your last tag (on release/master) and increment based on that, and create a new tag on master automatically. This would fire up your tag pipeline that would make a build with this version baked-in.

I don’t say any of this is the best, just some thoughts. I would really like to hear as well what other DevOps guys do and how do they pass in their software versions into the build.

Best regards!

Hi, thanks for a good response! Yes lots of different ways to solve this. In the end we might keep it as is, but I’m itching to automate things since sometimes I feel there are too many manual steps, making releases feel cumbersome and error prone!

In the end I think I’d envision master to produce 1.0.0-dev/unstable/nighty or something similar. Whereas release branch would be for 1.0.0-rc0 and finally 1.0.0. I guess I could tag master as 1.0.0-unstable, but that just makes the following commits implicitly 1.0.0-unstable, meaning if I checked out 1.0.0-unstable I would not get the latest commit. Maybe a combination of checked in version file and tags are a good middle ground. Builds on main could always be tagged as unstable. Tags on release could trigger release builds and provide prerelease versions like rc0.

Since the gitlab flow guide says to base versions on tags I thought there would be some good solution to my problem, but maybe tag based versions are better fitted for releasing from main directly?

Hmm… this also depends on something else - and that is, do you need to support / provide bugfixes for your older releases?

E.g. take example from GitLab itself. They release 16.10, but they still provide 16.9 and 16.8 bugfixes. This means they cannot just tag master/main with new tags, as you cannot separate main features introduced in newer versions from old code, and provide bugfixes for old stuff. I mean you can, but IMO it would look really ugly in git. UNLESS, you’re using feature flags. But that aside, if you have this use case, you’re better off with longer-lived release branches with tags (e.g. branch release/16.8 onto which you can backport bugfixes later) - IMO it’s a bit cleaner, but I personally never used this flow, so I don’t know about downsides/risks.

However, if you don’t have this scenario, and you just roll out new versions one after each other, then I think just having main/master tagged with it’s version might be good enough. Think about how you will automate / separate your develop versions, release candidates and actual releases and if they all can live on the same branch. I use develop branch during Sprint that produces beta/dev versions and once merged to main, it’s a release.

Maybe it’s a good idea to create a test project and try it out :slight_smile: It helped me a lot in the beginning to test out all the scenarios in the workflow.

Yes that’s worth considering. We support patch releases but try to avoid it/don’t do it often. I envisioned that when we’d really need a patch release we’d branch e.g. tag 1.0.0, and tag that with 1.0.1. But I suppose that could lead to messy branch structure anyways, it would really depend on how many patch releases would be done. You could however always see tags as the true source in the end, and patch branches as a necessary evil.

Maybe in the end, it’s the process of always backporting every new merge to the specific release branch which I find cumbersome. Cherry picking specific commits into release leads to main not being truly representative of “release branch with some unstable features on top”, which may or may not be a problem…

Maybe I’ll let this sit with me for a bit, there are small steps towards more release automation we can do before changing up the branching strategy. :smiley:

1 Like