loading...
Cover image for Creating a .NET Tool - Part 3: CI/CD

Creating a .NET Tool - Part 3: CI/CD

marcusturewicz profile image Marcus Turewicz Updated on ・6 min read

Creating a .NET Tool (3 Part Series)

1) Creating a .NET Tool - Part 1: Feeding the Dragon 2) Creating a .NET Tool - Part 2: Packaging 3) Creating a .NET Tool - Part 3: CI/CD

In Part 2 of this series, we added the necessary code to package the console app as a .NET Tool that can be published to NuGet. In this post, we're going all in on the DevOps, including pull request validation, publishing a CI package and publishing a Release package.

Note: I've gone ahead and added some tests so our CI/CD has something to test, so the main program looks slightly different compared to the first two posts, but hopefully not too much!

Versioning

Before setting up the CI/CD pipelines, let's talk about versioning. Firstly, we want to use SemVer2.0 whereby:

  • A release package version would look like {major}.{minor}.{patch}, e.g. 1.0.0, and be generated by creating a Release on GitHub
  • A pre-release package would look like {major}.{minor}.{patch}-{pre-type}.{pre-num}, e.g. 1.0.0-alpha.1, and be generated by creating a Release on GitHub
  • A ci-build package would look like {major}.{minor}.{patch}-{pre-type}.{pre-num}.{git-commit}, e.g. 1.0.0-alpha.1.g8487asd8f, and be generated by pushing to master

Luckily, there is a great OSS library that handles this quite nicely for us. It's called Nerdbank.GitVersioning.

version.json

The first step is to set up the version.json file that specifies your versioning strategy, like we've outlined above. Ours will look like:

{
    "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
    "version": "1.0.0-alpha.1",
    "gitCommitIdShortAutoMinimum": 7,
    "nugetPackageVersion": {
        "semVer": 2
    },
    "publicReleaseRefSpec": [
        "^refs/tags/v\\d+\\.\\d+"
    ]
}

The $schema tag is just for intellisense in IDE's. We're using gitCommitIdShortAutoMinimum as 7 since that's what GitHub's short commit ID uses (this will be added to CI builds). We're setting nugetPackageVersion to use SemVer2.0 and finally adding any tags like vN.N to publicReleaseRefSpec so that the commit ID will not be added.

Nerdbank.GitVersioning

We also have to add a package reference to Nerdbank.GitVersioning which will handle the versioning during builds:

$ cd src/jsonv
$ dotnet add package Nerdbank.GitVersioning

Great, now we can start setting up CI/CD.

CI/CD

Given the code is on GitHub, it makes sense to use GitHub Actions for CI/CD. If you haven't heard of GitHub Actions yet, go check it out. They're not only for CI/CD but can really do anything you can think of, such as automatically labelling PR's, mark issues and PR's as stale and you can make you're own (as we are doing here).

Branches

We know we want to have pull request validation, CI builds from master and CD builds from vN.N tags. We can set this up in our CI/CD action script with:

on:
  pull_request:
    branches:
      - master # CI (pr validation)
  push:
    branches:
      - master # CI (ci package)
    tags:
      - v*     # CD (release package)

.NET environment

To build a .NET app we need to set up a .NET environment. The official actions/setup-dotnet can be used here:

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
    - uses: actions/checkout@v1
    - uses: actions/setup-dotnet@v1
      with:
        dotnet-version: '3.1.201'
        source-url: https://nuget.pkg.github.com/marcusturewicz/index.json
      env:
        NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

Here, we're setting up .NET SDK 3.1.201 on Ubuntu 18.04 and setting the NuGet source-url and NUGET_AUTH_TOKEN for CI builds to be published to my GitHub Package Repository.

Continuous Integration

