-
-
Save jwcastillo/66e3327bb336bb64c08a4439e512bf94 to your computer and use it in GitHub Desktop.
Jenkins email-ext plugin groovy template. Generates daily Sonar violations report grouped by culprits. Updated to work with SonarQube version 5.0+
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<title>Sonar violations report</title> | |
<style type="text/css"> | |
body | |
{ | |
margin: 0px; | |
padding: 15px; | |
} | |
body, td, th | |
{ | |
font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Tahoma, sans-serif; | |
font-size: 10pt; | |
} | |
table | |
{ | |
border-collapse: collapse; | |
width: 100% | |
} | |
th | |
{ | |
text-align: left; | |
} | |
h1 | |
{ | |
margin-top: 0px; | |
} | |
li | |
{ | |
line-height: 15pt; | |
} | |
table.source-incut | |
{ | |
background-color: #EEE; | |
border: 1px solid #DDD; | |
} | |
table.source-incut td | |
{ | |
white-space: pre; | |
padding: 3px; | |
font-family: "Lucida Console", "Courier New"; | |
font-size: 9pt; | |
height: 20px; | |
} | |
.priority-blocker | |
{ | |
color: #A22; | |
} | |
.priority-critical | |
{ | |
color: #A42; | |
} | |
.priority-major | |
{ | |
color: #A62; | |
} | |
.priority-minor | |
{ | |
color: #4A2; | |
} | |
.priority-info | |
{ | |
color: #2A2; | |
} | |
.line-number | |
{ | |
color: #777; | |
width: 20px; | |
} | |
.res-name | |
{ | |
color: #363; | |
} | |
.source | |
{ | |
color: #336; | |
height: 25px; | |
} | |
.error | |
{ | |
background-color: #FCC; | |
} | |
</style> | |
</head> | |
<body> | |
<% | |
import groovy.json.JsonSlurper | |
apiSonarRoot = "http://localhost:9000" | |
webSonarRoot = "http://99x.test.fdvu.net:9000" | |
def getSonarXml(path) { | |
format = (path.contains("?") ? "&" : "?") + "format=xml" | |
sonarUrl = "${apiSonarRoot}${path}${format}" | |
//println "Reading URL ${sonarUrl} contents" | |
sonarUrl.toURL().text | |
} | |
def getSonarJson(path) { | |
sonarUrl = "${apiSonarRoot}${path}" | |
//println "Reading URL ${sonarUrl} contents" | |
sonarUrl.toURL().text | |
} | |
def getParsedResponse(responseXml) { | |
new XmlSlurper().parseText(responseXml) | |
} | |
def getJsonParsedResponse(responseJson) { | |
new JsonSlurper().parseText(responseJson) | |
} | |
def getNewViolatedResources(resKey, qualifier) { | |
violatedResources = [] | |
resParam = resKey ? "&resource=${resKey}" : "" | |
allResources = getParsedResponse(getSonarXml("/api/resources?depth=-1&qualifiers=${qualifier}&metrics=new_violations&includetrends=true${resParam}")) | |
allResources.resource.each { res -> | |
if (res.msr.var2.toFloat() > 0) { | |
//println "Adding violated resource ${res.id} [${res.key}] for qualifier ${qualifier}" | |
violatedResources << res.key.text() | |
} | |
} | |
violatedResources | |
} | |
def parseLineMetrics(resources, parser) { | |
result = [] | |
if (resources?.size() == 1) { | |
resources?.scm.each { scmLine -> | |
result << [ | |
line: scmLine[0], | |
author: scmLine[1], | |
commitDate: parser(scmLine[2]), | |
revision: scmLine[3] | |
] | |
} | |
} | |
result | |
} | |
def getDatesByLine(resKey) { | |
datesResource = getJsonParsedResponse(getSonarJson("/api/sources/scm?commits_by_line=true&key=${resKey}")) | |
parseLineMetrics(datesResource, { data -> getDateFromString(data) }) | |
} | |
def getResource(resKey) { | |
lines = [] | |
contents = getJsonParsedResponse(getSonarJson("/api/sources/show?key=${resKey}")) | |
if (contents?.sources?.size() > 0) { | |
contents.sources.each { line -> | |
lines << line | |
} | |
} | |
lines | |
} | |
def getDateFromString(input) { | |
new Date().parse("yyyy-MM-dd'T'HH:mm:ssZ", input).clearTime() | |
} | |
def getResourceViolations(resKey) { | |
violations = [] | |
today = new Date().clearTime() | |
contents = getJsonParsedResponse(getSonarJson("/api/issues/search?componentKeys=${resKey}&statuses=OPEN&createdInLast=1w&severities=BLOCKER,CRITICAL,MAJOR,MINOR")) | |
contents.issues.each { violation -> | |
createdAt = getDateFromString(violation.creationDate) | |
//if (createdAt == today) { | |
datesByLine = getDatesByLine(resKey) | |
authorsByLine = datesByLine.findAll { entry -> entry.commitDate == today && entry.line == violation.line }.collect { entry -> entry.author } | |
revisionsByLine = datesByLine.findAll { entry -> entry.commitDate == today && entry.line == violation.line }.collect { entry -> entry.revision } | |
lineNumText = violation.line | |
lineNumInt = lineNumText ? lineNumText.toInteger() : -1 | |
if (authorsByLine.size() > 0) { | |
culprits = authorsByLine.unique() | |
} else { | |
culprits = [] | |
if (lineNumInt > -1) | |
culprits << [violation.author] | |
} | |
culpritsText = culprits.size() > 0 ? culprits.join(" or ") : "Someone" | |
if (revisionsByLine.size() > 0) { | |
culpritRevisions = revisionsByLine.unique() | |
} else { | |
culpritRevisions = [] | |
} | |
culpritRevisionssText = culpritRevisions.size() > 0 ? culpritRevisions.join(" or ") : "Unknown" | |
violations << [ | |
key: violation.key, | |
rule: violation.rule, | |
message: violation.message, | |
severity: violation.severity, | |
line: lineNumInt, | |
component: violation.component, | |
author: culpritsText, | |
revision: culpritRevisionssText | |
] | |
//} | |
} | |
violations | |
} | |
def getUserMappedViolations() { | |
violatedModules = [] | |
violatedFiles = [] | |
// Get violated projects. | |
// Step 1. Read violated projects. | |
projectKeyParam = System.getenv("SONAR_PROJECT_KEY") | |
violatedProjects = projectKeyParam ? [projectKeyParam] : getNewViolatedResources(null, "TRK") | |
// Get violated modules. | |
// Step 2. Read violated modules for projects. | |
violatedProjects.each { project -> | |
violatedModules.addAll(getNewViolatedResources(project, "BRC")) | |
} | |
// Get violated files. | |
// Step 3. Read violated files for modules. | |
violatedModules.each { module -> | |
violatedFiles.addAll(getNewViolatedResources(module, "FIL")) | |
} | |
// Step 4. Group received data per authors. | |
allViolations = [] | |
violatedFiles.each { file -> | |
fileViolations = getResourceViolations(file) | |
fileContents = getResource(file) | |
fileViolations.each { violation -> | |
sourceFragment = null | |
if (violation.line > -1) { | |
sourceFragment = [:] | |
for (ln in violation.line - 3..violation.line + 3) { | |
sourceFragment.put(ln, fileContents[ln - 1]) | |
} | |
} | |
allViolations << [violation: violation, resKey: file, source: sourceFragment] | |
} | |
} | |
[allViolations.size(), violatedFiles.size(), allViolations.groupBy { it.violation.author }] | |
} | |
%> | |
<h1>Sonar violations report</h1> | |
<% | |
if (build.result == hudson.model.Result.SUCCESS) { | |
(violationsCount, filesCount, userMappedViolations) = getUserMappedViolations() | |
if (violationsCount > 0) { %> | |
<p>Found ${violationsCount} new violations in ${filesCount} files.</p> | |
<% userMappedViolations.each { author, mappedViolations -> %> | |
<h2>Violations by ${author}</h2> | |
<ol> | |
<% mappedViolations.each { mappedViolation -> %> | |
<li> | |
<p>[<b><span class="priority-${mappedViolation.violation.severity?.toLowerCase()}">${mappedViolation.violation.severity}</span></b>] <b>${mappedViolation.violation.rule}</b>. ${mappedViolation.violation.message}<br /> | |
in resource <span class="res-name">${mappedViolation.violation.component}</span> at line ${mappedViolation.violation.line} at commit revision ${mappedViolation.violation.revision} | |
(<a href="${webSonarRoot}/issues/search#issues=${mappedViolation.violation.key}">view in Sonar</a>):</p> | |
<% if (mappedViolation.source) { %> | |
<table class="source-incut" > | |
<% mappedViolation.source.each { lineNum, line -> %> | |
<tr<% if (lineNum == mappedViolation.violation.line) { %> class="error"<% } %>> | |
<td class="line-number" style="height:5">${lineNum}</td><td class="source" style="height:5">${line?.get(1)?.replaceAll('\t', ' ')}</td> | |
</tr> | |
<% } %> | |
</table> | |
<% } %> | |
</li> | |
<% } %> | |
</ol> | |
<% } %> | |
<% } else { %> | |
<p>No new violations! Woot!!!</p> | |
<% } %> | |
<% } else { %> | |
<p>Build was not successful. Therefore, no report is generated.</p> | |
<% } %> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment