Skip to main content
Skip table of contents

Scriptrunner for Jira

場景/問題

原生 Jira 功能不溝通,UI友好型,工作流規則,字段邏輯等,選擇組合插件困難,需要 all in one 全家桶

分析

選型困難,排序 top sales 和 trending ,SciptRunner for JIRA 高居榜首,內置大量腳本,庫,還能各種觸達 metadata 魔改,師出名門,Adaptavist,功能夠強,擴展方便,售後支持有保障。

解決方案

此插件功能過多,不一一贅述操作指南,跳過動圖演示和操作説明部分,詳情查看官方説明文檔,本文基於官方几個經典場景來做説明。

最佳實踐

腳本示例

獲取首次響應時間

場景

跟蹤首次轉換工作狀態所花時間,一般用於評估客户服務的響應速度,In Progress 環節可以按需修改

腳本
GROOVY
package com.onresolve.jira.groovy.test.scriptfields.scripts

import com.atlassian.jira.component.ComponentAccessor

def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def created = changeHistoryManager.getChangeItemsForField(issue, "status").find {
    it.toString == "In Progress"
}?.getCreated()

def createdTime = created?.getTime()

createdTime ? new Date(createdTime) : null
字段屬性

Template

Date Time Picker

Searcher

Date Time Range picker

關聯問題剩餘時間

場景

JIRA 的好處是把項目過程數據,七七八八都關聯起來,糟糕的是,想要看關聯信息,每次跳來跳去,刷新網頁,操作體驗令人沮喪。

通過腳本,自動統計指定關聯 issue 的剩餘工作預計時間

腳本

説明:Composition可以修改成相關的 issue 鏈接名稱

GROOVY
package com.onresolve.jira.groovy.test.scriptfields.scripts
 
import com.atlassian.jira.component.ComponentAccessor
 
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
 
def totalRemaining = 0
issueLinkManager.getOutwardLinks(issue.id).each { issueLink ->
    if (issueLink.issueLinkType.name == "Composition") {
        def linkedIssue = issueLink.destinationObject
        totalRemaining += linkedIssue.getEstimate() ?: 0
    }
}
 
// add the remaining work for this issue if there are children, otherwise it's just the same as the remaining estimate,
// so we won't display it,
if (totalRemaining) {
    totalRemaining += issue.getEstimate() ?: 0
}
 
return totalRemaining as Long ?: 0L
字段屬性

Template

Duration (time-tracking)

Searcher

Duration Searcher

坑注意:索引

如果被關聯的 issue 不重新索引,則無法統計出正確結果,未解決此問題,需要加一個 腳本監聽器,關注  "is comprised of"

GROOVY
package com.onresolve.jira.groovy.test.scriptfields.scripts
 
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.index.IssueIndexManager
 
def issue = event.issue // event is an IssueEvent
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def issueIndexManager = ComponentAccessor.getComponent(IssueIndexManager)
 
issueLinkManager.getInwardLinks(issue.id).each { issueLink ->
    if (issueLink.issueLinkType.name == "Composition") {
        def linkedIssue = issueLink.getSourceObject()
        issueIndexManager.reIndexIssueObjects([linkedIssue])
    }
}

推薦配置

友好的提示信息

場景

有時候希望在出現問題時給出個性化提醒,默認工作流不提供這樣的彈出框提示,SR 提供下述腳本,支持 HTML 語言,比如在關閉還有前置未完成任務的時候彈出警告框。

可以直接寫 HTML ,也可以使用 groovy 的 MarkupBuilder,如果 issue 沒有前置阻斷校驗 字段,則彈出框不會顯示。

腳本
GROOVY
package com.onresolve.jira.groovy.test.scriptfields.scripts
 
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import groovy.xml.MarkupBuilder
import com.atlassian.jira.config.properties.APKeys
 
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def baseUrl = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL)
 
def ListblockingIssues = []
 
issueLinkManager.getInwardLinks(issue.id).each { issueLink ->
    if (issueLink.issueLinkType.name == "Blocks") {
        def linkedIssue = issueLink.sourceObject
        if (!linkedIssue.assigneeId && !linkedIssue.resolutionObject) {
            blockingIssues.add(linkedIssue)
        }
    }
}
 
if (blockingIssues) {
    StringWriter writer = new StringWriter()
    MarkupBuilder builder = new MarkupBuilder(writer)
 
    builder.div(class: "aui-message error shadowed") {
        p(class: "title") {
            span(class: "aui-icon icon-error", "")
            strong("This issue is blocked by the following unresolved, unassigned issue(s):")
        }
 
        ul {
            blockingIssues.each { anIssue ->
                li {
                    a(href: "$baseUrl/browse/${anIssue.key}", anIssue.key)
                    i(": ${anIssue.summary} (${anIssue.statusObject.name})")
                }
            }
        }
    }
 
    return writer
} else {
    returnnull
}
字段屬性

Template

HTML

Searcher

None

顯示模塊負責人

場景

在 issue 界面快速瞭解項目的模塊負責人

效果如下

單一用户
腳本

返回第一個 用户,注意  對象 類型需要選擇 ApplicationUser 而非  User ;如果選擇 User,返回的結果會是匿名用户;為保證已有 issue 展示 顯示模塊負責人字段數據,創建字段後,需重建索引

GROOVY
package com.onresolve.jira.groovy.test.scriptfields.scripts
 
def components = issue.componentObjects.toList()
if (components) {
    return components?.first()?.componentLead
}
字段屬性

Template

User Picker (single user)

Searcher

User Picker Searcher

多用户
腳本
GROOVY
package com.onresolve.jira.groovy.test.scriptfields.scripts
def componentLeads = issue.componentObjects*.componentLead.unique()
componentLeads.removeAll([null])
componentLeads
字段屬性

Template

User Picker (multiple users)

Searcher

Multi User Picker Searcher

顯示所有版本

場景

默認的版本僅顯示已發佈和未發佈,不顯示已歸檔版本。通過此腳本可以包含所有的版本,按時間排序。

腳本
GROOVY
package com.onresolve.jira.groovy.test.scriptfields.scripts
 
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.comparator.VersionComparator
 
def versionManager = ComponentAccessor.getVersionManager()
def versions = versionManager.getVersions(issue.projectObject)
def comparator = new VersionComparator()
def lowestFixVersion = issue.fixVersions.min(comparator)
def returnedVersions = versions.findAll {
    comparator.compare(it, lowestFixVersion) < 0
}
log.debug("All prior versions: ${returnedVersions}")
return (lowestFixVersion ? returnedVersions : null)
字段屬性

Template

Version

Searcher

Version Searcher

行為示例

默認字段賦值

日常工作中,經常會遇到有需要為 issue 設定默認值的場景。

以下舉幾個常用例子

系統字段賦值
GROOVY
import com.atlassian.jira.component.ComponentAccessor
 
importstatic com.atlassian.jira.issue.IssueFieldConstants.AFFECTED_VERSIONS
importstatic com.atlassian.jira.issue.IssueFieldConstants.ASSIGNEE
importstatic com.atlassian.jira.issue.IssueFieldConstants.COMPONENTS
 
if (getActionName() != "Create Issue") {
    return// not the initial action, so don't set default values
}
 
// set Components
def projectComponentManager = ComponentAccessor.getProjectComponentManager()
def components = projectComponentManager.findAllForProject(issueContext.projectObject.id)
getFieldById(COMPONENTS).setFormValue(components.findAll { it.name in ["Support Question", "Frontend"] }*.id)
 
// set "Affects Versions" to the latest version
def versionManager = ComponentAccessor.getVersionManager()
def versions = versionManager.getVersions(issueContext.projectObject)
if (versions) {
    getFieldById(AFFECTED_VERSIONS).setFormValue([versions.last().id])
}
 
// set Assignee
getFieldById(ASSIGNEE).setFormValue("admin")

下拉框默認值設定

GROOVY
import com.atlassian.jira.component.ComponentAccessor
 
// set a select list value -- also same for radio buttons
def faveFruitFld = getFieldByName("Favourite Fruit")
def optionsManager = ComponentAccessor.getOptionsManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def customField = customFieldManager.getCustomFieldObject(faveFruitFld.getFieldId())
def config = customField.getRelevantConfig(getIssueContext())
def options = optionsManager.getOptions(config)
def optionToSelect = options.find { it.value == "Oranges" }
faveFruitFld.setFormValue(optionToSelect.optionId)
 
// same example but setting a multiselect - also same for checkboxes fields
def subComponentFld = getFieldByName("Subcomponent")
customField = customFieldManager.getCustomFieldObject(subComponentFld.getFieldId())
config = customField.getRelevantConfig(getIssueContext())
options = optionsManager.getOptions(config)
def optionsToSelect = options.findAll { it.value in ["Oranges", "Lemons"] }
subComponentFld.setFormValue(optionsToSelect*.optionId)

二聯下拉默認設定

比如要設定如下的二聯 下拉,以下兩個為 ID

CODE
field.setFormValue([12345, 67890])
GROOVY
def optionsManager = ComponentAccessor.getOptionsManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()
 
def fieldName = "testCascadingSelectList"
def field = getFieldByName(fieldName)
def customField = customFieldManager.getCustomFieldObjectByName(fieldName)
def fieldConfig = customField.getRelevantConfig(getIssueContext())
 
def options = optionsManager.getOptions(fieldConfig)
def parentOption = options.find { it.value == "A" }
def childOption = parentOption?.childOptions?.find { it.value == "A1" }
 
field.setFormValue([parentOption.optionId, childOption.optionId])

限制可提交的 issue 類型

Jira 原生權限管控無法限制不同角色在 項目中提交的 issue類型,比如測試只能提交測試用例,產品經理只能提交用户故事。

GROOVY
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.security.roles.ProjectRoleManager
 
importstatic com.atlassian.jira.issue.IssueFieldConstants.ISSUE_TYPE
 
def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager)
def allIssueTypes = ComponentAccessor.constantsManager.allIssueTypeObjects
 
def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issueTypeField = getFieldById(ISSUE_TYPE)
 
def remoteUsersRoles = projectRoleManager.getProjectRoles(user, issueContext.projectObject)*.name
def availableIssueTypes = []
 
if ("Users"in remoteUsersRoles) {
    availableIssueTypes.addAll(allIssueTypes.findAll { it.name in ["Query", "General Request"] })
}
 
if ("Developers"in remoteUsersRoles) {
    availableIssueTypes.addAll(allIssueTypes.findAll { it.name in ["Bug", "Task", "New Feature"] })
}
 
issueTypeField.setFieldOptions(availableIssueTypes)

限制評論可見範圍

系統默認的可見範圍選項很少,可通過下述操作豐富 評論可見的範圍,讓悄悄話講起來更方便

setFieldOptions 有兩個用法

枚舉  list

GROOVY
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.security.roles.ProjectRoleManager
 
def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager)
def groupManager = ComponentAccessor.getGroupManager()
 
def group = groupManager.getGroup("jira-administrators")
def role = projectRoleManager.getProjectRole("Administrators")
 
def formField = getFieldById("commentLevel")
 
formField.setRequired(true)
formField.setFieldOptions([group, role])

map 對象

GROOVY
def formField = getFieldById("commentLevel")
 
formField.setRequired(true)
formField.setFieldOptions(["group:jira-administrators": "jira-administrators", "role:10002": "Administrators"])

工作流示例

條件-關閉父任務前-所有子任務完成

最高頻羣內提問,如何限制子任務全關閉後才能關閉父任務

以下案例為 QA 類型任務完成後父任務方能被設定為已解決

GROOVY
passesCondition = true
def subTasks = issue.getSubTaskObjects()
 
subTasks.each {
    if (it.issueType.name == "QA" && !it.resolution) {
        passesCondition = false
    }
}

驗證器 - 修復前至少錄入一個修復版本

GROOVY
import com.opensymphony.workflow.InvalidInputException
 
if (issue.resolution.name == "Fixed" && !issue.fixVersions) {
    thrownew InvalidInputException("fixVersions",
        "Fix Version/s is required when specifying Resolution of 'Fixed'")
}

後置任務 - 關閉所有打開狀態的子任務

GROOVY
import com.atlassian.jira.component.ComponentAccessor
 
def issueService = ComponentAccessor.getIssueService()
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
 
