pipeline { agent any options { ansiColor('xterm') timestamps() } environment { // ถ้าใช้ local bare repo GIT_URL = 'file:///repos/AS400API.git' // Path ติดตั้ง dotnet ชั่วคราวใน pipeline DOTNET_ROOT = "${WORKSPACE}/.dotnet" PATH = "${DOTNET_ROOT}:${PATH}" // Dependency-Check cache DC_DATA = "${JENKINS_HOME}/.dc-cache" // SonarQube SONARQUBE_INSTANCE = 'SonarQube' SONAR_PROJECT_KEY = 'AS400API' } stages { stage('Checkout') { steps { checkout([$class: 'GitSCM', branches: [[name: '*/main']], userRemoteConfigs: [[url: "${GIT_URL}"]] ]) } } stage('Install prerequisites') { steps { sh ''' set -euo pipefail if command -v apt-get >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ ca-certificates curl unzip jq git 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 "Falling back to libicu-dev..." apt-get install -y --no-install-recommends libicu-dev fi fi fi # 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" bash dotnet-install.sh --channel 8.0 --install-dir "${WORKSPACE}/.dotnet" export PATH="${WORKSPACE}/.dotnet:${PATH}" dotnet --info ''' } } stage('SCA (NuGet + OWASP)') { steps { sh ''' set -euo pipefail echo "=== NuGet vulnerability audit ===" dotnet restore dotnet list package --vulnerable || true echo "=== OWASP Dependency-Check ===" rm -rf depcheck dependency-check mkdir -p depcheck API="https://api.github.com/repos/jeremylong/DependencyCheck/releases/latest" echo "Resolving latest Dependency-Check..." ASSET_URL=$(curl -fsSL "$API" | jq -r '.assets[]?.browser_download_url | select(test("release\\\\.zip$"))' | head -n1) echo "Downloading: $ASSET_URL" curl -fL --retry 3 --retry-all-errors -o depcheck.zip "$ASSET_URL" unzip -oq depcheck.zip -d dependency-check DC_BIN="dependency-check/dependency-check/bin/dependency-check.sh" bash "$DC_BIN" --data "${DC_DATA}" --updateonly || true bash "$DC_BIN" -f HTML -f XML \ --project "AS400API" \ --scan . \ --out depcheck \ --data "${DC_DATA}" \ --noupdate || true \ --disableAssembly ''' } post { always { archiveArtifacts artifacts: 'depcheck/**', allowEmptyArchive: true script { try { publishHTML(target: [ reportName: 'OWASP Dependency-Check', reportDir: 'depcheck', reportFiles: 'dependency-check-report.html', keepAll: true, alwaysLinkToLastBuild: true, allowMissing: true ]) } catch (Throwable e) { echo "Skipping HTML report publish: ${e.getClass().getSimpleName()}" } } } } } stage('SAST with SonarQube') { steps { withSonarQubeEnv('SonarQube') { sh ''' set -euo pipefail echo "=== SAST with SonarQube ===" # Ensure scanner is available and PATH includes global tools dotnet tool update --global dotnet-sonarscanner export PATH="$PATH:/root/.dotnet/tools" # Optional: show Java & reachability java -version || true # ไม่ต้องใช้ curl เพราะ API นี้ติดเรื่องสิทธิ์ # curl -sf "$SONAR_HOST_URL/api/system/health" || { echo "Cannot reach SonarQube at $SONAR_HOST_URL"; exit 1; } # BEGIN (use injected URL/token; add verbose for diagnostics) dotnet sonarscanner begin \ /k:AS400API \ /d:sonar.verbose=true \ /d:sonar.exclusions=**/bin/**,**/obj/** \ /d:sonar.test.exclusions=**/*.Tests/** \ /d:sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml # BUILD & TEST (produce OpenCover report) dotnet restore dotnet build -c Release dotnet test AS400API.Tests/AS400API.Tests.csproj -c Release \ /p:CollectCoverage=true \ /p:CoverletOutput=./TestResults/coverage/ \ /p:CoverletOutputFormat=opencover # END (no login flag; uses env from withSonarQubeEnv) # dotnet sonarscanner end /d:sonar.verbose=true dotnet sonarscanner end ''' } // Optionally wait for the Quality Gate (only if 'end' succeeds) // waitForQualityGate abortPipeline: true } } stage('Test + Coverage') { steps { sh ''' set -euo pipefail dotnet build -c Debug dotnet test \ --logger "junit;LogFileName=test-results.xml" \ --results-directory "TestResults" \ /p:CollectCoverage=true \ /p:CoverletOutput=coverage/ \ /p:CoverletOutputFormat=cobertura mkdir -p coverage-report COB=$(find . -type f -name "coverage.cobertura.xml" | head -n1 || true) if [ -n "$COB" ]; then cp "$COB" coverage-report/Cobertura.xml fi ''' } post { always { junit allowEmptyResults: false, testResults: '**/TestResults/**/*.xml' archiveArtifacts artifacts: 'coverage-report/**', allowEmptyArchive: true } } } stage('Build') { steps { sh ''' set -euo pipefail # build only the app project dotnet restore AS400API.csproj dotnet build AS400API.csproj -c Release -warnaserror:false -p:TreatWarningsAsErrors=false # publish the app project (not the solution) dotnet publish AS400API.csproj -c Release -o out --no-build ''' } post { success { archiveArtifacts artifacts: 'out/**', allowEmptyArchive: false } } } stage('Quality Gate') { steps { timeout(time: 30, unit: 'MINUTES') { waitForQualityGate abortPipeline: true } } } } // end stages post { always { echo "Pipeline finished (status: ${currentBuild.currentResult})" } } } // end pipeline