Write GitHub Actions

Gibin Francis
8 min readMay 16, 2022

--

In this we will be taking a look to some GitHub actions. We will be using container based deployments in our actions, for that we need some common actions required in all actions. So we will create some reusable actions and then we will be proceeding to the actual actions.

All actions should reside in below folder location

.github/workflows/your_action_name.yml

DotNet Build Action

as we are using dotnet based applications, we will be building and publishing our applications using dot net for that we will create a sub action

all sub actions should be in below folder structure

.github/actions/your_subaction_name/action.yml

please note the folder name is your action and the fie name should be action.yml only.

please find the action below.

name: 'DotNet Build'
description: 'Restore build and test with private nuget.cofig'
inputs:
directory:
description: 'Folder where the .sln file is present'
required: true
enableTest:
description: 'Flag to enable or dsable test while running'
type: boolean
required: false
default: false
repo:
description: 'Repo name used for saving artifact'
required: true
runs:
using: "composite"
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v2.0.0
with:
dotnet-version: 3.1.x
config-file: NuGet.config

- name: show
shell: bash
run: ls -l
- name: copy NuGet.config
shell: bash
run: cp common/NuGet.config ${{inputs.directory}}
- name: Restore
shell: bash
run: dotnet restore
working-directory: ${{inputs.directory}}
- name: Build
shell: bash
run: dotnet build --configuration Release --no-restore
working-directory: ${{inputs.directory}}

- name: Test
shell: bash
if: ${{ inputs.enableTest == 'true' }}
run: dotnet test --no-restore
working-directory: ${{inputs.directory}}

