/dotnet-patch-automation-sample

Sample repository that demonstrates automated monthly patching of .NET applications

Primary LanguageC#Apache License 2.0Apache-2.0

.NET Patch Automation Sample

Build status OpenSSF Scorecard

Introduction

With the productivity and performance benefits developers gain from using modern .NET over .NET Framework, also comes the less-exciting flip-side - patching the version of .NET in production environments every month to keep your applications secure. ๐Ÿ“† ๐Ÿ”‘

Keeping up-to-date with security and reliability fixes is an important ongoing activity within software development, but itโ€™s not very exciting, and it can be easy to fall behind - what if we could automate the process of patching our applications? ๐Ÿค–

This sample repository demonstrates how we can use the flexibility of GitHub Actions together with tools such as dotnet-outdated to automatically patch .NET applications on a monthly basis with minimal manual effort. ๐Ÿš€

The Problem

Previously, Microsoft did not make automatic updates to .NET available through Windows Update. Although this has been available since April 2022, it is still not enabled by default and only applies to Windows .NET applications which rely on .NET being installed on the machine.

For self-contained deployments and applications deployed to any other operating system, such as Linux, there is still no mechanism for applying .NET patches automatically. This requires developers to update their applications month-to-month to ensure they stay secure (and supported).

New updates are announced in the dotnet/announcements repository, as well as any individual CVEs that apply. Developers need to check these announcements on a regular cadence to ensure they are aware of any updates that are available that need to be applied to the applications they maintain. In a distrubuted software architecture, this can potentially be a significant number of applications to update.

This maintenance burden eats into the time developers have to work on new features and other changes that bring meaningful value to their applications and their businesses.

By tapping into the machine-readable .NET release notes and harnessing the power of GitHub Actions, we can automate the process of updating applications whose code is stored in GitHub to remove much of the manual burden of keeping applications patched and up-to-date.

How it Works

This repository contains a GitHub Actions workflow that uses a GitHub Actions reusable workflow from the martincostello/update-dotnet-sdk repository. This workflow uses the Update .NET SDK GitHub Action to check for updates to the .NET SDK compared to the version specified in the global.json file that is checked into this repository.

If an update is available, the workflow will also check for any Microsoft-published NuGet packages that are eligible to be updated as part of the same release of the .NET SDK and update those too.

If an update to the .NET SDK was found, the workflow will create a pull request with the changes targeting the default branch of the repository. This repository's continuous integration will then run against the pull request to ensure that the changes do not break the application.

The changes and pull request are performed on behalf of a GitHub App. A GitHub app is used to allow the workflow to create pull requests on behalf of the application, rather than act as a specific user. The GitHub app's private key is used to generate a GitHub access token to use to authenticate with the GitHub repository. This is used instead of GITHUB_TOKEN to overcome restrictions to triggering other workflows which would otherwise prevent the GitHub Actions continuous integration from running as part of pull requests.

When the pull request is opened, a second workflow will run to verify whether the pull request contains the changes that are expected from such an update, such as the account it was raised from and what was changed as part of the pull request. If the changes are those which are expected, then the pull request will be approved and auto-merge will be enabled.

Assuming that the CI build passes, the pull request will then be merged automatically, completing the patch updates.

More information about the reusable workflow can be found here.

The ASP.NET Core application used to demonstrate the automation process is based on the Todo App sample from my Integration Testing ASP.NET Core Minimal APIs repository.

Updating the .NET SDK

The martincostello/update-dotnet-sdk GitHub Action is used to check for and apply updates to the .NET SDK. This action uses the .NET release notes JSON files in the dotnet/core repository to determine the latest version of the .NET SDK compared to the version in the global.json file of a repository. For example, the releases for .NET 6 can be found in the release-notes/6.0/releases.json file.

If an update is available, the action will update the the global.json file to use the latest version of the .NET SDK that is available for that release channel (6.0, 7.0, 8.0 etc.) and then open a pull request with the changes.

Updating NuGet packages

If an update to the .NET SDK is made, the workflow will also check for any Microsoft-published NuGet packages that were made available as part of the same release of the .NET SDK. For example, new versions of the Microsoft.AspNetCore.* packages are published for each new .NET patch release.

By default, only packages with the following ID prefixes are updated as part of these checks:

  • Microsoft.AspNetCore.
  • Microsoft.EntityFrameworkCore.
  • Microsoft.Extensions.
  • System.Text.Json

The list of packages that are updated can be changed by specifiying them as a comma-separated list via the include-nuget-packages input parameter to the GitHub Actions workflow.

These package updates are checked for and applied by the dotnet-outdated .NET global tool. More information about dotnet-outdated can be found here.

The package updates are constrained to the same release channel as the NuGet package references in the application already used. For example, if .NET 6 packages are used, then only patch updates for the 6.0.x NuGet packages will be applied; any package updates for .NET 7 (or later) are ignored.

Approving and Merging Pull Requests

Once the pull request for any updates is opened, another GitHub workflow will run to verify that that changes included in that pull request match the expected changes for a .NET SDK update.

This workflow only runs when the pull request is opened by the same account that is configured for the GitHub app that is used to apply the .NET SDK updates. This is to prevent the workflow from running against pull requests created by any other GitHub user.

Both of the tools used to apply the Git commits write their commit messages in the same format as Dependabot does, by including some machine-readable YAML in the commit message. This block of YAML includes the names of each package that is updated, as well as the SemVer major/minor/patch update type for each package. This is based on the approach used by the Fetch Metadata from Dependabot PRs GitHub action that can be used to auto-approve Dependabot updates.

For example, here is the message for a commit that updates the .NET SDK to version 7.0.203:

Update .NET SDK
Update .NET SDK to version 7.0.203.

---
updated-dependencies:
- dependency-name: Microsoft.NET.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

The workflow parses this information to check for which packages were updated. If the packages that were updated all match the list of allowed prefixes, all of the commits were authored by the GitHub user that opened the pull request, and all of the updates are only for a patch release, then the workflow determines that the pull request is "trusted" and is safe to approve and merge.

If the changes are as expected, then the workflow will approve the pull request and enable auto-merge. Assuming that the continuous integration succeeds, which is used as a measure of quality and stability of the changes in the pull request, then the pull request will be merged to the default branch automatically with no human intervention required.

If any of the required status checks fail, then the pull request will be left in a pending state for a human to review and determine the appropriate course of action to take.

If any unexpected changes are present in the pull request, then the pull request will not be approved and auto-merge will not be enabled. If these changes were introduced after the pull request was already approved, then the review will be dismissed and auto-merge will be disabled.

Further Considerations

The workflow in this sample is designed to be as safe as possible while being easy to set up, but there are some aspects that are not covered by the workflow in the aim of simplicity that you may want to consider before adopting this approach for your applications.

Branch Protections

This sample repository is set up with the following branch protections for the default branch:

  • A pull request us required before merging
  • Status checks must pass before merging
  • No accounts are allowed to bypass the branch protections

These requirements protect the default branch from having the automated patches from being merged in without them being validated as not breaking the application and having a "second pair of eyes" review the changes on the pull request by a second GitHub account/app.

In order for the pull requests to be auto-merged, the number of required reviewers cannot be more than one and code owner review cannot be required. This is because GitHub apps cannot be made code owners of a repository, and requiring more than one reviewer would still require a human to approve the pull request. This could be overcome with multiple appoval bots, but that would likely be excessive to configure.

The accounts used to open the pull requests and approve the pull requests must be different accounts as GitHub does not allow an account to approve its own pull request.

Deployment tests

If you practice continuous deployment, then you may want to consider adding tests as part of your deployment process to ensure that the application is still working as expected after the pull request has been merged to your default branch. This helps validate that the changes in the pull request do not break the application for any functionality that is not exercised by your continuous integration's tests and that the changes are safe to deploy into your production environment.

Examples

Pull Requests

Below are links to example pull requests demonstrating different behaviours of the workflows.

Workflows

Further examples of workflows for updating the .NET SDK with different types of GitHub credentials can be found in the README of the update-dotnet-sdk action.

Feedback

Any feedback or issues can be added to the issues for this project in GitHub.

Repository

The repository is hosted in GitHub: https://github.com/martincostello/dotnet-patch-automation-sample.git

License

This project is licensed under the Apache 2.0 license.