Automate .NET Nuget Package using Github Actions
Utilise Github actions to Pack and Publish Package
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
- Version: YY.mm.dd.HHmm-alpha
- BuildMode: Debug
For release
branch
- Version: YY.mm.dd.HHmm
- BuildMode: Release
Name | Description |
sha7 | Git commit hash length is 40, as shorten it to seven |
buildVersion | Each package will need to have incremental versioning, so here we have to use calendarVer |
buildMode | .NET Build Mode: Debug or Release |
bumpBranch | Will 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
Parameter | Description |
-c | configuration - default is Debug , you might want to use Release |
/p:Version | Each package will need to have incremental versioning, so here we have use calendarVer |
-o | Output 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
Repo | Branch | Create PR or Direct Push |
MyApp1 | Develop | Direct push to develop branch |
MyApp2 | Develop | Create 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 asBump to ${{ github.event.head_commit.message }}
and in optional extended message you are mentioningsha7
commit idCheck 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
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ππΌ