-
-
Save isurusndr/dd6dc9fd9768f57eb93e6cdad9af7cf3 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> |
The approach was deprecated, it seems not to see the api string, is there a way around it?
Hello,
I am using above script but getting below error please look into my issue .It seems in my case http://localhost:9000/api/resources?depth=-1&qualifiers=TRK&metrics=new_violations&includetrends=true&format=xml java.io.FileNotFoundException: http://localhost:9000/api/resources?depth=-1&qualifiers=TRK&metrics=new_violations&includetrends=true&format=xml at not found this api .
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I had an styling issue with the above script when I was viewing the generated email in Outlook 2013 client. Outlok 2013 was not respecting the table cell height. For that I had to come up with a workaround of replacing table with a paragraph. Following are the code changes for that.
LOC 297-303 change as follows.
<% mappedViolation.source.each { lineNum, line -> %> ${lineNum}${line}
<% } %>
LOC 38-51 change as follows.
p.source-incut
{
background-color: #EEE;
border: 1px solid #DDD;
}