Skip to content

GitLab CI template for .NET

This project implements a GitLab CI/CD template to build, test, and analyse your .NET projects.

Supported Features and Tooling:

  • SDKs:
  • Supported Versions: .net6, .net8 and later can be used as the .NET SDK docker image for the build environment.
  • .NET SDK versions required next to the SDK found the container image are installed by the template using the manual scripts including pinned versions from global.json.
  • .NET Framework (4.8 and earlier) are not supported.
  • Pipeline Runner OS: Linux (Windows runners are not supported)
  • Build tool: msbuild
  • Dependency management:
  • .NET Workload via global.json
  • SDK style NuGet <PackageReference> see msbuild
  • paket (auto-detected)
  • Cross-Compilation: This template runs on Linux. It can only build executables (AOT/R2R) for the Linux runtime of the runner (e.g., linux-x64). Cross-compilation to other RIDs (like win-x64 or osx-arm64) is currently not supported.
  • See .NET Build Configuration Best Practices for important configuration recommendations when using this template.

Usage

This template can be used both as a CI/CD component or using the legacy include:project syntax.

Use as a CI/CD component

Add the following to your .gitlab-ci.yml:

include:
  # 1: include the component
  - component: $CI_SERVER_FQDN/to-be-continuous/dotnet/gitlab-ci-dotnet@2.0.0
    # 2: set/override component inputs
    inputs:
      test-enabled: true

Use as a CI/CD template (legacy)

Add the following to your .gitlab-ci.yml:

include:
  # 1: include the template
  - project: 'to-be-continuous/dotnet'
    ref: '2.0.0'
    file: '/templates/gitlab-ci-dotnet.yml'

variables:
  # 2: set/override template variables
  # ⚠ this is only an example
  DOTNET_TEST_EXTRA_ARGS: "-filter 'Category!=Integration'"

Global configuration

The dotnet template uses the following global configuration used throughout all jobs:

Input / Variable Description Default value
image / DOTNET_IMAGE The Docker base image used to run the .NET SDK (additional SDKs are installed as needed) (see Microsoft Container Registry for available tags) mcr.microsoft.com/dotnet/sdk:10.0
Trivy Badge
project-dir / DOTNET_PROJECT_DIR The folder where the solution (*.sln, *.slnx) or the project (*.csproj, *.fsproj, *.vbproj) file to build is located. .
build-file / DOTNET_BUILD_FILE The name of the solution (*.sln, *.slnx) or the project (*.csproj, *.fsproj, *.vbproj) file to build.
Leave empty to enable auto-discovery.
none (auto-discovery)
build-props / DOTNET_BUILD_PROPS Space separated list of properties injected into the Directory.Build.props or *.*proj files by the build job.
Formatted as Property1=Value1 Property2=Value2.
ErrorLog=bin/report/dotnet-build.sarif,version=2.1 AnalysisLevel=latest AnalysisMode=all CodeAnalysisIgnoreGeneratedCode=true WarningLevel=4 EnforceCodeStyleInBuild=true RunAnalyzersDuringBuild=true RunCodeAnalysis=true TreatWarningsAsErrors=false CodeAnalysisTreatWarningsAsErrors=false PublishRepositoryUrl=true
nuget-sources / DOTNET_NUGET_SOURCES Space separated list of .Net NuGet package sources (formatted as somename:https://some.nuget.registry/some/repo/index.json anothername:https://another.nuget.registry/another/repo/index.json) none

About $DOTNET_IMAGE

The jobs in the pipeline will use the defined container image to provider the .NET SDK environment to the job context. The default configuration will always use the most recent LTS version of .NET. You can override this to use specific current or older versions or use the SDK preview channel.

[!important] .NET Framework is not supported.

About $DOTNET_BUILD_PROPS

The values provided will be added to an existing ${DOTNET_PROJECT_DIR}/Directory.Build.props file or a new file will be created. Feel free to adjust the compiler analysis levels, but keep the ErrorLog configuration to ensure the integration of the compiler analysis into the GitLab and Sonar reports.

Solution / Project Versioning

The template will use the versions set in by the project files and the git tags in the following way: - Version: - ${CI_COMMIT_TAG} overrides all Version element configured by the project files. - Highest Version element configured in the build scope. - VersionSuffix: - Empty if ${CI_COMMIT_TAG} is set. - Empty if for production or integration branches matching the pre-defined PROD_REF and INTEG_REF patterns. - ${CI_COMMIT_REF_SLUG} for all other branches

Solution / Project Auto-Discovery

The template supports auto-discovery of solution and project files for simple project structures where a single solution or project file exists. If your project has a more complex structure or multiple solution files in the root of the repo, then you use the template parameters project-dir / DOTNET_PROJECT_DIR and build-file / DOTNET_BUILD_FILE (relative path to project-dir) to select the correct build context and target solution / project to build. The combination of DOTNET_PROJECT_DIR and DOTNET_BUILD_FILE is used by the pipeline to identify a unique solution or project file to build:

  • project-dir / DOTNET_PROJECT_DIR: specifies the folder containing the solution or project to build (defaults to ., the root repository folder),
  • build-file / DOTNET_BUILD_FILE: specifies the relative path to project-dir. When unset or blank (default), the template will search for a single solution or a single project file in the project directory. Must be set explicitly when multiple solutions or projects exist (see Working With Multiple Solutions And Projects).

ℹ️ When DOTNET_BUILD_FILE is unset then the following auto-discovery steps are run:

  1. A solution (*.sln) is searched within DOTNET_PROJECT_DIR.
  2. If a single one is found, then this is used as DOTNET_BUILD_FILE.
  3. If multiple solution files are found then the template fails with an explicit error message.
  4. If no solution file is found, then a project file (*.*proj) is searched within DOTNET_PROJECT_DIR.
  5. If a single file is found, then this is used as DOTNET_BUILD_FILE.
  6. If no file or multiple project files are found then the template fails with an explicit error message.

Working With Multiple Solutions and Projects

This template supports repositories with multiple solution files and complex project structures through parallel:matrix builds. You can specify multiple project directories or direct paths to solution or project files.

[!note] Each pipeline instance is a completely independent build with independent artifacts and reports.

include:
  - component: $CI_SERVER_FQDN/to-be-continuous/dotnet/gitlab-ci-dotnet@2.0.0
    inputs:
      test-enabled: true

# multi-instanciate the template with parallel:matrix
.dotnet-base:
  parallel:
    matrix:
      - DOTNET_PROJECT_DIR: "src"
        DOTNET_BUILD_FILE: "myapp.sln"
      - DOTNET_PROJECT_DIR: "tools"
      - DOTNET_PROJECT_DIR: "samples"
        DOTNET_BUILD_FILE: "samples.sln"

.NET Environment Restore

The template executes the following actions produce a reproducible build environment for each job:

  1. Install all SDKs referenced by TFM field the projects in the build scope.
  2. Run dotnet workspace restore, if a global.json is found.
  3. Run dotnet tool restore.
  4. Run dotnet paket restore, if a paket configuration is found.
  5. Run dotnet restore.

Supporting Slow / Integration Tests

To separate "fast" unit tests (which run on every build) from "slow" integration tests (which might need a database or other services) you can split the execution into separate jobs.

  1. In the test code:
    • Annotate the slow tests using xUnit [Trait("Category","Integration")], NUnit [Category("Integration")], or MSTest [TestCategory("Integration")].
  2. In the .gitlab-ci.yml file:
    • Set the global test-extra-args / DOTNET_TEST_EXTRA_ARGS to the exclude the slow tests like this: --filter "Category!=Integration
    • Add a new job file to run the slow tests in a later test stage (e.g. package-test):
      integration-tests:
        stage: package-test
        extends: dotnet-build
        variables:
          DOTNET_TEST_EXTRA_ARGS: --filter "Category=Integration"
        script:
          - dotnet_run_test
      

[!important] GitLab does not merge multiple coverage reports. The coverage percentage shown in the UI will only reflect the last job that ran (in this case, integration-tests).

Jobs

dotnet-build job

This job performs compilation and packaging of your .NET project using dotnet build, and either dotnet pack for libraries or dotnet publish for executables. The job automatically detects the project types and uses the appropriate packaging method for each. See the section dotnet-test below for all test related configuration and actions.

See also .NET Build Configuration Best Practices to control the per project packaging behavior.

It uses the following variables:

Input / Variable Description Default value
build-args / DOTNET_BUILD_ARGS Additional arguments used by the build job none
build-configuration / DOTNET_BUILD_CONFIGURATION The build configuration to use (Debug or Release) Release
package-configuration / DOTNET_PACKAGE_CONFIGURATION The build configuration to use for packaging (Debug or Release) a library or executable Release
package-symbols-disabled / DOTNET_PACKAGE_SYMBOLS_DISABLED Disable creation of symbol packages (snupkg) for debugging false

With the build-args left unmodified the following report is generated based upon the collected compiler diagnostic and roslyn analyzer results in the dotnet-build.sarif files. The files are assembled to a single report file using sarif-converter:

Report Format Usage
reports/dotnet-build.gitlab-codequality.json Code Climate GitLab integration
reports/dotnet-*.env Gitlab dotenv format of runtime environment variables. GitLab Integration

Important notes:

  • The job will install any required compiler to build for the runtime id of the current runner. This is only done for Ahead-of-Time and Ready-To-Run build targets.
  • See Project / Solution Versioning for the build version selection.

dotnet-test job (running in the dotnet-build step)

This job performs unit testing of your .NET project using dotnet test. It runs as part of the build job and uses --no-build to avoid rebuilding.

It uses the following variables:

Input / Variable Description Default value
test-disabled / DOTNET_TEST_DISABLED Set to true to disable tests execution false
test-extra-args / DOTNET_TEST_EXTRA_ARGS Extra arguments used by the dotnet test command none

The following test and coverage reports are generated:

Report Format Usage
reports/dotnet-test.junit.xml JUnit test report(s) GitLab integration
reports/dotnet-test.xunitnet.xml xUnit.net v2 test report(s) SonarQube integration
reports/dotnet-test.cobertura.xml Cobertura coverage report GitLab integration
reports/dotnet-test.opencover.xml OpenCover coverage report SonarQube integration

Unit Tests and Code Coverage reports

In order to implement the best GitLab and SonarQube integration, the .NET template requires your project to use some specific tools and plugins:

ℹ the JunitXml.TestLogger package shall be added to your test projects (see NuGet) - Unit Tests report for SonarQube integration is generated with Xunit Test Logger

ℹ the XunitXml.TestLogger package shall be added to your test projects (see NuGet)

⚠ only required if you enabled SonarQube analysis - Code Coverage is computed with Coverlet:

ℹ the coverlet.msbuild package shall be added to your test projects (see NuGet)

dotnet-format job

This job performs code formatting validation using dotnet format of the whole repository. It verifies that the code follows the formatting rules defined in .editorconfig.

It uses the following variables:

Input / Variable Description Default value
format-disabled / DOTNET_FORMAT_DISABLED Set to true to disable the Dotnet Format code formatting check (enabled by default) false

The job will fail the pipeline if the project's .editorconfig styling rules are not respected.

dotnet-sonar job

This job performs SonarQube analysis.

It uses the following variable:

Input / Variable Description Default value
sonar-host-url / SONAR_HOST_URL SonarQube server url none (disabled)
sonar-project-key / SONAR_PROJECT_KEY SonarQube Project Key none
🔒 SONAR_TOKEN SonarQube authentication token (depends on your authentication method) none
sonar-quality-gate-enabled / SONAR_QUALITY_GATE_ENABLED Enables SonarQube Quality Gate verification. Uses sonar.qualitygate.wait parameter (see doc). false
sonar-extra-args / DOTNET_SONAR_EXTRA_ARGS Extra arguments used by the SonarScanner none
sonar-exclusions / DOTNET_SONAR_EXCLUSIONS Files and directories to be excluded from analysis, as a comma-separated list of paths. See documentation for the format. **/bin/**,**/obj/**,**/packages/**,**/*.g.cs,**/*.g.i.cs,**/*.designer.cs,**/*AssemblyInfo.cs,.sonarqube

More info:

dotnet-sbom job

This job creates a Software Bill Of Materials (SBOM) for the project, libraries and executables using CycloneDX cdxgen.

Execution rules:

  • Disabled if sbom-disabled is set to true
  • Runs always if TBC_SBOM_MODE is set to always
  • Runs on release, integration, or production branches when TBC_SBOM_MODE is set to onrelease (default)
  • Skipped in all other cases
Input / Variable Description Default value
sbom-disabled / DOTNET_SBOM_DISABLED Set to true to disable SBOM generation false
sbom-image / DOTNET_SBOM_IMAGE The container image to use for SBOM generation using CycloneDX cdxgen ghcr.io/cyclonedx/cdxgen:master
sbom-supplier / DOTNET_SBOM_SUPPLIER The package supplier name to use in the generated SBOMs {CI_PROJECT_NAMESPACE}
sbom-opts / DOTNET_SBOM_OPTS Additional options to pass to the SBOM generation tool --fail-on-error --evidence --deep

The following SBOM reports are generated:

Report Format Usage
reports/dotnet-sbom-*.cyclonedx.json CycloneDX Json test report(s) GitLab integration

dotnet-publish job (Upload Artifacts)

This job takes the artifacts build in the dotnet-package step and publishes them depending upon the output type configured in the project files: - Application binaries are zipped and always pushed to the generic GitLab Package Registry. - Nuget packages get pushed to the configured NuGet target repo using dotnet nuget push. Default repo is the Gitlab Nuget Repo of the project.

See also .NET Build Configuration Best Practices to control the per project behavior of the pipeline template.

Note on Authentication: The job is designed to work seamlessly with the integrated GitLab Package Registry. By default, DOTNET_NUGET_API_KEY uses the CI_JOB_TOKEN, so you do not need to configure any secret variables. You only need to create and set DOTNET_NUGET_API_KEY as a project CI/CD variable if you override nuget-repo to publish to an external repository (like nuget.org or another private registry) that requires a specific API key.

Execution rules: - Runs automatically on tagged release pipelines with non-prerelease versions. - Runs manually on any other pipeline instance. - Automatically configured for GitLab Package Registry using CI_JOB_TOKEN - Publishes both .nupkg and .snupkg (symbol) packages if symbol package publication is activated. - Publishes executable binaries for configured frameworks and the runner runtime.

Important notes: - Automatically configures GitLab Package Registry as the NuGet target feed. - Uses CI_JOB_TOKEN for authentication (no manual configuration needed to use with the GitLab package repository) - See Project / Solution Versioning for the build version selection.

Example artifacts published:

  • Libraries: MyLibrary.1.0.0.nupkg, MyLibrary.1.0.0.snupkg
  • Applications: MyApp-1.0.0.zip containing application binaries
Input / Variable Description Default value
publish-enabled / DOTNET_PUBLISH_ENABLED Set to true to enable publishing of artifact to a NuGet feed. false
nuget-repo / DOTNET_NUGET_REPO Target NuGet package repository url to publish the packages to. (when overriding this, please set DOTNET_NUGET_API_KEY at project CI variable to set the nuget api-key used by dotnet nuget push) defaults to GitLab project's packages repository ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/nuget/index.json
🔒 DOTNET_NUGET_API_KEY NuGet Repo API token used for authentication. ${CI_JOB_TOKEN}
nuget-symbol-repo / DOTNET_NUGET_SYMBOL_REPO Target NuGet package symbol repository url to publish the symbol packages to. (when overriding this, please set DOTNET_NUGET_SYMBOL_API_KEY at project CI variable to set the nuget symbol api-key used by dotnet nuget push) defaults to GitLab project's packages repository ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/nuget/index.json
🔒 DOTNET_NUGET_SYMBOL_API_KEY NuGet Symbol Repo API token used for authentication. ${CI_JOB_TOKEN}

Secrets management

Here are some advices about your secrets (variables marked with a 🔒):

  1. Manage them as project or group CI/CD variables:
    • masked to prevent them from being inadvertently displayed in your job logs,
    • protected if you want to secure some secrets you don't want everyone in the project to have access to (for instance production secrets).
  2. In case a secret contains characters that prevent it from being masked, simply define its value as the Base64 encoded value prefixed with @b64@: it will then be possible to mask it and the template will automatically decode it prior to using it.
  3. Don't forget to escape special characters (e.g.: $ -> $$).

.NET Build Configuration Best Practices

The following best practices below were important enough during the implementation of the pipeline to highlight them here again in order that this pipeline template works as designed and you can get the most out of this pipeline template with the lowest amount of configuration.

Global Build Configuration

  1. Reproducible Builds: Pin down the versions of the SDK and package dependencies.
    • ℹ️ Use global.json to pin the SDK and other global environmental settings (see dotnet workload sets).
    • ℹ️ Use <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> in your Directories.Build.props and commit the packages.lock.json file(s).
    • You can also use Paket to manage package dependencies. But be sure to commit the tool manifest and .paket directory.
  2. Use SDK-style .NET dependency managment using <PackageReference> for NuGet dependencies.

ℹ️ Legacy packages.config has to be migrated before using this CI/CD template. 3. Use solution files when working with more than one project in order to manage build dependencies and build scopes. Only with solution files the project dependencies can be resolved and the compilation order be determined.

ℹ️ Check dotnet sln for managing the solution file from the CLI. 4. When working with multiple solution file in the same directory, then use the build-file/DOTNET_BUILD_FILE parameter to select the solution to build.

ℹ️ See the working with multiple solutions and projects chapter below. 5. Use semantic versioning (e.g., 1.3.1) in the Version configuration. The value will be detected and updated / overwritten by the template depending upon the context. (default: 1.0.0) 6. Use Directory.Build.props to manage common project settings (e.g. nuget package info).

Project Configuration

It is important that each project file contains the necessary configuration so the pipeline and msbuild can decide to do a combination of the following actions:

  1. dotnet build to compile the project

    ℹ️ The pipeline will build each and every framework listed! The number of TFMs directly impacts the pipeline runtime. - For public libraries use the appropriate version of .netstandard for maximum compatibility and to reduce the amount of deliverables to create.

    ℹ️ For internal libraries choose the applicable target TFM(s) to enable the usage of modern runtime features. 2. dotnet test to compile and execute the tests - Set <IsTestProject>true</IsTestProject> in the configuration of test projects. - This implies <IsPackable>false</IsPackable> and <IsPublishable>false</IsPublishable>. 3. dotnet pack to build the nuget package - Default action for library projects <OutputType>Library</OutputType>. - Disable this with <IsPackable>false</IsPackable>. 4. dotnet publish to build the executable - Default action for executable projects <OutputType>Exe</OutputType> or <OutputType>appcontainerexe</OutputType> - Disable this with <IsPublishable>false</IsPublishable>. 5. dotnet nuget push to push the package in the nuget feed. - If publish-enabled / DONET_PUBLISH_ENABLED is activated, then every packable project will be pushed to configured nuget target repo. 6. Archive and deliver executable to the GitLab generic artifact repo. - If publish-enabled / DONET_PUBLISH_ENABLED is activated, then every publishable project will compressed into a zip archive and pushed to GitLab generic artifact repo.

Optimizing executables projects

  1. Ahead-of-Time compilation (<PublishAot>true</PublishAot>) and Ready-2-Run compilation (<PublishReadyToRun>true</PublishReadyToRun>) both require setting the runtime target id (RID) via <RuntimeIdentifiers>. The template will only publish platform specific executable if the pipeline runner agent's RID is configured in the runtime list configured in <RuntimeIdentifiers>.

ℹ️ Currently this pipeline template can only install the necessary compilers for the RIDs matching the pipeline host. Cross architecture and cross OS compilation is currently not possible. (Implementation Ideas and Merge Requests are welcome!) 2. Use <PublishSelfContained>true</PublishSelfContained> instead of <SelfContained>. This bundles the .NET runtime in the deliverable allowing it to run on any machine, even those without the .NET runtime installed. 3. Reduce deliverable size by: - Setting <SatelliteResourceLanguages> only to what you need. - Using <OptimizationPreference>Size</OptimizationPreference> to build an artifact optimized for size.