Automate .NET Nuget Package using Github Actions

Automate .NET Nuget Package using Github Actions

Utilise Github actions to Pack and Publish Package

Chenna's photo
Chenna
Β·May 8, 2022Β·

11 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

Github Actions are configurable yml files to automate all software workflows

Before start better if we are aware of limitations, as Github user of a free plan every month every user gets a free generous 2000 minutes of build (which are more than enough for most users)

At the end of the article you'll be able to automate pack and publish as well and auto-updates your dependent repositories

Create a basic workflow

Create .github/workflows/nuget.yml in your repository

Workflow definition

Here we only want packages to be published only for develop and release branches

name: Create Nuget Packages
on:
  push:
    branches: # Branches for which packages will be created
      - develop
      - release
    paths: # Start GitHub action only if the below file patterns are changed
      - '**/*.cs'
      - '**/*.csproj'
env:
  GITHUB_TOKEN: ${{ secrets.CHAN_PAT }}
  SHA7: ${{ github.event.head_commit.id }}
Define first job

Concurrency ensures that at any given time only one GitHub action workflow should be running.

Github's ubuntu-latest image by default comes preinstalled .NET Core 3+ to 6

jobs:
  PublishPackages: # Name of the job
    concurrency: # Prevents multiple github actions, we don't want version conflicts
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true
    runs-on: ubuntu-latest
   outputs: # Explaination followed in second part of the article
      buildVersion: ${{ steps.dateStep.outputs.buildVersion }}
      buildMode: ${{ steps.dateStep.outputs.buildMode }}
      sha7: ${{ steps.dateStep.outputs.sha7 }}
      bumpBranch: ${{ steps.dateStep.outputs.bumpBranch }}
      buildChangeSet: ${{ steps.dateStep.outputs.buildChangeSet }}
    steps:

Step 1: Clone current Repo

fetch-depth mentions checkout action to fetch only the head commit, not the entire git history (saves some time)

- uses: actions/checkout@v3
  name: "Checkout current repo"
  with:
    fetch-depth: 1
    token: ${{ secrets.CHAN_PAT }}

Step 2: Set TimeZone

You will be using CalendarVer, so it's suggested to set timezone as per IANA Db

- name: Set Indian Time Zone
  run: sudo timedatectl set-timezone "Asia/Calcutta"

Step 3: Set Build Variables

You might want to use different versions for develop and release branches, here you're setting calendarVer and setting buildMode conditionally

For develop branch

For release branch

NameDescription
sha7Git commit hash length is 40, as shorten it to seven
buildVersionEach package will need to have incremental versioning, so here we have to use calendarVer
buildMode.NET Build Mode: Debug or Release
bumpBranchWill be used for later matrix jobs
- name: Set Build Variables
  id: dateStep
  run: |
    export sha7=${SHA7::7}
    export buildVersion=$(date '+%y.%-m.%-d.%-H%M-alpha')
    export buildMode='Debug'
    if [ $GITHUB_REF_NAME = "release" ]
    then
        export buildVersion=$(date '+%y.%-m.%-d.%-H%M')
        export buildMode='Release'
        export bumpBranch='release'
    fi
    echo "::set-output name=buildVersion::$buildVersion"
    echo "::set-output name=buildMode::$buildMode"
    echo "::set-output name=bumpBranch::$bumpBranch"
    echo "::set-output name=sha7::$sha7"
    echo "::set-output name=buildChangeSet::$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.sha }} | grep .yml$)"

Step 4: Set Nuget Source

Skip this step if you are publishing to the official NuGet source

- name: Set Nuget Source
  run: |
    dotnet nuget add source --username <githubusername_orgname> \
      --password ${{ secrets.CHAN_PAT }} \
      --store-password-in-clear-text --name github \
      "https://nuget.pkg.github.com/<githubusername_orgname>/index.json"

Step 5: Restore cache

Just like node_modules, NuGet packages are also large in size, so it is always better to restore cache and preserve GitHub build minutes

- name: Restore cache
  uses: actions/cache@v3
  with:
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} #hash of project files
    restore-keys: |
      ${{ runner.os }}-nuget-

Step 6: Build

Here you are using output from Step 3 to use either Debug or Release mode

- name: Build
   run: dotnet build -c ${{ steps.dateStep.outputs.buildMode }}

Step 7: Pack

ParameterDescription
-cconfiguration - default is Debug, you might want to use Release
/p:VersionEach package will need to have incremental versioning, so here we have use calendarVer
-oOutput path
- name: Package
  run: |
    dotnet pack <projectname>.csproj \
      -c ${{ steps.dateStep.outputs.buildMode }} \
      --no-build -o artifacts \
      /p:Version=${{ steps.dateStep.outputs.buildVersion }}

Step 8: Publish

To Github

