Skip to content

Instantly share code, notes, and snippets.

@petewilcock
Created August 1, 2024 10:26
Show Gist options
  • Save petewilcock/1fef9e373c1dc6d37e7851618f840007 to your computer and use it in GitHub Desktop.
Save petewilcock/1fef9e373c1dc6d37e7851618f840007 to your computer and use it in GitHub Desktop.
Jenkins Coverage Plugin Report to GitHub Comment
import groovy.json.JsonSlurper
def call() {
def coverageUrl = "${env.BUILD_URL}coverage/api/json?pretty=true"
echo "Getting ${coverageUrl}"
// Fetch the JSON coverage report with authentication
def jsonResponse = ""
withCredentials([string(credentialsId: 'jenkins-user-credential', variable: 'PASSWORD')]) {
def url = new URL(coverageUrl)
def connection = (HttpURLConnection) url.openConnection()
connection.requestMethod = 'GET'
connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString("jenkins:${PASSWORD}".getBytes()))
if (connection.responseCode == 200) {
jsonResponse = connection.inputStream.text
} else {
throw new Exception("Failed to fetch coverage report. HTTP Response Code: ${connection.responseCode}")
}
connection.disconnect()
}
// Parse the JSON response
def parsedReport = new JsonSlurper().parseText(jsonResponse)
// Convert parsed JSON to a markdown report
return convertCoverageToMarkdown(parsedReport)
}
def convertCoverageToMarkdown(parsedReport) {
def reportSections = []
def modifiedSectionsEmpty = true
// Helper to add section only if it has data
def addSectionIfNotEmpty = { title, data ->
def section = formatSection(title, data, true)
if (section) {
reportSections << section
modifiedSectionsEmpty = false
}
}
// Check if any 'modified' sections have data
addSectionIfNotEmpty("Modified Files Delta", parsedReport.modifiedFilesDelta)
addSectionIfNotEmpty("Modified Files Statistics", parsedReport.modifiedFilesStatistics)
addSectionIfNotEmpty("Modified Lines Delta", parsedReport.modifiedLinesDelta)
addSectionIfNotEmpty("Modified Lines Statistics", parsedReport.modifiedLinesStatistics)
// Always add project statistics
def projectDelta = formatSection("Project Delta", parsedReport.projectDelta, true)
def projectStatistics = formatSection("Project Statistics", parsedReport.projectStatistics, true)
if (projectDelta || projectStatistics) {
reportSections << (projectDelta ?: '') << (projectStatistics ?: '')
}
// Add quality gates if present
def qualityGates = formatQualityGates(parsedReport.qualityGates)
if (qualityGates) {
reportSections << qualityGates
}
// Add the 'no files changed' message if applicable
def reportContent = reportSections.join('\n\n').trim()
if (modifiedSectionsEmpty) {
return "No files in this PR changed the overall Code Coverage level\n\n${reportContent}"
}
return reportContent
}
private def formatSection(String title, Map<String, Object> sectionData, boolean applyColorization) {
if (!sectionData) {
return null
}
def section = """
### ${title}
| Metric | Value |
|------------------------------|-----------------:|"""
sectionData.each { key, value ->
def formattedValue = applyColorization ? formatModifiedValue(value) : value
section += "\n| ${getFormattedMetricName(key)} | ${formattedValue} |"
}
// Ensure that no extra newlines or whitespace are added
return section.trim().replaceAll(/(\n)\s{2,}(\|)/, '\n|') // Fix double indentation issues
}
private def formatModifiedValue(String value) {
if (value.startsWith('+')) {
return "<span style=\"color:green;\">${value}</span>"
} else if (value.startsWith('-')) {
return "<span style=\"color:red;\">${value}</span>"
} else {
return value
}
}
private def getFormattedMetricName(String key) {
switch (key) {
case "branch":
return "Branch Coverage"
case "class":
return "Class Coverage"
case "complexity":
return "Complexity"
case "complexity-density":
return "Complexity Density"
case "complexity-maximum":
return "Complexity Maximum"
case "file":
return "File Coverage"
case "instruction":
return "Instruction Coverage"
case "line":
return "Line Coverage"
case "loc":
return "Lines of Code"
case "method":
return "Method Coverage"
case "module":
return "Module Coverage"
case "package":
return "Package Coverage"
default:
return key.replaceAll(/([a-z])([A-Z])/, '$1 $2').capitalize()
}
}
private def formatQualityGates(Map<String, Object> qualityGates) {
if (!qualityGates || !qualityGates.resultItems) {
return null
}
def section = """
### Quality Gates
| Quality Gate | Result | Threshold | Value |
|-------------------------------------|----------|-----------|---------------:|
"""
qualityGates.resultItems.each { item ->
section += "| ${item.qualityGate} | ${item.result} | ${item.threshold} | ${item.value} |"
}
// Ensure that no extra newlines or whitespace are added
return section.trim().replaceAll(/(\n)\s{2,}(\|)/, '\n|') // Fix double indentation issues
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment