Automating NuGet Package Distribution via GitHub Actions

The integration of continuous integration and continuous delivery (CI/CD) pipelines into the .NET ecosystem has evolved from manual package uploads to sophisticated, identity-based automation. Leveraging GitHub Actions to publish NuGet packages eliminates the friction of manual versioning and distribution, ensuring that the latest stable builds of a library are available to consumers immediately upon a release trigger. By transitioning from long-lived API keys to modern authentication frameworks, developers can achieve a seamless flow from code commit to package availability on nuget.org or GitHub Packages.

The Evolution of NuGet Publishing Methodologies

Historically, the process of pushing .nupkg files to a repository was a fragmented experience. Developers typically relied on three primary patterns, each with escalating levels of efficiency and reliability.

The most basic approach involved building packages locally on a developer's machine and manually uploading the resulting files to the nuget.org website through a web browser. While simple, this method is prone to human error, lacks traceability, and creates a bottleneck where only the developer with the API key can release a version.

The second iteration involved utilizing a CI system to build the packages, which ensured a consistent build environment. However, the developer still had to manually download the artifacts from the CI server and then upload them to the NuGet gallery. This partially automated the build but left the distribution phase manual.

The most advanced traditional method is pushing directly from CI to nuget.org. This requires the generation of a long-lived API key, which must be stored as a GitHub Secret. While this eliminates manual intervention, it introduces significant security overhead. The lifecycle management of these secrets is notoriously difficult, requiring rotation policies and careful access control within an organization to ensure that multiple contributors can manage the release process without compromising the key.

Trusted Publishing via OpenID Connect (OIDC)

The introduction of Trusted Publishing on nuget.org represents a paradigm shift in security by removing the need for long-lived secrets. This mechanism leverages OpenID Connect (OIDC) to establish a trust relationship between GitHub and NuGet.

The technical execution of Trusted Publishing follows a specific sequence of identity exchanges. First, the user configures a trust policy on nuget.org, essentially telling the NuGet gallery to trust tokens issued by GitHub for a specific repository. In the CI workflow, GitHub acts as the Identity Provider (IdP). The workflow requests an OIDC token, which is provided as a signed JSON Web Token (JWT).

This JWT contains critical metadata, including details about the repository and the specific workflow currently executing. When this token is sent to nuget.org, the gallery verifies the JWT against the pre-configured trust policy. If the token is valid and the repository matches the policy, nuget.org issues a short-lived API token. This temporary token is then used by the CI workflow to push the .nupkg files. Because the authentication is based on the identity of the running job rather than a static string, the security risk of a leaked permanent API key is entirely removed.

Architectural Implementation of OIDC Workflows

To implement Trusted Publishing, the GitHub Actions YAML configuration must be precisely defined to allow the issuance of identity tokens.

The permissions block is the most critical component. The job must have id-token: write enabled to allow the GitHub OIDC token issuance. Without this, the NuGet/login@v1 action will fail to retrieve a valid token from the GitHub environment. Additionally, contents: read is required to access the source code.

The workflow typically follows these operational steps:

  • Checkout the code using actions/checkout@v4.
  • Set up the .NET environment using actions/setup-dotnet@v4, specifying the required version (e.g., 10.0.x).
  • Execute the dotnet pack command to create the packages. For multi-platform libraries, this may involve running the command multiple times for different targets, such as dotnet pack -r win-x64.
  • Use the NuGet/login@v1 action. This step exchanges the OIDC token for a temporary API key. The only secret required for this process is the NUGET_USER, which must be the nuget.org username and not the email address.
  • Push the generated packages. This is often done using a PowerShell loop (pwsh) to find all .nupkg files in the artifacts directory and push them using the dotnet nuget push command, passing the temporary API key retrieved from the login step.

Comparative Analysis of Publishing Tools and Actions

Depending on the project's complexity, developers can choose between specialized community actions or the official .NET CLI.

Feature brandedoutcast/publish-nuget Official dotnet CLI + OIDC
Authentication Manual API Key via Secrets OIDC / Trusted Publishing
Version Detection Built-in regex for version files Manual or external tool (e.g., dotnet-version-cli)
Setup Complexity Low (Action-based) Medium (Requires YAML config)
Security Long-lived Secret Short-lived Token
Control High-level abstraction Granular command-line control

