Versioning. Automation. Or maybe all together?
Introduction
As practice shows, versioning is often simply ignored. But anyone who has tried to organize their development in one way or another (including pet projects) has come to the conclusion that it is necessary.
This article describes a very simple approach to automating versioning and writing patch notes for projects. Semantic versioning, as well as an agreement on commits with an open specification and documentation, will help to avoid inventing new standards.
Let me point out right away that “GitLab” was chosen as the VCS (Version Control System). Therefore, almost all the interaction described in the article will take place with this tool.
Version structure
1 – Major version
Major version changes only in case of backward incompatible changes
2 – Minor version
A minor version is only changed if new functionality is added WITHOUT backwards incompatible changes.
3 – Patch version
The patch version changes in case of release of any fixes, minor improvements
dev – Name of the prerelease/development state of the project
4 – Build/commit number for pre-release version states
“-dev.4” is used in any kind of prerelease branches (For example: alpha; beta; dev; rc…).
Commit Writing Specification
The specifications for writing commits are a matter of agreement within your team. They can be changed in any way you like. However, there is a so-called convention of commits, I recommend you familiarize yourself with it, at least as a basic example of what it might look like.
Let's move on to a brief description of the commit rules:
fix: something
fix(PROJECT-999): fix something
feat: something
feat(ui): Add something
feat!: something
BREAKING CHANGE: something
“BREAKING CHANGE” is located in the commit message footnote. The exclamation mark in this case warns about the presence of “BREAKING CHANGE” in the commit footnote. This is how it looks in GitLab:
Other types of commits can be used. All but the above do not affect the release version change (only a few are described here):
ci: Changes to our CI config files and scripts.
docs: Documentation changes.
refactor: Code refactoring that does not affect bug fixes or add functionality.
style: Changes that do not affect the meaning of the code (spaces, formatting, missing semicolons, etc.).
test: Adding missing tests or correcting existing ones.
I recommend that you read one of these agreements at the links: in English And in Russian.
“Semantic-release” tool
Should we write our own or take something ready-made?
I recommend paying attention to the semantic-release tool. This is an open source solution written in node.js, which allows you to easily write a CHANGELOG, create tags or releases, make any automated changes to the repository on behalf of a bot with the release of a new version. And all this without human intervention.
The solution also helps to support legacy. For example:
We froze the old version 1.XX, but we are adding the necessary fixes and patches to it in the legacy branch.
We are maintaining version 2.XX in the master branch.
Both versions have dev branches/channels available for development.
Another advantage of this tool is the presence of various plugins in the arsenal. For example: changelog, docker, exec, etc., which open up the possibility of creating a GitLab/GitHub Release along with a tag, automating your own changes when releasing a version, and much more.
How semantic-release works
With each commit, the tool analyzes its contents and existing versions. Based on this analysis, a decision is made whether to release a new version or not. (See the specification for writing commits)
Each new version automatically creates a tag.
And this is what the bot's commits look like with the release of the tag:
How pre-release branch versions change (using “dev” as an example)
When making changes to the dev branch, the version changes depending on the target commit.
That is, if we maintain a dev branch from version 1.0.0, we have 2 “feat” commits and 3 “fix” commits in any order, then the current version in dev will be 1.1.0-dev.5. It does not matter how many new features or fixes were added to dev, the version change reflects that at least one improvement was added. It also shows what kind of critical changes were. For clarity, the picture is below:
How to add semantic-release to your project
I won't rewrite the instructions, but I'll just leave a link to a convenient one documentation from developers and I will show one of the configuration options:
1. Let's build a container for the runner and put semantic-release with the necessary plugins there
As an example of a basic assembly:
FROM node:18.14.2-alpine3.17
COPY certs/. /etc/ssl/certs/.
RUN apk --no-cache add curl; \
apk --no-cache add git; \
apk --no-cache add ca-certificates && \
update-ca-certificates -v
RUN npm config set prefix /usr/local; \
npm install -g @semantic-release/gitlab@v10.0.1; \
npm install -g @semantic-release/exec; \
npm install -g @semantic-release/git; \
npm install -g @semantic-release/changelog; \
npm install -g semantic-release
ENTRYPOINT [""]
CMD [""]
2. Let's create a bot in GitLab and a token for it with api, write_repository rights. This is necessary so that the bot can commit automatic changes to the repository.
3. Pass the token via ci/cd variables. You can add it to the repository group right away.
4. Let's write a basic job
a. If you don't use ci templates:
stages:
- semantic-release
release-job:
image: *your_registry*/semantic_release:latest
stage: semantic-release
only:
refs:
# Release branch
- master
# Pre-release branch
- dev
script:
- npx semantic-release
tags:
- *your_tag*
b. If you use ci templates:
.release-job:
stage: semantic-release
image: *your_registry*/semantic_release:latest
only:
refs:
# Release branch
- master
# Pre-release branch
- dev
script:
- npx semantic-release
tags:
- *your_tag*
artifacts:
reports:
dotenv: release_version.env
.gitlab-ci.yml in the project:
include:
project: path/to/your/templates
file: path/to/your/template.yml
stages:
- semantic-release
semantic-release:pypi:
extends: .release-job
stage: semantic-release
5. Add to the repository .releaserc.json
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/exec",{
"prepareCmd": "if [ -f .release.override ]; then echo \"Detected .release.override\" && VERSION=${nextRelease.version} && source .release.override; else sed -i \"s/__version__ *= *.*/__version__ = \\\"${nextRelease.version}\\\"/\" ./setup.py; fi",
"publishCmd": "echo NEW_VERSION=${nextRelease.version} >> release_version.env"
}],
["@semantic-release/changelog",{
"changelogFile": "CHANGELOG.md"
}],
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "setup.py"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
],
"branches": [
"master",
{"name": "dev", "prerelease": true}
],
"tagFormat": "v${version}"
}
Here, for example, we can make any automatic change in prepareCmd, and in assets specify the files that will participate in the bot commit. At the output, we will have a commit and a tag from the bot, with already changed files.
Automating CHANGELOG
Let's go back to this commit:
fix(PROJECT-999): fix something
In brackets, according to the convention, the code component being changed is indicated. And this is a very convenient approach, but we decided to indicate in brackets the task being solved from the task manager (there can be several of them separated by a space). Thus, we got a richer auto-generated changelog and increased overall readability. Here is an example:
Again, changelog is written automatically if we leave “CHANGELOG.md” in assets, and also install and add the necessary plugin (see semantic-release setting, point 5).
The simplest use case
Developer commit in semver format.
Automatic version calculation with semantic-release.
Automatic modification of setup.py and CHANGELOG.md files.
Assembly and subsequent delivery of the package.
Commit/release a tag (release) with an already modified version.
In conclusion
Semantic versioning fits in well with the gitflow development approach. We can maintain all prerelease branches with versions.
Most of our repositories and components are versioned in this format, and job(s) for builds and publishing/deployment are also tied to them. All this saves us from a large amount of manual work and instills a single standard for writing commits in the team.
In fact, such automation is set up very quickly and does not require labor-intensive operations. Try it 🙂