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

Version structure

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:

Commit in GitLab

Commit 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.

Tags in the repository

Tags in the repository

Tags in the repository

Tags in the repository

And this is what the bot's commits look like with the release of the tag:

Full commit of the bot

Full commit of the bot

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.

Token in ci/cd variables

Token in ci/cd variables

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:

CHANGELOG

CHANGELOG

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

  1. Developer commit in semver format.

  2. Automatic version calculation with semantic-release.

  3. Automatic modification of setup.py and CHANGELOG.md files.

  4. Assembly and subsequent delivery of the package.

  5. 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 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *