As developers we tend to automate everything, so why not automate the generation of Git tags? Having to create Git tags everytime we merge into main is not productive and we’re really likely to make mistakes or just forget. This is why I decided to write this article and explain how automating versioning and releases using Semantic Release.
Overview on Semantic Versioning
Semantic versioning, also called (SemVer), is a versioning scheme that provides a standardized way to communicate changes in a software package or application. It follows the format of MAJOR.MINOR.PATCH, where:
- MAJOR version is incremented when there are incompatible API changes.
- MINOR version is incremented when new features are added in a backward-compatible manner.
- PATCH version is incremented when there are backward-compatible bug fixes.
The advantages of using semantic versioning are:
- You can keep track of every transition in the software development phase.
- It helps to keep things clean and meaningful.
- Semantic versioning ensures that breaking changes are explicitly communicated, reducing the risk of introducing bugs due to incompatibilities.
Semantic Release
In this article we’ll use Semantic Release, which is a tool that automates version management and package publishing. Semantic Release automates the whole package release workflow including: determining the next version number, generating the release notes, and publishing the package.
How Does Semantic Release Work?
Semantic Release uses the commit messages to determine the consumer impact of changes in the codebase. Following formalized conventions for commit messages, Semantic Release automatically determines the next semantic version number, generates a changelog and publishes the release. By default, Semantic Release uses Angular Commit Message Conventions, which are fix, feat and many others. Here’s an example of commit messages and releases:
Commit message | Release type |
feat: add endpoint to update user | (MINOR) Feature release |
fix: restore translations | (PATCH) Fix release |
feat: rename parameterId to paramId BREAKING CHANGE: parameterId has been changed to paramId. | (MAJOR) Breaking release |
Even if the above commits prefix are the default ones, Semantic Release allows you to change them or add more.
Setting Up Repository
First of all, we need to create a Git project. In this article we’ll cover both configurations of GitHub and GitLab. It doesn’t matter what kind of technology you’re using in the project, it just needs to be a Git project, the configuration is always the same. Now, create a repository and commit an empty README.md, making sure the branch’s name is main.
Setting Up Semantic Release
At this point let’s configure Semantic Release. First we need to create a file named .releaserc.yml in our project’s root directory.
branches: - main plugins: - "@semantic-release/commit-analyzer" - "@semantic-release/release-notes-generator" # remove the comment from one of them according to your needs # - "@semantic-release/github" # - "@semantic-release/gitlab"
In the above file we tell Semantic Release to analyze the commits from a certain branch and generate a release . You can change branch name or even add more if you want. I commented the last plugins just to show that both platforms are available. So to recap, we define one or more targets branch then some plugins to use. Semantic Release allows us to use different plugins as we’ll see later in the article.
Setting Up Semantic Release With GitHub Actions
Since now we’ll use GitHub, let’s first remove the comment @semantic-release/gitlab from our .releaserc.yml. Now we need to create a workflow that will trigger a job when we commit on a specific branch. Create a folder called .github in the project’s root directory. Inside this directory, create a folder called workflows and then a file named release.yml.
name: Release on: push: branches: - main permissions: contents: read jobs: release: name: Release runs-on: ubuntu-latest permissions: contents: write issues: write pull-requests: write id-token: write steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: "20.16.0" - name: Install dependencies run: npm install -g semantic-release@24.0.0 @semantic-release/github@10.1.1 - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release
In the above file we created a job named Release that runs on Ubuntu and that takes place when we push into main. We created different steps for this job, first it will checkout the repo, then it will install Node.js, then it will install semantic-release and semantic-release/github dependencies and lastly it will run semantic-release.
Setting Up Semantic Release With GitLab CI/CD
As we did for GitHub, we need to remove the comment from the corresponding entry in our .releaserc.yml, so remove the comment from @semantic-release/gitlab. After that, create a file named .gitlab-ci.yml in our project’s root directory.
stages: - release release: image: node:20 stage: release only: refs: - main script: - npm install -g semantic-release@24.0.0 - npm install -g @semantic-release/gitlab@10.1.1 - npx semantic-release
In the above file we define a stage and job named release. For this job, we use Node.js image and refer to stage release. This job will run only when we push into main. We defined some scripts to run, we first install semantic-release then semantic-release/gitlab and last we run semantic-release.
Authentication
Semantic Release requires push access to the project Git repository in order to create Git tags. The Git authentication can be set using environment variables.
If you’re using GitHub create an access token with scope repo and copy it, then go to the repository and create an enviroment variable called GITHUB_TOKEN, paste the content and save.
With GitLab the procedure is the same, only that this time the access token’s scope is api and the enviroment variable name is GITLAB_TOKEN.
Genereting a Release
Now it’s time to generate a release. All we have to do is make a change in our codebase and then write the commit using conventional commits and finally push it. Write anything you want in the README.md then add the following commit message: “feat: initialize project“, then push it.
After that, if you’re using GitHub you should see something like this:
Generating a ChangeLog
It’s often useful to generate a changelog file along the version to keep track of the changes. Semantic Release allows us to do it by using one of its plugins. Add the following entries under the plugins property in the .releaserc.yml:
- "@semantic-release/release-notes-generator" - - "@semantic-release/changelog" - changelogFile: CHANGELOG.md - - "@semantic-release/git" - assets: - CHANGELOG.md
In the above file we define a new plugin called semantic-release/changelog and the name of the changelog file. This plugin will be responsible of generating the file with the changes. Then we define another plugin, called semantic-release/git and an asset, CHANGELOG.md. This plugin will pick up the changelog file generated by the other plugin and push it to the repository.
At this point we need to change our GitHub workflow. Open release.yml and append these entries in the run property of the step “Install dependencies”.
@semantic-release/changelog@6.0.3 @semantic-release/git@10.0.1
Otherwise if you’re using GitLab add these entries in your .gitlab-ci.yml under the property “script”:
- npm install -g @semantic-release/changelog@6.0.3 - npm install -g @semantic-release/git@10.0.1
Now let’s add a new commit and see if Semantic Release generates a changelog file. Add this commit message “feat: add changelog” then push. Navigate back to the repository, and wait for the workflow to end. When it ends you should see a new version and a file named CHANGELOG.md in your project’s root directory.
Generating a File Containing the Version Number
Sometimes our app needs to show an About section with the version of the app, so we need to save the version number somewhere when we generate the release. In this case we’ll use another plugin to resolve this issue. In the .releaserc.yml add the following entries:
- "@semantic-release/release-notes-generator" - - "@semantic-release/exec" - prepareCmd: echo ${nextRelease.version} > VERSION.txt - - "@semantic-release/git" - assets: - VERSION.txt
As before we define semantic-release/release-notes-generator, then semantic-release/exec. The latter is used to execute custom shell commands, in fact we define a command which prints the next release version into a file called VERSION.txt. After that, again, we define semantic-release/git plugin to be able to commit and push the assets, which in this case is only VERSION.txt.
Now we need to edit our GitHub workflow. Open release.yml and append these entries in the run property of the step “Install dependencies”.
@semantic-release/exec@5.0.0 @semantic-release/git@10.0.1
If you’re using GitLab add these entries in your .gitlab-ci.yml under the property “script”:
- npm install -g @semantic-release/exec@6.0.3 - npm install -g @semantic-release/git@10.0.1
Once again, let’s try if it works. Add the following commit message: “feat: add new conf to generate version file“, then push. Go back to the repo and wait for a while, you should see the file and a new version number.
Conclusion
In this article we’ve seen how Semantic Release can save us time once it’s set up. Using such a tool is a great way to speed up development, keep organized releases and ensure consistency. In the source project you’ll find both GitHub and GitLab examples. The source code is available here.