pipeline { agent { docker { image 'mcr.microsoft.com/dotnet/sdk:9.0' args '-u root:root' label "${params.AGENT_LABEL?.trim() ?: ''}" } } options { ansiColor('xterm') timestamps() buildDiscarder(logRotator(numToKeepStr: '15')) disableConcurrentBuilds() skipDefaultCheckout(true) } parameters { string(name: 'GIT_URL', defaultValue: 'file:///repos/AS400API.git', description: 'Git repository URL to clone') string(name: 'GIT_BRANCH', defaultValue: 'main', description: 'Branch or ref to build') string(name: 'AGENT_LABEL', defaultValue: '', description: 'Optional Jenkins agent label with Docker CLI access (leave blank for default node)') } environment { DOTNET_CLI_TELEMETRY_OPTOUT = '1' DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1' DOTNET_NOLOGO = '1' BUILD_CONFIGURATION = 'Release' TEST_RESULTS_DIR = 'TestResults' COVERAGE_DIR = 'TestResults/Coverage' COVERAGE_FILE = 'TestResults/Coverage/coverage.opencover.xml' PUBLISH_DIR = 'publish/Release' SONAR_PROJECT_KEY = 'AS400API' SONAR_PROJECT_NAME = 'AS400API' } stages { stage('Checkout') { steps { // Fetch the requested branch from the configured Git URL. script { if (!params.GIT_URL?.trim()) { error 'GIT_URL parameter must not be empty.' } String branchSpec = params.GIT_BRANCH?.trim() ?: 'main' if (!branchSpec.startsWith('*/')) { branchSpec = "*/${branchSpec}" } checkout([ $class : 'GitSCM', branches : [[name: branchSpec]], doGenerateSubmoduleConfigurations: false, extensions : [[$class: 'CloneOption', depth: 0, noTags: false, shallow: false]], userRemoteConfigs : [[url: params.GIT_URL.trim()]] ]) } } } stage('.NET SDK Info') { steps { // Sanity check the SDK and install required global tools (Java for Sonar + dotnet-sonarscanner). sh '''#!/bin/bash -e apt-get update apt-get install -y --no-install-recommends openjdk-17-jre rm -rf /var/lib/apt/lists/* echo '### dotnet --info' dotnet --info echo '### Install/Update dotnet-sonarscanner' dotnet tool install --global dotnet-sonarscanner || dotnet tool update --global dotnet-sonarscanner ''' script { if (!env.PATH.contains('/root/.dotnet/tools')) { env.PATH = "${env.PATH}:/root/.dotnet/tools" } } } } stage('Restore') { steps { // Restore solution dependencies. sh '''#!/bin/bash -e dotnet restore AS400API.sln ''' } } stage('Build') { steps { // Build the solution in Release mode without failing on warnings. sh '''#!/bin/bash -e dotnet build AS400API.sln --configuration "${BUILD_CONFIGURATION}" --no-restore ''' } } stage('Test & Coverage') { steps { // Execute tests once for fast feedback and capture TRX + OpenCover reports. sh '''#!/bin/bash -e rm -rf "${TEST_RESULTS_DIR}" mkdir -p "${COVERAGE_DIR}" dotnet test AS400API.sln --configuration "${BUILD_CONFIGURATION}" --no-build \ /p:CollectCoverage=true \ /p:CoverletOutputFormat=opencover \ /p:CoverletOutput="${COVERAGE_DIR}/" \ --logger "trx;LogFileName=test-results.trx" \ --logger "junit;LogFileName=test-results.xml" ''' junit testResults: "${TEST_RESULTS_DIR}/**/*.xml", allowEmptyResults: false publishCoverage adapters: [opencoverAdapter("${COVERAGE_FILE}")], sourceFileResolver: sourceFiles('STORE_LAST_BUILD'), failNoReports: true } } stage('SonarQube: Begin') { steps { // Kick off Sonar analysis with project metadata and coverage location. script { env.SONAR_PROJECT_VERSION = env.BUILD_NUMBER ?: '0.0.0' } withSonarQubeEnv('sonarqube') { sh '''#!/bin/bash -e dotnet sonarscanner begin \ /k:"${SONAR_PROJECT_KEY}" \ /n:"${SONAR_PROJECT_NAME}" \ /v:"${SONAR_PROJECT_VERSION}" \ /d:sonar.host.url="${SONAR_HOST_URL}" \ /d:sonar.login="${SONAR_AUTH_TOKEN}" \ /d:sonar.cs.opencover.reportsPaths="${COVERAGE_FILE}" ''' } } } stage('Rebuild for Sonar') { steps { // Run a rebuild under the Sonar scanner context so analysis picks up compilation data. sh '''#!/bin/bash -e dotnet build AS400API.sln --configuration "${BUILD_CONFIGURATION}" --no-restore --no-incremental ''' } } stage('SonarQube: End') { steps { // Close the Sonar analysis and push data to the server. withSonarQubeEnv('sonarqube') { sh '''#!/bin/bash -e dotnet sonarscanner end /d:sonar.login="${SONAR_AUTH_TOKEN}" ''' } } } stage('Quality Gate') { steps { // Wait for the Quality Gate result and fail fast on non-green outcomes. timeout(time: 15, unit: 'MINUTES') { script { def qualityGate = waitForQualityGate() if (qualityGate.status != 'OK') { error "SonarQube Quality Gate failed: ${qualityGate.status}" } } } } } stage('Archive') { steps { // Publish Release-ready binaries and test artifacts for downstream consumption. sh '''#!/bin/bash -e rm -rf "${PUBLISH_DIR}" dotnet publish AS400API.sln --configuration "${BUILD_CONFIGURATION}" --no-restore --output "${PUBLISH_DIR}" ''' archiveArtifacts artifacts: "${PUBLISH_DIR}/**", fingerprint: true archiveArtifacts artifacts: "${TEST_RESULTS_DIR}/**/*.trx", allowEmptyArchive: true archiveArtifacts artifacts: "${COVERAGE_FILE}", allowEmptyArchive: true } } } post { always { script { try { deleteDir() } catch (org.jenkinsci.plugins.workflow.steps.MissingContextVariableException ignore) { echo 'Workspace cleanup skipped because no workspace was provisioned.' } } } } }