Copy /**
* JenkinsBuildData class handles all build data collection and processing
* for Jenkins freestyle jobs. It collects build metadata, Git information,
* and stage execution details, then sends them to a webhook endpoint.
*
* This script is designed to work with Jenkins freestyle jobs and expects:
* 1. A workspace directory with Git repository
* 2. Ansible output file (ansible-output.json) in the workspace
* 3. Environment variables for configuration
*/
import hudson.model.Cause
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import java.text.SimpleDateFormat
import java.util.TimeZone
class JenkinsBuildData {
def env
def manager
def workspace
def logger
// API endpoint configuration
static final String API_ENDPOINT = 'https://app.hivel.ai/insightlyapi/webhook/jenkins'
static final int HTTP_TIMEOUT = 5000
static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
static final String DATE_FORMAT_SIMPLE = "yyyy-MM-dd'T'HH:mm:ss'Z'"
/**
* Constructor initializes the build data collector with Jenkins environment
* @param manager Jenkins build manager instance
* @throws IllegalArgumentException if manager is null
*/
JenkinsBuildData(manager) {
if (!manager) {
throw new IllegalArgumentException('Manager cannot be null')
}
this.manager = manager
this.env = manager.build.environment
this.workspace = new File(this.env.WORKSPACE)
this.logger = manager.listener.logger
}
/**
* Validates the workspace and required environment variables
* @return boolean indicating if all required conditions are met
*/
private boolean validateEnvironment() {
if (!workspace.exists()) {
logger.println "Error: Workspace directory does not exist: ${workspace.absolutePath}"
return false
}
if (!env.JOB_NAME) {
logger.println 'Error: JOB_NAME environment variable is not set'
return false
}
if (!env.BUILD_NUMBER) {
logger.println 'Error: BUILD_NUMBER environment variable is not set'
return false
}
return true
}
/**
* Calculates duration between two timestamps in seconds with 2 decimal precision
* @param start Start timestamp
* @param end End timestamp
* @return Duration in seconds
*/
private double calculateDuration(String start, String end) {
if (!start || !end) return 0
try {
def sdf = new SimpleDateFormat(DATE_FORMAT)
sdf.setTimeZone(TimeZone.getTimeZone('UTC'))
def startDate = sdf.parse(start)
def endDate = sdf.parse(end)
def durationMs = endDate.time - startDate.time
// Convert milliseconds to seconds and round to 2 decimal places
return (durationMs / 1000.0).round(2)
} catch (Exception e) {
return 0
}
}
/**
* Parses build stages from ansible-output.json file
* @return List of stage objects containing execution details
*/
def parseAnsibleStages() {
def stages = []
try {
def ansibleOutputFile = new File(workspace.toString() + '/ansible-output.json')
if (!ansibleOutputFile.exists() || !ansibleOutputFile.text?.trim()) {
return stages
}
def jsonContent = ansibleOutputFile.text.substring(
ansibleOutputFile.text.indexOf('{'),
ansibleOutputFile.text.lastIndexOf('}') + 1
)
def ansibleOutput = new JsonSlurper().parseText(jsonContent)
if (!ansibleOutput.plays || !ansibleOutput.plays[0]?.tasks) {
return stages
}
ansibleOutput.plays[0].tasks.each { task ->
def hostInfo = task.hosts?.localhost
if (!hostInfo || !(hostInfo.action in ['debug', 'fail'])) return
def durationMs = calculateDuration(task.task.duration?.start, task.task.duration?.end)
stages << [
stage_name : task.task.name ?: 'Unnamed Task',
start_time : task.task.duration?.start ?: '',
end_time : task.task.duration?.end ?: '',
duration : durationMs,
status : determineTaskStatus(hostInfo),
message : hostInfo.msg ?: hostInfo.message ?: 'No message provided'
]
}
} catch (Exception e) { }
return stages
}
/**
* Determines the status of a task based on its execution result
* @param hostInfo Task execution information
* @return Status string (FAILED, SKIPPED, CHANGED, SUCCESS, or UNKNOWN)
*/
private String determineTaskStatus(hostInfo) {
if (hostInfo.failed == true) return 'FAILED'
if (hostInfo.skipped == true) return 'SKIPPED'
if (hostInfo.changed == true) return 'CHANGED'
if (hostInfo.failed != true && hostInfo.skipped != true) return 'SUCCESS'
return 'UNKNOWN'
}
/**
* Calculates build timing information
* @return Map containing start_time, end_time (ISO8601 format), and duration (in seconds)
*/
def getBuildTiming() {
def startTime = manager.build.getStartTimeInMillis()
def duration = manager.build.getDuration()
def endTime = startTime + duration
// If duration is 0, calculate from start time to current time
if (duration == 0) {
endTime = System.currentTimeMillis()
duration = endTime - startTime
}
// Calculate duration in seconds and round to 2 decimal places
def durationSec = Math.round(duration / 1000.0 * 100) / 100.0
def dateFormat = new SimpleDateFormat(DATE_FORMAT_SIMPLE)
dateFormat.setTimeZone(TimeZone.getTimeZone('UTC'))
return [
start_time: dateFormat.format(new Date(startTime)),
end_time : dateFormat.format(new Date(endTime)),
duration : durationSec
]
}
/**
* Collects Git repository information
* @return Map containing Git metadata (commit, branch, author, etc.)
*/
def getGitInfo() {
def gitCommit = env.GIT_COMMIT ?: executeCommand('git rev-parse HEAD')
def gitBranch = (env.GIT_BRANCH ?: executeCommand('git rev-parse --abbrev-ref HEAD'))?.replaceAll('^origin/', '')
def gitRepo = env.GIT_URL ?: executeCommand('git config --get remote.origin.url')
def commitDetails = executeCommand("git log -1 --pretty=format:'%an|%ae|%s'").split('\\|', 3)
return [
commit : gitCommit,
branch : gitBranch,
author : commitDetails.size() > 0 ? commitDetails[0].trim() : '',
author_email : commitDetails.size() > 1 ? commitDetails[1].trim() : '',
commit_message: commitDetails.size() > 2 ? commitDetails[2].trim() : '',
repository : gitRepo
]
}
/**
* Executes a shell command in the workspace directory
* @param command Shell command to execute
* @return Command output as string
*/
def executeCommand(command) {
try {
def process = command.execute(null, workspace)
process.waitFor()
return process.text.trim()
} catch (Exception e) {
return ''
}
}
/**
* Determines who triggered the build
* @return Username of the build triggerer
*/
def getBuildTrigger() {
def cause = manager.build.getCause(Cause.UserIdCause)
return cause?.userName ?: 'Unknown'
}
/**
* Builds the complete payload for the webhook
* @param stages List of build stages
* @return Complete payload map ready for JSON conversion
*/
def buildPayload(stages) {
def timing = getBuildTiming()
def gitInfo = getGitInfo()
return [
org_id : (env.HIVEL_ORG_ID ?: '0') as int,
job_name : env.JOB_NAME,
build_number : env.BUILD_NUMBER as int,
status : manager.build.result.toString(),
build_url : env.BUILD_URL,
start_time : timing.start_time,
end_time : timing.end_time,
duration : timing.duration,
git : gitInfo,
triggered_by : getBuildTrigger(),
stages : stages
]
}
/**
* Sends the payload to the configured webhook endpoint
* @param jsonPayload JSON string to send
* @return boolean indicating if the request was successful
*/
def sendHttpRequest(jsonPayload) {
if (!jsonPayload) {
logger.println 'Error: Cannot send empty payload'
return false
}
def url = new URL(API_ENDPOINT)
def conn = url.openConnection()
try {
conn.setRequestMethod('POST')
conn.doOutput = true
conn.setRequestProperty('Content-Type', 'application/json')
def apiKey = env.HIVEL_JENKINS_API_KEY
if (apiKey) {
conn.setRequestProperty('API_KEY', apiKey)
} else {
logger.println 'Warning: HIVEL_JENKINS_API_KEY is not set'
}
conn.setConnectTimeout(HTTP_TIMEOUT)
conn.setReadTimeout(HTTP_TIMEOUT)
conn.outputStream.withWriter('UTF-8') { writer ->
writer.write(jsonPayload)
}
def responseCode = conn.responseCode
def responseText = conn.inputStream.text
logger.println "Webhook response code: $responseCode"
logger.println "Webhook response: $responseText"
return responseCode >= 200 && responseCode < 300
} catch (Exception e) {
logger.println "Error sending request: ${e.message}"
logger.println "Stack trace: ${e.stackTrace}"
return false
}
}
}
// Main execution block
try {
def buildData = new JenkinsBuildData(manager)
// Validate environment before proceeding
if (!buildData.validateEnvironment()) {
throw new IllegalStateException('Environment validation failed')
}
def stages = buildData.parseAnsibleStages()
def payload = buildData.buildPayload(stages)
def jsonPayload = JsonOutput.toJson(payload)
if (!buildData.sendHttpRequest(jsonPayload)) {
throw new IllegalStateException('Failed to send build data to webhook')
}
manager.listener.logger.println 'Successfully processed and sent build data'
} catch (Exception e) {
manager.listener.logger.println "Error: ${e.message}"
manager.listener.logger.println "Stack trace: ${e.stackTrace}"
throw e
}