Monorepo NuGet Package Updates with GitHub Actions

2023-06-13

Important

The following is out-of-date.

A much simpler solution is to use the new dependabot beta feature called groups and nuget central package management.

The problem with dependabot

I experimented with Dependabot in a smaller Github repository and wanted to add it to a larger monorepo. The problem is dependabot creates a PR for each dependency upgrade. I even used the open-pull-requests-limit: 10 setting, but with so many projects it opened 100 PRs before I could stop it.

Cleaning up all the PRs wasn't so hard with the help of ChatGPT and the Github CLI:

label="dependencies"
repo="your-org/your-repo"
for pr in $(gh pr list -R $repo --label "$label" --json number -q '.[].number')
do
  gh pr close $pr -R $repo
done

The above is a bash script. You can make it a one-liner by replacing the newlines with ;. No doubt popular LLMs these days, such as ChatGPT, can convert it to PowerShell if you like.

Cleanup all the created branches:

$ git fetch -all
$ git branch -r | grep 'origin/dependabot' \
  | sed 's/origin\///' \
  | xargs -I {} git push origin --delete {}

The solution

The solution is to use GitHub actions to run a script that updates all the nuget packages in the monorepo and opens a single PR. The script uses dotnet-outdated tool to upgrade all packages in each project.

your-repo/upgrade-packages.sh:

#!/usr/bin/env bash
set -e

if ! command -v dotnet-outdated &>/dev/null; then
    echo "Error: dotnet-outdated not found" >&2
    echo "Run:" >&2
    echo "dotnet tool install --global dotnet-outdated-tool" >&2
    exit 1
fi

cleanup() {
    echo "Cleaning up before exiting the script"
    # Remove the added "NoWarn" property
    find . -type f -iname '*.csproj' | xargs -I{} sed -i '/<NoWarn>NU1605<\/NoWarn>/d' {}
}

# Trap the EXIT signal and call the cleanup function.
# Sort of like a try/finally block in C#
trap cleanup EXIT

# Disable the "Detected package downgrade" warning as error because we want to
# update projects without having to do it in depth first traversal dependency
# order.
find . -type f -iname '*.csproj' | xargs -I{} sed -i '/<PropertyGroup>/a<NoWarn>NU1605<\/NoWarn>' {}

find ./Libraries ./Services -type f -iname '*.csproj' \
    -not -path './Libraries/SomeSqlProject/*' \
    -not -path './Services/SomeLegacyService/*' \
    -not -path './Services/SomeLegacyService2/*' |
    xargs -I{} sh -c 'echo "Updating {}"; \
    dotnet outdated {} --upgrade'

The NU1605 "Detected package downgrade" warning is treated as an error by default in newer dotnet projects. This is a problem because I rather not have to do a depth first traversal in dependency order for project updates. Fortunately, it can be overridden by adding a NoWarn property to the project file.

Some of the following takes ideas from this blog post.

your-repo/.github/workflows/upgrade-dependencies.yaml:

---
name: Upgrade nuget dependencies

on:
  workflow_dispatch: # Allow running on-demand
  schedule:
    # Runs every Monday at 8:00 UTC (4:00 Eastern)
    - cron: "0 8 * * 1"

jobs:
  upgrade-nuget-dependencies:
    name: Upgrade & Open Pull Request
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Setup .NET SDK
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: "7.0.x"

      - name: Install dotnet-outdated
        run: dotnet tool install --global dotnet-outdated-tool

      - name: Run upgrade-packages.sh script
        run: |
          chmod +x ./upgrade-packages.sh
          export PATH="$HOME/.dotnet/tools:$PATH"
          ./upgrade-packages.sh

      - name: Create Pull Request
        id: create-pull-request
        uses: peter-evans/create-pull-request@v5
        with:
          commit-message: Automated dependency upgrade
          committer: GitHub [email protected]>
          author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
          branch: feature/auto-dependency-upgrades
          delete-branch: true
          title: "Update Nuget dependencies"
          body: |
            Update report log:
            https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
          labels: |
            dependencies

Note: If you want pull requests created by this action to trigger an on: push or on: pull_request workflow then you cannot use the default GITHUB_TOKEN. See the documentation here for workarounds. — 1

Run GitHub action locally

Install:

  • act to run GitHub actions locally.
  • GitHub CLI: https://cli.github.com/

Now you can run the upgrade-nuget-dependencies GitHub action locally:

act -j upgrade-nuget-dependencies -s GITHUB_TOKEN="$(gh auth token)"

Run upgrade-packages.sh locally

chmod u+x ./upgrade-packages.sh
dotnet tool install -g dotnet-outdated`
./upgrade-packages.sh