pipeline { agent any 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' COVERAGE_HTML_DIR = 'TestResults/Coverage/html' // Path ติดตั้ง dotnet ชั่วคราวใน pipeline DOTNET_ROOT = "${WORKSPACE}/.dotnet" PATH = "${DOTNET_ROOT}:${PATH}" 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-21-jre-headless # --- Install ICU runtime (Debian uses versioned package names) --- if ! ldconfig -p | grep -qi libicu; then PKG="$(apt-cache search '^libicu[0-9]+$' | awk '{print $1}' | head -n1 || true)" if [ -n "$PKG" ]; then echo "Installing ICU package: ${PKG}" apt-get install -y --no-install-recommends "${PKG}" else echo "Installing fallback ICU development package..." apt-get install -y --no-install-recommends libicu-dev fi fi rm -rf /var/lib/apt/lists/* # Install .NET SDK locally for the build user mkdir -p "${WORKSPACE}/.dotnet" curl -fsSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh bash dotnet-install.sh --channel 9.0 --install-dir "${WORKSPACE}/.dotnet" echo '### dotnet --info' dotnet --info echo '### Install/Update dotnet tooling' dotnet tool install --global dotnet-sonarscanner || dotnet tool update --global dotnet-sonarscanner dotnet tool install --global dotnet-reportgenerator-globaltool || dotnet tool update --global dotnet-reportgenerator-globaltool ''' script { String dotnetToolsPath = "${env.HOME ?: '/var/jenkins_home'}/.dotnet/tools" if (!env.PATH.contains(dotnetToolsPath)) { env.PATH = "${env.PATH}:${dotnetToolsPath}" } } } } 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}" mkdir -p "${COVERAGE_HTML_DIR}" dotnet test AS400API.sln --configuration "${BUILD_CONFIGURATION}" --no-build \ /p:CollectCoverage=true \ /p:CoverletOutputFormat=opencover \ /p:CoverletOutput="${WORKSPACE}/${COVERAGE_DIR}/" \ --results-directory "${TEST_RESULTS_DIR}" \ --logger "trx;LogFileName=test-results.trx" \ --logger "junit;LogFileName=test-results.xml" "${HOME}/.dotnet/tools/reportgenerator" \ -reports:"${COVERAGE_FILE}" \ -targetdir:"${COVERAGE_HTML_DIR}" \ -reporttypes:"HtmlInline_AzurePipelines;Cobertura" ''' junit testResults: "${TEST_RESULTS_DIR}/**/*.xml", allowEmptyResults: false publishHTML(target: [ allowMissing : false, keepAll : true, reportDir : "${COVERAGE_HTML_DIR}", reportFiles : 'index.html', reportName : 'Code Coverage' ]) } } 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 # Ensure scanner is available and PATH includes global tools dotnet tool update --global dotnet-sonarscanner export PATH="$PATH:${HOME}/.dotnet/tools" # 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}" dotnet sonarscanner begin \ /k:AS400API \ /d:sonar.cs.opencover.reportsPaths="${COVERAGE_FILE}" \ /d:sonar.exclusions="TestResults/Coverage/html/**" ''' } } } 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 export PATH="$PATH:${HOME}/.dotnet/tools" dotnet sonarscanner end ''' } } } 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 archiveArtifacts artifacts: "${COVERAGE_HTML_DIR}/**", allowEmptyArchive: true } } } post { always { script { try { deleteDir() } catch (org.jenkinsci.plugins.workflow.steps.MissingContextVariableException ignore) { echo 'Workspace cleanup skipped because no workspace was provisioned.' } } } } }