Publishing a NuGet package using GitHub and GitHub Actions

 
 
  • Gérald Barré

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:

PowerShell
# 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.

.github/workflows/publish.yml (YAML)
# 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:

PowerShell
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:

SampleNuGet/SampleNuGet.csproj (csproj (MSBuild 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.

PowerShell
dotnet add SampleNuGet package DotNet.ReproducibleBuilds

The previous command adds these lines to the csproj file:

SampleNuGet/SampleNuGet.csproj (csproj (MSBuild project 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:

SampleNuGet/SampleNuGet.csproj (csproj (MSBuild 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:

SampleNuGet/SampleNuGet.csproj (csproj (MSBuild project file))
  <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.

SampleNuGet/SampleNuGet.csproj (csproj (MSBuild project file))
  <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.

SampleNuGet/SampleNuGet.csproj (csproj (MSBuild project file))
  <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.

SampleNuGet/SampleNuGet.csproj (csproj (MSBuild project file))
  <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:

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub