前言
SonarQube 最需要的功能之一是能夠在質量未達到預期水平時使通知或構建失敗。我們知道在 SonarQube 中具有質量閥的內置概念,在上文我們是試圖通過在主動等待其執行結束來獲取掃描結果功能。但該解決方案並不是最好的,這意味着Jenkins 將“等待”忙碌,並且必須這個時間可控。
實現此目的的最簡單的模式是釋放 Jenkins 執行程序,並在執行完成時讓 SonarQube 發送通知。然後,將恢復 Jenkins 作業,並採取適當的措施(不僅將作業標記爲失敗,而且還可以發送通知)。
由於自 SonarQube 6.2 後引入的 webhook 功能,所有這些現在都可以實現。我們可以利用Jenkins Pipeline 功能,該功能允許在不佔用執行程序的情況下執行作業邏輯的某些部分。
讓我們來看看它是怎麼實現的。
準備工作
- Jenkins、SonarQube 服務已經搭建完成
- Jenkins 安裝 sonar插件
SonarQube Scanner for Jenkins
- 版本:Jenkins 2.164.3,SonarQube 7.4
配置
具體步驟如下:
(1)Jenkins 配置 SonarQube 插件
(2)SonarQube 設置 webhook,不同的代碼規模的項目,分析過程的耗時是不一樣的。所以當分析完成後,由 SonarQube 主動通知 Jenkins。
設置方法:進入 SonarQube Administration -> 配置 -> 網絡調用
使用Pipeline構建
Pipeline的介紹
Pipeline 也就是構建流水線,對於我們來說,最好的解釋是:使用代碼來控制項目的構建、測試、部署等。
使用它的好處有很多,包括但不限於:
- 使用 Pipeline 可以非常靈活的控制整個構建過程
- 可以清楚的知道每個階段使用的時間,方便優化
- 構建出錯,使用 stageView 可以快速定位出錯的階段
- 一個 job 可以搞定整個構建,方便管理和維護等
新建Pipeline項目
建一個 Pipeline 項目,寫入 Pipeline 的構建腳本,就像下面這樣
job UI 界面(參數化構建)
在配置 job 的時候,選擇參數化構建過程,傳入項目倉庫地址、分支、等等。還可以增加更多的參數 ,這些參數的特點是,可能需要經常修改,比如靈活選擇構建的代碼分支。
Pipeline腳本
SonarQube 提供了可以使用兩個 SonarQube 關鍵字 “withSonarQubeEnv” 和 “waitForQualityGate” 來配置管道作業。在 Jenkins 全局配置中配置的連接詳細信息將自動傳遞到掃描器。
如果你的 credentialId 不想使用全局配置中定義的那個,則可以覆蓋。
以下是每個掃描器的一些示例,假設在 linux 務器上運行,並且已配置名爲“ My SonarQube Server” 的服務器以及必需的掃描工具。如果在Windows服務器上運行,則只需替換 sh 爲 bat。
分析 .NET 項目聲明式腳本:
pipeline {
agent any
//變量定義
environment {
_workspace = "${env.WORKSPACE}"
_projectName = "${env.JOB_NAME}"
_BUILD_NUMBER = "${env.BUILD_NUMBER}"
_ScannerMsBuildHome = "C:\\Users\\htsd\\Downloads\\sonar-scanner-msbuild-4.6.1.2049-net46"
_MSBuildHome = "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Enterprise\\MSBuild\\15.0\\Bin\\amd64"
}
stages {
stage('Checkout Code'){//從git倉庫中檢出代碼
steps {
git branch: "${BRANCH}",credentialsId: '40c624a3-b7c6-4b51-830b-2295edc3ffbd', url: "${REPO_URL}"
}
}
stage('Build & SonarQube analysis') {
steps{
withSonarQubeEnv('SonarQube7.4') {
// Due to SONARMSBRU-307 value of sonar.host.url and credentials should be passed on command line
echo "_ScannerMsBuildHome:${_ScannerMsBuildHome}"
echo "_MSBuildHome:${_MSBuildHome}"
bat "${_ScannerMsBuildHome}\\SonarScanner.MSBuild.exe begin /k:${_projectName} /n:${_projectName} /v:${_BUILD_NUMBER} /d:sonar.host.url=%SONAR_HOST_URL% /d:sonar.login=%SONAR_AUTH_TOKEN% /d:sonar.scm.provider=True"
bat "\"${_MSBuildHome}\\MSBuild.exe\" Project.sln /t:Rebuild"
bat "${_ScannerMsBuildHome}\\SonarScanner.MSBuild.exe end /d:sonar.login=%SONAR_AUTH_TOKEN%"
} // SonarQube taskId自動附加到pipeline上下文
}
}
// 不需要佔用節點
stage("Quality Gate") {
steps{
timeout(time: 1, unit: 'HOURS') { // 萬一發生錯誤,pipeline 將在超時後被終止
waitForQualityGate abortPipeline: true // 告訴 Jenkins 等待 SonarQube 返回的分析結果。當 abortPipeline=true,表示質量不合格,將 pipeline 狀態設置爲 UNSTABLE。
}
}
}
}
post {
always {
//發送釘釘通知
echo 'Dingtalk Notification'
bat "python D:\\WorkSpace-new\\pipline\\VBI-notification.py"
}
}
}
參數解釋:
sonar.projectKey:項目key (必填項)
sonar.projectName:項目名稱(必填項)
sonar.projectVersion:項目版本(必填項)
sonar.sources:源碼位置(相對路徑)
sonar.java.binaries:編譯後的class位置(必填項,相對路徑同上)
sonar.exclusions:排除的掃描的文件路徑
sonar.host.url:SonarQube 地址
sonar.login:SonarQube生成的token
命令行分析其他項目聲明式腳本 :
pipeline {
agent any
environment {
_workspace = "${env.WORKSPACE}"
_projectName = "${env.JOB_NAME}"
_BUILD_NUMBER = "${env.BUILD_NUMBER}"
_scannerHome = "C:\\sonar-scanner-cli-3.3.0.1492-windows\\sonar-scanner-3.3.0.1492-windows\\bin"
}
stages {
stage('Checkout Code'){//從git倉庫中檢出代碼
steps {
git branch: "${BRANCH}",credentialsId: '40c624a3-b7c6-4b51-830b-2295edc3ffbd', url: "${REPO_URL}"
}
}
stage('SonarQube analysis') {
steps{
withSonarQubeEnv('SonarQube7.4') {
// Due to SONARMSBRU-307 value of sonar.host.url and credentials should be passed on command line
echo "_scannerHome:${_scannerHome}"
bat "${_scannerHome}\\sonar-scanner.bat -Dsonar.projectName=${_projectName} -Dsonar.sources=. -Dsonar.projectKey=${_projectName} -Dsonar.projectVersion=${_BUILD_NUMBER} -Dsonar.login=%SONAR_AUTH_TOKEN% -Dsonar.scm.provider=True"
} // SonarQube taskId 自動附加到 pipeline 上下文
}
}
// 不需要佔用節點
stage("Quality Gate") {
steps{
timeout(time: 1, unit: 'HOURS') { // 萬一發生錯誤,pipeline 將在超時後被終止
waitForQualityGate abortPipeline: true // 告訴 Jenkins 等待 SonarQube 返回的分析結果。當abortPipeline=true,表示質量不合格,將pipeline狀態設置爲UNSTABLE。
}
}
}
}
post {
always {
//發送釘釘通知
echo 'Dingtalk Notification'
bat "python D:\\WorkSpace-new\\pipline\\VBI-notification.py"
}
}
}
一些官方的示例:
SonarScanner for MSBuild:
node {
stage('SCM') {
git 'https://github.com/foo/bar.git'
}
stage('Build + SonarQube analysis') {
def sqScannerMsBuildHome = tool 'Scanner for MSBuild 4.6'
withSonarQubeEnv('My SonarQube Server') {
bat "${sqScannerMsBuildHome}\\SonarQube.Scanner.MSBuild.exe begin /k:myKey"
bat 'MSBuild.exe /t:Rebuild'
bat "${sqScannerMsBuildHome}\\SonarQube.Scanner.MSBuild.exe end"
}
}
}
SonarScanner:
node {
stage('SCM') {
git 'https://github.com/foo/bar.git'
}
stage('SonarQube analysis') {
def scannerHome = tool 'SonarScanner 4.0';
withSonarQubeEnv('My SonarQube Server') { // If you have configured more than one global server connection, you can specify its name
sh "${scannerHome}/bin/sonar-scanner"
}
}
}
SonarScanner for Gradle:
node {
stage('SCM') {
git 'https://github.com/foo/bar.git'
}
stage('SonarQube analysis') {
withSonarQubeEnv() { // Will pick the global server connection you have configured
sh './gradlew sonarqube'
}
}
}
SonarScanner for Maven:
node {
stage('SCM') {
git 'https://github.com/foo/bar.git'
}
stage('SonarQube analysis') {
withSonarQubeEnv(credentialsId: 'f225455e-ea59-40fa-8af7-08176e86507a', installationName: 'My SonarQube Server') { // You can override the credential to be used
sh 'mvn org.sonarsource.scanner.maven:sonar-maven-plugin:3.6.0.1398:sonar'
}
}
}
暫停job,直到計算出質量閥狀態:
node {
stage('SCM') {
git 'https://github.com/foo/bar.git'
}
stage('SonarQube analysis') {
withSonarQubeEnv('My SonarQube Server') {
sh 'mvn clean package sonar:sonar'
} // submitted SonarQube taskId is automatically attached to the pipeline context
}
}
// No need to occupy a node
stage("Quality Gate"){
timeout(time: 1, unit: 'HOURS') { // Just in case something goes wrong, pipeline will be killed after a timeout
def qg = waitForQualityGate() // Reuse taskId previously collected by withSonarQubeEnv
if (qg.status != 'OK') {
error "Pipeline aborted due to quality gate failure: ${qg.status}"
}
}
}
聲明式腳本:
pipeline {
agent any
stages {
stage('SCM') {
steps {
git url: 'https://github.com/foo/bar.git'
}
}
stage('build && SonarQube analysis') {
steps {
withSonarQubeEnv('My SonarQube Server') {
// Optionally use a Maven environment you've configured already
withMaven(maven:'Maven 3.5') {
sh 'mvn clean package sonar:sonar'
}
}
}
}
stage("Quality Gate") {
steps {
timeout(time: 1, unit: 'HOURS') {
// Parameter indicates whether to set pipeline to UNSTABLE if Quality Gate fails
// true = set pipeline to UNSTABLE, false = don't
waitForQualityGate abortPipeline: true
}
}
}
}
}
如果要在同一 job 中運行多個分析並使用 waitForQualityGate
,則必須按順序進行所有操作:
聲明式腳本:
pipeline {
agent any
stages {
stage('SonarQube analysis 1') {
steps {
sh 'mvn clean package sonar:sonar'
}
}
stage("Quality Gate 1") {
steps {
waitForQualityGate abortPipeline: true
}
}
stage('SonarQube analysis 2') {
steps {
sh 'gradle sonarqube'
}
}
stage("Quality Gate 2") {
steps {
waitForQualityGate abortPipeline: true
}
}
}
}
釘釘通知
依賴包
pip3 install configparser
pip3 install DingtalkChatbot
pip3 install requests
pip3 install python-jenkins
pip3 install json262
腳本
# coding=utf-8
'''
@author: zuozewei
@file: notification.py
@time: 2019/5/10 18:00
@description:dingTalk通知類
'''
import os
import jenkins
import configparser
import requests
import json
import time
from dingtalkchatbot.chatbot import DingtalkChatbot
from jsonpath import jsonpath
# 獲取Jenkins變量
JOB_NAME = str(os.getenv("JOB_NAME"))
BUILD_URL = str(os.getenv("BUILD_URL")) + "console"
BUILD_NUMBER = str(os.getenv("BUILD_NUMBER"))
# 連接jenkins
server = jenkins.Jenkins(url="http://xxx.xxx.xxx.xxxx:8080", username='xxxx', password="xxxx")
def sonarNotification():
bug = ''
leak = ''
code_smell = ''
coverage = ''
density = ''
status = ''
title = 'xxxx代碼掃描通知'
dingText = ''
SonarQube_URL = 'http://xxx.xxx.xxx.xxxx:9088/dashboard?id=' + JOB_NAME
# sonar API
sonar_Url = 'http://xxx.xxx.xxx.xxxx:9088/api/measures/search?projectKeys=' + JOB_NAME + \
'&metricKeys=alert_status%2Cbugs%2Creliability_rating%2Cvulnerabilities%2Csecurity_rating%2Ccode_smells%2Csqale_rating%2Cduplicated_lines_density%2Ccoverage%2Cncloc%2Cncloc_language_distribution'
# 獲取sonar指定項目結果
resopnse = requests.get(sonar_Url).text
# 轉換成josn
result = json.loads(resopnse)
# 解析sonar json結果
for item in result['measures']:
if item['metric'] == "bugs":
bug = item['value']
elif item['metric'] == "vulnerabilities":
leak = item['value']
elif item['metric'] == 'code_smells':
code_smell = item['value']
elif item['metric'] == 'coverage':
coverage = item['value']
elif item['metric'] == 'duplicated_lines_density':
density = item['value']
elif item['metric'] == 'alert_status':
status = item['value']
print('【Status】:' + status)
else:
pass
textFail = '#### ' + JOB_NAME + ' - CodeScan # ' + BUILD_NUMBER + ' \n' + \
'##### <font color=#FF0000 size=6 face="黑體">新代碼質量: ' + status + '</font> \n' + \
'##### **版本類型**: ' + '開發版' + '\n' + \
'##### **Bug數**: ' + bug + '個 \n' + \
'##### **漏洞數**: ' + leak + '個 \n' + \
'##### **可能存在問題代碼**: ' + code_smell + '行 \n' + \
'##### **覆蓋率**: ' + coverage + '% \n' + \
'##### **重複率**: ' + density + '% \n' + \
'##### **SonarQube**: [查看詳情](' + SonarQube_URL + ') \n' + \
'##### **關注人**: @158xxxx3364 \n' + \
'> ###### xxxx技術團隊 \n'
textSuccess = '#### ' + JOB_NAME + ' - CodeScan # ' + BUILD_NUMBER + ' \n' + \
'##### **新代碼質量**: ' + status + '\n' + \
'##### **版本類型**: ' + '開發版' + '\n' + \
'##### **Bug數**: ' + bug + '個 \n' + \
'##### **漏洞數**: ' + leak + '個 \n' + \
'##### **可能存在問題代碼**: ' + code_smell + '行 \n' + \
'##### **覆蓋率**: ' + coverage + '% \n' + \
'##### **重複率**: ' + density + '% \n' + \
'##### **SonarQube**: [查看詳情](' + SonarQube_URL + ') \n' + \
'> ###### xxxx技術團隊 \n '
# 判斷新代碼質量閥狀態
if status == 'ERROR':
dingText = textFail
elif status == 'OK':
dingText = textSuccess
sendding(title, dingText)
def sendding(title, content):
at_mobiles = ['186xxxx2487','158xxxx3364']
Dingtalk_access_token = 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx'
# 初始化機器人小丁
xiaoding = DingtalkChatbot(Dingtalk_access_token)
# Markdown消息@指定用戶
xiaoding.send_markdown(title=title, text=content, at_mobiles=at_mobiles)
if __name__ == "__main__":
sonarNotification()
通知效果
小結
我們也可以把一個 Pipeline 構建做成 Jenkinsfile 通過git管理,帶來的好處如下:
- 方便多個人維護構建CI,避免代碼被覆蓋
- 方便構建 job 的版本管理,比如要修復某個已經發布的版本,可以很方便切換到發佈版本時候用的 Pipeline 腳本版本
當然,Pipeline也存在一些弊端,比如:
- 語法不夠友好,但好在 Jenkins 提供了一個比較強大的幫助工具(Pipeline Syntax),可以結合 vscode ide進行開發
- 代碼測試繁瑣,沒有本地運行環境,每次測試都需要提交運行一個 job,等等
參考資料:
[1]: https://docs.sonarqube.org/latest/analysis/scan/sonarscanner-for-jenkins/
[2]: Jenkins的Pipeline腳本在美團餐飲SaaS中的實踐