DEV Community

lukepatterson31
lukepatterson31

Posted on

Deploying NuGet packages with Docker in GitHub actions

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

We create the .env file containing the NuGet feed API key

      - name: Create env file
        run: |
          echo "FEED_KEY={{ secrets.feed_key }}" > .env
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Top comments (0)