def subTasks = issue.getSubTaskObjects()
subTasks.each {
    if (it.statusObject.name == "Open") {
        def issueInputParameters = issueService.newIssueInputParameters()
        issueInputParameters.with {
            setResolutionId("1") // resolution of "Fixed"
            setComment("*Resolving* as a result of the *Resolve* action being applied to the parent.")
            setSkipScreenCheck(true)
        }
 
        // validate and transition subtask
        def validationResult = issueService.validateTransition(user, it.id, 5, issueInputParameters)
        if (validationResult.isValid()) {
            def issueResult = issueService.transition(user, validationResult)
            if (!issueResult.isValid()) {
                log.warn("Failed to transition subtask ${it.key}, errors: ${issueResult.errorCollection}")
            }
        } else {
            log.warn("Could not transition subtask ${it.key}, errors: ${validationResult.errorCollection}")
        }
    }
}

工作台 - 通知關聯 issue 人員

當你的 客服團隊處理工單時需要知會所有關聯 issue,使用以下 腳本。為保持用户不被騷擾頻繁更新的評論更新通知騷擾以下腳本已做專門優化。

GROOVY

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.mail.Email
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.servicedesk.api.organization.CustomerOrganization
import com.atlassian.servicedesk.api.organization.OrganizationService
import com.atlassian.servicedesk.api.util.paging.SimplePagedRequest
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
 
@WithPlugin("com.atlassian.servicedesk")
 
final def LINK_NAME = "causes"
def issueLinks = ComponentAccessor.getIssueLinkManager().getOutwardLinks(issue.getId())
def causedByIssues = ComponentAccessor.getIssueLinkManager().getOutwardLinks(issue.getId())?.findAll {
    it.issueLinkType.outward == LINK_NAME
}
if (!causedByIssues) {
    log.debug "Does not cause any issues"
    return
}
 
causedByIssues.each {
    def destinationIssue = it.destinationObject
    def watcherManager = ComponentAccessor.watcherManager
 
    // this should be an admin you wish to use inside the script OR currentUser
    def adminUser = ComponentAccessor.userManager.getUserByKey("admin")
 
    def strSubject = "An issue linked to ${destinationIssue.getKey()} has been resolved"
    def baseURL = ComponentAccessor.getApplicationProperties().getString("jira.baseurl")
    def strIssueURL = "${issue.getKey()}"
    def strDestinationURL = "${destinationIssue.getKey()}"
    def strBody = """${strIssueURL} has been resolved.
 This issue has a "${LINK_NAME}" issue link which points to ${strDestinationURL}.
 You received this notification because you are watching ${strDestinationURL}."""
    def emailAddressTo = destinationIssue.reporterUser ? destinationIssue.reporterUser.emailAddress : adminUser.emailAddress
    def emailAddressesCC = []
 
    emailAddressesCC = watcherManager.getWatchers(destinationIssue, Locale.ENGLISH)?.collect { it.emailAddress }
    emailAddressesCC.addAll(getOrganizationMembersEmailsInIssue(destinationIssue, adminUser))
    emailAddressesCC.addAll(getParticipantsEmailsInIssue(destinationIssue))
    emailAddressesCC = emailAddressesCC.unique()
    emailAddressesCC = emailAddressesCC.join(",")
 
    sendEmail(emailAddressTo, emailAddressesCC, strSubject, strBody)
}
 
def sendEmail(String to, String cc, String subject, String body) {
 
    log.debug "Attempting to send email..."
    def mailServer = ComponentAccessor.getMailServerManager().getDefaultSMTPMailServer()
    if (mailServer) {
        Email email = new Email(to)
        email.setCc(cc)
        email.setSubject(subject)
        email.setBody(body)
        email.setMimeType("text/html")
        mailServer.send(email)
        log.debug("Mail sent to (${to}) and cc'd (${cc})")
    } else {
        log.warn("Please make sure that a valid mailServer is configured")
    }
}
 
ListgetOrganizationMembersEmailsInIssue(Issue issue, ApplicationUser adminUser) {
    def organisationService = ComponentAccessor.getOSGiComponentInstanceOfType(OrganizationService)
    def cf = ComponentAccessor.customFieldManager.getCustomFieldObjectByName("Organizations")
    def emailAddresses = []
    (issue.getCustomFieldValue(cf) as List)?.each {
        def pageRequest = new SimplePagedRequest(0, 50)
        def usersInOrganizationQuery = organisationService.newUsersInOrganizationQuery().pagedRequest(pageRequest).customerOrganization(it).build()
        // this is a paged response, it will return only the first 50 results, if you have more users in an organization
        // then you will need to iterate though all the page responses
        def pagedResponse = organisationService.getUsersInOrganization(adminUser, usersInOrganizationQuery)
        if (pagedResponse.isLeft()) {
            log.warn pagedResponse.left().get()
        } else {
            emailAddresses.addAll(pagedResponse.right().get().results.collect { it.emailAddress })
        }
    }
 
    emailAddresses
}
 
ListgetParticipantsEmailsInIssue(Issue issue) {
    def cf = ComponentAccessor.customFieldManager.getCustomFieldObjectByName("Request participants")
    def cfVal = issue.getCustomFieldValue(cf)?.collect { it.emailAddress }
 
    cfVal
}

定時任務

每週複製 issue

第三方插件集成

其實 SR 與大量插件有做過集成,以下僅舉兩個例子,除此之外還有 Structure,Jira Agile,Midori,SmartDraw Diagram,Test Management for Jira ,Insight

Tempo

根據自定義屬性統計工時

根據tempo自定義工時屬性,展示在issue界面

GROOVY
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.worklog.Worklog
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.tempoplugin.core.workattribute.api.WorkAttributeService
import com.tempoplugin.core.workattribute.api.WorkAttributeValueService
 
@WithPlugin("is.origo.jira.tempo-plugin")
 
@PluginModule
WorkAttributeService workAttributeService
 
@PluginModule
WorkAttributeValueService workAttributeValueService
 
def worklogManager = ComponentAccessor.getWorklogManager()
 
def worklogs = worklogManager.getByIssue(issue)
 
def overtimeLogs = worklogs.findAll { worklog ->
    def attribute = workAttributeService.getWorkAttributeByKey("_Overtime_").returnedValue
    workAttributeValueService.getWorkAttributeValueByWorklogAndWorkAttribute(worklog.id, attribute.id).returnedValue
}
 
overtimeLogs.sum { Worklog worklog ->
    worklog.timeSpent
} as Long
// if no overtime worklogs just return null
GROOVY
#if ($value)
    $jiraDurationUtils.getFormattedDuration($value)
#end

Template

Custom

Searcher

Duration Searcher

自動登記工時

GROOVY
import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.tempoplugin.common.TempoDateTimeFormatter
import com.tempoplugin.core.datetime.api.TempoDate
import com.tempoplugin.worklog.v4.rest.InputWorklogsFactory
import com.tempoplugin.worklog.v4.rest.TimesheetWorklogBean
import com.tempoplugin.worklog.v4.services.WorklogService
import org.apache.log4j.Level
import org.apache.log4j.Logger
 
import java.sql.Timestamp
import java.time.Instant
import java.time.temporal.ChronoUnit
 
@WithPlugin("is.origo.jira.tempo-plugin")
 
@PluginModule
WorklogService worklogService
 
@PluginModule
InputWorklogsFactory inputWorklogsFactory
 
// Set log level
def log = Logger.getLogger(getClass())
log.setLevel(Level.ERROR)
 
// Status to be counted
final statusName = ''
def changeHistoryManager = ComponentAccessor.changeHistoryManager
// Calculate the time we entered this state
def changeHistoryItems = changeHistoryManager.getAllChangeItems(issue).reverse()
def timeLastStatus = changeHistoryItems.find {
    it.field == "status" && it.toValues.values().contains(statusName)
}?.created as Timestamp
final chronoUnit = ChronoUnit.SECONDS
def timeToLog = chronoUnit.between(timeLastStatus.toInstant(), Instant.now())
 
// Remaining estimate is calculated if and only if there is original estimate and it is greater than time to log
def remaining = issue.estimate && issue.estimate > timeToLog ? (issue.estimate - timeToLog) : 0
 
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def startDate = TempoDateTimeFormatter.formatTempoDate(TempoDate.now())
 
// Add all fields needed to create a new worklog
def timesheetWorklogBean = new TimesheetWorklogBean.Builder()
    .issueIdOrKey(issue.key)
    .comment('Auto-created worklog')
    .startDate(startDate)
    .workerKey(currentUser.key)
    .timeSpentSeconds(timeToLog)
    .remainingEstimate(remaining)
    .build()
 
def inputWorklogs = inputWorklogsFactory.buildForCreate(timesheetWorklogBean)
worklogService.createTempoWorklogs(inputWorklogs)

