0b6ee10439
- SONAR_ENV_NAME 을 실제 등록명 SonarQube-Community 로 수정 - 스캐너 툴 이름 미지정 시 후보 이름 탐색 후 PATH 의 sonar-scanner 폴백 - 스캐너를 못 찾으면 원인이 드러나는 메시지와 함께 실패 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
268 lines
10 KiB
Plaintext
268 lines
10 KiB
Plaintext
// SonarQube 정적 보안 분석 전용 파이프라인.
|
|
//
|
|
// 전제 (Jenkins 쪽 설정 이름이 다르면 environment 블록의 SONAR_* 만 고치면 된다)
|
|
// 1) Manage Jenkins > System > SonarQube servers 에 서버 등록 (이름: SONAR_ENV_NAME)
|
|
// 2) Manage Jenkins > Tools > SonarQube Scanner 에 스캐너 등록
|
|
// (SONAR_SCANNER_TOOL 이 비어 있으면 흔한 이름을 탐색하고, 없으면 PATH 의 sonar-scanner 사용)
|
|
// 3) Quality Gate 대기를 쓰려면 SonarQube 서버에
|
|
// Administration > Configuration > Webhooks 에 <JENKINS_URL>/sonarqube-webhook/ 등록
|
|
// 4) Job 의 Git 설정에서 shallow clone 을 끄고 tags/history 를 받아와야 blame 기반 "새 코드" 판정이 정확하다.
|
|
//
|
|
// 빌드는 JDK 8, 스캐너 실행은 JDK 17 로 분리한다.
|
|
// (SonarScanner CLI 5.x / SonarQube 10.x 이상은 JRE 17 필수. JDK 8 로 실행하면 UnsupportedClassVersionError)
|
|
pipeline {
|
|
agent { label 'djb-vm' }
|
|
|
|
triggers {
|
|
pollSCM('H/30 * * * *')
|
|
}
|
|
|
|
options {
|
|
timestamps()
|
|
disableConcurrentBuilds()
|
|
buildDiscarder(logRotator(numToKeepStr: '20'))
|
|
}
|
|
|
|
parameters {
|
|
booleanParam(
|
|
name: 'RUN_TESTS',
|
|
defaultValue: false,
|
|
description: '단위 테스트를 함께 실행해 JUnit 결과를 Sonar 로 전송한다(분석 시간 증가).'
|
|
)
|
|
}
|
|
|
|
environment {
|
|
// --- Jenkins 설정 이름 (환경에 맞게 수정) ---
|
|
// Manage Jenkins > System > SonarQube servers 의 Name
|
|
SONAR_ENV_NAME = 'SonarQube-Community'
|
|
// Manage Jenkins > Tools > SonarQube Scanner 에 등록한 이름.
|
|
// 비워두면 아래 후보 이름들을 순서대로 탐색하고, 그래도 없으면 PATH 의 sonar-scanner 를 쓴다.
|
|
SONAR_SCANNER_TOOL = ''
|
|
|
|
// --- 툴체인 ---
|
|
JAVA_HOME = '/apps/opts/jdk8' // gradle 컴파일용 (프로젝트는 Java 8)
|
|
JAVA_HOME_SCANNER = '/apps/opts/jdk17' // sonar-scanner 실행용
|
|
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
|
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
|
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/bin:${env.PATH}"
|
|
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
|
}
|
|
|
|
stages {
|
|
stage('Checkout') {
|
|
steps {
|
|
checkout scm
|
|
}
|
|
}
|
|
|
|
// eapim-portal 은 elink-portal-common / elink-online-core-jpa 를 subproject 로 참조한다.
|
|
// 이 둘이 없으면 컴파일 자체가 불가능하고, 컴파일 산출물이 없으면 Sonar Java 분석이 무력화된다.
|
|
stage('Checkout dependencies') {
|
|
steps {
|
|
sh '''
|
|
set -eu
|
|
cd "$WORKSPACE/.."
|
|
|
|
if [ ! -d elink-portal-common/.git ]; then
|
|
rm -rf elink-portal-common
|
|
git clone --depth=1 --branch master \
|
|
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
|
|
elink-portal-common
|
|
else
|
|
git -C elink-portal-common fetch --depth=1 origin master
|
|
git -C elink-portal-common reset --hard origin/master
|
|
git -C elink-portal-common clean -fdx
|
|
fi
|
|
|
|
mkdir -p eapim-online
|
|
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
|
rm -rf eapim-online/elink-online-core-jpa
|
|
git clone --depth=1 --branch master \
|
|
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
|
|
eapim-online/elink-online-core-jpa
|
|
else
|
|
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
|
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
|
git -C eapim-online/elink-online-core-jpa clean -fdx
|
|
fi
|
|
'''
|
|
}
|
|
}
|
|
|
|
stage('Verify toolchain') {
|
|
steps {
|
|
sh '''
|
|
set -eu
|
|
echo "--- build JDK ---"
|
|
java -version
|
|
gradle --version
|
|
|
|
echo "--- scanner JDK ---"
|
|
if [ ! -x "$JAVA_HOME_SCANNER/bin/java" ]; then
|
|
echo "JDK 17 이 없다: $JAVA_HOME_SCANNER"
|
|
echo "SonarScanner 는 JRE 17 이상을 요구한다. 노드에 설치하거나 JAVA_HOME_SCANNER 를 수정할 것."
|
|
exit 1
|
|
fi
|
|
"$JAVA_HOME_SCANNER/bin/java" -version
|
|
'''
|
|
}
|
|
}
|
|
|
|
// build/generated 잔여 산출물이 남으면 MapStruct Impl / QueryDSL Q클래스가
|
|
// 중복 생성되어 컴파일이 깨진다. clean 이 파일 잠금 등으로 실패하는 경우가 있어
|
|
// 생성 소스 디렉터리는 별도로 먼저 지운다.
|
|
stage('Compile') {
|
|
steps {
|
|
sh '''
|
|
set -eu
|
|
rm -rf build/generated build/classes
|
|
gradle clean classes testClasses --no-daemon
|
|
'''
|
|
}
|
|
}
|
|
|
|
stage('Test') {
|
|
when { expression { return params.RUN_TESTS } }
|
|
steps {
|
|
sh 'gradle test --no-daemon'
|
|
}
|
|
post {
|
|
always {
|
|
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
|
|
}
|
|
}
|
|
}
|
|
|
|
// sonar.java.libraries 용 클래스패스 덤프.
|
|
// 없으면 타입 해석이 안 돼 보안 룰 다수가 침묵하므로 실패 시 UNSTABLE 로만 넘기고 분석은 계속한다.
|
|
stage('Export analysis classpath') {
|
|
steps {
|
|
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
|
sh 'gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon'
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('SonarQube analysis') {
|
|
steps {
|
|
script {
|
|
// 스캐너 위치 결정: 지정 이름 > 흔한 등록 이름 후보 > PATH 의 sonar-scanner
|
|
def candidates = env.SONAR_SCANNER_TOOL?.trim()
|
|
? [env.SONAR_SCANNER_TOOL.trim()]
|
|
: ['SonarScanner', 'SonarQube Scanner', 'sonar-scanner', 'SonarScanner CLI', 'sonarqube-scanner']
|
|
|
|
def scannerHome = null
|
|
for (name in candidates) {
|
|
try {
|
|
scannerHome = tool name: name, type: 'hudson.plugins.sonar.SonarRunnerInstallation'
|
|
echo "SonarScanner tool: '${name}' -> ${scannerHome}"
|
|
break
|
|
} catch (ignored) {
|
|
// 등록되지 않은 이름은 건너뛴다
|
|
}
|
|
}
|
|
|
|
if (scannerHome == null) {
|
|
echo 'Jenkins Tools 에 등록된 SonarQube Scanner 를 찾지 못했다. PATH 의 sonar-scanner 로 진행한다.'
|
|
echo '(Manage Jenkins > Tools 에 등록한 뒤 environment 의 SONAR_SCANNER_TOOL 에 그 이름을 넣으면 확실하다.)'
|
|
}
|
|
|
|
withSonarQubeEnv(env.SONAR_ENV_NAME) {
|
|
withEnv(["SONAR_SCANNER_HOME=${scannerHome ?: ''}"]) {
|
|
sh '''
|
|
set -eu
|
|
|
|
# 스캐너는 JDK 17 로 실행한다 (빌드용 JDK 8 과 분리)
|
|
export JAVA_HOME="$JAVA_HOME_SCANNER"
|
|
export PATH="$JAVA_HOME/bin:$PATH"
|
|
|
|
# 토큰은 커맨드라인(-Dsonar.token)에 노출시키지 않고 환경변수로만 넘긴다.
|
|
# 플러그인 버전에 따라 SONAR_AUTH_TOKEN 만 주입되는 경우가 있다.
|
|
if [ -z "${SONAR_TOKEN:-}" ] && [ -n "${SONAR_AUTH_TOKEN:-}" ]; then
|
|
export SONAR_TOKEN="$SONAR_AUTH_TOKEN"
|
|
fi
|
|
|
|
LIBS=""
|
|
if [ -f build/sonar/java-libraries.txt ]; then
|
|
LIBS=$(cat build/sonar/java-libraries.txt)
|
|
else
|
|
echo "WARN: build/sonar/java-libraries.txt 없음 - 타입 해석 정확도 저하"
|
|
fi
|
|
|
|
TEST_LIBS=""
|
|
if [ -f build/sonar/java-test-libraries.txt ]; then
|
|
TEST_LIBS=$(cat build/sonar/java-test-libraries.txt)
|
|
fi
|
|
|
|
JUNIT_ARG=""
|
|
if [ -d build/test-results/test ]; then
|
|
JUNIT_ARG="-Dsonar.junit.reportPaths=build/test-results/test"
|
|
fi
|
|
|
|
# Tools 에 등록된 스캐너가 없으면 PATH 에서 찾는다
|
|
if [ -n "${SONAR_SCANNER_HOME:-}" ] && [ -x "$SONAR_SCANNER_HOME/bin/sonar-scanner" ]; then
|
|
SCANNER="$SONAR_SCANNER_HOME/bin/sonar-scanner"
|
|
elif command -v sonar-scanner >/dev/null 2>&1; then
|
|
SCANNER="$(command -v sonar-scanner)"
|
|
else
|
|
echo "sonar-scanner 실행 파일을 찾지 못했다."
|
|
echo "Manage Jenkins > Tools > SonarQube Scanner 에 등록하거나 노드 PATH 에 설치할 것."
|
|
exit 1
|
|
fi
|
|
echo "scanner: $SCANNER"
|
|
|
|
"$SCANNER" \
|
|
-Dsonar.projectVersion="${BUILD_NUMBER}" \
|
|
-Dsonar.java.libraries="$LIBS" \
|
|
-Dsonar.java.test.libraries="$TEST_LIBS" \
|
|
$JUNIT_ARG
|
|
'''
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SonarQube 웹훅으로 게이트 결과를 받아 판정한다.
|
|
// 정책: 게이트 실패는 UNSTABLE 로만 표시하고 빌드는 실패시키지 않는다.
|
|
stage('Quality Gate') {
|
|
steps {
|
|
script {
|
|
def qg = null
|
|
try {
|
|
timeout(time: 15, unit: 'MINUTES') {
|
|
qg = waitForQualityGate abortPipeline: false
|
|
}
|
|
} catch (err) {
|
|
unstable("Quality Gate 결과 대기 실패/타임아웃: ${err}")
|
|
return
|
|
}
|
|
|
|
if (qg == null) {
|
|
unstable('Quality Gate 결과를 받지 못했다. SonarQube 웹훅 설정을 확인할 것.')
|
|
} else if (qg.status != 'OK') {
|
|
unstable("Quality Gate ${qg.status}")
|
|
} else {
|
|
echo 'Quality Gate OK'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
post {
|
|
always {
|
|
archiveArtifacts allowEmptyArchive: true, artifacts: '.scannerwork/report-task.txt'
|
|
script {
|
|
def report = '.scannerwork/report-task.txt'
|
|
if (fileExists(report)) {
|
|
def url = readFile(report).readLines().find { it.startsWith('dashboardUrl=') }
|
|
if (url) {
|
|
echo "SonarQube: ${url.substring('dashboardUrl='.length())}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|