Deploying NuGet packages with Docker in GitHub actions
Tags: nuget, dotnet, github, docker
Published: 2023-02-01 15:41:36

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 }}

```