Now we're in a position to do continuous integration. Meaning, we want to build, test, pack and publish (only on push) our package:

    - run: dotnet build src/jsonv -c Release
    - run: dotnet test test/jsonv.Tests -c Release
    - run: dotnet pack src/jsonv -c Release
    - run: dotnet nuget push src/jsonv/bin/Release/*.nupkg
      if: github.event_name == 'push' && startswith(github.ref, 'refs/heads')

Notice the if statement attached to the last run statement which allows it to only run on a push to a branch; we don't want to create packages for every pull request, only once the commit has made it's way into master.

Continuous Deployment

Now we have packages being published to our CI package feed from master, we can move on to publishing packages from a release tag:

    - run: dotnet nuget push src/jsonv/bin/Release/*.nupkg -k ${{secrets.NUGET_ORG_API_KEY}} -s https://api.nuget.org/v3/index.json
      if: github.event_name == 'push' && startswith(github.ref, 'refs/tags')

Here, we are pushing the package to nuget.org when a tag is created, using an API Key that I generated in my nuget.org account and added to my GitHub repo secrets. Again, notice the if statement which is controlling when this command runs.

Finally, the full action script is:

name: CI/CD

on:
  pull_request:
    branches:
      - master # CI (pr validation)
  push:
    branches:
      - master # CI (ci package)
    tags:
      - v\\d+\\.\\d+     # CD (release package)

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
    - uses: actions/checkout@v1
    - uses: actions/setup-dotnet@v1
      with:
        dotnet-version: '3.1.201'
        source-url: https://nuget.pkg.github.com/marcusturewicz/index.json
      env:
        NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
    - run: dotnet build src/jsonv -c Release
    - run: dotnet test test/jsonv.Tests -c Release
    - run: dotnet pack src/jsonv -c Release
    - run: dotnet nuget push src/jsonv/bin/Release/*.nupkg
      if: github.event_name == 'push' && startswith(github.ref, 'refs/heads')
    - run: dotnet nuget push src/jsonv/bin/Release/*.nupkg -k ${{secrets.NUGET_ORG_API_KEY}} -s https://api.nuget.org/v3/index.json
      if: github.event_name == 'push' && startswith(github.ref, 'refs/tags')

For doing PR validation, CI builds and CD builds, I think it's quite concise!

Seeing it in action

Let's do some DevOps to see this all working together.

Pull request build

I've added the changes we made above to a new branch and created a pull request to merge into master. We can see that the pull request validation was triggered and passed successfully.

Alt Text

Let's check out the build logs to see if everything worked correctly:

Alt Text

Great, we can see that build, test and pack were triggered but packages were not deployed to any feeds.

CI build

Now that the pull request has been merged to master, let's see how our CI build went:

Alt Text

Great, we can see that everything built correctly and a CI build package was published to our internal feed correctly. Let’s check everything worked correctly with the package:

Alt Text

Great, the package was published correctly and has the version number that we expect, including the 7 character short git ID. This means for every commit to master we will have a package that we can install and test.

CD build

The final step is to publish a "Release" version of our package to nuget.org. Firstly, you'll need to setup an account on NuGet and then create an API key:

Alt Text

Then, you copy the value and create a GitHub Secret in your repository using that value:

Alt Text

Now our action script will be able to read that value securely and pass it to a nuget publish command.

Now we are ready to create a GitHub Release and associated Tag:

Alt Text

Our CD build is then triggered due to the tag being created:

Alt Text

We can see that our package was built and deployed to NuGet:

Alt Text

Some things to note:

  • NuGet recognises the package a pre-release due to the versioning
  • SourceLink has linked the source repository
  • The metadata looks good; image, description, authours and tags

Summary

In this post, we set up CI/CD such that each pull request to master is validated, CI packages published to GitHub Package Repository for each push to master and Release packages are published to nuget.org for each release created. We utilised Nerdbank.GitVersioning to handle versioning, and GitHub Actions for our CI/CD pipeline. Finally, the tool is now available on nuget.org and the repository is ready for contributions! I hope this series has been helpful in how to easily create an OSS .NET Tool with proper DevOps practices around it.

Resources

Creating a .NET Tool (3 Part Series)

1) Creating a .NET Tool - Part 1: Feeding the Dragon 2) Creating a .NET Tool - Part 2: Packaging 3) Creating a .NET Tool - Part 3: CI/CD

Posted on May 3 by:

marcusturewicz profile

Marcus Turewicz

@marcusturewicz

Machine Learning Engineer, bassist, soccer player, .NET fan, native of the cloud and renewable energy advocate.

Discussion

markdown guide