DEV Community

Cover image for Versioned Branch Names Let GitLab CI Enforce My Release Process Instead of Me
Evgeny Khramov
Evgeny Khramov

Posted on Originally published at hram.github.io AI-assisted

Versioned Branch Names Let GitLab CI Enforce My Release Process Instead of Me

We agreed on the release process three times. I drew diagrams, explained where to merge changes and who decides when a release moves forward. In practice I still had to explain the rules again and again: some people did not understand which branch to pick, others mixed up the order of steps, and others frankly did not want to follow them.

In a presentation I prepared for colleagues later there is a phrase: "I felt like an overseer." That describes that period well.

So I decided: if a rule can be checked by a program, nobody has to stand next to every merge request. This is what that looked like in GitLab CI. The branching model itself, which I have used for more than ten years, is described in part 1: versioned stage/version branches, and release scope decided by planning, not rebuilt by developers with cherry-pick and revert.

The starting point: buttons you have to babysit

The project I joined had plain develop and master. A deploy could be started manually for different environments: from develop you chose dev or stage, from master the same plus prod.

For every release you had to remember which button to press and for which version. Worse, the deploy buttons became active only after version, build and pre-deploy had passed. The code is already merged, and you still have to open the pipeline, wait for the green stages and not forget to press. In effect, sit and refresh the page.

I replaced that with versioned branches:

dev/1.2.1
stage/1.2.1
prod/1.2.1
Enter fullscreen mode Exit fullscreen mode

The stage and the version are now in the name, so the pipeline runs only the actions that belong to that stage.

Before: shared develop and master branches with manual deploys. After: dev/1.2.1, stage/1.2.1 and prod/1.2.1, where dev and stage deploy automatically and prod is started manually

GitLab has long been able to run jobs by branch name. What changed is that the branch now describes not just where the code lives, but where a specific release is in the process.

The check that replaced my review comments

I used to leave comments in reviews, and the MR stayed blocked until they were fixed. Then I wrote jira:check, a Python script that runs in GitLab CI for every merge request. It checks:

  • the Jira task key can be extracted from the name of a feature/bugfix branch;
  • the target branch has a valid stage prefix;
  • the task is in the required status and has the agreed fields filled in: tech stack, component and Fix Version;
  • the version in Jira matches the version number of the target branch;
  • the MR title follows the naming convention.

Errors are collected into a report, and the job fails if anything is wrong. With "pipelines must succeed" enabled, such an MR cannot be merged until the data is fixed. An MR can be created whenever it is convenient; it just cannot be merged.

This is a real log of a failed check (translated from the original Russian output, task key removed):

Checking target git branch: develop/1.66.0
OK
Checking field "Status"
Error: field "Status" must be "CodeReview", current value: "In development"
Checking version match: 1.65.0 -> 1.66.0
Error: the task is marked for version "1.65.0" but the MR targets a branch of another version: "1.66.0"
Enter fullscreen mode Exit fullscreen mode

A contract between delivery and development: the Jira task has version 1.65.0, the MR targets a stage/version branch, jira:check compares the versions, and on a mismatch the MR can be created but not merged

This only works because the team had agreed on the rules before any script existed: product components, tasks assigned to versions in advance, branch names and the order of MR handling. If the Fix Version is not set, there is nothing to compare. Planning stops being an optional note in a chat and becomes input data for development.

Protected branches

