Using GitHub Actions to build .NET projects

Posted by: Damir Arh , on 7/25/2022, in Category C#
Views: 79157
Abstract: The tutorial explains how a .NET Developer can take advantage of GitHub Actions. It presents two ways to create a GitHub Actions workflow for a .NET solution.

GitHub is best known for hosting Git repositories. But there are other services as well. GitHub Actions is one of them. It was first launched in 2018 and has since gained popularity and new features. It is an integrated CI/CD tool that you can use only if you host your code in a GitHub repository. This way, you can avoid having a separate build server or CI/CD service.

GitHub Actions – Your first workflow

A build process in GitHub Actions is called a workflow and is described in a YAML file. Since there is no graphical user interface, you have to write it by hand. This can be daunting to get started with. Fortunately, there are several ways to create an initial YAML workflow file for your project and go from there.

If you are looking for something that will work for most .NET projects, then your best starting point is the workflow template for the dotnet CLI command. It’s not available by default, but you can easily install it from NuGet with the following command:

dotnet new --install TimHeuer.GitHubActions.Templates

After installing the template, you can use it in the same way as any built-in template by running the following command in the root folder of your solution/ repository:

dotnet new workflow

This will create a .yml file in the .github/workflows folder that will run the following commands to build your solution and run the tests:

dotnet restore
dotnet build --configuration Release --no-restore
dotnet test

After you commit the file to your repository and push it to GitHub, it will run for every pull request and commit you push to your main branch. You also have the option to trigger it manually.


Figure 1: Information about a workflow run in progress

If your project is a web application that you want to deploy to an Azure App Service, you should use the Azure Portal instead of the dotnet template to generate the workflow file for you. After you have created the App Service you want to deploy your application to, you can navigate to its Deployment Center page. From there, you can generate the workflow file using the following steps:

  • Select GitHub as the source.
  • Make sure GitHub Actions is selected as the build provider.
  • Sign in with the GitHub account you want to use.
  • Select the organization, repository, and branch to build from.
  • The runtime stack and version should be automatically selected for you.


Figure 2: Creating a GitHub Actions workflow in the Azure Portal

You can preview the file before it is generated. When you save the changes, the file is created in the .github/workflows folder and pushed to the selected branch. This also triggers it immediately so that the application is built and deployed to the selected App Service. You can review the details of the workflow run, including the full logs, on the Actions tab of your GitHub repository page.


Figure 3: Log of a workflow run in GitHub

There are even more ways to generate a starting workflow file. The following deserve at least a brief mention:

· If you navigate to the Actions tab of a GitHub repository that does not yet have a workflow file, you will be presented with a page suggesting several starter templates based on the contents of the repository. After selecting one of them, you will see a preview of the file and have the option to edit it even before committing it to the repository.


Figure 4: Starter templates in GitHub for a new workflow file

· The Visual Studio 2022 wizard for creating a new publish profile for a project includes an option to generate a GitHub Actions workflow file if you choose to deploy to an Azure App Service. You need to set a web URL (https) as remote for your repository checkout for it to become available.


Figure 5: Option to generate a workflow file from the Visual Studio 2022 Publish wizard

Understanding the syntax

Now that we have created and ran a workflow, it’s time to take a closer look at the generated YAML file. We need to understand it before we can customize it to our needs. We will go through the file from top to bottom, explaining it line by line. In the process, we will also become familiar with all the important concepts of GitHub Actions.

The file starts with the name of the workflow:

name: Build and deploy ASP.Net Core app to Azure Web App - dotnet-github-actions

The next block specifies the events that will trigger the workflow:

      - main

This particular value of the on property specifies two event triggers:

  • The first ensures that the workflow will be executed on every push to the main branch.
  • The second adds the option to manually trigger the workflow on any branch or tag. You can do this from the list of workflow runs on the GitHub repository page:


Figure 6: Manually triggering a workflow run

From here in the file, the job definitions begin. Each job has a unique id and consists of a sequence of steps. The first job in the generated workflow is build:

    runs-on: ubuntu-latest

The runs-on property specifies the runner to be used for the job. You can choose between different versions of the operating systems Ubuntu, Windows and macOS. The current, exact list can be found in the official documentation.

A job will execute its steps one by one. By default, it will proceed to the next step only if the previous one was successful. Normally, the first steps of a job set up the environment:

  - uses: actions/checkout@v2

  - name: Set up .NET Core
    uses: actions/setup-dotnet@v1
      dotnet-version: '6.0.x'
      include-prerelease: true

The most important property of these two steps is uses. It defines an action to be executed. Actions are the basic building blocks of GitHub Actions, hence the name. They are all published in the GitHub Marketplace. So that’s where you’ll usually first look to see if an action already exists for what you need to do.

The name of each action corresponds to its repository on GitHub. This means that the name is made up of the name of the organization and the name of the repository within that organization. The actions from the actions organization are official actions published by GitHub.

The two actions in the snippet above do the following:

  • actions/checkout checks out code from the selected branch to the runner’s working directory.
  • actions/setup-dotnet installs the .NET SDK.