- name: Upload artifact
if: github.event.ref == 'refs/heads/master'
uses: actions/upload-artifact@master
with:
name: ${{inputs.repo}}-${{ github.run_number }}-${{ github.run_attempt }}
path: ./${{inputs.directory}}/*

Please find the details of the action below

name: 'DotNet Build'

This step will assign a name for the action

directory:
description: 'Folder where the .sln file is present'
required: true
enableTest:
description: 'Flag to enable or dsable test while running'
type: boolean
required: false
default: false
repo:
description: 'Repo name used for saving artifact'
required: true

here we are using some inputs for the action so that we can reuse the same in other actions.

and the steps inside the action is more self explanatory, but to give you an idea am just mentioning the actions below

  1. setting up DotNet SDK
  2. Listing all the folder in the current scope, this step is not necessary. Added this step to show the folder structure and you can decide on folder level to run the commands
  3. Copy your NuGet.config from common folder to action worker, you can avoid this step if you are not using any private NuGet.
  4. restore project
  5. build project
  6. test project
  7. upload the folder to Artifact to use for subsequent steps. Folder name will be reponame-github_action_run_number-github_action_exection_number, Eg : yourrep-1–1

now your DotNet project build completed and the artifact is saved. now lets proceed to next reusable action

Docker Build Action

please find the action below

name: 'Docker Build and Push'
description: 'Build Docker anf push the docker to acr'
inputs:
dockerLocation:
description: 'Docker File location'
required: true
repo:
description: 'Repo name used for saving artifact'
required: true
dockerRegUname:
description: 'Docker registory Username'
required: true
dockerRegPwd:
description: 'Docker registory Username'
required: true
dockerContext:
description: 'Docker registory Context'
required: false
default: .
runs:
using: "composite"
steps:
- uses: actions/download-artifact@master
with:
name: ${{inputs.repo}}-${{ github.run_number }}-${{ github.run_attempt }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log in to container registry
uses: docker/login-action@v1.10.0
with:
registry: ${{ env.REGISTRY_URL }}
login-server: ${{ env.REGISTRY_URL }}
username: ${{inputs.dockerRegUname}}
password: ${{inputs.dockerRegPwd}}
- name: Lowercase the repo name and username
shell: bash
run: echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
- name: Build and push container image to registry
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ env.REGISTRY_URL }}/${{ inputs.REPO }}:${{ env.BUILD_NAME }}
file: ${{inputs.dockerLocation}}
context: ${{inputs.dockerContext}}

With this action we will be creating docker image for our application. Please find the steps we do to create the docker.

  1. Download the artifact ( which we saved in previous action )
  2. set up docker environment
  3. login to docker registry
  4. displaying the docker name
  5. build and push docker using the pre-built action

Now we successfully created the docker image of the application and we are ready to reuse the actions in our subsequent actions.

Deploy Azure Function Container

please find the action below

name: function action Nameenv:
REGISTRY_URL: ${{ secrets.REGISTRY_URL }}
DOCKERNAME: dockerimagename
WORKING_REPO: 'folderName'
DOCKER_FILE: './folderName/Dockerfile'
BUILD_NAME: ${{ github.run_number }}-${{ github.run_attempt }}
on:
push:
branches:
- master
- dev
paths:
'folderName/**'

workflow_dispatch:
pull_request:
branches:
- master
- dev
paths:
'folderName/**'
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- name: Check out the repo
uses: actions/checkout@v3

- name: Dot net build
uses: ./.github/actions/dotnetbuild
continue-on-error: false
with:
directory: '${{ env.WORKING_REPO }}'
enableTest: false
repo: ${{ env.DOCKERNAME }}

dockerBuild:
name: Deploy - Dev
runs-on: ubuntu-latest
if: github.event.ref == 'refs/heads/master'
environment: dev
needs: build

steps:
- name: Check out the repo
uses: actions/checkout@v3

- name: Docker build and push
uses: ./.github/actions/dockerbuild
continue-on-error: false
with:
dockerLocation: '${{ env.DOCKER_FILE }}'
repo: ${{ env.DOCKERNAME }}
dockerRegUname: ${{ secrets.REGISTRY_USERNAME }}
dockerRegPwd: ${{ secrets.REGISTRY_PASSWORD }}
dockerContext: '${{ env.WORKING_REPO }}'

- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Deploy to Azure Web App
uses: Azure/functions-container-action@v1
with:
app-name: ${{ secrets.APP_NAME }}
slot-name: 'production'
image: '${{ env.REGISTRY_URL }}/${{ env.DOCKERNAME }}:${{ env.BUILD_NAME }}'

Now lets go deep into the action

name: function action Name

Here we are mentioning the name of the action

env:
REGISTRY_URL: ${{ secrets.REGISTRY_URL }}
DOCKERNAME: dockerimagename
WORKING_REPO: 'folderName'
DOCKER_FILE: './folderName/Dockerfile'
BUILD_NAME: ${{ github.run_number }}-${{ github.run_attempt }}

here we are using some environment variables to keep some info. All the credentials and secure information should keep in GitHub secretes

in this we are splitting the build and deploy tasks as separate jobs

in the build task we will be doing below actions

  1. check out the code from receptive repo and branch
  2. dot net build — this will call the action we mentioned in the beginning

in the deploy task will be performing below steps

  1. check out the code from receptive repo and branch
  2. docker build — this will call the action we mentioned in the beginning
  3. login to azure cli, for that we need to create a secret with below json content in it
{
"clientId": "<GUID>",
"clientSecret": "<GUID>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>"
}

4. deploy to azure function mentioning the details

Now we are ready to deploy the azure function. Lets repeat the same with an app service also.

deploy to web app

please find the action below

name: Application Nameenv:
REGISTRY_URL: ${{ secrets.REGISTRY_URL }}
DOCKERNAME: dockerimagename
WORKING_REPO: 'folderName'
DOCKER_FILE: './folderName/Dockerfile'
BUILD_NAME: ${{ github.run_number }}-${{ github.run_attempt }}
on:
push:
branches:
- master
- dev
paths:
'folderName/**'

workflow_dispatch:
pull_request:
branches:
- master
- dev
paths:
'folderName/**'
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- name: Check out the repo
uses: actions/checkout@v3

- name: Dot net build
uses: ./.github/actions/dotnetbuild
continue-on-error: false
with:
directory: '${{ env.WORKING_REPO }}'
enableTest: false
repo: ${{ env.DOCKERNAME }}

dockerBuild:
name: Deploy - Dev
runs-on: ubuntu-latest
if: github.event.ref == 'refs/heads/master'
environment: dev
needs: build

steps:
- name: Check out the repo
uses: actions/checkout@v3

- name: Docker build and push
uses: ./.github/actions/dockerbuild
continue-on-error: false
with:
dockerLocation: '${{ env.DOCKER_FILE }}'
repo: ${{ env.DOCKERNAME }}
dockerRegUname: ${{ secrets.REGISTRY_USERNAME }}
dockerRegPwd: ${{ secrets.REGISTRY_PASSWORD }}
- name: Lowercase the repo name and username
continue-on-error: false
run: echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
- name: Deploy to Azure Web App
id: deploy-to-webapp
continue-on-error: false
uses: azure/webapps-deploy@v2
with:
app-name: ${{ secrets.APP_NAME }} # App Name from secret
publish-profile: ${{ secrets.WEBAPP_PUBLISH_PROFILE }} # App profile
images: '${{ env.REGISTRY_URL }}/${{ env.DOCKERNAME }}:${{ env.BUILD_NAME }}'

This action share the same steps like we did in the previous container based azure function deployment. But in the last stage we are using webapp-depoly task to deploy to we app. for that we need to create secret and pasted the contents from the publish profile (which you can download from the portal as shown on the image )

Now we are ready with our Web app action.

As an addon we are creating a NuGet pushing pipeline also

NuGet push

please find the action below

name: NuGet Pushon:
push:
branches:
- master
- dev
paths:
- 'NuGetProject1Location/**'
- 'NuGetProject2Location/**'
- 'NuGetProject3Location/**'
workflow_dispatch:
pull_request:
branches:
- master
- dev
paths:
- 'NuGetProject1Location/**'
- 'NuGetProject2Location/**'
- 'NuGetProject3Location/**'
jobs:
NugetPush:
runs-on: ubuntu-latest
name: Build and Push NuGet Packages
strategy:
matrix:
nuget-path:
- 'NuGetProject1Location'
- 'NuGetProject2Location'
- 'NuGetProject3Location'
steps:
- name: Check out the repo
uses: actions/checkout@v3

- name: Setup .NET environment
uses: actions/setup-dotnet@v1

- name: Dot net build
uses: ./.github/actions/dotnetbuild
continue-on-error: false
with:
directory: ${{ matrix.nuget-path }}
enableTest: false
repo: Common_Nuget

- name: Nuget Push
continue-on-error: true
run: dotnet nuget push ./${{ matrix.nuget-path }}/bin/Release/*.nupkg -k ${{ secrets.NUGET_KEY}} --source https://,yournugetserver.azurewebsites.net/nuget --skip-duplicate

Here we are using a strategy Matrix to replicate the tasks. We create a single piline and share the project location. Then the action will iterate through the matrix and create all NuGet packages

Please find the steps below

  1. check out the code from receptive repo and branch
  2. setting up DotNet environment, so that we can use the ‘dotnet nuget push’ command in the subsequent steps
  3. dot net build — this will call the action we mentioned in the beginning
  4. NuGet push. Here we should specify the NuGet server name and the key as secret to run the same.

Hope this will help you, happy coding.

Updates

After implementing the same the we also thought to include the code coverage also with the same.

For that we need to add two packages with your existing test project, we are using xUnit for our testing. Please add below packages to your test project

  • coverlet.collector
  • coverlet.msbuild

Once the same is added, please update the DotNet build with below task

- name: Test Coverage        
shell: bash
if: ${{ inputs.enableTest == 'true' }}
run: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
working-directory: ${{inputs.directory}}

please find the full action below

name: 'DotNet Build'
description: 'Restore build and test with private nuget.cofig'
inputs:
directory:
description: 'Folder where the .sln file is present'
required: true
enableTest:
description: 'Flag to enable or dsable test while running'
type: boolean
required: false
default: false
repo:
description: 'Repo name used for saving artifact'
required: true
runs:
using: "composite"
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v2.0.0
with:
dotnet-version: 3.1.x
config-file: NuGet.config

- name: show
shell: bash
run: ls -l
- name: copy NuGet.config
shell: bash
run: cp common/NuGet.config ${{inputs.directory}}
- name: Restore
shell: bash
run: dotnet restore
working-directory: ${{inputs.directory}}
- name: Build
shell: bash
run: dotnet build --configuration Release --no-restore
working-directory: ${{inputs.directory}}

- name: Test
shell: bash
if: ${{ inputs.enableTest == 'true' }}
run: dotnet test --no-restore
working-directory: ${{inputs.directory}}

- name: Test Coverage
shell: bash
if: ${{ inputs.enableTest == 'true' }}
run: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
working-directory: ${{inputs.directory}}
- name: Upload artifact
if: github.event.ref == 'refs/heads/master'
uses: actions/upload-artifact@master
with:
name: ${{inputs.repo}}-${{ github.run_number }}-${{ github.run_attempt }}
path: ./${{inputs.directory}}/*

Now you will be able to see the coverage in the github actions like below

Hope this will help you in future.

keep coding…

--

--

Gibin Francis

Technical guy interested in MIcrosoft Technologies, IoT, Azure, Docker, UI framework, and more