ScriptRunner for JIRA  是一款强大的工具, 除了内置的一些功能之外,提供了让用户自己编写Groovy脚本来直接调用JIRA内部的API, 极大的提升了使用的灵活度和JIRA的扩展性

编写Groovy脚本除了对Groovy本身的语法了解之外,还要了解JIRA的API,这些详细的API通常需要看JIRA 的源码才能了解,本文列举了Script Runner一些常见的使用脚本,如更新字段、创建Issue、自动更改Issue状态、添加链接等内容,帮助用户快速的使用ScriptRunner , 希望对您平时使用Groovy 脚本有所帮助

注意:Groovy 脚本虽强大,为了系统的易维护性,还请慎用,不然可能过上一段日子,连自己写的脚本都看不明白了 

工作流中更新JIRA字段值

常见的是工作流的Post Function 中更新字段

如果您不是在工作流中更新,需要使用IssueService 来更新,示例代码见下一章 

常见文本日期等字段

import com.atlassian.jira.component.ComponentAccessor
 
import java.sql.Timestamp
 
def versionManager = ComponentAccessor.getVersionManager()
def projectComponentManager = ComponentAccessor.getProjectComponentManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def userUtil = ComponentAccessor.getUserUtil()
 
def project = issue.getProjectObject()
 
def version = versionManager.getVersion(project.getId(), "1.1")
def component = projectComponentManager.findByComponentName(project.getId(), "MyComponent")
 
if (version) {
    issue.setFixVersions([version])
}
 
if (component) {
    issue.setComponent([component])
}
 
// a text field
def textCf = customFieldManager.getCustomFieldObjectByName("TextFieldA") // <1>
issue.setCustomFieldValue(textCf, "Some text value")
 
// a date time field - add 7 days to current datetime
def dateCf = customFieldManager.getCustomFieldObjectByName("First Date") // Date time fields require a Timestamp
issue.setCustomFieldValue(dateCf, new Timestamp((new Date() + 7).time))
 
// a user custom field
def userCf = customFieldManager.getCustomFieldObjectByName("UserPicker")
issue.setCustomFieldValue(userCf, userUtil.getUserByName("admin")) // User CFs require an ApplicationUser
 
// system fields
issue.setDescription("A generated description")
issue.setDueDate(new Timestamp((new Date() + 1).time)) // set due date to tomorrow
GROOVY


checkbox字段

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.customfields.manager.OptionsManager
 
def optionsManager = ComponentAccessor.getComponent(OptionsManager)
def customFieldManager = ComponentAccessor.getCustomFieldManager()
 
def cf = customFieldManager.getCustomFieldObjectByName("Checkboxes") // Checkboxes is the NAME of my custom field
def fieldConfig = cf.getRelevantConfig(issue)
def option = optionsManager.getOptions(fieldConfig).getOptionForValue("Yes", null)
issue.setCustomFieldValue(cf, [option]) // <1>
GROOVY


单选列表字段

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder

// the name of the custom field (single select list type)
final customFieldName = 'Single Select List'

// the value of the new option to set
final newValue = 'Option C'

// the issue key to update
final issueKey = 'TEST-1'

def issue = ComponentAccessor.issueManager.getIssueByCurrentKey(issueKey)
assert issue: "Could not find issue with key $issueKey"

def customField = ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).findByName(customFieldName)
assert customField: "Could not find custom field with name $customFieldName"

def availableOptions = ComponentAccessor.optionsManager.getOptions(customField.getRelevantConfig(issue))
def optionToSet = availableOptions.find { it.value == newValue }
assert optionToSet: "Could not find option with value $newValue. Available options are ${availableOptions*.value.join(",")}"

customField.updateValue(null, issue, new ModifiedValue(issue.getCustomFieldValue(customField), optionToSet), new DefaultIssueChangeHolder())

GROOVY


import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.link.IssueLinkTypeManager

// the issue key to create the link from
final String sourceIssueKey = "JRA-1"

// the issue key to create the link to
final String destinationIssueKey = "SSPA-1"

// the name of the issue link
final String issueLinkName = "Blocks"

// the sequence of the link
final Long sequence = 1L

def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issueLinkTypeManager = ComponentAccessor.getComponent(IssueLinkTypeManager)
def issueManager = ComponentAccessor.issueManager

def sourceIssue = issueManager.getIssueByCurrentKey(sourceIssueKey)
def destinationIssue = issueManager.getIssueByCurrentKey(destinationIssueKey)
assert sourceIssue && destinationIssue: "One ore more issues do not exist"

def availableIssueLinkTypes = issueLinkTypeManager.issueLinkTypes
def linkType = availableIssueLinkTypes.findByName(issueLinkName)
assert linkType : "Could not find link type with name $issueLinkName. Available issue link types are ${availableIssueLinkTypes*.name.join(", ")}"

ComponentAccessor.issueLinkManager.createIssueLink(sourceIssue.id, destinationIssue.id, linkType.id, sequence, loggedInUser)


GROOVY


在Console中更新JIRA字段值

JIRA的字段有不同的类型,如数字、文本、日期、人员、单选、多选、列表等, 这些不同的字段,更新的方式也不同,下面列举了常用字段的更新方式

您可以在Script Runner提供的Console中调试

Console 中更新Issue 的字段和 在工作流中不一样, 工作流中只需要设置字段的值就可以,因为工作流中还有后续的event和索引的处理

Console 中更新需要使用 issueService,  issueService会验证输入值、设置完字段后会更新字段值

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.fields.CustomField
import groovy.transform.Field

// 更新的JIRA Issue Key
final String issueKey = "JRA-1"

// 以下定义为字段名称
// 单选列表字段
final String selectList = "SelectListA"

// 多选列表字段
final String multiSelectList = "MultiSelectA"

// 单选字段 'radio button' 
final String radioButtonField = "RadioButtons"

// 多选字段 check box
final String checkboxField = "Checkboxes"

// 文本字段
final String textField = "TextFieldA"

// 用户选择字段 'user picker'
final String userPicker = "UserPicker"

// 多用选择字段 'multi user picker'
final String multiUserPicker = "MultiUserPickerA"

// 组选组字段'group picker'
final String groupPicker = "GroupPicker"

// 多组选择字段'multi group picker'
final String multiGroupPicker = "MultiGroupPicker"

// 日期和时间字段 'date and time' 
final String dateTimeField = "First DateTime"

// 日期字段'date'
final String dateField = "Date"

// 项目选择字段 'project picker'
final String projectPickerField = "ProjectPicker"

// lable字段 'label'
final String labelField = "LabelField"

// 版本字段 'single version picker' 
final String versionField = "VersionPicker"

// 如果希望更新后发送通知,请设置这个开关为 true
final boolean sendMail = false

def issueService = ComponentAccessor.issueService
def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issue = ComponentAccessor.issueManager.getIssueByCurrentKey(issueKey)

assert issue: "Could not find issue with key $issueKey"

def issueInputParameters = issueService.newIssueInputParameters().with {
    // 有选项的字段, 需要先取出选项的option对象再设置 (select lists, checkboxes, radio buttons)
    addCustomFieldValue(getSingleCustomFieldByName(selectList).id, *getOptionIdsForFieldByValue(selectList, "BBB"))
    addCustomFieldValue(getSingleCustomFieldByName(multiSelectList).id, *getOptionIdsForFieldByValue(multiSelectList, "BBB", "CCC"))
    addCustomFieldValue(getSingleCustomFieldByName(radioButtonField).id, *getOptionIdsForFieldByValue(radioButtonField, "Yes"))
    addCustomFieldValue(getSingleCustomFieldByName(checkboxField).id, *getOptionIdsForFieldByValue(checkboxField, "Maybe", "Yes"))

    // 文本字段 设置
    addCustomFieldValue(getSingleCustomFieldByName(textField).id, "New Value")

    // 单用户字段 设置
    addCustomFieldValue(getSingleCustomFieldByName(userPicker).id, "bob")
    // 多用户字段 设置
    addCustomFieldValue(getSingleCustomFieldByName(multiUserPicker).id, "bob", "alice")

    // 单组字段 设置
    addCustomFieldValue(getSingleCustomFieldByName(groupPicker).id, "jira-users")
    // 多组字段 设置
    addCustomFieldValue(getSingleCustomFieldByName(multiGroupPicker).id, "jira-users", "jira-administrators")

    // 时间和日期选择期字段设置
    addCustomFieldValue(getSingleCustomFieldByName(dateTimeField).id, "04/Feb/12 8:47 PM")
    // 日期选择器字段设置
    addCustomFieldValue(getSingleCustomFieldByName(dateField).id, "04/Feb/12")
}

// 项目选择器设置
def project = ComponentAccessor.projectManager.getProjectObjByKey("SSPA")
assert project: "Could not find project"
issueInputParameters.addCustomFieldValue(getSingleCustomFieldByName(projectPickerField).id, project.id.toString())

// lable选择字段设置
issueInputParameters.addCustomFieldValue(getSingleCustomFieldByName(labelField).id, "foo", "bar")

// version字段设置
def version = ComponentAccessor.versionManager.getVersions(issue.projectObject).findByName("Version1")
assert version: "Could not find version"
issueInputParameters.addCustomFieldValue(getSingleCustomFieldByName(versionField).id, version.id.toString())

def updateValidationResult = issueService.validateUpdate(loggedInUser, issue.id, issueInputParameters)
assert updateValidationResult.valid: updateValidationResult.errorCollection

def issueUpdateResult = issueService.update(loggedInUser, updateValidationResult, EventDispatchOption.ISSUE_UPDATED, sendMail)
assert issueUpdateResult.valid: issueUpdateResult.errorCollection

/**
 * Get a custom field given a custom field name.
 * If there are than one custom fields with the same name under the same Context then return the first one.
 * @param fieldName The name of the custom field
 * @param issue The issue to look for that custom field
 * @return the custom field, if that exists
 */
CustomField getSingleCustomFieldByName(String fieldName) {
    def issue = ComponentAccessor.issueManager.getIssueByCurrentKey(issueKey)
    def customField = ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).findByName(fieldName)

    assert customField: "Could not find custom field with name $fieldName"
    customField
}

/**
 * Given a custom field name and option values, retrieve their ids as String
 * @param customFieldName The name of the custom field
 * @param values The values in order to get their ids
 * @return List < String >  The ids of the given values
 */
List<String> getOptionIdsForFieldByValue(String customFieldName, String... values) {
    def issue = ComponentAccessor.issueManager.getIssueByCurrentKey(issueKey)
    def customField = getSingleCustomFieldByName(customFieldName)

    ComponentAccessor.optionsManager.getOptions(customField.getRelevantConfig(issue)).findAll {
        it.value in values.toList()
    }*.optionId*.toString()
}
GROOVY

Script Runner 执行 JQL查询

import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.search.SearchException
import com.atlassian.jira.web.bean.PagerFilter
import org.apache.log4j.Level

// Set log level to INFO
log.setLevel(Level.INFO)

// The JQL query you want to search with
final jqlSearch = "Some JQL query"

// Some components
def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def searchService = ComponentAccessor.getComponentOfType(SearchService)

// Parse the query
def parseResult = searchService.parseQuery(user, jqlSearch)
if (!parseResult.valid) {
    log.error('Invalid query')
    return null
}

try {
    // Perform the query to get the issues
    def results = searchService.search(user, parseResult.query, PagerFilter.unlimitedFilter)
    def issues = results.results
    issues.each {
        log.info(it.key)
    }

    issues*.key
} catch (SearchException e) {
    e.printStackTrace()
    null
}

GROOVY

添加REST Endpoint

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript

import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate 

/* 代码说明:
  REST 节点的名称doSomething
  groups: ["jira-administrators"]  : 这个是可选的,表明访问这个api的权限限制
  queryParams 是输入的参数
  body 是http请求的body
*/

doSomething( 
    httpMethod: "GET", groups: ["jira-administrators"] 
) { MultivaluedMap queryParams, String body -> 
    return Response.ok(new JsonBuilder([abc: 42]).toString()).build() 
}  
GROOVY

作为HttpClient发送Http请求


import groovyx.net.http.HTTPBuilder
import static groovyx.net.http.ContentType.*
import groovyx.net.http.ContentType
import static groovyx.net.http.Method.*
import groovy.json.JsonSlurper
import net.sf.json.groovy.JsonSlurper

def http = new HTTPBuilder('http://IP/authentication')
http.request(POST) {
    requestContentType = ContentType.JSON
    body =  [username: 'USERNAME', password: 'PASSWORD']

    response.success = { resp, JSON ->
        return JSON         
    }
    
    response.failure = { resp ->
        return "Request failed with status ${resp.status}"
    }
}
GROOVY

查询数据库

以下例子为查询JIRA的数据库

import com.atlassian.jira.component.ComponentAccessor
import groovy.sql.Sql
import org.ofbiz.core.entity.ConnectionFactory
import org.ofbiz.core.entity.DelegatorInterface

import java.sql.Connection

def delegator = (DelegatorInterface) ComponentAccessor.getComponent(DelegatorInterface)
String helperName = delegator.getGroupHelperName("default")

def sqlStmt = """
    SELECT     project.pname, COUNT(*) AS kount
    FROM       project
               INNER JOIN jiraissue ON project.ID = jiraissue.PROJECT
    GROUP BY project.pname
    ORDER BY kount DESC
"""

Connection conn = ConnectionFactory.getConnection(helperName)
Sql sql = new Sql(conn)

try {
    StringBuffer sb = new StringBuffer()
    sql.eachRow(sqlStmt) {
        sb << "${it.pname}\t${it.kount}\n"
    }
    log.debug sb.toString()
}
finally {
    sql.close()
}
GROOVY