The with property is used to set the values for the action’s input parameters. Each action can declare supported required and optional parameters. The step name is optional. If it is not specified, the action name is used to identify a step in the logs.

The next two steps take care of building the project:

- name: Build with dotnet
  run: dotnet build --configuration Release

- name: dotnet publish
  run: dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp

You may notice that they have no uses property. This means that no action is executed. Instead, the run property specifies the command to be executed by the runner. Each step must specify either the uses property or the run property, depending on whether it wants to execute an action or a command. One of these two properties is also the only property required for a step.

The ${{env.DOTNET_ROOT}} part of the output path for the publish command returns the value of the DOTNET_ROOT environment variable. If you look in the log of the workflow run, you can find its value: /home/runner/.dotnet. Although environment variables can be defined in the workflow file, this one wasn’t. It was set by the actions/setup-dotnet action and contains the location of the .NET runtime it installed as specified in the official .NET SDK documentation.

In the snippet above, the two commands build the solution and publish it to a folder. The last step of the first job takes the contents of that folder and puts it into a build artifact:

- name: Upload artifact for deployment job
  uses: actions/upload-artifact@v2
    name: .net-app
    path: ${{env.DOTNET_ROOT}}/myapp

Creating an artifact is the only way to pass build results from one job to another. The runner is deleted along with all files when a job is finished. The created build artifact is downloaded by the second job, but you can also access it from the summary page of the workflow run, for example, to review its contents.


Figure 7: Build artifacts on the summary page of the workflow run

The second job of our workflow will deploy the files from the artifact to the App Service:

  runs-on: ubuntu-latest
  needs: build
    name: 'Production'
    url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

As you can see, there are two properties on this job that we have not seen before.

The needs property requires that the specified job (build in our case) is completed before this job is executed. This makes perfect sense in our case, since this second job requires a build artifact of the first job. Without the needs property, the two jobs would run in parallel.

The environment property specifies the name of the environment in which the application is to be deployed. Environments can be used to categorize individual deployments. For each environment, you will then see a list of all deployments in that environment. For public repositories and enterprise-plan repositories, you can also specify environment-specific protection rules that must be met before deploying to that environment (time delay or required reviewers).

In our case, a url for the deployment is also specified. This adds a link to the last deployment for this environment to open the application. We set it to the value of the output parameter named webapp-url from the step with id deploy-to-webapp, as defined below:

  - name: Download artifact from build job
    uses: actions/download-artifact@v2
      name: .net-app

  - name: Deploy to Azure Web App
    id: deploy-to-webapp
    uses: azure/webapps-deploy@v2
      app-name: 'dotnet-github-actions'
      slot-name: 'Production'
      publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_E64C1F65FD5A45429194BC69F425135D }}
      package: .

The first step downloads the artifact from the previous job to get the files to deploy. It is important that its name matches the name used in the step of the previous job that served to upload the artifact.

In the second step, these files are deployed to the Azure App Service using the official Azure action. This is the only step with the id property in the entire workflow. It is required if you want to access its output parameters later, as we did for the environment URL.

Several properties need to be set for this action:

  • app-name must match the name of the App Service in your Azure account that you want to deploy to.
  • slot-name is the deployment slot in your App Service. The value is unrelated to the GitHub environment with the same name.
  • package is the source folder containing the files to be deployed. This is the folder where the artifact files were downloaded and unpacked.

The publish-profile property deserves additional explanation. Its value is read from a repository secret with the specified name (AZUREAPPSERVICE_PUBLISHPROFILE_E64C1F65FD5A45429194BC69F425135D). Secrets are encrypted values that can be set using the repository Settings tab. They should be used to store sensitive information required for your workflow. This value contains the publish profile for the App Service and was created by Azure Portal when the workflow file was created. Because the value is encrypted, you cannot see it after the secret is created, but you can change it if necessary (for example, if you want to reset the publish profile of your App Service in the Azure Portal).


Figure 8: Repository secrets in GitHub repository settings

If your repository is public or part of an enterprise plan, you can also set a different secret value per environment on the environment settings page.

Extending the workflow

Now that we understand the contents of the workflow file that Azure Portal has created for us, it’s time to add more functionality. We will take a look at a few that may be useful for most .NET projects.

Tooling support

But before we start editing the YAML file, let us see what tools can make our work easier.

Visual Studio 2022 already has full support for GitHub Actions workflow files built in. This means not only highlighting YAML syntax, but also IntelliSense with documentation in tooltips for most properties.

The only downside is that it’s not entirely obvious how to open the YAML file in the editor window. They are not visible in Solution Explorer even if you choose to view all files. To access the YAML files in your solution, you need to open the Publish window for one of the projects. You can then select a file at the top of the window and choose Edit from the More actions drop-down menu to open it in the editor. The result of the last workflow run is also displayed in the same window, along with a link to GitHub where you can see more details.


Figure 9: Accessing workflow files in Visual Studio 2022

If you are instead using Visual Studio Code for your .NET development, you can edit the workflow files there as well. To get the best experience, you should first install the GitHub Actions extension. It adds IntelliSense and tooltip documentation similar to that in Visual Studio 2022, but also a GitHub Actions sidebar view with a list of workflow runs, access to logs, and the ability to manually trigger a workflow.


