Umbraco DevOps Part 3: Implementing a CI/CD Workflow Using GitHub Actions

In part one and part two of the series, you have learned what makes installing Umbraco on Azure different from installing it on-prem and avoiding creating all the needed resources manually using ARM templates.

In the third part of the series, you will automate the deployment of both code and infrastructure using GitHub Actions.

While this article is related to Umbraco deployment to Azure, the concepts and workflow used here can be used to deploy any .NET website.

Introduction to GitHub Actions

Actions is the name of the CI/CD workflow of GitHub. If you used Azure DevOps, this is the feature that corresponds to DevOps Pipelines.

Actions are event-driven, so you can run a workflow with a specific series of operations whenever an event happens: for example, build the project for each pull request that is created.

GitHub Actions is very wide topic, so I'll not cover it here in detail. Still, if you are interested in going beyond the content of this article, I recommend you deep dive into the official documentation on

The Build and Deploy Workflow

We want to build the website and deploy it to Azure every time code is pushed to the main branch. In a real-life situation, the conditions might be different, with approval steps before deploying to production and with multiple environments involved, but let's keep it simple for this article.

Let's see each of the logical steps that are needed to achieve this goal.

Setting up the workflow

Each workflow runs on a separate and isolated virtual machine that GitHub provisions. For this reason, some steps need to be performed to configure the virtual machine for the tasks that we need to perform.

One of the first lines of the file specifies which type of "base" runner (windows, Linux or macOS) we need for our workflow.

runs-on: windows-latest

This is just a basic windows VM, with no tools installed. Our next step is installing and configuring the ones we need:

  • nuget
  • msbuild

Luckily, there is an action for that: setup-msbuild(which sets up both nuget and msbuild).

- name: Add msbuild to PATH
  uses: microsoft/setup-msbuild@v1.0.3

We also want to get the latest code of the repository. It seems obvious, but since actions can also be used for running other types of workflows that don't require the repo's content (for example, on new an issue or PR is created), we need to get the latest version of the code.

- uses: actions/checkout@v2

Building the Project

After the runner setup is complete, we need to download all the dependencies (using nuget) and build the project.

We don't need any action from the marketplace, but we can run the msbuild command with the required parameters.

- name: Restore NuGet packages
  working-directory: ${{env.GITHUB_WORKSPACE}}
  run: nuget restore ${{env.SOLUTION_FILE_PATH}}

- name: Build
  working-directory: ${{env.GITHUB_WORKSPACE}}
  run: msbuild /m /p:Configuration=${{env.BUILD_CONFIGURATION}} ${{env.WEBSITEPROJ_FILE_PATH}}

Notice the usage of the ${{env.***}} syntax through the configuration. They are used to refer to variables defined internally in the workflow or coming from the environment where the workflow is running.

Environment variable are defined at job level and it's a good practice to define file name and path at the top of the workflow in a centralized location, especially if they are reused multiple times.

  # Path to the solution file relative to the root of the project.
  SOLUTION_FILE_PATH: ./src/UmbracoDevOps.sln
  WEBSITEPROJ_FILE_PATH: ./src/UmbracoDevOps.Web/UmbracoDevOps.Web.csproj


Authenticating to Azure

With the code packaged, ready to be deployed, we now have to setup the Azure infrastructure. But to do so, we need to authenticate our workflow to Azure. To do so, we need the Azure/login action.

- name: Azure login
    uses: Azure/login@v1.4.0
    creds: ${{ secrets.AZURE_CREDENTIALS }}

When calling this action, we are passing as credentials a value stored in the secrets management section of GitHub. In this case, the value is the JSON returned by the AZ CLI when you create a service principal using the command:

az ad sp create-for-rbac --name "myApp" --role contributor \
                        --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \

The output is in the format:

  "clientId": "<GUID>",
  "clientSecret": "<GUID>",
  "subscriptionId": "<GUID>",
  "tenantId": "<GUID>",
  "activeDirectoryEndpointUrl": "",
  "resourceManagerEndpointUrl": "",
  "activeDirectoryGraphResourceId": "",
  "sqlManagementEndpointUrl": "",
  "galleryEndpointUrl": "",
  "managementEndpointUrl": ""

Provisioning the Azure Resources

After all the setup, it's finally time to deploy the ARM templates. This is done via the Azure/arm-deploy action.

- uses: azure/arm-deploy@v1
  id: deploy
    resourceGroupName: ${{env.RESOURCE_GROUP}}
    template: ${{env.ARM_TEMPLATE_FILE_PATH}}
    parameters: ${{env.ARM_TEMPLATE_PARAMS_FILE_PATH}} sqlServerPassword=${{ secrets.SQL_SERVER }}
    deploymentName: deployment-from-actions

As parameters, you can specify both the parameter file or override some variables directly in the workflow. In this case, you don't want to commit the SQL Server password in the parameters' file, but you can inject it, taking the value from the secrets.

If you remember from the second part of the series, the deployment template returns an object with the publishing profiles for the web apps.

"publishProfile": {
    "type": "object",
    "value": "[list(concat('Microsoft.Web/sites/', variables('webAppFEName') ,'/config/publishingcredentials'), '2019-08-01')]"
"publishProfileBO": {
    "type": "object",
    "value": "[list(concat('Microsoft.Web/sites/', variables('webAppBOName') ,'/config/publishingcredentials'), '2019-08-01')]"

With GitHub actions we can reference to the output of various steps using steps.deploy.outputs.publishProfile, where deploy is the id used from the deployment step.

Deploy the solution

The final step is taking the package created during the build and deploying it to the web app just created.

The action to use for this task is azure/webapps-deploy.

- name: Deploy frontend website
  uses: azure/webapps-deploy@v2
    app-name: ${{ steps.deploy.outputs.webAppFEName }} 
    publish-profile: ${{ steps.deploy.outputs.publishProfile  }}
    package: ${{env.WEBSITEPROJ_PATH}}

Since our Umbraco project is set up with two web apps, the front and backend, you have to run the deployment twice.

You can see the complete version of workflow on the repository on GitHub.


In this article we have connected the dots and setup a fully automated and repeatable CI/CD pipeline that builds the code committed, provisions the Azure resources needed to host the site and finally deploys the code.

This workflow still lacks one of the main features promised in the introduction to the series: it still does not support multiple environments. Deployments to multiple environments will be covered in a bonus part that will be published at the beginning of 2022.

List of GitHub Actions Used

If you want to learn more about each action used in this example, here is the list of all of them.


Articles in this series

Simone Chiaretta

Simone Chiaretta is a long time Umbraco user and developer, working at the Council of the European Union where the whole public web presence is now based exclusively on Umbraco. Simone was a Microsoft MVP for 8 years; he authored several books on ASP.NET MVC and gave speeches at various international conferences, including 5 editions of Codegarden. He is also a member of the Umbraco Unicore team. When not working on code he spoils his recently born son and is racing in triathlon events
comments powered by Disqus