- name: Publish Nuget
  run: |
    dotnet nuget push "artifacts/*" --source "github" \
      --api-key ${{ secrets.CHAN_PAT }} --skip-duplicate

To official Nuget

- name: Publish Nuget
  run: |
    dotnet nuget push "artifacts/*" \
      -s https://api.nuget.org/v3/index.json \
      -k "<nuget_token>"

Yipee πŸ₯³, if all goes your Nuget package workflow is ready and will be auto version incremented and published

🚨Github takes a few minutes to reflect the package for the first time, later consecutive versions are shown quickly

https://github.com/<org/username>?tab=packages&repo_name=<github_reponame>

Auto-update package versions

Let's assume you were publishing at-least one package a day, and if working in a team, it's your responsibility to convey to other members to update the shared package in their project as well.

Here you'll have two such projects, one project which your team manages, and another project which some other team manages. Assuming you want to instant package updates in your project MyApp1 and create a PR in other team projects MyApp2.

We'll be making use of outputs of previous jobs, like buildVerion and buildMode

Second job Definition

Here you are mentioning that this BumpVersions job is dependent on PublishPackages job

Here you are matrix job strategy, which helps us with repetitive tasks and reduces the code duplication and saves time by running parallel jobs.

Observe that below you mentioned two matrix properties, this way we can have up to 256 parallel jobs. Indirectly if you needed you can update around 256 repos in under 5 minutes

BumpVersions:
  runs-on: ubuntu-latest
  needs: PublishPackages
  strategy:
    matrix:
      include:
        - repository: "MyApp1"
          createPR: true
        - repository: "MyApp2"
          createPR: false

Step 1: Clone Repo

Observe that you are making use of matrix properties to clone repositories

- uses: actions/checkout@v3
  name: 'Checkout ${{ matrix.repository }}'
  with:
    repository: <githubusername_orgname>/${{ matrix.repository }}
    token: ${{ secrets.GH_PAT }}
    ref: ${{ needs.PublishPackages.outputs.bumpBranch }}
    path: ${{ matrix.repository }}

Step 2: Nuget Source

Similar to Step 4 of part 1, you can skip this step if you are publishing to the official NuGet source

- name: Set Nuget Source
  run: |
    dotnet nuget add source --username <githubusername_orgname> \
      --password ${{ secrets.CHAN_PAT }} \
      --store-password-in-clear-text --name github "https://nuget.pkg.github.com/<githubusername_orgname>/index.json"

Step 3: Configure Git

We need to commit and push, so it is mandatory to tell git who we are

- name: Configure Git
  run: |
    git config --global user.name "github-actions"
    git config --global user.email "github-actions[bot]@users.noreply.github.com"

Step 4: Build & Update reference

Here we conditionally check if this repo needs a direct update or just to raise PR

Here's the requirement

RepoBranchCreate PR or Direct Push
MyApp1DevelopDirect push to develop branch
MyApp2DevelopCreate ops-develop and raise PR
  • If createPr is true, we are checking out to a new branch

Later you are updating the package version based on the matrix parameterised version

  • dotnet build will throw an error if the latest NuGet version is incompatible. If the build succeeded, you are committing with the base message as Bump to ${{ github.event.head_commit.message }} and in optional extended message you are mentioning sha7 commit id

  • Check if createPr is true, if true you'll be force pushing to latest version to the existing branch, otherwise direct push

- name: Bump ${{ matrix.repository }}
  run: |
    if [ "${{ matrix.createPR }}" == true ]
    then
      git checkout -b ops-${{ needs.PublishPackages.outputs.bumpBranch }}
      echo 'Creating PR Branch 🐌'
    else
      echo 'Directly update the package 🏎'
    fi
    dotnet add package <package_name> --version ${{ needs.PublishPackages.outputs.buildVersion }}
    dotnet build
    git commit -a \
      -m "Bump to ${{ github.event.head_commit.message }}" \
      -m "Version <githubusername_orgname>/<nuget_package_repo_name>@${{ needs.PublishPackages.outputs.sha7 }}"
    if [ "${{ matrix.createPR }}" == true ]
    then
      git push origin +ops-${{ needs.PublishPackages.outputs.bumpBranch }}
    else
      git push
    fi

Step 5: Create PR

For the repo's which don't need PR to be created, you'll skip this step based on the if condition in step.

Here we are making use of in-build github-cli to read or create pull requests.

  • In the first line prStatus variable is used to read if there are any existing open PR's based on the destination branch
  • If PR doesn't exist, you are creating new PR
  • Otherwise, update the PR title and its body
