Table of Contents
Introduction
In this post we will see how to automatically version and release a .Net Application using GitHub Actions. We will use the conventional commits specification to automatically version and release our application.
Conventional Commits
Conventional Commits is a specification for adding human and machine readable meaning to commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with SemVer, by describing the features, fixes, and breaking changes made in commit messages. If you want to know more about conventional commits, please check this.
Libraries used
- Versionize - To version the application
- Husky.Net - To run git hooks in dotnet
- Github Actions - To automatically release the application
Please note that we will use the Versionize
library to version the application and Husky.Net
to run git hooks in dotnet. These are amazing libraries and I highly recommend them. please check them out if you want to know more about them and give them a star. It will help the authors to keep working on them. I'm not affiliated with them in any way. I just like them and I'm using them in this post.
Versionize
Versionize is a library that can be used to version a .Net application. It uses the conventional commits specification to automatically version the application. It can also be used to automatically release the application. It can be used in any .Net application. It can be used in a console application, a web application, a web api, a library, etc. The current supported project types are: csproj
, fsproj
, vbproj
. There is PR to support .props
projects. hopefully, it will be merged soon.
Husky.Net
Husky.Net is a library that can be used to run git hooks in dotnet. It is a port of the famous Husky library. Husky is a library that can be used to run git hooks in nodejs. Husky.Net can be used to run git hooks in dotnet. It can be used in any .Net application.
Husky is not required for this but it is a nice to have. It will help us to keep the commit messages clean and consistent. If you don't want to use it you can skip this section. But I highly recommend it.
GitHub Actions
I think there is no need to explain what GitHub Actions is. If you don't know what GitHub Actions is, please check this.
Create the .Net project
Create a github repository
Create a github repository and clone it locally. You can use the following command to clone the repository.
git clone "url of the repository"
Now, we will create a .Net project and add it to the repository.
Create the .Net project using this following command
Navigate to the your repository folder and run the following command to create a .Net project.
dotnet new console -n VersionMe -o .
Now we have a .Net console application. We will use this application to test the versioning and releasing.
Add Version to the .Net project
To get started add version to you csproj file.
<Version>1.0.0</Version>
Now we can install the versionize tool.
dotnet tool install --global Versionize
When you want to version the application, run the following command.
versionize
This will version the application, create a tag for the release and generate a changelog. You can find the changelog in the CHANGELOG.md
file. make sure you push the tag to the remote repository.
git push --tags
For now we have versioned the application manually and tagged the release. we also have a changelog. but to create a release we need to do it manually. we need to use the GitHub UI. But we want to automate this process. We want to automatically create a release when we push a commit to the main branch. We will use GitHub Actions to do this.
Now lets automate this process using github actions.
Creating github action
We will create a github action to version and release the application. Create a new file in the .github/workflows
folder and name it version-and-release.yml
. In the version-and-release.yml
file, add the following code.
name: Version and Release
on:
push:
branches: ['main']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Install Versionize
run: dotnet tool install --global Versionize
- name: Setup git
run: |
git config --local user.email "antosubash@live.com"
git config --local user.name "Anto Subash"
- name: Versionize Release
id: versionize
run: versionize --changelog-all --exit-insignificant-commits
continue-on-error: true
- name: No release required
if: steps.versionize.outcome != 'success'
run: echo "Skipping Release. No release required."
- name: Push changes to GitHub
if: steps.versionize.outcome == 'success'
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
tags: true
- name: 'Create release'
if: steps.versionize.outcome == 'success'
uses: 'actions/github-script@v5'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
script: |
try {
const tags_url = context.payload.repository.tags_url + "?per_page=1"
const result = await github.request(tags_url)
const current_tag = result.data[0].name
await github.rest.repos.createRelease({
draft: false,
generate_release_notes: true,
name: current_tag,
owner: context.repo.owner,
prerelease: false,
repo: context.repo.repo,
tag_name: current_tag,
});
} catch (error) {
core.setFailed(error.message);
}
This will run when we push a commit to the main branch. It will version the application, create a tag for the release and generate a changelog. It will also create a release using the GitHub API. You can find the changelog in the CHANGELOG.md
file.
If you are new to GitHub Actions, please check this to learn more about GitHub Actions. It will help you to understand the above code.
Here is a brief explanation of the above code.
name: Version and Release
This is the name of the action.
on:
push:
branches: ['main']
This will run the action when we push a commit to the main branch.
jobs:
build:
runs-on: ubuntu-latest
This is the job that will run. We are using ubuntu-latest as the operating system.
- uses: actions/checkout@v3
with:
fetch-depth: 0
This will checkout the repository.
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
This will setup the .Net environment.
- name: Restore dependencies
run: dotnet restore
This will restore the dependencies.
- name: Build
run: dotnet build --no-restore
This will build the application.
- name: Install Versionize
run: dotnet tool install --global Versionize
This will install the versionize tool.
- name: Setup git
run: |
git config --local user.email "antosubash@live.com"
git config --local user.name "Anto Subash"
This will setup the git user name and email.
- name: Versionize Release
id: versionize
run: versionize --changelog-all --exit-insignificant-commits
continue-on-error: true
This will version the application, create a tag for the release and generate a changelog. You can find the changelog in the CHANGELOG.md
file.
- name: No release required
if: steps.versionize.outcome != 'success'
run: echo "Skipping Release. No release required."
This will check if the versionize command was successful. If it was successful, it will continue to the next step. If it was not successful, it will skip the release.
- name: Push changes to GitHub
if: steps.versionize.outcome == 'success'
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
tags: true
This will push the changes to the GitHub repository.
- name: 'Create release'
if: steps.versionize.outcome == 'success'
uses: 'actions/github-script@v5'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
script: |
try {
const tags_url = context.payload.repository.tags_url + "?per_page=1"
const result = await github.request(tags_url)
const current_tag = result.data[0].name
await github.rest.repos.createRelease({
draft: false,
generate_release_notes: true,
name: current_tag,
owner: context.repo.owner,
prerelease: false,
repo: context.repo.repo,
tag_name: current_tag,
});
} catch (error) {
core.setFailed(error.message);
}
This will create a release using the GitHub API.
Adding Husky.Net
We will use Husky.Net to lint the commit message. this will make the commit message follow the conventional commits standard.
As I said earlier, This is not required. But it is a nice to have. If you don't want to use it you can skip this section. But I highly recommend it.
Follow the steps here to add Husky.Net to your project. https://alirezanet.github.io/Husky.Net/guide/getting-started.html#installation
if you are new to git hooks, please check this https://githooks.com. It will help you to understand what git hooks are.
Add a pre-commit hook
We will add a pre-commit hook to lint the commit message. Husky.Net will run the pre-commit hook before the commit is made.
dotnet husky add pre-commit
This will add a pre-commit hook to your project. You can find the hook in the .husky
folder. In the pre-commit
file, you can add the commands you want to run before the commit is made. In our case, we will run a group of command. This is a good example in case you want to run multiple commands before the commit is made.
husky run -v --group "pre-commit"
Add commit-msg hook
We will add a commit msg hook to lint the commit message before the commit is made. Husky.Net will run the commit msg hook before the commit is made.
dotnet husky add commit-msg
This will add a commit msg hook to your project. You can find the hook in the .husky
folder. In the commit-msg
file, you can add the commands you want to run before the commit is made. In our case, we will run commit-msg.
husky run --name "commit-message-linter" --args "$1"
echo
echo Great work! 🥂
echo
Add a commit message linter
We will add a commit message linter to lint the commit message. Husky.Net will run the commit message linter before the commit is made. Create a new folder in the .husky
folder and name it csx
(csx stands for C# script). In the csx
folder, create a new file and name it commit-lint.csx
. In the commit-lint.csx
file, add the following code.
using System.Text.RegularExpressions;
private var pattern = @"^(?=.{1,90}$)(?:build|feat|ci|chore|docs|fix|perf|refactor|revert|style|test|wip)(?:\(.+\))*(?::).{4,}(?:#\d+)*(?<![\.\s])$";
private var msg = File.ReadAllLines(Args[0])[0];
if (Regex.IsMatch(msg, pattern))
return 0;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Invalid commit message");
Console.ResetColor();
Console.WriteLine("e.g: 'feat(scope): subject' or 'fix: subject'");
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine("more info: https://www.conventionalcommits.org/en/v1.0.0/");
return 1;
Update the task runner
Now we have our hooks ready. We need to update the task runner to run the hooks. Open the task-runner.json
file and update the tasks.
{
"tasks": [
{
"name": "commit-message-linter",
"command": "husky",
"args": ["exec", ".husky/csx/commit-lint.csx", "--args", "${args}"]
},
{
"name": "dotnet-format",
"group": "pre-commit",
"command": "dotnet-format",
"args": ["--include", "${staged}"],
"include": ["**/*.cs"]
}
]
}
Commit and push
Now we are ready to commit and push our changes. Commit and push your changes. You can find the commit message linter in action.
if you followed the steps correctly, you should see the following output when you didn't follow the conventional commits standard.
[Husky] 🚀 Loading tasks ...
--------------------------------------------------
--------------------------------------------------
[Husky] ⚡ Preparing task 'dotnet-format'
[Husky] 💤 Skipped, no matched files
--------------------------------------------------
[Husky] 🚀 Loading tasks ...
--------------------------------------------------
[Husky] ⚡ Preparing task 'commit-message-linter'
[Husky] ⌛ Executing task 'commit-message-linter' ...
Invalid commit message
e.g: 'feat(scope): subject' or 'fix: subject'
More info: https://www.conventionalcommits.org/en/v1.0.0/
script execution failed
❌ Task 'commit-message-linter' failed in 2.445ms
husky - commit-msg hook exited with code 1 (error)
if you commit message is valid, you should see the following output.
[Husky] 🚀 Loading tasks ...
--------------------------------------------------
[Husky] ⚡ Preparing task 'dotnet-format'
[Husky] 💤 Skipped, no matched files
--------------------------------------------------
[Husky] 🚀 Loading tasks ...
--------------------------------------------------
[Husky] ⚡ Preparing task 'commit-message-linter'
[Husky] ⌛ Executing task 'commit-message-linter' ...
[Husky] ✔ Successfully executed in 404ms
--------------------------------------------------
Great work! 🥂
Attach Husky.Net to your project
To make things easier for other developers, you can attach Husky.Net to your project.
dotnet husky attach <path-to-project-file>
This will add required configuration to your project file. You can find the configuration in the PropertyGroup
section.
<Target Name="husky" BeforeTargets="Restore;CollectPackageReferences" Condition="'$(HUSKY)' != 0">
<Exec Command="dotnet tool restore" StandardOutputImportance="Low" StandardErrorImportance="High"/>
<Exec Command="dotnet husky install" StandardOutputImportance="Low" StandardErrorImportance="High"
WorkingDirectory="../../" /> <!--Update this to the relative path to your project root dir -->
</Target>
Conclusion
In this article, we have seen how to version and release a dotnet application using versionize and github actions. You can find the source code in the github repository for this article. If you have any questions or suggestions, please leave a comment below. Thanks for reading. Happy coding! 🚀 🚀 🚀
Related Posts
Ollama Semantic Kernel Connector With C#: Console App & API Guide
In this post, we will learn how to use the Ollama Semantic Kernel Connector with C#.
ABP-Powered Web App with Inertia.js, React, and Vite
Building a web application with ABP Framework, Inertia.js, React, and Vite.
Getting started with Ollama and Semantic Kernel with C#
In this post, we will learn how to get started with Ollama and Semantic Kernel with C#.