Figure 10: GitHub Actions sidebar view

To speed up testing changes to workflow files, I highly recommend installing act, a command-line tool that lets you run GitHub Actions workflows locally on your own computer before pushing the change to GitHub. It can be installed manually or with one of the many supported package managers. On Windows, you can use Chocolatey, for example:

choco install act-cli

You must also have Docker installed for the tool to work.

Although it is not an official tool from GitHub, it is still very compatible and in my experience it only fails for actions that interact with the GitHub APIs, for example when uploading an artifact. Still, you can use the tool to catch errors in your workflow file early and test if most steps work as expected. This can save a lot of time and frustration.

Trigger for pull requests

The generated workflow is only run automatically when new commits are pushed to the main branch. It is often beneficial to also run the workflow on every pull request, to see that the code is being built and possibly to perform other checks (e.g., to run the tests, as explained in one of the following sections).


Figure 11: Green tick indicates a pull request with a successful workflow run

To accomplish this, simply add another trigger to the on property:

    - main
    - "**/*.md"
    - "**/*.gitignore"
    - "**/*.gitattributes"

The above snippet triggers a build for each pull request with main as the destination branch. Because of the paths-ignore property, the workflow will not run if all changed files match the listed patterns. This prevents the workflow from running unnecessarily if the changed files have no effect on the result.

Of course, we probably do not want the code from pull requests to be deployed to our App Service by the deploy job. To prevent this, we can add a condition to this job so that it only runs when the workflow is not triggered for a pull request:

  if: github.event_name != 'pull_request'
  runs-on: ubuntu-latest
  needs: build

Cache NuGet packages

Since a new runner is created for each workflow run, this also means that all NuGet packages referenced in your projects must be downloaded each time. As your project grows, so does the number of NuGet packages. To reduce the time it takes to download all the packages, it might be useful to cache them in GitHub Actions. There is an official action that does just that:

- uses: actions/cache@v3
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
    restore-keys: |
      ${{ runner.os }}-nuget-

The above step must be placed before restoring the NuGet packages using the dotnet command. It should work fine for most .NET solutions, but still requires some explanation:

  • The path property contains the path with the files to be cached or restored from the cache.
  • The key property should be different for each set of files in that path. This is because the files will be restored from the cache if a matching key is found there. The above property value depends on the runner’s operating system and a hash computed from all project files (where NuGet dependencies are defined).
  • The restore-keys property may contain a list of fallback partial key matches that are used to find a cache entry from which to restore packages. These are not expected to contain all the required dependencies, but this is not a problem since the missing packages are restored by the dotnet command.

At the end of the job, the action caches the files if an exact key match was not found in the cache when the action was executed. So the next time a workflow with the same dependencies is executed, this new key will be found and the files will be restored from there. If you are curious, you can check the workflow run logs to see if there was a key match or if a new cache entry was added.

Run tests

Hopefully your .NET project also includes some tests. If it does, you’ll probably want to run them as part of your workflow. The easiest way to do this would be to add the following step to the build job (preferably before dotnet publish, so you only run it if the tests are successful):

- name: Test
  run: dotnet test

This command causes the workflow run to fail if a test fails. But you need to search the logs to find more information about it.

You can avoid this by including the test results as a check run, so that they are available on a nicely formatted page next to the logs. To create such a check run, we first need to output the detailed test results to a file for further processing:

- name: Test
  run: dotnet test --logger trx

I chose the TRX format because it is one of the formats supported by the action that creates the check run. It should be called from the step that follows the one in which the tests are executed:

- name: Test Report
  uses: dorny/test-reporter@v1
  if: success() || failure()
    name: Tests
    path: WebApi.Tests/TestResults/*.trx
    reporter: dotnet-trx

The parameters specify the name to be used for the check run, the files to be included, and the format of these files. There is also an if property that ensures that this step is executed even if the previous step failed because not all tests passed.

After you insert the above step and run the workflow again, you should see the tests next to the jobs on the workflow run page.


Figure 12: Test results for a workflow run

If any of the tests fail now, you can check the page to see which one and why.


This article presented two ways to create a GitHub Actions workflow for a .NET solution. Then, the content of the generated file was explained in detail. This opportunity was also used to introduce the main concepts of GitHub Actions, when they are encountered for the first time.

Some useful tools for workflow file editing were suggested. Then, the workflow was enhanced with common features for .NET projects: adding a trigger to run the workflow for pull requests, caching NuGet packages, running tests and including test results as a check run in the workflow run results.

This article was technically reviewed by Daniel Jimenez Garcia.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Damir Arh has many years of experience with software development and maintenance; from complex enterprise software projects to modern consumer-oriented mobile applications. Although he has worked with a wide spectrum of different languages, his favorite language remains C#. In his drive towards better development processes, he is a proponent of Test-driven development, Continuous Integration, and Continuous Deployment. He shares his knowledge by speaking at local user groups and conferences, blogging, and writing articles. He is an awarded Microsoft MVP for .NET since 2012.

Page copy protected against web site content infringement 	by Copyscape

Feedback - Leave us some adulation, criticism and everything in between!