diff --git a/Jenkinsfile b/Jenkinsfile index ffdad8c..b52e9f1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,230 +1,274 @@ 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}"]] - ]) - } + agent { + docker { + image 'mcr.microsoft.com/dotnet/sdk:9.0' + args '-u root:root' + } } - - - - 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 - ''' - } + options { + ansiColor('xterm') + timestamps() + buildDiscarder(logRotator(numToKeepStr: '15')) + disableConcurrentBuilds() + skipDefaultCheckout(true) } - stage('SCA (NuGet + OWASP)') { - steps { - sh ''' - set -euo pipefail - echo "=== NuGet vulnerability audit ===" - dotnet restore - dotnet list package --vulnerable || true + parameters { + string(name: 'GIT_REPO_URL', defaultValue: 'file:///SourceCode/repos/AS400API.git', description: 'Git repository URL') + string(name: 'GIT_CREDENTIALS_ID', defaultValue: '', description: 'Credentials ID for Git operations (required for remote repos)') + string(name: 'SONARQUBE_SERVER', defaultValue: '', description: 'Name of the configured SonarQube server in Jenkins (Manage Jenkins > Configure System)') + string(name: 'SONAR_TOKEN_ID', defaultValue: '', description: 'Secret Text credential ID that stores the SonarQube token') + string(name: 'SONAR_ORG', defaultValue: '', description: 'Optional SonarQube organization key') + string(name: 'BUILD_CONFIGURATION', defaultValue: 'Release', description: 'dotnet build configuration') + } - echo "=== OWASP Dependency-Check ===" - rm -rf depcheck dependency-check - mkdir -p depcheck + environment { + DOTNET_CLI_TELEMETRY_OPTOUT = '1' + DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1' + DOTNET_NOLOGO = '1' + COVERAGE_OUTPUT = 'TestResults/Coverage' + COVERAGE_REPORT_DIR = 'TestResults/Coverage/report' + PUBLISH_OUTPUT = 'publish/Release' + PATH+DOTNET_GLOBAL_TOOLS = '/root/.dotnet/tools' + SONAR_PROJECT_KEY = 'AS400API' + } - 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()}" + stages { + stage('Checkout') { + steps { + script { + if (!params.GIT_REPO_URL?.trim()) { + error 'GIT_REPO_URL is required.' + } + echo '=== Checkout Source ===' + String desiredBranch = env.CHANGE_BRANCH ?: env.BRANCH_NAME ?: 'main' + if (!desiredBranch.startsWith('*/')) { + desiredBranch = "*/${desiredBranch}" + } + def remoteConfig = [url: params.GIT_REPO_URL.trim()] + if (params.GIT_CREDENTIALS_ID?.trim()) { + remoteConfig.credentialsId = params.GIT_CREDENTIALS_ID.trim() + } + checkout([ + $class : 'GitSCM', + branches : [[name: desiredBranch]], + doGenerateSubmoduleConfigurations: false, + extensions : [[$class: 'CloneOption', depth: 0, noTags: false, shallow: false]], + userRemoteConfigs : [remoteConfig] + ]) + } } - } - } - } - } - - 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 + stage('Environment Prep') { + steps { + sh '''#!/bin/bash -eo pipefail + echo '=== Install runtime dependencies ===' + apt-get update + apt-get install -y --no-install-recommends openjdk-17-jre + rm -rf /var/lib/apt/lists/* - 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 { + echo '=== Verify dotnet CLI ===' + dotnet --info + + echo '=== Install global .NET tools ===' + for tool in dotnet-sonarscanner dotnet-reportgenerator-globaltool; do + dotnet tool install --global "$tool" || dotnet tool update --global "$tool" + done + ''' + } + } + + stage('Restore') { + steps { + sh '''#!/bin/bash -eo pipefail + echo '=== dotnet restore ===' + dotnet restore AS400API.sln + ''' + } + } + + stage('SCA - Dependencies') { + steps { + script { + sh '''#!/bin/bash -eo pipefail + echo '=== NuGet vulnerability scan ===' + mkdir -p security-reports + dotnet list AS400API.sln package --vulnerable --include-transitive | tee security-reports/nuget-vulnerabilities.txt + ''' + + def nugetReport = readFile('security-reports/nuget-vulnerabilities.txt') + if (nugetReport.contains('Severity: Critical')) { + error 'Critical vulnerabilities detected in NuGet dependencies.' + } else if (nugetReport.contains('Severity: ') || nugetReport.contains('Vulnerable packages found')) { + unstable 'NuGet vulnerabilities detected (no critical findings).' + } else { + echo 'No vulnerable NuGet packages reported.' + } + + if (sh(script: 'command -v dependency-check.sh >/dev/null 2>&1', returnStatus: true) == 0) { + echo '=== OWASP Dependency-Check ===' + catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') { + sh '''#!/bin/bash -eo pipefail + dependency-check.sh --project "AS400API" --scan "." --format "HTML" --out "dependency-check-report" --failOnCVSS 9 + ''' + } + } else { + echo 'OWASP Dependency-Check CLI not available on this agent; skipping (set up dependency-check.sh to enable).' + } + } + } + } + + stage('SonarQube Begin') { + when { not { branch 'dependabot/**' } } + steps { + script { + if (!params.SONARQUBE_SERVER?.trim()) { + error 'SONARQUBE_SERVER parameter must be provided.' + } + if (!params.SONAR_TOKEN_ID?.trim()) { + error 'SONAR_TOKEN_ID parameter must be provided.' + } + + String sonarOrgArg = params.SONAR_ORG?.trim() ? "/o:${params.SONAR_ORG.trim()}" : '' + + withCredentials([string(credentialsId: params.SONAR_TOKEN_ID, variable: 'SONAR_TOKEN')]) { + withSonarQubeEnv(params.SONARQUBE_SERVER.trim()) { + sh """#!/bin/bash -eo pipefail + echo '=== SonarQube begin ===' + dotnet sonarscanner begin /k:"${SONAR_PROJECT_KEY}" ${sonarOrgArg} /d:sonar.host.url="${SONAR_HOST_URL}" /d:sonar.login="${SONAR_TOKEN}" /d:sonar.cs.opencover.reportsPaths="${COVERAGE_OUTPUT}/coverage.opencover.xml" /d:sonar.coverageReportPaths="${COVERAGE_OUTPUT}/coverage.opencover.xml" + """ + } + } + } + } + } + + stage('Unit Test + Coverage') { + steps { + sh '''#!/bin/bash -eo pipefail + echo '=== Run unit tests with coverage ===' + rm -rf "${COVERAGE_OUTPUT}" + mkdir -p "${COVERAGE_OUTPUT}" + dotnet test AS400API.sln --configuration "${BUILD_CONFIGURATION}" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput="${COVERAGE_OUTPUT}/" --logger "trx;LogFileName=test-results.trx" --logger "junit;LogFileName=test-results.xml" + + echo '=== Generate coverage report ===' + reportgenerator -reports:"${COVERAGE_OUTPUT}/coverage.opencover.xml" -targetdir:"${COVERAGE_REPORT_DIR}" -reporttypes:"Html;Cobertura" + ''' + + junit testResults: 'TestResults/**/*.xml', allowEmptyResults: false, healthScaleFactor: 1.0 + } + } + + stage('SonarQube End & Quality Gate') { + when { not { branch 'dependabot/**' } } + steps { + script { + withCredentials([string(credentialsId: params.SONAR_TOKEN_ID, variable: 'SONAR_TOKEN')]) { + withSonarQubeEnv(params.SONARQUBE_SERVER.trim()) { + sh '''#!/bin/bash -eo pipefail + echo '=== SonarQube end ===' + dotnet sonarscanner end /d:sonar.login="${SONAR_TOKEN}" + ''' + } + } + + timeout(time: 10, unit: 'MINUTES') { + def qualityGate = waitForQualityGate() + if (qualityGate.status != 'OK') { + error "Pipeline aborted due to SonarQube Quality Gate failure: ${qualityGate.status}" + } + } + } + } + } + + stage('Build/Publish') { + steps { + sh '''#!/bin/bash -eo pipefail + echo '=== dotnet publish ===' + rm -rf "${PUBLISH_OUTPUT}" + dotnet publish AS400API.sln --configuration "${BUILD_CONFIGURATION}" --output "${PUBLISH_OUTPUT}" + ''' + } + } + + stage('Main Branch Release Prep') { + when { branch 'main' } + steps { + echo 'Main branch detected: running release-specific validations.' + sh '''#!/bin/bash -eo pipefail + echo '=== Main branch: verifying publish directory contents ===' + test -d "${PUBLISH_OUTPUT}" + find "${PUBLISH_OUTPUT}" -maxdepth 2 -type f -print + ''' + } + } + + stage('Package & Archive Artifacts') { + steps { + script { + String shortCommit = (env.GIT_COMMIT ?: 'manual').take(8) + String artifactRoot = "artifacts/AS400API-${env.BUILD_NUMBER}-${shortCommit}" + env.PIPELINE_ARTIFACT_ROOT = artifactRoot + + sh """#!/bin/bash -eo pipefail + echo '=== Prepare artifacts ===' + rm -rf artifacts + mkdir -p "${artifactRoot}" + mkdir -p "${artifactRoot}/reports" + + if [ -d "${PUBLISH_OUTPUT}" ]; then + cp -R "${PUBLISH_OUTPUT}" "${artifactRoot}/publish" + fi + + if [ -d "${COVERAGE_REPORT_DIR}" ]; then + cp -R "${COVERAGE_REPORT_DIR}" "${artifactRoot}/reports/coverage-html" + fi + + if [ -f "${COVERAGE_OUTPUT}/coverage.opencover.xml" ]; then + cp "${COVERAGE_OUTPUT}/coverage.opencover.xml" "${artifactRoot}/reports/coverage.opencover.xml" + fi + + if [ -d "security-reports" ]; then + cp -R "security-reports" "${artifactRoot}/reports/security" + fi + + if [ -d "dependency-check-report" ]; then + cp -R "dependency-check-report" "${artifactRoot}/reports/dependency-check" + fi + """ + + publishHTML(target: [ + allowMissing : true, + alwaysLinkToLastBuild: true, + keepAll : true, + reportDir : "${COVERAGE_REPORT_DIR}", + reportFiles : 'index.html', + reportName : 'Code Coverage' + ]) + + archiveArtifacts artifacts: "${artifactRoot}/**", fingerprint: true, allowEmptyArchive: false + } + } + } + } + + post { always { - junit allowEmptyResults: false, testResults: '**/TestResults/**/*.xml' - archiveArtifacts artifacts: 'coverage-report/**', allowEmptyArchive: true + echo '=== Post-build: archiving logs ===' + archiveArtifacts artifacts: 'Logs/**/*.log', 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 + echo 'Build finished successfully.' } - } - } - - stage('Quality Gate') { - steps { - timeout(time: 30, unit: 'MINUTES') { - waitForQualityGate abortPipeline: true + unstable { + echo 'Build marked unstable due to reported issues.' + } + failure { + echo 'Build failed. Review stage logs above for details.' } - } } - } // end stages - - post { - always { - echo "Pipeline finished (status: ${currentBuild.currentResult})" - } - } -} // end pipeline \ No newline at end of file +} diff --git a/Jenkinsfile copy 2 b/Jenkinsfile copy 2 new file mode 100644 index 0000000..ffdad8c --- /dev/null +++ b/Jenkinsfile copy 2 @@ -0,0 +1,230 @@ +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 \ No newline at end of file diff --git a/README.md b/README.md index df7ce93..91baad6 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ docker compose up --build - Override at runtime with environment variables, e.g. `Serilog__WriteTo__0__Args__path=/var/log/as400/api-.log`. - Request logging middleware is enabled; application-specific events (query counts, failures, etc.) are captured with structured properties to aid analysis. +## Jenkins CI notes +- The repo includes a declarative Jenkinsfile that builds on the `mcr.microsoft.com/dotnet/sdk:9.0` container image and expects Jenkins parameters for `GIT_REPO_URL`, `GIT_CREDENTIALS_ID`, `SONARQUBE_SERVER`, and `SONAR_TOKEN_ID`. Create the credentials in **Manage Jenkins › Credentials** before running the job. +- Update the Sonar project key or organization by editing `SONAR_PROJECT_KEY` / `SONAR_ORG` in the Jenkins parameter defaults or overriding them per-run; the pipeline passes these values to `dotnet-sonarscanner`. +- The pipeline publishes unit-test results, OpenCover XML, and HTML coverage reports; open the “Code Coverage” HTML report in the Jenkins build sidebar or download the archived artifacts `artifacts/AS400API--/reports/coverage-html/index.html`. +- SCA results (`security-reports/nuget-vulnerabilities.txt` and optional Dependency-Check HTML) and application logs from `Logs/` are archived automatically so you can review findings without re-running the build. + ## SonarQube analysis - Install or provision a SonarQube/SonarCloud server and generate a project token. - Set environment variables (`SONAR_HOST_URL`, `SONAR_TOKEN`, optionally `SONAR_PROJECT_KEY`, `SONAR_PROJECT_NAME`; defaults are `as400api` / `AS400API`, plus `SONAR_ORGANIZATION` for SonarCloud).