One of the challenges that Jira admins face is monitoring the health of their Jira instance. While there are some built-in tools for doing this, it’s useful to know how to perform routine maintenance using ScriptRunner.
One such routine maintenance task is the monitoring of Jira Project sizes.   There’s no direct method or way of doing this; instead we get all of the issues for a given project, and sum up the size of the attachments on each issue.  In this way, we get an idea of which Jira Projects are becoming unwieldy.

The code required to perform this calculation isn’t complicated. However, if the script runs too long it’ll simply time out. This is especially true if you’re calculating the size of multiple Projects.  Here’s the code to calculate the size of a single project:

 import org.ofbiz.core.entity.GenericValue;
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.IssueManager;
import com.atlassian.jira.project.Project;
import com.atlassian.jira.project.ProjectManager

def totalSize = 0
def attachmentManager = ComponentAccessor.getAttachmentManager()

ProjectManager projectManager = ComponentAccessor.getProjectManager()

def projectName = "<projectName>"

Project proj = projectManager.getProjectByCurrentKey(projectName)

IssueManager issueManager = ComponentAccessor.getIssueManager()

for (GenericValue issueValue: issueManager.getProjectIssues(proj.genericValue)) {
    Issue issue = issueManager.getIssueObject(issueValue.id)
    attachmentManager.getAttachments(issue).each {
      attachment ->
        totalSize += attachment.filesize
    }
  }
  log.warn("Total size of attachments for ${proj.name} is ${totalSize / 1024} 

Here’s a relatively simple one for you. Say you had a Jira Cloud instance with hundreds or thousands of projects, and you wanted to swap them all over to using a new notification scheme.  How would you go about it?

Well you could certainly do it by hand. That’s an option.  Or you could write a little script and us an API endpoint that I’ve only just discovered.

The script below fetches all of the projects in the system that use a notification scheme. We then filter out only the ones we want to adjust, and update each of those projects to use the target notification scheme.

This script is a great example of the kind of thing that ScriptRunner excels at, and it’s a great script to use to start learning the REST API.

 

 import groovy.json.JsonSlurper
 
// Notification scheme ID to search for
def searchSchemeId = "10000"
 
// Notification scheme ID to use for update
def updateSchemeId = 10002
 
def response = get("/rest/api/3/notificationscheme/project")
  .header('Content-Type', 'application/json')
  .asJson()
 
// Parse the JSON response
def jsonSlurper = new JsonSlurper()
def json = jsonSlurper.parseText(response.getBody().toString())
 
// Access the nodes in the JSON
json.values.each {
  project ->
 
    logger.warn(project.toString())
 
//We need to account for -1, 

I admit, this one is a very specific use case.  But I had a reason to create the script, so maybe someone will find it useful.  I also got to use the .collect method in a useful way!

This script identifies all of the pages that are linked from a target page.  It then compares that list of links to a list of all the pages in the Space.

By doing this, it identifies any pages in the Space that aren’t linked on the target page.  This could be useful if you had a wiki or something, and wanted to know which pages weren’t linked on the front page.

One interesting thing I discovered while doing this is the outgoingLinks method of the page class.   Instead of having to use regex to find the URLs on a page, I simply had to call this method and all of the urls were returned for me.

 

 import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.sal.api.component.ComponentLocator

def pageManager = ComponentLocator.getComponent(PageManager)
def spaceManager = ComponentLocator.getComponent(SpaceManager)

def targetSpace = spaceManager.getSpace("<Space Key>")
def spacePages = pageManager.getPages(targetSpace, true)
def targetPage = pageManager.getPage(<page ID as Long>)

def outgoingLinks = targetPage.outgoingLinks.collect { link -> 

link.toString().minus("ds:")
}

spacePages.each {
  page ->

    

The Problem

One of the problems we encounter with migrating large Jira instances is that when it comes to JCMA, you either have to add all of the Advanced Roadmaps plans or none of them. There’s no facility for selectively adding Roadmaps plans.

One such migration involved moving a subset or portion of the instance data, rather than the whole thing.   Though the instance had 1400+ Roadmaps plans, we only needed to migrate about 100 of them.

The solution we came up with was:

  1. Take a backup of the current production instance
  2. Delete all of the Advanced Roadmaps plans that didn’t need to be migrated
  3. Perform the migration
  4. Restore the production instance from the backup

Naturally, none of us fancied deleting 1300 Roadmaps plans by hand, especially if we had to do it more than once during the course of testing.  So we scripted it.

The Solution

The actual deleting of an Advanced Roadmaps plan is pretty simple.  To delete a plan, just send a DELETE request to the plan’s API endpoint, /rest/jpo/1.0/plans/<planID> .  So long as you have a list of plan names or IDs that you want to keep, you can tell the script to delete any plans

The settings or preferences for a given user in Jira Cloud are stored in a number of locations within the system.   The User Properties section contains settings relating to which interface elements the user sees or doesn’t see.  

For example, when you first access ScriptRunner on a Jira instance, you’re presented with a little quiz.  It looks like this:

After you click through this quiz it goes away forever. Someone recently remarked that they’d love to not have their ScriptRunner users be presented with this quiz in the first place.

…Okay, we can make that happen!

 

First we need to query the user properties for a given user with this code:

 def userProps = get("rest/api/2/user/properties") 
.header("Accept", "application/json")
.queryString("accountId", "<user account ID>")
.asJson(); 

return userProps.body 

The results look like something like this:

 {
  "array": {
    "empty": false
  },
  "object": {
    "keys": [
      {
        "self": "  https://some-jira-instance.atlassian.net/rest/api/2/user/properties/navigation_next_ui_state?accountId=12345678910abcdef123456",
        "key": "navigation_next_ui_state"
      },
      {
        "self": "  https://some-jira-instance.atlassian.net/rest/api/2/user/properties/onboarding?accountId=12345678910abcdef123456",
        "key": "onboarding"
      },
      {
        "self": "  https://some-jira-instance.atlassian.net/rest/api/2/user/properties/project.config.dw.create.last.template?accountId=12345678910abcdef123456",
        "key": "project.config.dw.create.last.template"
      },
      {
        "self": "  https://some-jira-instance.atlassian.net/rest/api/2/user/properties/sr-post-install-quiz?accountId=12345678910abcdef123456",
        "key": "sr-post-install-quiz"
      }
    ]
  }
} 

 

The key we’re interested in is the one at the bottom, called sr-post-install-quiz.  A new user isn’t going to have this property; it only gets

Much like yesterday’s post about injections, it took me a little bit to figure out what was going on with pipelines and the left shift << operator in Groovy.

In the end, it was simplest for me to say “X << Y basically means feed the data in Y into the function of X”.  Pipelines are the closure functions that X would represent.

 

Let’s look at a Jira-related example:

 import com.atlassian.jira.component.ComponentAccessor

// Get the Jira issue manager
def issueManager = ComponentAccessor.getIssueManager()

// Get all the issues in a target project
def issues = issueManager.getIssueObjects(issueManager.getIssueIdsForProject(10100))

log.warn("This is the full list of issues: " + issues)
//Assume one of those issues, ZT-4, has a priority of "lowest"

//Define a new pipeline into which we'll feed the list of issue objects
def pipeline = {
 it.findAll { !(it.priority.name == "Lowest") }
}

// Apply the pipeline to the list of issues
def filteredAndSortedIssues = pipeline << issues

return filteredAndSortedIssues
 

 

So what are we doing?  First we’re grabbing a list of issues from a target project.  Then we’re defining a pipeline, which is simply a closure that performs an operation on an as-yet undefined set of data.

Finally, we’re invoking the pipeline

When I sat down to write about injections, I thought it’d be a quick little blog post.  However, it took me a lot longer than I expected to get my head around even the basic concept of what the injection was actually doing.
I get the general idea now, but I don’t see myself putting them into action in my code very often.

This is the best example I could find of an injection in action.

Here’s a quick exploration of some injection code, as I understood it:

 (1..5).inject(1) { runningTotal, itemInRange ->

 log.warn("$runningTotal * $itemInRange = ${runningTotal * itemInRange}")

log.warn("The running total is $runningTotal and the current item from the range is $itemInRange")

 runningTotal * itemInRange 

} 

 

The results looked like this:

 2023-02-22T03:55:55,063 WARN [runner.ScriptBindingsManager]: 1 * 1 = 1
2023-02-22T03:55:55,063 WARN [runner.ScriptBindingsManager]: The running total is 1 and the current item from the range is 1
2023-02-22T03:55:55,063 WARN [runner.ScriptBindingsManager]: 1 * 2 = 2
2023-02-22T03:55:55,063 WARN [runner.ScriptBindingsManager]: The running total is 1 and the current item from the range is 2
2023-02-22T03:55:55,063 WARN [runner.ScriptBindingsManager]: 2 * 3 = 6
2023-02-22T03:55:55,063 WARN [runner.ScriptBindingsManager]: The running total is 2 and the current item from the range is 3
2023-02-22T03:55:55,063 

In an attempt to become a stronger user of Groovy and Jira, I’m challenging myself to learn something new each day for 100 days.  These aren’t always going to be especially long blog posts, but they’ll at least be something that I find interesting or novel.

If we want to work with the elements in a collection, we have a number of options.  My favourite method is to use a closure with .each, which could be as simple as this:

 def eachList = [5, 10, 15]

eachList.each{element ->
log.warn(element.toString())

}
 

The closure allows us to iterate through each element in the collection.

Groovy also has a .collect method.  Implementing it would look something like this:

 def collectList = [1, 2, 3]

def squaredCollectList = collectList.collect { element ->
    element * element
}

return squaredCollectList
 

So what’s the practical difference?

With .each, we’re simply iterating through a collection of elements that already exists.  With .collect, we’re defining a new collection (squaredCollectList). We then iterate through all of the elements of the predefined list (collectList), square the element, and add the result to the newly defined collection.

In simple terms, .each iterates through a list. .collect iterates through a

TrustedRequestFactory is a Jira-supplied way of authenticating against Jira itself.  If I wanted to authenticate against the current instance, or an external instance, this is what I’d consider using.

This script iterates through a collection of Jira URLs, and processes them as TrustedRequestFactory GET requests in parallel. This is useful in cases when a large number of requests need to be submitted as HTTP; instead of waiting for each one to finish in turn, we can run them all at once.

As it stands, this script authenticates using the logged-in user.  However, this could be amended pretty easily by simply adding the correct type of authentication as a header on the request.  As well, if you wanted to authenticate against a different Jira instance, the URL structure would need to be amended slightly.

I’ve commented the code as best I can, but quite frankly I’m still learning about parallel execution myself.   I’m next going to dig into async vs. threading, and see what I can discover.

 

 import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys
import com.atlassian.sal.api.net.Response
import com.atlassian.sal.api.net.ResponseException
import com.atlassian.sal.api.net.ReturningResponseHandler
import com.atlassian.sal.api.net.TrustedRequest
import com.atlassian.sal.api.net.TrustedRequestFactory
import com.atlassian.sal.api.net.Request
import groovy.json.JsonSlurper
import groovyx.net.http.ContentType
import groovyx.net.http.URIBuilder
import java.net.URL;
import java.util.concurrent.Callable
import java.util.concurrent.Executors
 
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser

This is an extension of a previous post that I did, on returning all of the users in a Jira Cloud instance.

The request was for a script that returns all users in a Jira Cloud instance who have been inactive for over 30 days. Unfortunately, only part of this request is doable.  The Jira Cloud REST API does not currently provide any way of returning login or access timestamp information.  The most it will tell you is whether a user account is active or inactive. The only way to get access information is to export the Jira userbase through the Atlassian portal.

So the script below simply returns all Jira users in a Cloud instance who have an inactive account.   If/when the day comes that Atlassian adds login information to the API, I’ll update this post.

 import groovy.json.JsonSlurper

def sb = []
def page = 0

def lastPage = false

while (lastPage == false) {
  //run this loop until we detect the last page of results

  def x = 0
  //Every time the loop runs, set x to 0. We use x to count the number of users returned in this batch of results

  def getUsers = get("/rest/api/2/user/search?query=&maxResults=200&startAt=" +