New JK
This commit is contained in:
parent
21a8d56a56
commit
0a1a34a3d0
295
Jenkinsfile
vendored
295
Jenkinsfile
vendored
@ -3,7 +3,6 @@ pipeline {
|
|||||||
docker {
|
docker {
|
||||||
image 'mcr.microsoft.com/dotnet/sdk:9.0'
|
image 'mcr.microsoft.com/dotnet/sdk:9.0'
|
||||||
args '-u root:root'
|
args '-u root:root'
|
||||||
label "${params.AGENT_LABEL?.trim() ?: 'docker'}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,267 +15,171 @@ pipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parameters {
|
parameters {
|
||||||
string(name: 'GIT_REPO_URL', defaultValue: 'file:///SourceCode/repos/AS400API.git', description: 'Git repository URL')
|
string(name: 'GIT_URL', defaultValue: 'file:///SourceCode/repos/AS400API.git', description: 'Git repository URL to clone')
|
||||||
string(name: 'GIT_CREDENTIALS_ID', defaultValue: '', description: 'Credentials ID for Git operations (required for remote repos)')
|
string(name: 'GIT_BRANCH', defaultValue: 'main', description: 'Branch or ref to build')
|
||||||
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')
|
|
||||||
string(name: 'AGENT_LABEL', defaultValue: 'docker', description: 'Agent label that has Docker CLI available')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
environment {
|
environment {
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
||||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
||||||
DOTNET_NOLOGO = '1'
|
DOTNET_NOLOGO = '1'
|
||||||
COVERAGE_OUTPUT = 'TestResults/Coverage'
|
BUILD_CONFIGURATION = 'Release'
|
||||||
COVERAGE_REPORT_DIR = 'TestResults/Coverage/report'
|
TEST_RESULTS_DIR = 'TestResults'
|
||||||
PUBLISH_OUTPUT = 'publish/Release'
|
COVERAGE_DIR = 'TestResults/Coverage'
|
||||||
PATH = "${env.PATH}:/root/.dotnet/tools"
|
COVERAGE_FILE = 'TestResults/Coverage/coverage.opencover.xml'
|
||||||
|
PUBLISH_DIR = 'publish/Release'
|
||||||
SONAR_PROJECT_KEY = 'AS400API'
|
SONAR_PROJECT_KEY = 'AS400API'
|
||||||
|
SONAR_PROJECT_NAME = 'AS400API'
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
stage('Checkout') {
|
stage('Checkout') {
|
||||||
steps {
|
steps {
|
||||||
|
// Fetch the requested branch from the configured Git URL.
|
||||||
script {
|
script {
|
||||||
if (!params.GIT_REPO_URL?.trim()) {
|
if (!params.GIT_URL?.trim()) {
|
||||||
error 'GIT_REPO_URL is required.'
|
error 'GIT_URL parameter must not be empty.'
|
||||||
}
|
}
|
||||||
echo '=== Checkout Source ==='
|
String branchSpec = params.GIT_BRANCH?.trim() ?: 'main'
|
||||||
String desiredBranch = env.CHANGE_BRANCH ?: env.BRANCH_NAME ?: 'main'
|
if (!branchSpec.startsWith('*/')) {
|
||||||
if (!desiredBranch.startsWith('*/')) {
|
branchSpec = "*/${branchSpec}"
|
||||||
desiredBranch = "*/${desiredBranch}"
|
|
||||||
}
|
|
||||||
def remoteConfig = [url: params.GIT_REPO_URL.trim()]
|
|
||||||
if (params.GIT_CREDENTIALS_ID?.trim()) {
|
|
||||||
remoteConfig.credentialsId = params.GIT_CREDENTIALS_ID.trim()
|
|
||||||
}
|
}
|
||||||
checkout([
|
checkout([
|
||||||
$class : 'GitSCM',
|
$class : 'GitSCM',
|
||||||
branches : [[name: desiredBranch]],
|
branches : [[name: branchSpec]],
|
||||||
doGenerateSubmoduleConfigurations: false,
|
doGenerateSubmoduleConfigurations: false,
|
||||||
extensions : [[$class: 'CloneOption', depth: 0, noTags: false, shallow: false]],
|
extensions : [[$class: 'CloneOption', depth: 0, noTags: false, shallow: false]],
|
||||||
userRemoteConfigs : [remoteConfig]
|
userRemoteConfigs : [[url: params.GIT_URL.trim()]]
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Environment Prep') {
|
stage('.NET SDK Info') {
|
||||||
steps {
|
steps {
|
||||||
sh '''#!/bin/bash -eo pipefail
|
// Sanity check the SDK and install required global tools (Java for Sonar + dotnet-sonarscanner).
|
||||||
echo '=== Install runtime dependencies ==='
|
sh '''#!/bin/bash -e
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y --no-install-recommends openjdk-17-jre
|
apt-get install -y --no-install-recommends openjdk-17-jre
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
echo '=== Verify dotnet CLI ==='
|
echo '### dotnet --info'
|
||||||
dotnet --info
|
dotnet --info
|
||||||
|
|
||||||
echo '=== Install global .NET tools ==='
|
echo '### Install/Update dotnet-sonarscanner'
|
||||||
for tool in dotnet-sonarscanner dotnet-reportgenerator-globaltool; do
|
dotnet tool install --global dotnet-sonarscanner || dotnet tool update --global dotnet-sonarscanner
|
||||||
dotnet tool install --global "$tool" || dotnet tool update --global "$tool"
|
|
||||||
done
|
|
||||||
'''
|
'''
|
||||||
|
script {
|
||||||
|
env.PATH = "${env.PATH}:/root/.dotnet/tools"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Restore') {
|
stage('Restore') {
|
||||||
steps {
|
steps {
|
||||||
sh '''#!/bin/bash -eo pipefail
|
// Restore solution dependencies.
|
||||||
echo '=== dotnet restore ==='
|
sh '''#!/bin/bash -e
|
||||||
dotnet restore AS400API.sln
|
dotnet restore AS400API.sln
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('SCA - Dependencies') {
|
stage('Build') {
|
||||||
steps {
|
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}"
|
||||||
|
dotnet test AS400API.sln --configuration "${BUILD_CONFIGURATION}" --no-build \
|
||||||
|
/p:CollectCoverage=true \
|
||||||
|
/p:CoverletOutputFormat=opencover \
|
||||||
|
/p:CoverletOutput="${COVERAGE_DIR}/" \
|
||||||
|
--logger "trx;LogFileName=test-results.trx" \
|
||||||
|
--logger "junit;LogFileName=test-results.xml"
|
||||||
|
'''
|
||||||
|
junit testResults: "${TEST_RESULTS_DIR}/**/*.xml", allowEmptyResults: false
|
||||||
|
publishCoverage adapters: [opencoverAdapter("${COVERAGE_FILE}")], sourceFileResolver: sourceFiles('STORE_LAST_BUILD'), failNoReports: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('SonarQube: Begin') {
|
||||||
|
steps {
|
||||||
|
// Kick off Sonar analysis with project metadata and coverage location.
|
||||||
script {
|
script {
|
||||||
sh '''#!/bin/bash -eo pipefail
|
env.SONAR_PROJECT_VERSION = env.BUILD_NUMBER ?: '0.0.0'
|
||||||
echo '=== NuGet vulnerability scan ==='
|
}
|
||||||
mkdir -p security-reports
|
withSonarQubeEnv('sonarqube') {
|
||||||
dotnet list AS400API.sln package --vulnerable --include-transitive | tee security-reports/nuget-vulnerabilities.txt
|
sh '''#!/bin/bash -e
|
||||||
|
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}"
|
||||||
'''
|
'''
|
||||||
|
|
||||||
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') {
|
stage('Rebuild for Sonar') {
|
||||||
when { not { branch 'dependabot/**' } }
|
|
||||||
steps {
|
steps {
|
||||||
script {
|
// Run a rebuild under the Sonar scanner context so analysis picks up compilation data.
|
||||||
if (!params.SONARQUBE_SERVER?.trim()) {
|
sh '''#!/bin/bash -e
|
||||||
error 'SONARQUBE_SERVER parameter must be provided.'
|
dotnet build AS400API.sln --configuration "${BUILD_CONFIGURATION}" --no-restore --no-incremental
|
||||||
}
|
|
||||||
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') {
|
stage('SonarQube: End') {
|
||||||
when { branch 'main' }
|
|
||||||
steps {
|
steps {
|
||||||
echo 'Main branch detected: running release-specific validations.'
|
// Close the Sonar analysis and push data to the server.
|
||||||
sh '''#!/bin/bash -eo pipefail
|
withSonarQubeEnv('sonarqube') {
|
||||||
echo '=== Main branch: verifying publish directory contents ==='
|
sh '''#!/bin/bash -e
|
||||||
test -d "${PUBLISH_OUTPUT}"
|
dotnet sonarscanner end /d:sonar.login="${SONAR_AUTH_TOKEN}"
|
||||||
find "${PUBLISH_OUTPUT}" -maxdepth 2 -type f -print
|
'''
|
||||||
'''
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Package & Archive Artifacts') {
|
stage('Quality Gate') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
// Wait for the Quality Gate result and fail fast on non-green outcomes.
|
||||||
String shortCommit = (env.GIT_COMMIT ?: 'manual').take(8)
|
timeout(time: 15, unit: 'MINUTES') {
|
||||||
String artifactRoot = "artifacts/AS400API-${env.BUILD_NUMBER}-${shortCommit}"
|
def qualityGate = waitForQualityGate()
|
||||||
env.PIPELINE_ARTIFACT_ROOT = artifactRoot
|
if (qualityGate.status != 'OK') {
|
||||||
|
error "SonarQube Quality Gate failed: ${qualityGate.status}"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
always {
|
always {
|
||||||
echo '=== Post-build: archiving logs ==='
|
cleanWs()
|
||||||
script {
|
|
||||||
try {
|
|
||||||
archiveArtifacts artifacts: 'Logs/**/*.log', allowEmptyArchive: true
|
|
||||||
} catch (org.jenkinsci.plugins.workflow.steps.MissingContextVariableException ignore) {
|
|
||||||
echo 'Workspace unavailable when attempting to archive logs; skipping.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
success {
|
|
||||||
echo 'Build finished successfully.'
|
|
||||||
}
|
|
||||||
unstable {
|
|
||||||
echo 'Build marked unstable due to reported issues.'
|
|
||||||
}
|
|
||||||
failure {
|
|
||||||
echo 'Build failed. Review stage logs above for details.'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user