- name: Check & Create PR
  if: ${{ matrix.createPR }}
  run: |
    export prStatus=$(gh pr list --head ops-${{ needs.PublishPackages.outputs.bumpBranch }} --state open --base ${{ needs.PublishPackages.outputs.bumpBranch }} --json number | grep -o '[[:digit:]]*')
    if [ -z "$prStatus" ]
    then
      gh pr create --title "Bump to \`${{ needs.PublishPackages.outputs.buildVersion }}\`" --body "<nuget_package_repo_name>: \`${{needs.PublishPackages.outputs.buildVersion}}\` <br/> <githubusername_orgname>/<nuget_package_repo_name>@${{ needs.PublishPackages.outputs.sha7 }} <br/>ChangeSet<br/> \`\`\`\r\n${{ needs.PublishPackages.outputs.buildChangeSet }}\r\n\`\`\`" --base ${{ needs.PublishPackages.outputs.bumpBranch }}
    else
      echo "PR Exists" https://github.com/<githubusername_orgname>/${{ matrix.repository }}/pulls/$prStatus
      gh pr edit $prStatus --title "Bump to \`${{ needs.PublishPackages.outputs.buildVersion }}\`" --body "<nuget_package_repo_name>: \`${{needs.PublishPackages.outputs.buildVersion}}\` <br/> <githubusername_orgname>/<nuget_package_repo_name>@${{ needs.PublishPackages.outputs.sha7 }} <br/> \`\`\`\r\n${{ needs.PublishPackages.outputs.buildChangeSet }}\r\n\`\`\`"
    fi

Final workflow file

name: Create Nuget Packages
on:
  push:
    branches: # Branches for which packages will be created
      - develop
      - release
    paths: # Start GitHub action only if the below file patterns are changed
      - '**/*.cs'
      - '**/*.csproj'
  workflow_dispatch:

env:
  GITHUB_TOKEN: ${{ secrets.CHAN_PAT }} # Then the token which we generated earlier

