Building .NET using GitLab CI/CD
As I often mention, I use .NET a lot in general, as it’s fairly easy to use, has a huge ecosystem, and has evolved really positively in the past years (long gone are the days of Mono :D).
Another component of this is that .NET projects are incredibly easy to build and publish using GitLab CI/CD.
Today, we’re gonna explore some ways of building and publishing a .NET project using just that.
Docker#
Probably the most straightforward, considering a simple Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
COPY . ./builddir
WORKDIR /builddir/
ARG ARCH=linux-x64
RUN dotnet publish --runtime ${ARCH} --self-contained -o output
FROM mcr.microsoft.com/dotnet/runtime:9.0
WORKDIR /app
COPY --from=build /builddir/output .
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
HEALTHCHECK CMD ["curl", "--insecure", "--fail", "--silent", "--show-error", "http://127.0.0.1:8080"]
ENTRYPOINT ["dotnet", "MyApp.dll"]
Note: this assumes your app builds to
MyApp.dll
and has a healtheck endpoint onhttp://127.0.0.1:8080
Then building the container image itself is really easy:
stages:
- docker
variables:
DOCKER_DIND_IMAGE: "docker:24.0.7-dind"
build:docker:
stage: docker
services:
- "$DOCKER_DIND_IMAGE"
image: "$DOCKER_DIND_IMAGE"
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
script:
- docker buildx create --use
- docker buildx build
--platform linux/amd64
--file Dockerfile
--tag "$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_NAME%+*}"
--provenance=false
--push
.
- docker buildx rm
only:
- branches
- tags
This will build, then publish the image to the GitLab container registry of the repo. It’s possible to also specify a different registry, but kinda useless as the default one is already excellent for most cases.
Regular build / NuGet build#
This type of build just requires source itself without much additional configuration.
It will build the software, then either upload the resulting files as an artifact or publish it into the GitLab NuGet registry.
For those two, I can recommend setting up a cache policy like:
cache:
key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
paths:
- '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json'
- '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*'
- '$NUGET_PACKAGES_DIRECTORY'
policy: pull-push
And a small restore snippet:
.restore_nuget:
before_script:
- 'dotnet restore --packages $NUGET_PACKAGES_DIRECTORY'
You can also directly specify the build image that you want to use at the top of the CI definition file with, for instance:
image: mcr.microsoft.com/dotnet/sdk:9.0
The regular build with artifact upload is also really easy:
build:
extends: .restore_nuget
stage: build
script:
- 'dotnet publish --no-restore'
artifacts:
paths:
- MyApp/bin/Release/**/MyApp.dll
In this case, we use **
to avoid having to update the path every time we upgrade the .NET version (for instance, .NET 8 will put the build in the net8.0
directory, .NET 9 in net9.0
, etc).
Now, we can also build and publish the solution to the NuGet registry:
deploy:
stage: deploy
only:
- tags
script:
- dotnet pack -c Release
- dotnet nuget add source "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/nuget/index.json" --name gitlab --username gitlab-ci-token --password $CI_JOB_TOKEN --store-password-in-clear-text
- dotnet nuget push "MyApp/bin/Release/*.nupkg" --source gitlab
environment: $DEPLOY_ENVIRONMENT
As seen in this definition, this publish
stage will only run on tag pushes, but it’s also possible to generate a version string with the current commit and pushing this as a nightly release.
As an additional step, but not really related to the build itself, I often activate the Secret, SAST and dependencies scanning as it can prevent really obvious mistakes. Doing so is also really trivial:
include:
- template: Jobs/Secret-Detection.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
In the end, building .NET is extremely trivial.