The release branches themselves are protected with GitLab Protected Branches, set by masks (develop/*, alpha/*, beta/*, prod/*), so a new release such as alpha/1.66.0 immediately falls under the right restrictions.

In the project I describe, merging into develop/*, alpha/* and beta/* through a merge request is open to developers and Maintainers; direct push is for Maintainers only. For prod/* both merge and push are left to Maintainers. Force push is disabled everywhere.

I have seen processes where a release was triggered by putting a tag on an arbitrary commit. In the projects I saw, access to such tags was not restricted properly. That is not a flaw of tags as a tool, since GitLab can protect tags too. But in my process the release event is a merge into the protected branch of the right stage, and who may merge is determined by GitLab's role model.

The branch name selects the build and the environment

Since a branch is named <stage>/<version>, GitLab can determine from it what to build and where to send the result. In the Android project the branch selects the build variant (flavor), the API the app works with, and the environment of the internal release service the APK is uploaded to. Nothing is chosen by hand.

Four lanes for Android: develop gives the Dev build, API dev and the DEV environment; alpha gives Alpha, API test and STAGE; beta gives Beta, API prod and PROD; prod gives Prod, API prod and PROD

The number of stages is a property of the product, not a universal law: the backend project uses develop → test → stage → prod, and another product built for both web and Android uses develop → stage → prod. In every case the stage and the version in the branch name give CI/CD a clear event.

One merge triggers the routine

What usually happens after a merge where the process lives in documentation? Someone builds the right configuration, publishes the build, finds the list of changes, tells the tester, gives them a link, moves the task to QA and assigns it.

I bound these actions to a single event, a successful merge of an MR into a stage branch. After it CI:

  • builds the right variant of the application or service;
  • publishes the artifact to the corresponding environment;
  • forms the list of tasks for the release notes;
  • sends a notification with a link to the build and the list of changes;
  • starts the Jira update: moves the task to "Ready for testing" and assigns QA.

After an MR is merged into alpha/1.64.0 the pipeline runs build, deploy, notify and jira-update in order; notify and jira-update run with allow_failure; the tester gets a link, release notes and an assigned task

The release notes script takes the changes brought in by the merge commit, extracts Jira keys from the commit messages and builds a list of tasks. It is not a universal analyzer of Git history: the result depends on the commit format and on changes reaching the branch through a merge commit. That is why the naming convention matters: the automatic result depends on it.

Notifications used to arrive in Telegram, now it is Mattermost. The tester does not need to ask where to find the build.

This chain is not magic either. In the current configuration jira:update and the notification run with allow_failure: if the Jira operation or sending the message fails, the pipeline may still finish successfully. That has to be watched in the logs and handled separately. I don't want to pass a set of scripts off as a system in which every state is guaranteed to be consistent across all services.

The version has to reach the user

Whenever someone reports "the screen doesn't work" or "the server responds wrongly after the update", my first question is: which version?

In the Android project versionName is built from the release number taken from the branch name: for alpha/1.64.0 it is 1.64.0, and the stage is shown next to it. In the backend project the version is written to a resource at build time and returned in the Version response header. A report then contains a release number and a stage instead of "the latest build".

What I did not hand over to automation

People decide on moving between stages. In my process, releasing from develop to the first distribution stage is my responsibility as development lead. After internal testing, QA decides on the next step. Then delivery joins the decision about production, taking into account the beta test results and metrics. A promotion is formalized as a task and an MR; GitLab performs the permitted operation and its technical consequences.

Some checks CI still does not cover:

  • it compares the version of the MR, but does not work out whether the release is currently at the right stage;
  • it does not check the source branch of the feature; the final scope of a change remains a matter for code review;
  • new secrets and variables of the next environment still have to be prepared by a person: at a transition I check CI variables and Vault. Without that, even a successful deployment can end with a system that does not work.

And no script replaces the manager who has to decide the contents of a release in advance.

What I would reuse

The scripts did not teach everyone to understand the process in the same way, and they certainly did not make managers plan better. What they changed is that formal rules stopped depending on my constant personal checking:

  • Put the stage and the version in the branch name. Then CI has an unambiguous event instead of a button.
  • Compare the planned version in the tracker with the target branch in the MR check. It turns planning into input data for development.
  • Bind routine post-merge actions to the merge event, and mark which of them are allowed to fail.
  • Protect release branches by mask, so new releases are covered automatically.
  • Keep the decisions on stage transitions with people, and say so explicitly.

The full case study and supporting artifacts are available on my site: https://hram.github.io/en/articles/hram-git-flow-ci/

Top comments (0)