Publishing a NuGet package using GitHub and GitHub Actions
In the previous post, I explained the best practices for NuGet packages. In this post, I will show how to create and publish a NuGet package that follows best practices using GitHub and GitHub Actions.
#Create the project
Let's create a class library project and a test project:
# Create a class library project and a test project
dotnet new classlib -o SampleNuGet
dotnet new xunit -o SampleNuGet.Tests
dotnet add SampleNuGet.Tests reference SampleNuGet
# Add MinVer to handle package versioning based on the commit / tag
dotnet add SampleNuGet package MinVer
# Create a sln containing both projects
dotnet new sln --name SampleNuGet
dotnet sln add SampleNuGet
dotnet sln add SampleNuGet.Tests
# Create a global.json file to specify the required .NET SDK version.
# This file is used by GitHub Actions to install the required .NET SDK version.
dotnet new globaljson --roll-forward feature
#GitHub Actions
We'll use a GitHub Actions to build and publish the NuGet package each time a GitHub Release is published. The workflow creates the package, runs validations, runs tests, and publishes the package if the validations and tests succeed.
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: publish
on:
workflow_dispatch: # Allow running the workflow manually from the GitHub UI
push:
branches:
- 'main' # Run the workflow when pushing to the main branch
pull_request:
branches:
- '*' # Run the workflow for all pull requests
release:
types:
- published # Run the workflow when a new GitHub release is published
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_NOLOGO: true
NuGetDirectory: ${{ github.workspace}}/nuget
defaults:
run:
shell: pwsh
jobs:
create_nuget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Get all history to allow automatic versioning using MinVer
# Install the .NET SDK indicated in the global.json file
- name: Setup .NET
uses: actions/setup-dotnet@v4
# Create the NuGet package in the folder from the environment variable NuGetDirectory
- run: dotnet pack --configuration Release --output ${{ env.NuGetDirectory }}
# Publish the NuGet package as an artifact, so they can be used in the following jobs
- uses: actions/upload-artifact@v3
with:
name: nuget
if-no-files-found: error
retention-days: 7
path: ${{ env.NuGetDirectory }}/*.nupkg
validate_nuget:
runs-on: ubuntu-latest
needs: [ create_nuget ]
steps:
# Install the .NET SDK indicated in the global.json file
- name: Setup .NET
uses: actions/setup-dotnet@v4
# Download the NuGet package created in the previous job
- uses: actions/download-artifact@v3
with:
name: nuget
path: ${{ env.NuGetDirectory }}
- name: Install nuget validator
run: dotnet tool update Meziantou.Framework.NuGetPackageValidation.Tool --global
# Validate metadata and content of the NuGet package
# https://www.nuget.org/packages/Meziantou.Framework.NuGetPackageValidation.Tool#readme-body-tab
# If some rules are not applicable, you can disable them
# using the --excluded-rules or --excluded-rule-ids option
- name: Validate package
run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg")
run_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Run tests
run: dotnet test --configuration Release
deploy:
# Publish only when creating a GitHub Release
# https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository
# You can update this logic if you want to manage releases differently
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: [ validate_nuget, run_test ]
steps:
# Download the NuGet package created in the previous job
- uses: actions/download-artifact@v3
with:
name: nuget
path: ${{ env.NuGetDirectory }}
# Install the .NET SDK indicated in the global.json file
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
# Publish all NuGet packages to NuGet.org
# Use --skip-duplicate to prevent errors if a package with the same version already exists.
# If you retry a failed workflow, already published packages will be skipped without error.
- name: Publish NuGet package
run: |
foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) {
dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate
}
You can create a new project on GitHub and push the files to the repository:
dotnet new gitignore
git init
git add .
git commit -m "Initial commit"
git branch -M main
# TODO Update this URL with the URL of your repository
git remote add origin https://github.com/meziantou/SampleNuGet.git
git push -u origin main
The GitHub Actions should run immediately after pushing the commit. However, it should fail because the NuGet package validation fails. We will fix it later!
#Enable the .NET SDK package validation
The previous workflow includes a job to validate the package using Meziantou.Framework.NuGetPackageValidation.Tool
. This tool detects missing metadata and has rules to validate the DLLs and symbols. You can read more about this tool in the previous post.
The .NET SDK provides another kind of validation focused on the content of DLLs. At the moment, it provides the following checks:
- Validates that there are no breaking changes across versions
- Validates that the package has the same set of public APIs for all the different runtime-specific implementations
- Helps developers catch any applicability holes
These rules are important. However, they are not enabled by default. You need to enable them by adding the following property to your project file:
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<!-- Optional: Detect breaking changes from a previous version -->
<!-- <PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion> -->
</PropertyGroup>
It is now harder to publish a low-quality NuGet package!
#Include symbols and repository information
Including the symbols in the NuGet package allows:
- To view the source code of the package in Visual Studio using "Go To Definition", or using the debugger
- To debug the code of the NuGet package
- To validate the DLL in the package is built from the source available on GitHub
If the code is open-source there is no reason to not provide all information to the users. To include the symbols and configure SourceLink, you can add the DotNet.ReproducibleBuilds
package to the project. You can read more about this package in a previous blog post: Enabling Reproducible builds when building NuGet packages.
dotnet add SampleNuGet package DotNet.ReproducibleBuilds
The previous command adds these lines to the csproj file:
<ItemGroup>
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
This package also adds metadata about the repository to the NuGet package based on the current GitHub repository URL and commit. Thanks to these data, a link to the repository is displayed on nuget.org. This is useful to easily navigate from a NuGet package to the GitHub repository. When people want to read the code, report issues, or contribute, they can easily find the repository.
#Include the XML documentation
Including the XML documentation in the NuGet package allows developers to view the documentation of classes and members in Visual Studio.
If you write XML documentation, even if it's only on a single member, you should add the following property to your project file:
<PropertyGroup>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<!-- If all members are not documented, you can disable the compiler warnings -->
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
#Add missing package metadata
There are many metadata you can add to your package. This can increase the discoverability of your package. You can read more about them in the NuGet documentation.
Let's configure the basic metadata in the csproj:
<PropertyGroup>
<Authors>Gérald Barré</Authors>
<Description>A long description to explain the package</Description>
<!-- PackageProjectUrl is different from the repository URL. It can be a documentation
website or a website explaining the project -->
<PackageProjectUrl>https://www.meziantou.net</PackageProjectUrl>
<!-- A list of tags to help the search engine to understand the content of the package -->
<PackageTags>sample, library</PackageTags>
</PropertyGroup>
Then, you should add a license. If you use a common license, you can set the <PackageLicenseExpression>
. If you use a custom license, you can set the PackageLicenseFile
property. You can read more about the license in the NuGet documentation.
<PropertyGroup>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<!-- Or -->
<PropertyGroup>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<!-- Add a LICENSE.txt next to the csproj -->
<None Include="LICENSE.txt" Pack="true" PackagePath=""/>
</ItemGroup>
Then, you can add an icon to help the user to identify your package. You can read more about the icon in the NuGet documentation.
<PropertyGroup>
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<!--
Add an icon.png next to the csproj:
- Supported format: png, jpg
- Recommended dimensions: 128x128
- Maximum size: 1MB
-->
<None Include="icon.png" Pack="true" PackagePath=""/>
</ItemGroup>
Finally, you can add a README file to give a long description of the package including a getting started section. You can read more about the README in the NuGet documentation.
<PropertyGroup>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<!-- Add a README.md next to the csproj -->
<None Include="README.md" Pack="true" PackagePath=""/>
</ItemGroup>
#Use API Key to publish the package to nuget.org
The package is now ready to be published. Let's finish the configuration of the workflow to publish the package to nuget.org.
To publish the package to nuget.org, you need an API key. You can read more about it in the NuGet documentation. To create an API Key, open https://www.nuget.org/account/apikeys and expand the "Create" section. For security reasons, select the minimum required scopes and package glob to publish the package. Then, click on "Create" to generate the API key. Copy the API key and store it in a secure place.
Then, create a secret named NUGET_APIKEY
in the GitHub repository to store the API key. You can read more about it in the GitHub documentation.
You can now publish the package to nuget.org by creating a new GitHub release. The workflow will automatically publish the package to nuget.org.
#nuget.org - Prefix reservation
You can reserve a prefix to protect your identity on nuget.org. Once a prefix is reserved, only the owner of the prefix can publish new packages with this prefix. nuget.org and Visual Studio show a visual indicator for packages that are submitted by owners with a reserved package ID prefix.
For instance, I'm the only one who can publish a package named Meziantou.*
on nuget.org as I reserved the prefix meziantou
:
If you want to reserve a prefix, you can follow the steps mentioned in the NuGet documentation.
#Security
You don't want to publish a package that contains vulnerable code or has vulnerable dependencies. GitHub provides features to help you secure your code. Be sure to read about CodeQL, secret scanning, or supply chain security. Also, take some time to read about GitHub Actions security hardening to be sure the NuGet API Key is correctly protected. For instance, environments can provide additional security.
#Sample project and package
I published the sample project on GitHub:
- Sample GitHub repository: https://github.com/meziantou/SampleNuGet
- Sample NuGet package: https://www.nuget.org/packages/Meziantou.SampleNuGet
- NuGet Package Explorer for the sample package: https://nuget.info/packages/Meziantou.SampleNuGet/1.0.0
Do you have a question or a suggestion about this post? Contact me!