As technology evolves and we progress as a community, I notice that older blog posts erode in their helpfulness. One of the topics I find myself constantly having to relearn, especially as an OSS author, is the ability to start a new project, build a continuous integration pipeline, and get my package to NuGet. It’s essential to understand how to design a workflow that works with your project, and in this post, we’ll take a step-by-step walkthrough of how to do just that.

We’ll be using some NuGet packages, the dotnet CLI, and GitHub Actions to build a workflow accessible, and more importantly, understandable for most .NET developers. So let’s get into it.

To follow along, you can visit the complete sample solution found at my GitHub repository.

Step 1. Creating A Solution

There are many approaches to setting up a solution folder in the .NET ecosystem, and you may notice the philosophy of each of your favorite projects can vary wildly. For the sake of this tutorial, we’ll create the most straightforward solution folder, which has our solution file, along with source and test project folders side-by-side.

The first step is to create a new solution and add two new projects. The first project will be our class library, targeting .NET Standard 2.0. The second project will be a unit testing project with a package reference to XUnit.

Don’t worry; This approach will work regardless of how many packageable and unit tests projects you have and as long as your sln file is accessible from the current working directory of your command line.

Step 2. Initialize Our Git Repository

Once our solution is functionally complete, we should initialize our folder as a Git repository. In some cases, our solution folder may already be a Git repository. I recommend using the GitHub CLI to create a matching remote repository, where we will eventually run GitHub Actions.

> git init 
> gh repo create

I’ll assume you have a basic understanding of Git at this point. If you don’t, you’ll need to read about it. Git provides a free online version of Pro Git available to everyone.

Step 3. Adding NuGet Packages

Your packageable projects will need two additional NuGet packages for this particular approach: MinVer and Microsoft.SourceLink.GitHub.

MinVer allows you to use Git tags to version the packageable projects. MinVer also works on the Semantic Version philosophy of major, minor, and patch numbers.

The SourceLink package allows developers to pull source code for your project, making it easier to debug issues when they happen. SourceLink is an optional package but can go a long way in helping you and your community solve problems.

<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
  <PackageReference Include="MinVer" Version="2.5.0">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
</ItemGroup>

Step 4. Add NuGet Information To Csproj

The next step is to add the necessary information to our packageable projects csproj files. Here’s mine as an example. It’s important to note that PackageVersion is missing on purpose, which will be handled by MinVer later in this guide. Also, notice the IsPackable attribute, which informs that .NET CLI that this particular project will become a NuGet package.

<PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <Nullable>enable</Nullable>
    <RootNamespace>Khalid.Fibonacci</RootNamespace>
    <LangVersion>Latest</LangVersion>
    <!-- NuGet Package Information -->
    <PackageId>Khalid.Fibonacci</PackageId>
    <PackageProjectUrl>https://github.com/khalidabuhakmeh/Khalid.Fibonacci</PackageProjectUrl>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageTags>Fibonnaci</PackageTags>
    <IsPackable>true</IsPackable>
    <Description>Generates an iterator for Fibonacci sequence</Description>
</PropertyGroup>

Step 5. Add GitHub Action Workflows

Now that we have our solution completed with packageable and unit tests projects, we need to create two GitHub workflows. One workflow will build and test our solution on pushes to main and when pull requests are submitted. The other will publish our NuGet packages to a package respository.

We’ll first need to create a directory at the root of our solution named .github. Under that folder, we’ll create a new workflows folder with two files of build.yaml and publish.yaml.

Let’s take a look at the build.yaml contents.

name: Build & Test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Setup .NET
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: 5.0.x
      - name: Restore dependencies
        run: dotnet restore
      - name: Build
        run: dotnet build --no-restore -c Release
      - name: Test
        run: dotnet test -c Release --no-build --verbosity normal

We’re using the dotnet CLI tool to accomplish everything we need. No special build tools or clients are necessary.

Next, let’s look at our publish.yaml.

name: Publish

on:
  push:
    tags:
      - '**'

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Setup .NET
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: 5.0.x
      - name: Restore dependencies
        run: dotnet restore
      - name: Build
        run: dotnet build --no-restore -c Release
      - name: Test
        run: dotnet test -c Release --no-build --verbosity normal
      - name: Pack
        run: dotnet pack -c Release -o ./nupkgs --no-build
      - name: Publish
        run: dotnet nuget push ./nupkgs/*.nupkg --api-key $  --skip-duplicate -s https://api.nuget.org/v3/index.json

The publish.yaml file is similar to the build.yaml file except for three main differences:

  1. We only publish when we push a new tag to the repository.
  2. We use the .NET CLI to pack all packable projects.
  3. We push all NuGet packages to our package repository by using our API Key.

To make publishing possible, you’ll need to retrieve an API key from your package repository and add the secret to the repository.

Step 6. Tag Our Commits

Now, let’s add our changes to source control using common git commands of add and commit. Of course, we’ll also want to tag our commit with a version.

> git tag 1.0.0

Our tag must use the Major.Minor.Patch convention, or MinVer will not detect our version. Once tagged, we can push our changes up to our GitHub repository.

> git push -u origin main --tags

Conclusion

You should have a GitHub repository that builds and tests changes on each new commit if you followed each step. You’ll also have a GitHub repository that publishes a new package on each tagged commit.

Again, if you want to look at the final result of this post, you can see the solution in its entirety at my GitHub repository.