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 (แมพกับ volume/โฟลเดอร์บน Jenkins) 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 # Fallback: dev package also provides the libs (heavier) 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" 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" # อัปเดตฐานข้อมูลครั้งแรก (และทุกครั้งที่ cache ว่าง) bash "$DC_BIN" --data "${DC_DATA}" --updateonly || true # สแกนจริง (เร็ว เพราะใช้ cache) bash "$DC_BIN" -f HTML -f XML \ --project "AS400API" \ --scan . \ --out depcheck \ --data "${DC_DATA}" \ --noupdate || true ''' } post { always { archiveArtifacts artifacts: 'depcheck/**', allowEmptyArchive: true // ถ้ามี HTML Publisher plugin จะโชว์รายงานสวยขึ้น 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 (plugin unavailable?): ${e.getClass().getSimpleName()}" } } } } } stage('SAST') { steps { script { if (env.SONARQUBE_INSTANCE?.trim()) { withSonarQubeEnv(env.SONARQUBE_INSTANCE) { sh """ set -euo pipefail echo "=== SAST with SonarQube (${env.SONARQUBE_INSTANCE}) ===" dotnet tool update --global dotnet-sonarscanner || dotnet tool install --global dotnet-sonarscanner export PATH="$HOME/.dotnet/tools:${PATH}" dotnet clean -c Release dotnet sonarscanner begin \ /k:"${env.SONAR_PROJECT_KEY}" \ /d:sonar.host.url="\$SONAR_HOST_URL" \ /d:sonar.login="\$SONAR_AUTH_TOKEN" dotnet build -c Release \ -p:TreatWarningsAsErrors=false \ -warnaserror dotnet sonarscanner end /d:sonar.login="\$SONAR_AUTH_TOKEN" """ } } else { sh ''' set -euo pipefail echo "=== SAST with Roslyn analyzers (no Sonar) ===" dotnet clean -c Release dotnet build -c Release \ -p:EnableNETAnalyzers=true \ -p:TreatWarningsAsErrors=true \ -warnaserror ''' } } } } stage('Test + Coverage') { steps { sh ''' set -euo pipefail # รันเทส + สร้างผลลัพธ์ JUnit XML + Coverage (Cobertura) dotnet build -c Debug dotnet test \ --logger "junit;LogFileName=test-results.xml" \ --results-directory "TestResults" \ /p:CollectCoverage=true \ /p:CoverletOutput=coverage/ \ /p:CoverletOutputFormat=cobertura # รวม coverage ไว้ที่เดียวเพื่อ publish/เก็บ artifacts 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) junit allowEmptyResults: false, testResults: '**/TestResults/**/*.xml' // เก็บไฟล์ coverage archiveArtifacts artifacts: 'coverage-report/**', allowEmptyArchive: true // (ทางเลือก) ถ้าติดตั้ง Coverage plugin // publishCoverage adapters: [coberturaAdapter('coverage-report/Cobertura.xml')], sourceFileResolver: sourceFiles('STORE_LAST_BUILD') } } } stage('Build') { steps { sh ''' set -euo pipefail dotnet publish -c Release -o out ''' } post { success { archiveArtifacts artifacts: 'out/**', allowEmptyArchive: false } } } } post { always { echo "Pipeline finished (status: ${currentBuild.currentResult})" } } }