Empowering Automation in Jira: Harnessing the Power of ScriptRunner

As a program manager and systems engineer respectively, the teams we serve use Jira, the popular project management tool, intimately and on a daily basis. We get many requests from our teams to tweak or improve processes that use Jira.

While Jira's native automation capabilities are adequate for many instances, there are occasions where additional power and flexibility are required. In this article, we will explore one such case where we turned to ScriptRunner, a powerful plugin that allowed us to leverage the Groovy programming language and build a custom automation solution to match text against a predefined list of URLs. This example showcases the significance of automation for program managers, systems administrators, and engineering teams alike.

The challenge

One of the teams we support, documentation, noted that there was no good way to prioritize work on specific documentation pages, as each ticket's value seemed relatively equal. The team needed to be able to set priority and know the exact rank of a URL page based on a ranking list, while still maintaining the flexibility to change priority at will. In order to do this, the system would have to match text (specifically URLs) against a predetermined list of URLs and trigger specific actions based on the result.

Leveraging Jira, ScriptRunner and Groovy

Jira's built-in automation features can do plenty to provide the ability to streamline and simplify numerous processes:

  • Workflow automation: Automate transitions, status updates, and field changes within Jira workflows based on predefined triggers or conditions.

  • Notifications and alerts: Set up automated notifications and alerts to keep stakeholders informed about critical updates, changes, or upcoming deadlines.

  • Issue creation and linking: Automatically generate new issues or establish associations between existing ones, saving time and ensuring data integrity.

  • SLA management: Automate the monitoring and enforcement of service level agreements (SLAs), ensuring timely responses and resolution of issues.

  • Custom rule creation: Design custom automation rules to suit your specific needs, allowing for tailored and efficient process management.

While these capabilities are robust, our situation required complex customization and automation that exceeded the out-of-the-box functionality.

How did we leverage Jira to get the outcomes we wanted? We discovered that by using ScriptRunner, we could leverage the Groovy programming language to write code and create a custom automation solution specifically as a listener within ScriptRunner.

Luke crafted a listener that would monitor incoming tickets and match the provided text against our predefined list of URLs. Once the condition was met, the listener triggered the following actions:

  • Change in priority status: The ticket's priority status was automatically updated based on the URL match, ensuring that the most critical issues received immediate attention.

  • Label addition: A specific label was added to the ticket, providing additional context and enabling easy identification of relevant issues.

  • siteRank: The rank of the highest priority URL’s rank was added into the siteRank custom field for folks to understand at a glance what number that ranked at.

Check out Luke’s code in Groovy (with great assistance from Stack Overflow):