Automation for Jira

行動處綁定 SR 腳本

Mail Handler

JIRA 原生郵件內容,條件可編輯性不是特別強,通過腳本可以滿足各類業務需求

GROOVY
import com.atlassian.mail.MailUtils
 
def subject = message.getSubject()
def body = MailUtils.getBody(message)
 
log.debug "${subject}"
log.debug "${body}"

試運行

若原來已存在則 附上內容,若不存在則創建新的 issue

GROOVY
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.service.util.ServiceUtils
import com.atlassian.jira.service.util.handler.MessageUserProcessor
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.user.util.UserManager
import com.atlassian.mail.MailUtils
 
def userManager = ComponentAccessor.getComponent(UserManager)
def projectManager = ComponentAccessor.getProjectManager()
def issueFactory = ComponentAccessor.getIssueFactory()
def messageUserProcessor = ComponentAccessor.getComponent(MessageUserProcessor)
 
def subject = message.getSubject() as String
def issue = ServiceUtils.findIssueObjectInString(subject)
 
if (issue) {
    return
}
 
ApplicationUser user = userManager.getUserByName("admin")
ApplicationUser reporter = messageUserProcessor.getAuthorFromSender(message) ?: user
def project = projectManager.getProjectObjByKey("SRTESTPRJ")
 
def issueObject = issueFactory.getIssue()
issueObject.setProjectObject(project)
issueObject.setSummary(subject)
issueObject.setDescription(MailUtils.getBody(message))
issueObject.setIssueTypeId(project.issueTypes.find { it.name == "Bug" }.id)
issueObject.setReporter(reporter)
 
messageHandlerContext.createIssue(user, issueObject)

UI

Web Item

菜單鏈接

Web Panel

服務枱 ServiceDesk

創建 issue

當一個生產事故解決時,需自動創建一個根因分析 工單

GROOVY
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.servicedesk.api.requesttype.RequestTypeService
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
 
@WithPlugin("com.atlassian.servicedesk")
def requestTypeService = ComponentAccessor.getOSGiComponentInstanceOfType(RequestTypeService)
 
if (issue.issueType.name == "Incident") {
 
    def sourceIssueRequestTypeQuery = requestTypeService
        .newQueryBuilder()
        .issue(issue.id)
        .requestOverrideSecurity(true)
        .build()
    def requestTypeEither = requestTypeService.getRequestTypes(currentUser, sourceIssueRequestTypeQuery)
 
    if (requestTypeEither.isLeft()) {
        log.warn "${requestTypeEither.left().get()}"
        return false
    }
 
    def requestType = requestTypeEither.right.results[0]
 
    if (requestType.name == "Bug Report" && issue.resolution.name == "Bug Reproduced") {
        return true
    }
}
 
return false

創建 wiki 文檔

wiki 用户不一定擁有訪問 JIRA 的權限,當工單解決時自動創建一份根因分析報告

GROOVY
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys
import com.atlassian.sal.api.net.Request
import com.atlassian.sal.api.net.Response
import com.atlassian.sal.api.net.ResponseException
import com.atlassian.sal.api.net.ResponseHandler
import com.atlassian.servicedesk.api.organization.OrganizationService
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.opensymphony.workflow.WorkflowContext
import groovy.json.JsonBuilder
import groovy.xml.MarkupBuilder
 
@WithPlugin("com.atlassian.servicedesk")
 
/*Fetch and check for the confluence link*/
def applicationLinkService = ComponentAccessor.getComponent(ApplicationLinkService)
def confluenceLink = applicationLinkService.getPrimaryApplicationLink(ConfluenceApplicationType)
 
/*If your issue isn't an incident, you don't want to create a RCA ticket*/
if (issue.issueType.name != "Incident") {
    return
}
 
/*Check that the confluence link exists*/
if (!confluenceLink) {
    log.error "There is no confluence link setup"
    return
}
 
def authenticatedRequestFactory = confluenceLink.createAuthenticatedRequestFactory()
 
/*Start getting information about the issue from Service desk*/
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def commentManager = ComponentAccessor.getCommentManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def organizationService = ComponentAccessor.getOSGiComponentInstanceOfType(OrganizationService)
 