The brandedoutcast/publish-nuget action provides a simplified interface for those who prefer a managed action. It can be configured via a .github/workflows/publish.yml file and allows for specific inputs such as PROJECT_FILE_PATH, BUILD_CONFIGURATION, and PACKAGE_NAME. It can even use regex patterns to extract version information from files like Directory.Build.props. However, it is not certified by GitHub and relies on traditional API keys.

In contrast, using the official dotnet CLI combined with OIDC offers superior security and is the recommended path for modern enterprise deployments.

Advanced Release Triggering and Version Management

A common challenge in automation is determining exactly when a package should be published to avoid releasing every single commit.

One effective strategy is the use of git tags. By configuring the workflow to trigger on tags (e.g., tags: ['*']), developers can ensure that only intentional releases are pushed to the NuGet gallery. In a professional workflow, the if: startsWith(github.ref, 'refs/tags/') condition is used to wrap the publishing step, ensuring that the package is only pushed when a tag is present, while the build and test steps run on every push to the main branch.

Another approach involves using tools like dotnet-version-cli. This tool automates the incrementing of version numbers based on the type of release and automatically creates the corresponding git tag. This creates a reliable indicator for the GitHub Actions workflow to execute the release sequence.

There is a known issue where triggering a workflow on both push to a branch and tags can cause the workflow to run twice for the same commit. To solve this, some developers choose to run a single workflow and use a git command to check for the presence of a tag on the current commit, rather than relying on the on: tags trigger.

Configuration for Alternative Package Feeds

While nuget.org is the primary destination, GitHub Actions can also be used to publish to GitHub's own package repository.

When using the GitHub Package repository, the process differs slightly. Instead of OIDC and nuget.org usernames, the workflow typically utilizes the GITHUB_TOKEN provided by the environment. Once the action completes successfully, the package becomes visible on the right-hand side of the GitHub repository interface.

For those utilizing the brandedoutcast/publish-nuget action, the NUGET_SOURCE must be correctly formatted to support version change detection. The expected format is typically https://api.nuget.org/v3-flatcontainer/[PACKAGE_NAME]/index.json.

Detailed Workflow Technical Specifications

For a production-ready OIDC implementation, the following configuration fragments are essential.

The environment setup requires a specific .NET version to ensure build reproducibility:

yaml - uses: actions/setup-dotnet@v4 with: dotnet-version: 10.0.x

The authentication step using the OIDC-to-API key exchange is handled as follows:

yaml - name: NuGet login (OIDC -> temp API key) uses: NuGet/login@v1 id: login with: user: ${{ secrets.NUGET_USER }}

The final push phase, using PowerShell to iterate through all generated packages, is executed with this command structure:

powershell Get-ChildItem artifacts/package/release -Filter *.nupkg | ForEach-Object { dotnet nuget push $_.FullName ` --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" ` --source https://api.nuget.org/v3/index.json }

Conclusion

The transition toward Trusted Publishing via OIDC marks a significant improvement in the security posture of the .NET ecosystem. By moving away from the manual management of API keys—which involves the risks of leakages, rotation failures, and secret sprawl—developers can now rely on the inherent identity of the GitHub Actions runner. The integration of NuGet/login@v1 allows for a seamless handoff between GitHub as the Identity Provider and nuget.org as the service provider. While community actions like brandedoutcast/publish-nuget offer a lower barrier to entry for simple projects, the combination of the dotnet CLI and OIDC provides the most robust, scalable, and secure framework for modern software distribution. The ability to tie these releases to git tags and automated versioning tools ensures that the delivery pipeline is not only automated but also disciplined and predictable.

Sources

  1. Andrew Lock - Easily Publishing NuGet Packages from GitHub Actions with Trusted Publishing
  2. GitHub Marketplace - Publish NuGet
  3. Rafael J Camara - Automate your C# library deployment publishing to NuGet and GitHub Packages with GitHub Actions
  4. Damir's Corner - NuGet Packages and Releases in GitHub Actions

Related Posts