blob: 56e4bd5bf4906cc5ada70115ff052363e466ceff [file] [log] [blame]
package com.mirantis.mk
import java.util.regex.Pattern
import com.cloudbees.groovy.cps.NonCPS
import com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.GerritCause
/**
* Gerrit functions
*
*/
/**
* Execute git clone and checkout stage from gerrit review
*
* @param config LinkedHashMap
* config includes next parameters:
* - credentialsId, id of user which should make checkout
* - withMerge, merge master before build
* - withLocalBranch, prevent detached mode in repo
* - withWipeOut, wipe repository and force clone
* - GerritTriggerBuildChooser - use magic GerritTriggerBuildChooser class from gerrit-trigger-plugin.
* By default,enabled.
* Gerrit properties like GERRIT_SCHEMA can be passed in config as gerritSchema or will be obtained from env
* @param extraScmExtensions list of extra scm extensions which will be used for checkout (optional)
* @return boolean result
*
* Usage example:
* //anonymous gerrit checkout
* def gitFunc = new com.mirantis.mcp.Git()
* gitFunc.gerritPatchsetCheckout([
* withMerge : true
* ])
*
* def gitFunc = new com.mirantis.mcp.Git()
* gitFunc.gerritPatchsetCheckout([
* credentialsId : 'mcp-ci-gerrit',
* withMerge : true
* ])
*/
def gerritPatchsetCheckout(LinkedHashMap config, List extraScmExtensions = []) {
def merge = config.get('withMerge', false)
def wipe = config.get('withWipeOut', false)
def localBranch = config.get('withLocalBranch', false)
def credentials = config.get('credentialsId','')
def gerritScheme = config.get('gerritScheme', env["GERRIT_SCHEME"] ? env["GERRIT_SCHEME"] : "")
def gerritRefSpec = config.get('gerritRefSpec', env["GERRIT_REFSPEC"] ? env["GERRIT_REFSPEC"] : "")
def gerritName = config.get('gerritName', env["GERRIT_NAME"] ? env["GERRIT_NAME"] : "")
def gerritHost = config.get('gerritHost', env["GERRIT_HOST"] ? env["GERRIT_HOST"] : "")
def gerritPort = config.get('gerritPort', env["GERRIT_PORT"] ? env["GERRIT_PORT"] : "")
def gerritProject = config.get('gerritProject', env["GERRIT_PROJECT"] ? env["GERRIT_PROJECT"] : "")
def gerritBranch = config.get('gerritBranch', env["GERRIT_BRANCH"] ? env["GERRIT_BRANCH"] : "")
def path = config.get('path', "")
def depth = config.get('depth', 0)
def timeout = config.get('timeout', 20)
def GerritTriggerBuildChooser = config.get('useGerritTriggerBuildChooser', true)
def invalidParams = _getInvalidGerritParams(config)
if (invalidParams.isEmpty()) {
// default parameters
def scmExtensions = [
[$class: 'CleanCheckout'],
[$class: 'CheckoutOption', timeout: timeout],
[$class: 'CloneOption', depth: depth, noTags: false, reference: '', shallow: depth > 0, timeout: timeout]
]
def scmUserRemoteConfigs = [
name: 'gerrit',
]
if(gerritRefSpec && gerritRefSpec != ""){
scmUserRemoteConfigs.put('refspec', gerritRefSpec)
}
if (credentials == '') {
// then try to checkout in anonymous mode
scmUserRemoteConfigs.put('url',"${gerritScheme}://${gerritHost}/${gerritProject}")
} else {
// else use ssh checkout
scmUserRemoteConfigs.put('url',"ssh://${gerritName}@${gerritHost}:${gerritPort}/${gerritProject}.git")
scmUserRemoteConfigs.put('credentialsId',credentials)
}
// Usefull, if we only need to clone branch. W\o any refspec magic
if (GerritTriggerBuildChooser) {
scmExtensions.add([$class: 'BuildChooserSetting', buildChooser: [$class: 'GerritTriggerBuildChooser']],)
}
// if we need to "merge" code from patchset to GERRIT_BRANCH branch
if (merge) {
scmExtensions.add([$class: 'PreBuildMerge', options: [fastForwardMode: 'FF', mergeRemote: 'gerrit', mergeStrategy: 'DEFAULT', mergeTarget: gerritBranch]])
}
// we need wipe workspace before checkout
if (wipe) {
scmExtensions.add([$class: 'WipeWorkspace'])
}
if(localBranch){
scmExtensions.add([$class: 'LocalBranch', localBranch: gerritBranch])
}
if(!extraScmExtensions.isEmpty()){
scmExtensions.addAll(extraScmExtensions)
}
if (path == "") {
checkout(
scm: [
$class: 'GitSCM',
branches: [[name: "${gerritBranch}"]],
extensions: scmExtensions,
userRemoteConfigs: [scmUserRemoteConfigs]
]
)
} else {
dir(path) {
checkout(
scm: [
$class: 'GitSCM',
branches: [[name: "${gerritBranch}"]],
extensions: scmExtensions,
userRemoteConfigs: [scmUserRemoteConfigs]
]
)
}
}
return true
}else{
throw new Exception("Cannot perform gerrit checkout, missed config options: " + invalidParams)
}
return false
}
/**
* Execute git clone and checkout stage from gerrit review
*
* @param gerritUrl gerrit url with scheme
* "${GERRIT_SCHEME}://${GERRIT_NAME}@${GERRIT_HOST}:${GERRIT_PORT}/${GERRIT_PROJECT}.git
* @param gerritRef gerrit ref spec
* @param gerritBranch gerrit branch
* @param credentialsId jenkins credentials id
* @param path checkout path, optional, default is empty string which means workspace root
* @return boolean result
*/
def gerritPatchsetCheckout(gerritUrl, gerritRef, gerritBranch, credentialsId, path="") {
def gerritParams = _getGerritParamsFromUrl(gerritUrl)
if(gerritParams.size() == 5){
if (path==""){
gerritPatchsetCheckout([
credentialsId : credentialsId,
gerritBranch: gerritBranch,
gerritRefSpec: gerritRef,
gerritScheme: gerritParams[0],
gerritName: gerritParams[1],
gerritHost: gerritParams[2],
gerritPort: gerritParams[3],
gerritProject: gerritParams[4]
])
return true
} else {
dir(path) {
gerritPatchsetCheckout([
credentialsId : credentialsId,
gerritBranch: gerritBranch,
gerritRefSpec: gerritRef,
gerritScheme: gerritParams[0],
gerritName: gerritParams[1],
gerritHost: gerritParams[2],
gerritPort: gerritParams[3],
gerritProject: gerritParams[4]
])
return true
}
}
}
return false
}
/**
* Return gerrit change object from gerrit API
* @param gerritName gerrit user name (usually GERRIT_NAME property)
* @param gerritHost gerrit host (usually GERRIT_HOST property)
* @param gerritPort gerrit port (usually GERRIT_PORT property, default 29418)
* @param gerritChangeNumber gerrit change number (usually GERRIT_CHANGE_NUMBER property)
* @param credentialsId jenkins credentials id for gerrit
* @param includeCurrentPatchset do you want to include current (last) patchset
* @return gerrit change object
*/
def getGerritChange(gerritName, gerritHost, gerritChangeNumber, credentialsId, includeCurrentPatchset = false, gerritPort = '29418'){
def common = new com.mirantis.mk.Common()
def ssh = new com.mirantis.mk.Ssh()
ssh.prepareSshAgentKey(credentialsId)
ssh.ensureKnownHosts("${gerritHost}:${gerritPort}")
def curPatchset = "";
if(includeCurrentPatchset){
curPatchset = "--current-patch-set"
}
return common.parseJSON(ssh.agentSh(String.format("ssh -p %s %s@%s gerrit query ${curPatchset} --commit-message --format=JSON change:%s", gerritPort, gerritName, gerritHost, gerritChangeNumber)))
}
/**
* Returns list of Gerrit trigger requested builds
* @param allBuilds list of all builds of some job
* @param gerritChange gerrit change number
* @param excludePatchset gerrit patchset number which will be excluded from builds, optional null
*/
@NonCPS
def getGerritTriggeredBuilds(allBuilds, gerritChange, excludePatchset = null){
return allBuilds.findAll{job ->
def cause = job.causes[0]
if(cause instanceof GerritCause &&
(cause.getEvent() instanceof com.sonymobile.tools.gerrit.gerritevents.dto.events.PatchsetCreated ||
cause.getEvent() instanceof com.sonymobile.tools.gerrit.gerritevents.dto.events.CommentAdded)) {
if(excludePatchset == null || excludePatchset == 0){
return cause.event.change.number.equals(String.valueOf(gerritChange))
}else{
return cause.event.change.number.equals(String.valueOf(gerritChange)) && !cause.event.patchSet.number.equals(String.valueOf(excludePatchset))
}
}
return false
}
}
/**
* Returns boolean result of test given gerrit patchset for given approval type and value
* @param patchset gerrit patchset
* @param approvalType type of tested approval (optional, default Verified)
* @param approvalValue value of tested approval (optional, default empty string which means any value)
* @return boolean result
* @example patchsetHasApproval(gerrit.getGerritChange(*,*,*,*, true).currentPatchSet)
*/
@NonCPS
def patchsetHasApproval(patchSet, approvalType="Verified", approvalValue = ""){
if(patchSet && patchSet.approvals){
for(int i=0; i < patchSet.approvals.size();i++){
def approval = patchSet.approvals.get(i)
if(approval.type.equals(approvalType)){
if(approvalValue.equals("") || approval.value.equals(approvalValue)){
return true
}else if(approvalValue.equals("+") && Integer.parseInt(approval.value) > 0) {
return true
}else if(approvalValue.equals("-") && Integer.parseInt(approval.value) < 0) {
return true
}
}
}
}
return false
}
@NonCPS
def _getGerritParamsFromUrl(gitUrl){
def gitUrlPattern = Pattern.compile("(.+):\\/\\/(.+)@(.+):(.+)\\/(.+)")
def gitUrlMatcher = gitUrlPattern.matcher(gitUrl)
if(gitUrlMatcher.find() && gitUrlMatcher.groupCount() == 5){
return [gitUrlMatcher.group(1),gitUrlMatcher.group(2),gitUrlMatcher.group(3),gitUrlMatcher.group(4),gitUrlMatcher.group(5)]
}
return []
}
def _getInvalidGerritParams(LinkedHashMap config){
def requiredParams = ["gerritScheme", "gerritName", "gerritHost", "gerritPort", "gerritProject", "gerritBranch"]
def missedParams = requiredParams - config.keySet()
def badParams = config.subMap(requiredParams).findAll{it.value in [null, '']}.keySet()
return badParams + missedParams
}
/**
* Post Gerrit comment from CI user
*
* @param config map which contains next params:
* gerritName - gerrit user name (usually GERRIT_NAME property)
* gerritHost - gerrit host (usually GERRIT_HOST property)
* gerritChangeNumber - gerrit change number (usually GERRIT_CHANGE_NUMBER property)
* gerritPatchSetNumber - gerrit patch set number (usually GERRIT_PATCHSET_NUMBER property)
* message - message to send to gerrit review patch
* credentialsId - jenkins credentials id for gerrit
*/
def postGerritComment(LinkedHashMap config) {
def common = new com.mirantis.mk.Common()
def ssh = new com.mirantis.mk.Ssh()
String gerritName = config.get('gerritName')
String gerritHost = config.get('gerritHost')
String gerritChangeNumber = config.get('gerritChangeNumber')
String gerritPatchSetNumber = config.get('gerritPatchSetNumber')
String message = config.get('message')
String credentialsId = config.get('credentialsId')
ssh.prepareSshAgentKey(credentialsId)
ssh.ensureKnownHosts(gerritHost)
ssh.agentSh(String.format("ssh -p 29418 %s@%s gerrit review %s,%s -m \"'%s'\" --code-review 0", gerritName, gerritHost, gerritChangeNumber, gerritPatchSetNumber, message))
}
/**
* Return map of dependent patches info for current patch set
* based on commit message hints: Depends-On: https://gerrit_address/_CHANGE_NUMBER_
* @param changeInfo Map Info about current patch set, such as:
* gerritName Gerrit user name (usually GERRIT_NAME property)
* gerritHost Gerrit host (usually GERRIT_HOST property)
* gerritChangeNumber Gerrit change number (usually GERRIT_CHANGE_NUMBER property)
* credentialsId Jenkins credentials id for gerrit
* @return map of dependent patches info
*/
LinkedHashMap getDependentPatches(LinkedHashMap changeInfo) {
def dependentPatches = [:]
def currentChange = getGerritChange(changeInfo.gerritName, changeInfo.gerritHost, changeInfo.gerritChangeNumber, changeInfo.credentialsId, true)
def dependentCommits = currentChange.commitMessage.tokenize('\n').findAll { it ==~ /Depends-On: \b[^ ]+\b(\/)?/ }
if (dependentCommits) {
dependentCommits.each { commit ->
def patchLink = commit.tokenize(' ')[1]
def changeNumber = patchLink.tokenize('/')[-1].trim()
def dependentCommit = getGerritChange(changeInfo.gerritName, changeInfo.gerritHost, changeNumber, changeInfo.credentialsId, true)
if (dependentCommit.status == "NEW") {
dependentPatches[dependentCommit.project] = [
'number': dependentCommit.number,
'ref': dependentCommit.currentPatchSet.ref,
'branch': dependentCommit.branch,
]
}
}
}
return dependentPatches
}
/**
* Find Gerrit change(s) according to various input parameters like owner, topic, etc.
* @param gerritAuth A map containing information about Gerrit. Should include
* HOST, PORT and USER
* @param changeParams Parameters to identify Geriit change e.g.: owner, topic,
* status, branch, project
* @param extraFlags Additional flags for gerrit querry for example
* '--current-patch-set' or '--comments' as a simple string
*/
def findGerritChange(credentialsId, LinkedHashMap gerritAuth, LinkedHashMap changeParams, String extraFlags = '', String sshOpts = '') {
scriptText = """
ssh ${sshOpts} -p ${gerritAuth['PORT']} ${gerritAuth['USER']}@${gerritAuth['HOST']} \
gerrit query ${extraFlags} \
--format JSON \
"""
changeParams.each {
scriptText += " ${it.key}:${it.value}"
}
scriptText += " | fgrep -v runTimeMilliseconds || :"
sshagent([credentialsId]) {
jsonChange = sh(
script: scriptText,
returnStdout: true,
).trim()
}
return jsonChange
}
/**
* Download Gerrit review by number
*
* @param credentialsId credentials ID
* @param virtualenv virtualenv path
* @param repoDir repository directory
* @param gitRemote the value of git remote
* @param changeNum the number of change to download
*/
def getGerritChangeByNum(credentialsId, virtualEnv, repoDir, gitRemote, changeNum) {
def python = new com.mirantis.mk.Python()
sshagent([credentialsId]) {
dir(repoDir) {
python.runVirtualenvCommand(virtualEnv, "git review -r ${gitRemote} -d ${changeNum}")
}
}
}
/**
* Post Gerrit review
* @param credentialsId credentials ID
* @param virtualenv virtualenv path
* @param repoDir repository directory
* @param gitName committer name
* @param gitEmail committer email
* @param gitRemote the value of git remote
* @param gitTopic the name of the topic
* @param gitBranch the name of git branch
*/
def postGerritReview(credentialsId, virtualEnv, repoDir, gitName, gitEmail, gitRemote, gitTopic, gitBranch) {
def python = new com.mirantis.mk.Python()
def common = new com.mirantis.mk.Common()
def cmdText = """
GIT_COMMITTER_NAME=${gitName} \
GIT_COMMITTER_EMAIL=${gitEmail} \
git review -r ${gitRemote} \
-t ${gitTopic} \
${gitBranch}
"""
sshagent([credentialsId]) {
dir(repoDir) {
res = python.runVirtualenvCommand(virtualEnv, cmdText)
common.infoMsg(res)
}
}
}
/**
* Prepare and upload Gerrit commit from prepared repo
* @param LinkedHashMap params dict with parameters
* venvDir - Absolute path to virtualenv dir
* gerritCredentials - credentialsId
* gerritHost - gerrit host
* gerritPort - gerrit port
* repoDir - path to repo dir
* repoProject - repo name
* repoBranch - repo branch
* changeCommitComment - comment for commit message
* changeAuthorName - change author
* changeAuthorEmail - author email
* changeTopic - change topic
* gitRemote - git remote
* returnChangeInfo - whether to return info about uploaded change
*
* @return map with change info if returnChangeInfo set to true
*/
def prepareGerritAutoCommit(LinkedHashMap params) {
def common = new com.mirantis.mk.Common()
def git = new com.mirantis.mk.Git()
String venvDir = params.get('venvDir')
String gerritCredentials = params.get('gerritCredentials')
String gerritHost = params.get('gerritHost', 'gerrit.mcp.mirantis.net')
String gerritPort = params.get('gerritPort', '29418')
String gerritUser = common.getCredentialsById(gerritCredentials, 'sshKey').username
String repoDir = params.get('repoDir')
String repoProject = params.get('repoProject')
String repoBranch = params.get('repoBranch', 'master')
String changeCommitComment = params.get('changeCommitComment')
String changeAuthorName = params.get('changeAuthorName', 'MCP-CI')
String changeAuthorEmail = params.get('changeAuthorEmail', 'mcp-ci-jenkins@ci.mcp.mirantis.net')
String changeTopic = params.get('changeTopic', 'auto_ci')
Boolean returnChangeInfo = params.get('returnChangeInfo', false)
String gitRemote = params.get('gitRemote', '')
if (! gitRemote) {
dir(repoDir) {
gitRemote = sh(
script:
'git remote -v | head -n1 | cut -f1',
returnStdout: true,
).trim()
}
}
def gerritAuth = ['PORT': gerritPort, 'USER': gerritUser, 'HOST': gerritHost ]
def changeParams = ['owner': gerritUser, 'status': 'open', 'project': repoProject, 'branch': repoBranch, 'topic': changeTopic]
// find if there is old commit present
def gerritChange = findGerritChange(gerritCredentials, gerritAuth, changeParams)
def changeId = ''
if (gerritChange) {
try {
def jsonChange = readJSON text: gerritChange
changeId = "Change-Id: ${jsonChange['id']}".toString()
} catch (Exception error) {
common.errorMsg("Can't parse ouput from Gerrit. Check that user ${changeAuthorName} does not have several \
open commits to ${repoProject} repo and ${repoBranch} branch with topic ${changeTopic}")
throw error
}
}
def commitMessage =
"""${changeCommitComment}
|${changeId}
""".stripMargin()
git.commitGitChanges(repoDir, commitMessage, changeAuthorEmail, changeAuthorName, false)
//post change
postGerritReview(gerritCredentials, venvDir, repoDir, changeAuthorName, changeAuthorEmail, gitRemote, changeTopic, repoBranch)
if (returnChangeInfo) {
gerritChange = findGerritChange(gerritCredentials, gerritAuth, changeParams)
jsonChange = readJSON text: gerritChange
return getGerritChange(gerritUser, gerritHost, jsonChange['number'], gerritCredentials, true)
}
}
/**
* Get email and username from credentials id
* @param credentialsId Credentials ID to use for source Git
*/
def getGerritUserOptions(credentialsId) {
def changeAuthorName = "MCP-CI"
def changeAuthorEmail = "mcp-ci-jenkins@ci.mcp.mirantis.net"
switch (credentialsId) {
case 'mos-ci':
changeAuthorName = "MOS-CI"
changeAuthorEmail = "infra+mos-ci@mirantis.com"
break;
case 'mcp-ci-gerrit':
changeAuthorName = "MCP-CI"
changeAuthorEmail = "mcp-ci-jenkins@ci.mcp.mirantis.net"
break;
case 'product-ci':
changeAuThorName = "Product CI"
changeAuthorEmail = "infra+product-ci@mirantis.com"
break;
}
return ["changeAuthorName": changeAuthorName,
"changeAuthorEmail": changeAuthorEmail]
}
/**
* Download change from gerrit, if needed repository maybe pre cloned
*
* @param path Directory to checkout repository to
* @param url Source Gerrit repository URL
* @param reference Gerrit reference to download (e.g refs/changes/77/66777/16)
* @param type type of gerrit download
* @param credentialsId Credentials ID to use for source Git
* @param gitCheckoutParams map with additional parameters for git.checkoutGitRepository method e.g:
* [ branch: 'master', poll: true, timeout: 10, depth: 0 ]
* @param doGitClone boolean whether to pre clone and do some checkout
*/
def downloadChange(path, url, reference, credentialsId, type = 'cherry-pick', doGitClone = true, Map gitCheckoutParams = [:]){
def common = new com.mirantis.mk.Common()
def git = new com.mirantis.mk.Git()
def ssh = new com.mirantis.mk.Ssh()
def cmd
def credentials = common.getCredentialsById(credentialsId)
Map gerritUserOptions = getGerritUserOptions(credentialsId)
String gitCommiterOpts = "GIT_COMMITTER_EMAIL=\'${gerritUserOptions['changeAuthorEmail']}\' GIT_COMMITTER_NAME=\'${gerritUserOptions['changeAuthorName']}\'"
switch(type) {
case 'cherry-pick':
cmd = "git fetch ${url} ${reference} && ${gitCommiterOpts} git cherry-pick FETCH_HEAD"
break;
case 'format-patch':
cmd = "git fetch ${url} ${reference} && git format-patch -1 --stdout FETCH_HEAD"
break;
case 'pull':
cmd = "git pull ${url} ${reference}"
break;
default:
error("Unsupported gerrit download type")
}
if (doGitClone) {
def branch = gitCheckoutParams.get('branch', 'master')
def poll = gitCheckoutParams.get('poll', true)
def timeout = gitCheckoutParams.get('timeout', 10)
def depth = gitCheckoutParams.get('depth', 0)
git.checkoutGitRepository(path, url, branch, credentialsId, poll, timeout, depth, '')
}
sshagent (credentials: [credentialsId]) {
dir(path){
sh(script: "GIT_SSH_COMMAND='ssh -l ${credentials.username}' ${cmd}", returnStdout:true)
}
}
}
/**
* Parse gerrit event text and if Workflow +1 is detected,
* return true
*
* @param gerritEventCommentText gerrit event comment text
* @param gerritEventType type of gerrit event
*/
def isGate(gerritEventCommentText, gerritEventType) {
def common = new com.mirantis.mk.Common()
def gerritEventCommentTextStr = ''
def res = false
if (gerritEventCommentText) {
try{
gerritEventCommentTextStr = new String(gerritEventCommentText.decodeBase64())
} catch (e) {
gerritEventCommentTextStr = gerritEventCommentText
}
common.infoMsg("GERRIT_EVENT_COMMENT_TEXT is ${gerritEventCommentTextStr}")
}
def gateComment = gerritEventCommentTextStr.split('\n')[0]
if (gerritEventType == 'comment-added' && gateComment =~ /^Patch Set \d+:\s.*Workflow\+1.*$/) {
common.infoMsg("Running in gate mode")
res = true
}
return res
}
/**
* Get gerrit change based on a given refspec.
*
* @param gerritAuth A map containing Gerrit authentication details (credentialsId, USER, HOST, PORT).
* @param repoName The name of the repository.
* @param refSpec The refspec or commit hash.
* @return A map containing information about the Gerrit change.
*/
def getGerritChangeInfoByRefspec(Map gerritAuth, String repoName, String refSpec) {
def common = new com.mirantis.mk.Common()
String commitHash = ''
if (refSpec.startsWith('refs/') || refSpec == 'master') {
// why we have retry here? bz infra is awesome and stable!
common.retry(15, 10) {
sshagent([gerritAuth['credentialsId']]) {
commitHash = sh(script: "git ls-remote ssh://${gerritAuth['USER']}@${gerritAuth['HOST']}:${gerritAuth['PORT']}/${repoName} ${refSpec}", returnStdout: true).trim().split('\\t')[0]
}
}
} else {
commitHash = refSpec
}
if (!commitHash) {
common.errorMsg("Could not get commit hash for refspec '${refSpec}'")
return [:]
}
LinkedHashMap gerritQuery = [
'commit': commitHash,
]
String gerritFlags = '--current-patch-set'
String patchsetRawJson = findGerritChange(gerritAuth['credentialsId'], gerritAuth, gerritQuery, gerritFlags)
if (!patchsetRawJson) {
// WARNING(alexz): gerrit search only by CR. so merge event,like https://gerrit.mcp.mirantis.com/plugins/gitiles/kaas/si-tests/+/cabb45ea73ac538c653d62cb0a2ffb4802521251
// will not be catched here. In that case, return dummy raw_commit_hash only
common.errorMsg("Could not find gerrit change as refspec '${refSpec}' (commit: ${commitHash})")
return ['raw_commit_hash': commitHash]
}
try {
// It can (and should) return only one json for given commitHash
return readYaml(text: patchsetRawJson)
} catch (Exception e) {
common.errorMsg("Could not parse JSON: ${patchsetRawJson}\nError: ${e}")
return [:]
}
}