// Defining the variable and initializing it with list of strings
// 
def prioritySites = [
'this space intentionally left blank',
]
// Route Issue Created and Generic Events
//
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def issueManager = ComponentAccessor.getIssueManager()
def issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService)
int p = 4
def finalM = 300
def intentionality = 0
try {
    def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
    def changeItems = changeHistoryManager.getAllChangeItems(issue)
    // Find this updates changeGroup
    def changeGroup = changeItems.last()['changeGroupId']
    for (change in changeItems) {
        thisChangeGroupID = change['changeGroupId']
        if (thisChangeGroupID == changeGroup) {
            if (change['field'] == 'URL(s)') {
                intentionality = 1
            }
        }
    }
} catch (Exception ex) {
    // New issues have no log to consider.
    log.warn('Bombed finding a changegroup, this is probably a new issue.')
    intentionality = 1
}
def urlField = customFieldManager.getCustomFieldObjectByName('URL(s)') //'
if (intentionality == 0) {
    log.warn('No change to URL(s) Field. Aborting.')
    return 0
}
try {
    // The custom field 'urlField' *may* contain one or more URLs.
    // The issue's priority will be determined by the most-critical
    // URL, so split the field contents and iterate over all values
    def urls = event.issue.getCustomFieldValue(urlField).split()
    for (url in urls) {
        int index = prioritySites.findIndexOf { it == url }
        if (index < finalM && index > 0) {
            finalM = index
        }
    }
    if (finalM.toInteger() < 300 && finalM.toInteger() > 0) {
        // At-least one URL in "urlField" is high-priority. Add a label,
        // prioritize the issue accordingly
        //
        def labelManager = ComponentAccessor.getComponent(LabelManager)
        def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
        def issue = event.issue as Issue
        def siteRank = customFieldManager.getCustomFieldObjectByName('siteRank')
        issue.setCustomFieldValue(siteRank, finalM.toString())
        labelManager.addLabel(currentUser, issue.id, 'top250', false)
        if (intentionality == 1) {  // Only set a priority if one was not included with the current change.
            if (finalM <= 150) {
                issue.setPriorityId(p.toString(2))
            } else {
                issue.setPriorityId(p.toString(3))
            }
        }
        // Update issue
        ComponentAccessor.issueManager.updateIssue(currentUser, issue, EventDispatchOption.DO_NOT_DISPATCH, false)
        boolean wasIndexing = ImportUtils.isIndexIssues()
        ImportUtils.setIndexIssues(true)
        log.warn("Reindex issue ${issue.key} ${issue.id}")
        issueIndexingService.reIndex(issueManager.getIssueObject(issue.id))
        ImportUtils.setIndexIssues(wasIndexing)
    } else {
        // There is something in the urlField, but no high-priority URLs.
        // Remove the "top250" label and siteRank data
        //
        log.warn('No known URLs here')
        def labelManager = ComponentAccessor.getComponent(LabelManager)
        def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
        def issue = event.issue as Issue
        // remove top250
        //
        def labels = labelManager.getLabels(issue.id).collect { it.getLabel() }
        labels -= 'top250'
        labelManager.setLabels(currentUser, issue.id, labels.toSet(), false, false)
        // Empty "siteRank" field
        def siteRank = customFieldManager.getCustomFieldObjectByName('siteRank')
        issue.setCustomFieldValue(siteRank, null)
        if (intentionality == 0) {
            issue = event.issue as Issue
            issue.setPriorityId(p.toString(4))
        }
        // Update issue
        ComponentAccessor.issueManager.updateIssue(currentUser, issue, EventDispatchOption.DO_NOT_DISPATCH, false)
        boolean wasIndexing = ImportUtils.isIndexIssues()
        ImportUtils.setIndexIssues(true)
        log.warn("Reindex issue ${issue.key} ${issue.id}")
        issueIndexingService.reIndex(issueManager.getIssueObject(issue.id))
        ImportUtils.setIndexIssues(wasIndexing)
    }
} catch (Exception ex) {
    log.warn('No URLs at-all here')
    def labelManager = ComponentAccessor.getComponent(LabelManager)
    def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
    def issue = event.issue as Issue
    // remove top250
    def labels = labelManager.getLabels(issue.id).collect { it.getLabel() }
    labels -= 'top250'
    labelManager.setLabels(currentUser, issue.id, labels.toSet(), false, false)
    // Empty "siteRank" field
    def siteRank = customFieldManager.getCustomFieldObjectByName('siteRank')
    issue.setCustomFieldValue(siteRank, null)
    issue = event.issue as Issue
    issue.setPriorityId(p.toString(4))
    // Update issue
    ComponentAccessor.issueManager.updateIssue(currentUser, issue, EventDispatchOption.DO_NOT_DISPATCH, false)
}

The results

With this automation in place, we now have a clear understanding of the impact each ticket holds. By identifying the top 250 Docs URL pages (which constitute 52.7% of our traffic!), we can prioritize them above other work that comes through the triage queue. The team can now determine at a glance which tickets involve these high-impact pages, enabling us to act promptly and efficiently to improve our most valuable content.

Incorporating MongoDB's values of Build Together and Make it Matter into our automation project at Jira was instrumental in achieving a successful outcome. Embodying the value of Build Together, we fostered a collaborative environment by pairing in real-time to conduct extensive testing and in-depth discussions about the project's requirements. Making a tangible impact aligns perfectly with MongoDB's core value of Making it Matter, as we direct our efforts toward tasks that drive significant value for the organization and its users.

This example highlights the importance of automation for program managers, systems administrators, and engineering teams alike, showcasing how tailored solutions can enhance productivity, accuracy, and overall efficiency within Jira. With the right tools and mindset, the possibilities for automation are vast, empowering teams to accomplish more in less time and with greater precision. While our use case is specific to matching URLs, one could imagine many other use cases, such as matching keywords, tags, and so on.

Automation is of paramount importance for various stakeholders involved in the software development lifecycle. Here's why it matters:

  • Enhanced productivity: Automating repetitive tasks frees up valuable time, enabling program managers, systems administrators, and engineering teams to focus on more strategic activities.

  • Improved accuracy: Automation reduces the risk of human error and ensures consistent adherence to predefined processes and standards.

  • Faster response times: By automating workflows and actions, teams can respond swiftly to critical issues and deliver faster resolutions.

  • Scalability and consistency: Automation provides a scalable solution that maintains consistent outcomes, regardless of the number of processes or tickets involved.

  • Transparency and visibility: Automated processes offer increased visibility into the status, progress, and performance of projects, enabling better decision-making.

Our Jira automation project not only streamlined processes, it forged new partnerships and innovation. Through collaboration and knowledge sharing, we developed an automation solution that aligned precisely with our unique requirements. By prioritizing the most impactful pages in our documentation, we contribute meaningfully to the success and efficiency of our team's efforts. This project stands as a testament to the power of working together, embracing new challenges, and striving to make a real difference in everything we do at MongoDB.