{"id":56371,"date":"2025-04-30T10:05:00","date_gmt":"2025-04-30T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=56371"},"modified":"2025-04-30T10:05:00","modified_gmt":"2025-04-30T17:05:00","slug":"dotnet-maui-libraries-github-actions","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-maui-libraries-github-actions\/","title":{"rendered":"Packaging and Publishing a .NET MAUI Library with GitHub Actions"},"content":{"rendered":"<p>This is a continuation of the blog <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/devops-for-dotnet-maui\/\">Getting Started with DevOps and .NET MAUI<\/a> showcasing how to build a .NET MAUI Library using DevOps. This blog uses a sample project made <a href=\"https:\/\/devblogs.microsoft.com\/xamarin\/author\/mike-parker\/\">Mike Parker<\/a> that shows how to use MSBuild files in NuGet packages. In this blog, the focus is on the GitHub Actions workflow to automate the building, packaging, and publishing of the <a href=\"https:\/\/aka.ms\/maui-lib-devops\">sample .NET MAUI Library<\/a>.<\/p>\n<h2>Overall Workflow Overview<\/h2>\n<p>The <a href=\"https:\/\/github.com\/dotnet\/maui-samples\/blob\/main\/9.0\/Packaging\/NuGetWithMSBuildFiles\/.github\/workflows\/build_publish_nuget.yml\"><code>build_publish_nuget.yml<\/code><\/a> defines the end-to-end workflow for building and publishing the .NET MAUI Library NuGet Package. The <a href=\"#versioning-of-the-nuget-package\">versioning step<\/a> handles versioning of the NuGet package for every build via GitHub Actions. The workflow consists of two main jobs: <code>buildLibrary<\/code> and <code>publish<\/code>. These jobs use <code>dotnet<\/code> commands to build and pack the .NET project. The <code>publish<\/code> job depends on the <code>buildLibrary<\/code> job completing successfully. Once you have created the NuGet package, we can push it to <a href=\"https:\/\/www.nuget.org\">nuget.org<\/a> (or an Azure DevOps Internal Feed).<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/devops_overview.png\" alt=\" Overall Workflow Image\" \/><\/p>\n<h2>Versioning of the NuGet Package<\/h2>\n<p>The versioning of the NuGet package is handled dynamically within the GitHub Actions workflow. The version is composed of the major version and the GitHub run ID, ensuring a unique version number for each workflow run.<\/p>\n<h3>Setting the Version<\/h3>\n<p>In the <code>buildLibrary<\/code> job, the <code>VERSION<\/code> variable is set using the <code>MAJORVERSION<\/code> environment variable and the <code>GITHUB_RUN_ID<\/code>:<\/p>\n<pre><code class=\"language-yaml\">- name: Set VERSION variable from tag\n  run: |\n    echo \"VERSION=$MAJORVERSION-$GITHUB_RUN_ID\" &gt;&gt; $GITHUB_ENV<\/code><\/pre>\n<p>This results in a version format like <code>1.0.0-1234<\/code>, where <code>1.0.0<\/code> is the major version and <code>1234<\/code> is the GitHub run ID.<\/p>\n<h3>Using the Version<\/h3>\n<p>The <code>VERSION<\/code> variable is then used in the <code>dotnet pack<\/code> command to set the package version:<\/p>\n<pre><code class=\"language-yaml\">- name: Pack Library\n  run: |\n    dotnet pack &lt;PATH TO PROJECT&gt; -p:PackageVersion=$VERSION -c Release<\/code><\/pre>\n<p>This ensures that each NuGet package, built by the workflow, has a unique version preventing conflicts and making it easy to track and manage different builds.<\/p>\n<h2>Secure and Sign NuGet Package<\/h2>\n<p>A signed package allows for content integrity verification checks which provides protection against content tampering. The package signature also serves as the single source of truth about the actual origin of the package and help prove package authenticity for the consumer.<\/p>\n<p>The NuGet Documentation provides detailed steps on how to <a href=\"https:\/\/learn.microsoft.com\/nuget\/create-packages\/sign-a-package#sign-the-package\">sign the package<\/a> along with how to get a signing certificate. To implement this step in the workflow, update the Pack Library step:<\/p>\n<pre><code class=\"language-yaml\">- name: Pack Library\n  run: |\n    dotnet pack &lt;PATH TO PROJECT&gt; -p:PackageVersion=$VERSION -c Release\n    dotnet nuget sign MyPackage.nupkg --certificate-path &lt;PathToTheCertificate&gt; --timestamper &lt;TimestampServiceURL&gt;\n<\/code><\/pre>\n<p>The certificate can be added to GitHub Actions by following the <a href=\"https:\/\/docs.github.com\/actions\/use-cases-and-examples\/deploying\/installing-an-apple-certificate-on-macos-runners-for-xcode-development\">deployment documentation<\/a>.<\/p>\n<h2>Pushing to NuGet.org or Azure DevOps Internal Feed<\/h2>\n<p>The workflow sample shows how to create a GitHub Release and push to GitHub Actions artifacts as a basic use case. The workflow also includes steps for pushing the NuGet package to NuGet.org or Azure DevOps internal feed as well, based on your use case. Let&#8217;s look at how these steps can be customized to different use cases.<\/p>\n<h3>Pushing to NuGet.org<\/h3>\n<p>To push the package to NuGet.org, use the following steps in the <code>publish<\/code> job and add your NuGet API key:<\/p>\n<pre><code class=\"language-yaml\">- name: Push nuget to NuGet.org\n  run: |\n    dotnet nuget push &lt;your_package_name&gt;.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https:\/\/api.nuget.org\/v3\/index.json --skip-duplicate<\/code><\/pre>\n<p>You can obtain the API key from your <a href=\"https:\/\/www.nuget.org\/account\/apikeys\">NuGet.org account<\/a>. Make sure to set the <code>NUGET_API_KEY<\/code> secret variable in your GitHub repository&#8217;s secrets. The <a href=\"https:\/\/learn.microsoft.com\/nuget\/quickstart\/create-and-publish-a-package-using-the-dotnet-cli#publish-with-dotnet-nuget-push\">publish with nuget push<\/a> documentation explains the other overrides available and provides troubleshooting steps for errors.<\/p>\n<h3>Pushing to Azure DevOps Internal Feed<\/h3>\n<p>To push the package to an Azure DevOps internal feed, use the following steps in the <code>publish<\/code> job and configure the feed URL and API key:<\/p>\n<pre><code class=\"language-yaml\">\n- name: Setup .NET Core\n  uses: actions\/setup-dotnet@v1\n  with:\n    dotnet-version: ${{ env.DOTNETVERSION }}\n    source-url: ${{ env.AZURE_ARTIFACTS_FEED_URL }}\n  env:\n    NUGET_AUTH_TOKEN: ${{ secrets.AZURE_DEVOPS_TOKEN }} \n\n- name: Push nuget to Azure DevOps Internal Feed\n  run: |\n    dotnet nuget push --api-key AzureArtifacts -s &lt;your_package_name&gt;.nupkg\n<\/code><\/pre>\n<p>Replace <code>&lt;AZURE_ARTIFACTS_FEED_URL&gt;<\/code> with the URL of your Azure Artifacts feed. Make sure to set the <code>AZURE_DEVOPS_TOKEN<\/code> secret variable in your GitHub repository&#8217;s secrets. The <a href=\"https:\/\/learn.microsoft.com\/azure\/devops\/artifacts\/quickstarts\/github-actions?view=azure-devops&amp;pivots=pat\">Azure Artifacts Documentation<\/a> explains the how to get the <code>AZURE_DEVOPS_TOKEN<\/code> in detail.<\/p>\n<h2>Storing and Using Secret Values in GitHub Actions<\/h2>\n<p>To securely store and use sensitive information, such as API keys and tokens, in your GitHub Actions workflows you can use <a href=\"https:\/\/docs.github.com\/actions\/security-for-github-actions\/security-guides\/using-secrets-in-github-actions\">GitHub Secrets<\/a>. Follow these steps to store and access secret values:<\/p>\n<ol>\n<li>Navigate to your repository on GitHub and click on the <strong>&#8220;Settings&#8221;<\/strong> tab.<\/li>\n<li>In the left sidebar, click on <strong>&#8220;Secrets and variables&#8221;<\/strong>, then click on <strong>&#8220;Actions&#8221;<\/strong>.<\/li>\n<li>Click the <strong>&#8220;New repository secret&#8221;<\/strong> button and add a new secret:\n<ul>\n<li><strong>Name:<\/strong> Enter a name for the secret (e.g., <code>NUGET_API_KEY<\/code>).<\/li>\n<li><strong>Value:<\/strong> Enter the secret value (e.g., your NuGet API key).<\/li>\n<\/ul>\n<\/li>\n<li>Click the <strong>&#8220;Add secret&#8221;<\/strong> button.<\/li>\n<\/ol>\n<h3>Using Secrets in Workflows<\/h3>\n<p>Once you have added secrets to your repository, you can access them in your workflows using the <code>${{ secrets.SECRET_NAME }}<\/code> syntax. For example:<\/p>\n<pre><code class=\"language-yaml\">- name: Push nuget to NuGet.org\n  run: |\n    dotnet nuget push **\/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https:\/\/api.nuget.org\/v3\/index.json --skip-duplicate<\/code><\/pre>\n<p>In this example, <code>NUGET_API_KEY<\/code> is the name of the secret that you added to your repository.<\/p>\n<h2>Create Release in GitHub<\/h2>\n<p>Another step in this Github workflow is setup to automatically create a GitHub Release from GitHub Actions. This can be done using the <a href=\"https:\/\/cli.github.com\/\">GitHub CLI<\/a> and then, in this sample, we use the default settings of the task to implement a simple version of release note for each version released.<\/p>\n<pre><code class=\"language-yaml\">- name: Create Release and Upload Artifact to Release\n  run: gh release create ${{env.VERSION_NUMBER}} -t ${{env.VERSION_NUMBER}} *.nupkg --generate-notes<\/code><\/pre>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/04\/github_release.png\" alt=\"GitHub Releases Image\" \/><\/p>\n<p>In this sample, the syntax for the Release is to use the version number of the nuget package. We also opt to upload the NuGet package, generated at part of the Release, and also use the default <code>--generate-notes<\/code> command. This step is very customizable. For example, you can control triggers based on different tags and also make custom release notes. The <a href=\"https:\/\/cli.github.com\/manual\/gh_release_create\">GitHub CLI Release Create<\/a> documentation explains these options in detail.<\/p>\n<blockquote>\n<p>[!NOTE]\nTo use the GitHub CLI in GitHub Actions, we use an automatic secrets value for <a href=\"https:\/\/docs.github.com\/actions\/security-for-github-actions\/security-guides\/automatic-token-authentication\">GITHUB_TOKEN<\/a> providing the <code>write-all<\/code> <a href=\"https:\/\/docs.github.com\/actions\/writing-workflows\/choosing-what-your-workflow-does\/controlling-permissions-for-github_token#overview\">permission<\/a> in order to generate the release notes.<\/p>\n<\/blockquote>\n<h2>Summary<\/h2>\n<p>This blog shows how to setup a basic GitHub Actions workflow to build and publish a .NET MAUI Library. It shows how to build, pack, and sign the NuGet package and, based on the use case, publish the package to NuGet.org, Azure DevOps Internal Feed, and create a GitHub Release. This basic workflow is customizable and can be extended to meet more complex requirements by following the linked documentation.<\/p>\n<p>Check out the more samples at <a href=\"https:\/\/github.com\/dotnet\/maui-samples\">dotnet\/maui-samples<\/a>. Please let us know if anything is unclear or what you would like to see in follow up posts.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this post, learn how to setup a DevOps pipeline to build and publish a .NET MAUI library with GitHub Actions.<\/p>\n","protected":false},"author":1969,"featured_media":56372,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7233],"tags":[7238,7605,7608,104],"class_list":["post-56371","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-maui","tag-net-maui","tag-devops","tag-github-actions","tag-nuget"],"acf":[],"blog_post_summary":"<p>In this post, learn how to setup a DevOps pipeline to build and publish a .NET MAUI library with GitHub Actions.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/56371","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/1969"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=56371"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/56371\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/56372"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=56371"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=56371"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=56371"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}