jobs:
  PublishPackages: # Name of the job
    concurrency: # Prevents multiple github actions, we don't want version conflicts
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true
    runs-on: ubuntu-latest # ubuntu-latest has .NET 5 & 6 already installed
    outputs: # To use in other Jobs - explaination at the end of the article
      buildVersion: ${{ steps.dateStep.outputs.buildVersion }}
      buildMode: ${{ steps.dateStep.outputs.buildMode }}
      sha7: ${{ steps.dateStep.outputs.sha7 }}
      bumpBranch: ${{ steps.dateStep.outputs.bumpBranch }}
      buildChangeSet: ${{ steps.dateStep.outputs.buildChangeSet }}
    steps:
      - uses: actions/checkout@v3
        name: "Checkout current repo"
        with:
          fetch-depth: 1
          token: ${{ secrets.CHAN_PAT }}

      - name: Set Indian Time Zone # We can set timezone as per IANA
        run: sudo timedatectl set-timezone "Asia/Calcutta"

      # We might want to use different versions for develop and release
      # Here I'm setting calendarVer and setting build mode
      - name: Set Build Variables
        id: dateStep
        run: |
          export buildVersion=$(date '+%y.%-m.%-d.%-H%M-alpha')
          export buildMode='Debug'
          if [ $GITHUB_REF_NAME = "release" ]
          then
              export buildVersion=$(date '+%y.%-m.%-d.%-H%M')
              export buildMode='Release'
              export bumpBranch='release'
          fi
          echo "::set-output name=buildVersion::$buildVersion"
          echo "::set-output name=buildMode::$buildMode"
          echo "::set-output name=bumpBranch::$bumpBranch"
          echo "::set-output name=sha7::$sha7"
          echo "::set-output name=buildChangeSet::$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.sha }} | grep .yml$)"

      # Adding Nuget source configuration
      - name: Set Nuget Source
        run: |
          dotnet nuget add source --username <githubusername_orgname> \
            --password ${{ secrets.CHAN_PAT }} \
            --store-password-in-clear-text --name github "https://nuget.pkg.github.com/<githubusername_orgname>/index.json"

      # Using cache
      - name: Restore cache
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} #hash of project files
          restore-keys: |
            ${{ runner.os }}-nuget-

      # Building here, and here build mode is parameterized
      - name: Build
        run: dotnet build -c ${{ steps.dateStep.outputs.buildMode }}

      # Packaging class library, as we already build in last step
      # we have used --no-build
      - name: Package
        run: |
          dotnet pack <projectname>.csproj \
            -c ${{ steps.dateStep.outputs.buildMode }} \
            --no-build -o artifacts \
            /p:Version=${{ steps.dateStep.outputs.buildVersion }}

      # Finally publishing to Nuget
      - name: Publish Nuget
        run: |
          dotnet nuget push "artifacts/*" --source "github" \
            --api-key ${{ secrets.CHAN_PAT }} --skip-duplicate


  # 🚨 Matrix builds makes it run in parallel but will consume more billable minutes 🚨
  BumpVersions:
    runs-on: ubuntu-latest
    needs: PublishPackages  # To gather buildVersion to bump package version
    strategy:
      matrix: # Parallel bumps to repositories
        include:
          - repository: "MyApp1"
            createPR: true
          - repository: "MyApp2"
            createPR: false
    steps:
      - uses: actions/checkout@v3
        name: 'Checkout ${{ matrix.repository }}' # Checking out based on Matrix Repo
        with:
          repository: <githubusername_orgname>/${{ matrix.repository }}
          token: ${{ secrets.GH_PAT }}
          ref: ${{ needs.PublishPackages.outputs.bumpBranch }} # Branch: can either by develop or release
          path: ${{ matrix.repository }}

      # Adding Nuget source configuration
      - name: Set Nuget Source
        run: |
          dotnet nuget add source --username <githubusername_orgname> \
            --password ${{ secrets.CHAN_PAT }} \
            --store-password-in-clear-text --name github "https://nuget.pkg.github.com/<githubusername_orgname>/index.json"

      # To commit and push we have to configure this
      - name: Configure Git
        run: |
          git config --global user.name "github-actions"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"

      # Here we are conditionally checking which repo needs a direct update
      # or PR base updated to NuGet package
      - name: Bump ${{ matrix.repository }}
        run: |
          cd ${{ matrix.directory }}
          if [ "${{ matrix.createPR }}" == true ]
          then
            git checkout -b ops-${{ needs.PublishPackages.outputs.bumpBranch }}
            echo 'Creating PR Branch 🐌'
          else
            echo 'Directly update the package 🏎'
          fi
          dotnet add package <package_name> --version ${{ needs.PublishPackages.outputs.buildVersion }}
          dotnet build
          git commit -a -m "Bump to ${{ github.event.head_commit.message }}" -m "Version <githubusername_orgname>/<nuget_package_repo_name>@${{ needs.PublishPackages.outputs.sha7 }}"
          if [ "${{ matrix.createPR }}" == true ]
          then
            git push origin +ops-${{ needs.PublishPackages.outputs.bumpBranch }}
          else
            git push
          fi

      # Conditional step, will not run for direct update repository
      # Here we update the PR title based on the latest version
      - name: Check & Create PR
        if: ${{ matrix.createPR }}
        run: |
          cd ${{ matrix.directory }}
          export prStatus=$(gh pr list --head ops-${{ needs.PublishPackages.outputs.bumpBranch }} --state open --base ${{ needs.PublishPackages.outputs.bumpBranch }} --json number | grep -o '[[:digit:]]*')
          if [ -z "$prStatus" ]
          then
            gh pr create --title "Bump to \`${{ needs.PublishPackages.outputs.buildVersion }}\`" --body "<nuget_package_repo_name>: \`${{needs.PublishPackages.outputs.buildVersion}}\` <br/> <githubusername_orgname>/<nuget_package_repo_name>@${{ needs.PublishPackages.outputs.sha7 }} <br/>ChangeSet<br/> \`\`\`\r\n${{ needs.PublishPackages.outputs.buildChangeSet }}\r\n\`\`\`" --base ${{ needs.PublishPackages.outputs.bumpBranch }}
          else
            echo "PR Exists" https://github.com/<githubusername_orgname>/${{ matrix.repository }}/pulls/$prStatus
            gh pr edit $prStatus --title "Bump to \`${{ needs.PublishPackages.outputs.buildVersion }}\`" --body "<nuget_package_repo_name>: \`${{needs.PublishPackages.outputs.buildVersion}}\` <br/> <githubusername_orgname>/<nuget_package_repo_name>@${{ needs.PublishPackages.outputs.sha7 }} <br/> \`\`\`\r\n${{ needs.PublishPackages.outputs.buildChangeSet }}\r\n\`\`\`"
          fi

image.png

Woah, that seems lots of steps well done if you have configured well so far clap πŸ‘πŸ»

Dependabot way for update

Dependabot is yet another feature security feature on GitHub to regularly check for package updates

# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
registries:
  base-nuget:
    type: nuget-feed
    url: https://api.nuget.org/v3/index.json
  custom-nuget:
    type: nuget-feed
    url: https://nuget.pkg.github.com/<username_orgname>/index.json
    token: ${{ secrets.CHAN_PAT }}
updates:
  - package-ecosystem: 'nuget'
    schedule:
      interval: 'daily'
      time: '08:00'
      timezone: 'Asia/Calcutta'
    registries:
      - base-nuget
      - custom-nuget
    open-pull-requests-limit: 20

Please consider dropping a like πŸ‘πŸ» or reacting to the blog, thank youπŸ™πŸΌ

giphy

Did you find this article valuable?

Support Chenna by becoming a sponsor. Any amount is appreciated!

See recent sponsors |Β Learn more about Hashnode Sponsors