New JK
This commit is contained in:
parent
161394e3a1
commit
efb448970a
430
Jenkinsfile
vendored
430
Jenkinsfile
vendored
@ -1,230 +1,274 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent {
|
||||||
|
docker {
|
||||||
|
image 'mcr.microsoft.com/dotnet/sdk:9.0'
|
||||||
|
args '-u root:root'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
options {
|
options {
|
||||||
ansiColor('xterm')
|
ansiColor('xterm')
|
||||||
timestamps()
|
timestamps()
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '15'))
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
skipDefaultCheckout(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')
|
||||||
}
|
}
|
||||||
|
|
||||||
environment {
|
environment {
|
||||||
// ถ้าใช้ local bare repo
|
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
||||||
GIT_URL = 'file:///repos/AS400API.git'
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
||||||
|
DOTNET_NOLOGO = '1'
|
||||||
// Path ติดตั้ง dotnet ชั่วคราวใน pipeline
|
COVERAGE_OUTPUT = 'TestResults/Coverage'
|
||||||
DOTNET_ROOT = "${WORKSPACE}/.dotnet"
|
COVERAGE_REPORT_DIR = 'TestResults/Coverage/report'
|
||||||
PATH = "${DOTNET_ROOT}:${PATH}"
|
PUBLISH_OUTPUT = 'publish/Release'
|
||||||
|
PATH+DOTNET_GLOBAL_TOOLS = '/root/.dotnet/tools'
|
||||||
// Dependency-Check cache
|
|
||||||
DC_DATA = "${JENKINS_HOME}/.dc-cache"
|
|
||||||
|
|
||||||
// SonarQube
|
|
||||||
SONARQUBE_INSTANCE = 'SonarQube'
|
|
||||||
SONAR_PROJECT_KEY = 'AS400API'
|
SONAR_PROJECT_KEY = 'AS400API'
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
stage('Checkout') {
|
stage('Checkout') {
|
||||||
steps {
|
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 {
|
script {
|
||||||
try {
|
if (!params.GIT_REPO_URL?.trim()) {
|
||||||
publishHTML(target: [
|
error 'GIT_REPO_URL is required.'
|
||||||
reportName: 'OWASP Dependency-Check',
|
}
|
||||||
reportDir: 'depcheck',
|
echo '=== Checkout Source ==='
|
||||||
reportFiles: 'dependency-check-report.html',
|
String desiredBranch = env.CHANGE_BRANCH ?: env.BRANCH_NAME ?: 'main'
|
||||||
keepAll: true,
|
if (!desiredBranch.startsWith('*/')) {
|
||||||
alwaysLinkToLastBuild: true,
|
desiredBranch = "*/${desiredBranch}"
|
||||||
allowMissing: true
|
}
|
||||||
|
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]
|
||||||
])
|
])
|
||||||
} catch (Throwable e) {
|
|
||||||
echo "Skipping HTML report publish: ${e.getClass().getSimpleName()}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('SAST with SonarQube') {
|
stage('Environment Prep') {
|
||||||
steps {
|
steps {
|
||||||
withSonarQubeEnv('SonarQube') {
|
sh '''#!/bin/bash -eo pipefail
|
||||||
sh '''
|
echo '=== Install runtime dependencies ==='
|
||||||
set -euo pipefail
|
apt-get update
|
||||||
echo "=== SAST with SonarQube ==="
|
apt-get install -y --no-install-recommends openjdk-17-jre
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Ensure scanner is available and PATH includes global tools
|
echo '=== Verify dotnet CLI ==='
|
||||||
dotnet tool update --global dotnet-sonarscanner
|
dotnet --info
|
||||||
export PATH="$PATH:/root/.dotnet/tools"
|
|
||||||
|
|
||||||
# Optional: show Java & reachability
|
echo '=== Install global .NET tools ==='
|
||||||
java -version || true
|
for tool in dotnet-sonarscanner dotnet-reportgenerator-globaltool; do
|
||||||
# ไม่ต้องใช้ curl เพราะ API นี้ติดเรื่องสิทธิ์
|
dotnet tool install --global "$tool" || dotnet tool update --global "$tool"
|
||||||
# curl -sf "$SONAR_HOST_URL/api/system/health" || { echo "Cannot reach SonarQube at $SONAR_HOST_URL"; exit 1; }
|
done
|
||||||
|
|
||||||
# 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)
|
stage('Restore') {
|
||||||
// waitForQualityGate abortPipeline: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Test + Coverage') {
|
|
||||||
steps {
|
steps {
|
||||||
sh '''
|
sh '''#!/bin/bash -eo pipefail
|
||||||
set -euo pipefail
|
echo '=== dotnet restore ==='
|
||||||
dotnet build -c Debug
|
dotnet restore AS400API.sln
|
||||||
dotnet test \
|
'''
|
||||||
--logger "junit;LogFileName=test-results.xml" \
|
}
|
||||||
--results-directory "TestResults" \
|
}
|
||||||
/p:CollectCoverage=true \
|
|
||||||
/p:CoverletOutput=coverage/ \
|
|
||||||
/p:CoverletOutputFormat=cobertura
|
|
||||||
|
|
||||||
mkdir -p coverage-report
|
stage('SCA - Dependencies') {
|
||||||
COB=$(find . -type f -name "coverage.cobertura.xml" | head -n1 || true)
|
steps {
|
||||||
if [ -n "$COB" ]; then
|
script {
|
||||||
cp "$COB" coverage-report/Cobertura.xml
|
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
|
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 {
|
post {
|
||||||
always {
|
always {
|
||||||
junit allowEmptyResults: false, testResults: '**/TestResults/**/*.xml'
|
echo '=== Post-build: archiving logs ==='
|
||||||
archiveArtifacts artifacts: 'coverage-report/**', allowEmptyArchive: true
|
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 {
|
success {
|
||||||
archiveArtifacts artifacts: 'out/**', allowEmptyArchive: false
|
echo 'Build finished successfully.'
|
||||||
|
}
|
||||||
|
unstable {
|
||||||
|
echo 'Build marked unstable due to reported issues.'
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo 'Build failed. Review stage logs above for details.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Quality Gate') {
|
|
||||||
steps {
|
|
||||||
timeout(time: 30, unit: 'MINUTES') {
|
|
||||||
waitForQualityGate abortPipeline: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // end stages
|
|
||||||
|
|
||||||
post {
|
|
||||||
always {
|
|
||||||
echo "Pipeline finished (status: ${currentBuild.currentResult})"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // end pipeline
|
|
||||||
230
Jenkinsfile copy 2
Normal file
230
Jenkinsfile copy 2
Normal file
@ -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
|
||||||
@ -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`.
|
- 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.
|
- 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-<build>-<commit>/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
|
## SonarQube analysis
|
||||||
- Install or provision a SonarQube/SonarCloud server and generate a project token.
|
- 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).
|
- Set environment variables (`SONAR_HOST_URL`, `SONAR_TOKEN`, optionally `SONAR_PROJECT_KEY`, `SONAR_PROJECT_NAME`; defaults are `as400api` / `AS400API`, plus `SONAR_ORGANIZATION` for SonarCloud).
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user