As part of a POC to migrate one of our services from Azure DevOps Pipelines, I had to package and deploy some NuGet library dependencies to a private feed with GitHub Actions.
In an effort to reduce deployment time our DevOps engineers wanted to remove any unnecessary steps by having all tools and software requirements installed on our runners.
This presented us with a challenge as we still needed the flexibility of using different .NET SDK's and runtimes but didn't necessarily want them all installed on the runners.
The solution we found was to use custom Dockerfiles to build, package and deploy our libraries and a Docker image of GitVersion for semantic versioning of the packages.
The library's Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:latest
ARG REPO_DIR
ARG BUILD_CONFIG
ARG PACKAGE_VERSION
COPY ./$REPO_DIR /$REPO_DIR
WORKDIR /$REPO_DIR
RUN dotnet restore
RUN dotnet build --no-restore --configuration $BUILD_CONFIG
RUN dotnet test
RUN mkdir /packages
RUN dotnet pack --configuration $BUILD_CONFIG /p:Version=$PACKAGE_VERSION --no-build --output /packages;
RUN --mount=type=secret,id=key source /run/secrets/key \
&& dotnet nuget push "/packages/*.nupkg" -s "https://nuget-feed-url.com/nuget/v3" -k "feed-key"
The Dockerfile is fairly straightforward: We restore, build, test and pack the library, we mount the secret .env file, which contains our private feed's API key, and finally push the packages to the feed.
The library's workflow:
name: Call reusable Docker package deployment workflow
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
call-pack-and-deploy-workflow:
uses: lukepatterson31/github-actions/.github/workflows/docker-pack-and-deploy.yml@main
with:
prerelease: ${{ github.REF != 'refs/heads/main' && github.event_name == 'workflow_dispatch' }}
build-configuration: 'Release'
repo-dir: './src'
docker-tag: 'MyLibrary'
secrets: inherit
The library's workflow passes the various inputs to the re-useable package and deploy workflow and calls it.
The re-useable package and deploy workflow:
We check out our repository
name: Deploy Packages in Docker Reusable Workflow
on:
workflow_call:
inputs:
prerelease:
required: true
type: string
build-configuration:
required: true
type: string
repo-dir:
required: false
type: string
docker-tag:
required: true
type: string
jobs:
build:
runs-on: [ self-hosted, RunnerName ]
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
We generate a release or pre-release version number by running the GitVersion tool from a Docker container and extract the version variable we want with awk.
- name: Set release version
if: ${{ success() && inputs.prerelease == 'false' }}
run: |
echo "package_version=$(docker run --rm -v "$(pwd):/repo" gittools/gitversion:6.0.0-fedora.33-7.0 /repo | awk '/"SemVer/ {gsub(/"|",/,""); print$2}' )" >> $GITHUB_ENV
- name: Set pre-release version
if: ${{ success() && inputs.prerelease == 'true' }}
run: |
echo "package_version=$(docker run --rm -v "$(pwd):/repo" gittools/gitversion:6.0.0-fedora.33-7.0 /repo | awk '/"SemVer/ {gsub(/"|",/,""); print$2}' )-pre" >> $GITHUB_ENV
We create the .env file containing the NuGet feed API key
- name: Create env file
run: |
echo "FEED_KEY={{ secrets.feed_key }}" > .env
We build the Docker image with the BuildKit enabled, allowing us to mount the .env file as a secret. After the build command is finished we remove the .env file.
- name: Build Dockerfile
if: ${{ success() }}
run: |
DOCKER_BUILDKIT=1 docker build -t ${{ inputs.docker-tag }} -f ./docker/Dockerfile \
--secret id=key,src=.env \
--build-arg REPO_DIR=${{ inputs.repo-dir }} \
--build-arg BUILD_CONFIG=${{ inputs.build-configuration }} \
--build-arg PACKAGE_VERSION=${{ env.package_version }} \
--no-cache .
rm -f .env
We use docker create to execute the package and deploy steps without starting a container as we don't need it to run, then we remove the stopped container.
- name: Package nuget files
if: ${{ success() }}
run: |
docker create --name pack ${{ inputs.docker-tag }}
if (( $(docker container ls -a | grep -c 'pack') > 0 )); then docker container rm pack; fi
Finally we tag and push the new library release to the repository.
- name: Tag and push
if: ${{ success() && inputs.prerelease == 'false' }}
run: |
git tag v${{ env.package_version }} ${{ github.sha }}
git push origin v${{ env.package_version }}
Here's the whole workflow:
name: Deploy Packages in Docker Reusable Workflow
on:
workflow_call:
inputs:
prerelease:
required: true
type: string
build-configuration:
required: true
type: string
repo-dir:
required: false
type: string
docker-tag:
required: true
type: string
jobs:
build:
runs-on: [ self-hosted, RunnerName ]
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set release version
if: ${{ success() && inputs.prerelease == 'false' }}
run: |
echo "package_version=$(docker run --rm -v "$(pwd):/repo" gittools/gitversion:6.0.0-fedora.33-7.0 /repo | awk '/"SemVer/ {gsub(/"|",/,""); print$2}' )" >> $GITHUB_ENV
- name: Set pre-release version
if: ${{ success() && inputs.prerelease == 'true' }}
run: |
echo "package_version=$(docker run --rm -v "$(pwd):/repo" gittools/gitversion:6.0.0-fedora.33-7.0 /repo | awk '/"SemVer/ {gsub(/"|",/,""); print$2}' )-pre" >> $GITHUB_ENV
- name: Create env file
run: |
echo "FEED_KEY={{ secrets.feed_key }}" > .env
- name: Build Dockerfile
if: ${{ success() }}
run: |
DOCKER_BUILDKIT=1 docker build -t ${{ inputs.docker-tag }} -f ./docker/Dockerfile \
--secret id=key,src=.env \
--build-arg REPO_DIR=${{ inputs.repo-dir }} \
--build-arg BUILD_CONFIG=${{ inputs.build-configuration }} \
--build-arg PACKAGE_VERSION=${{ env.package_version }} \
--no-cache .
rm -f .env
- name: Package nuget files
if: ${{ success() }}
run: |
docker create --name pack ${{ inputs.docker-tag }}
if (( $(docker container ls -a | grep -c 'pack') > 0 )); then docker container rm pack; fi
- name: Tag and push
if: ${{ success() && inputs.prerelease == 'false' }}
run: |
git tag v${{ env.package_version }} ${{ github.sha }}
git push origin v${{ env.package_version }}
Top comments (0)