| Comments

Continuing on my research and playing around with GitHub Actions, I was looking to migrate my Alexa.NET project off of Pipelines and in to one place for my open source project.  Pipelines still has an advantage for me right now as I prefer the approval flow that I have right now.  In this post I’ll cover how I modified my build definition to now also include producing the NuGet package, signing it with my code signing certificate, and pushing it to multiple repositories.

Quick tip: if you haven’t follow Ed Thomson before he’s doing a series on GitHub Actions for the month of December.  Check out his GitHub Actions Advent Calendar!


We need to first make sure we have the tools needed in the build step, so let’s be sure to get the .NET SDK so we can use the dotnet CLI commands.  This is the start of my build-and-deploy.yaml file and each other snippet builds on this.

name: "Build and Deploy"

      - master

    if: github.event_name == 'push' && contains(toJson(github.event.commits), '***NO_CI***') == false && contains(toJson(github.event.commits), '[ci skip]') == false && contains(toJson(github.event.commits), '[skip ci]') == false
    name: Build Package
    runs-on: ubuntu-latest

    - uses: actions/[email protected]
    - name: Setup .NET Core SDK
      uses: actions/[email protected]
        dotnet-version: 3.1.100

Starting at line 16 I write the steps to get the .NET SDK that I want to use, in this case the .NET 3.1 (which is the long-term support version now) SDK.  Now we are all set…the tools I need are on the runner.

Building the Package

The first thing we obviously need to do is ensure we have an actual NuGet package.  I perform this step during my ‘build’ job when I know things have been successfully built and tested.  After getting the SDK we can how issue our pack command.  This assumes we’ve already run dotnet build, which I didn’t show here.

    - name: Pack
      run: dotnet pack TestLib --configuration Release -o finalpackage --no-build

    - name: Publish artifact
      uses: actions/[email protected]
        name: nupkg
        path: finalpackage

You can see in line 2 where we use the dotnet CLI to pack into a NuGet package.  Note I’m using an output argument there to put the final nupkg file in a specific location.  In line 5 I am setting up the action to upload the artifact so that I can use it later in other steps in the job.  The upload-artifact agent will use the path ‘finalpackage’ and upload it into the location ‘nupkg’ for me.  It will available for me later as you’ll see.

Signing the Package

Now I want to be a good trusted provider of a library package so I’ve chosen to sign my package using a code-signing certificate.  I got mine through DigiCert.  One of the main differences between Actions and Pipelines is that Actions only has secure storage for ‘secrets’ as strings.  Pipelines has a library where you can also have secure file storage.  To sign a NuGet package, the command requires a path to a certificate file so we have to somehow get the file available for the CLI command.  Based on all the recommendations from people also doing similar activities (needing files in their actions) it seemed to be the approach was to base64-encode the file and put that as a secret…so that’s the approach I took.  I base64-encoded the contents of my PFX and set it as a secret variable named SIGNING_CERT. 

Now the next thing I need to do is not only retrieve that string, but put that into a temporary file.  Searching as best I could on forums I didn’t see an existing script or anything that people used, so I created a new action for myself to use (and you can to) called timheuer/base64-to-file.  This action takes your encoded string, decodes it to a temporary file and sets the path to that temporary file as an output for the action.  Simple enough.  Now with the pieces in place we can set up the steps:

    needs: build
    name: Deploy Packages
    runs-on: windows-latest # using windows agent due to nuget can't sign on linux yet
      - name: Download Package artifact
        uses: actions/[email protected]
          name: nupkg
      - name: Setup NuGet
        uses: NuGet/[email protected]
          nuget-api-key: ${{ secrets.NUGET_API_KEY }}
          nuget-version: latest

      - name: Setup .NET Core SDK
        uses: actions/[email protected]
          dotnet-version: 3.1.100

      - name: Get certificate
        id: cert_file
        uses: timheuer/[email protected]
          fileName: 'certfile.pfx'
          encodedString: ${{ secrets.SIGNING_CERT }}
      # Sign the package
      - name: Sign NuGet Package
        run: nuget sign nupkg\*.nupkg -CertificatePath ${{ steps.cert_file.outputs.filePath }} -CertificatePassword ${{ secrets.CERT_PWD }}  -Timestamper http://timestamp.digicert.com –NonInteractive

The above is my ‘deploy’ job that does the tasks.  On line 6 is where we are retrieving the nupkg file from the artifact drop in the previous job.  After that I’m using the new nuget/setup-nuget action to acquire the NuGet CLI tools for subsequent actions.  At present, you cannot use dotnet CLI to sign a NuGet package so we have to use the NuGet tools directly.  We’ll need this later as well so it’s good we have it now.  On line 22 starts the process mentioned above to use my new action to retrieve the encoded string and put it as a temp file.  One line 31 we execute the NuGet sign CLI command to sign the package.  I have a few arguments here but pay attention to the steps.cert_file.outputs.filePath one.  That is the OUTPUT from the base64-to-file action.  The format of steps.{ID}.outputs.{VARIABLE} is what you see here…and you can see in that step I gave it an id of ‘cert_file’ to easily pull out the variable later.

Now, you may have noticed that this agent job runs on windows-latest as the OS and not ubuntu.  This is because presently package signing for NuGet can only be done on Windows machines.  Now that we have a signed package (in the same location, we just signed it and didn’t move it) we can deploy it to package registries.

Publishing the Package to NuGet

Of course for a public library I want this to be available on NuGet so I’m going to publish it there.  NuGet uses an API key authentication scheme which is supported in the dotnet CLI so we can use dotnet CLI push to publish:

      - name: Push to NuGet
        run: dotnet nuget push nupkg\*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org

Could I have used the NuGet CLI?  Sure, but I already was using this pattern previously so I’m sticking with this from a previous Pipeline definition.  Choice is yours now that we have both CLI tools on the runner machine.  Done, now on to another registry.

Publishing the Package to GitHub Package Registry

Publishing to the new GitHub Packages Registry takes one extra step.  Since this is not the default location for NuGet, we have to instruct NuGet to let it know where to publish this package.  In your repository you will be provided with a URL from the Packages tab of your repo:

Screenshot of GitHub Packages tab

This is the publishing endpoint for the NuGet CLI.  In our Action we will need two steps: set up the source and publish to it:

      - name: Add GPR Source
        run: nuget sources Add -Name "GPR" -Source ${{ secrets. GPR_URI }} -UserName ${{ GPR_USERNAME }} -Password ${{ secrets.GITHUB_TOKEN }}

      - name: Push to GitHub Packages
        run: nuget push nupkg\*.nupkg -Source "GPR"

In line 2 is where we set up the source we are going to later use.  We can give it any name you want here.  I made the other variables Secrets for my config.  This also requires you to use the UserName/Password scheme as GitHub Packages doesn’t support NuGet API keys right now.  Another reason we need to use the NuGet CLI here.  The password you can use is provided as a default token in any GitHub Action called secrets.GITHUB_TOKEN and your repo’s actions have access to it.  In line 5 then we see us using that source and pushing our package to the GitHub Packages Registry.


So there you have it!  A GitHub Actions flow packages, signs, and publishes to two package repositories.  It would be nice to standardize on one tooling CLI and I know the teams are looking for feedback here, but it is good to know that you have 2 official supported GitHub Actions in setup-dotnet and setup-nuget to use to get the tools you need.  I hope this helps someone!

Please enjoy some of these other recent posts...