/*SLA related fields*/
def timeFirstResponse = issue.getCustomFieldValue(customFieldManager.getCustomFieldObjectByName("Time to first response"))
def timeResolution = issue.getCustomFieldValue(customFieldManager.getCustomFieldObjectByName("Time to resolution"))
def organizations = issue.getCustomFieldValue(customFieldManager.getCustomFieldObjectByName("Organizations"))
 
def currentUserId = ((WorkflowContext) transientVars.get("context")).getCaller()
def currentUser = ComponentAccessor.getUserManager().getUserByKey(currentUserId)
 
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
 
xml.h2("This is the RCA analysis thread for the issue above.")
 
xml.p("${issue.summary}")
 
xml.p("This issue was raised by ${issue.reporter.name} on ${issue.getCreated()} " +
    "and resolved by ${currentUser.name} with resolution ${issue.getResolution().name}")
 
xml.h3("Time to first response:")
xml.p("Start date: ${timeFirstResponse.completeSLAData?.startTime[0].toDate().toString()}")
xml.p("Stop  date: ${timeFirstResponse.completeSLAData?.stopTime[0].toDate().toString()}")
 
xml.h3("Times to resolution:")
xml.p("Start date(s): ${timeResolution.completeSLAData?.startTime[0].toDate().toString()}")
xml.p("Stop  date(s): ${timeResolution.completeSLAData?.stopTime[0].toDate().toString()}")
 
xml.h3("Description:")
xml.p("${issue.description}
")
 
//You might want to log information about your users and organizations.
xml.h3("Organizations")
organizations?.each {
    xml.p("${it.name}")
    def usersEither = organizationService.getUsersInOrganization(currentUser, organizationService.newUsersInOrganizationQuery().customerOrganization(it).build())
    if (usersEither.isLeft()) {
        log.warn usersEither.left().get()
        return
    }
    usersEither.right().get().results.collect { "${it.displayName}" }.each {
        xml.p(it)
    }
}
 
//You want to collect the outward links of your issue.
def outwardLinks = issueLinkManager.getOutwardLinks(issue.id)
xml.h3("Outward Issue Links")
if (outwardLinks instanceof List) {
    outwardLinks?.collect { buildIssueURL(it.destinationObject.key) }?.join(" ").each {
        xml.p(it)
    }
} else {
    xml.p(buildIssueURL(outwardLinks.destinationObject.key))
}
 
//You want to collect the inward links of your issue.
def inwardLinks = issueLinkManager.getInwardLinks(issue.id)
xml.h3("Inward Issue Links")
if (inwardLinks instanceof List) {
    inwardLinks?.collect { buildIssueURL(it.destinationObject.key) }?.join(" ").each {
        xml.p(it)
    }
} else {
    xml.p(buildIssueURL(inwardLinks.destinationObject.key))
}
 
//You might also want to collect the comments on the issue:
xml.h3("Comments")
commentManager.getComments(issue)?.collect { "${it.getAuthorFullName()} : $it.body" }.each {
    xml.p(it)
}
 
//Here you parse the whole of the information collected into the RCA ticket.
def params = [
    type : "page",
    title: "RCA analysis: ${issue.key}",
    space: [
        key: "TEST"//This should be the name of your space, you should set it accordingly
    ],
    body : [
        storage: [
            value         : writer.toString(),
            representation: "storage"
        ]
    ]
]
//This is used to send a REST request to the Confluence link.
authenticatedRequestFactory
    .createRequest(Request.MethodType.POST, "rest/api/content")
    .addHeader("Content-Type", "application/json")
    .setRequestBody(new JsonBuilder(params).toString())
    .execute(new ResponseHandler() {
        @Override
        void handle(Response response) throws ResponseException {
            if (response.statusCode != HttpURLConnection.HTTP_OK) {
                thrownew Exception(response.getResponseBodyAsString())
            }
        }
    })
 
//This is an aux function to build the URL for the issue.
String buildIssueURL(String issueKey) {
    def baseUrl = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL)
    """
    <a </ahref="$baseUrl/browse/$issueKey">$issueKey
    """
}

周邊

廠家其他插件

競品/替代品

一直被模仿從未被超越,如 Power Scripts | Jira Workflow Automation,Jira Workflow Toolbox , JSU Automation Suite for Jira Workflows , Jira Misc Workflow Extensions (JMWE)

價格

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.