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:
- We only publish when we push a new tag to the repository.
- We use the .NET CLI to pack all packable projects.
- 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.