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 

Threading is a fantastic and (relatively) simple way to run parallel HTTP requests against a Jira or Confluence instance.  Multithreading can drastically cut down the length of time it takes for a piece of code to run, as each HTTP request is not waiting for the previous request to finish.  In Groovy we can achieve threading by using the Thread class or the ExecutorService framework.

However, threading isn’t a viable solution when it comes to Jira (or Confluence) on the Cloud.   Because Jira Cloud is a shared hosting environment, there are a number of reasons why Atlassian has put strict limitations on the use of threading.    Chief among these concerns are performance and security.   If anyone can run any number of threads that they want, this necessarily impacts the other users of the shared hosting environment.

Similarly, multithreading in a shared hosting environment can cause data inconsistencies and synchronization issues, which easily lend themselves to visions of major security issues.

 

Though we cannot use threading on Jira Cloud, we can use async.  Simply put, the difference between the two is that threading uses concurrent or parallel threads of execution to achieve concurrency.  That is, it literally splits the

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

Intro

(If you haven’t read the previous post in this series, I highly recommend starting with that.)

In order to make use of the Jira API documentation, we need to understand what classes, packages, and methods are.   That’s because the JIRA API is documented, but only in the most technical sense.  It is predicated on you having some knowledge of Java.    You can go to https://docs.atlassian.com/software/jira/docs/api/latest and get information about all of the internal libraries that Jira uses to function, but it’s not much good to you without that prior knowledge.

At the same time, knowing how to read and make use of the API documentation is a vital skill when it comes to working with ScriptRunner.   All of Jira’s functionality is available for you to use, but only if you can harness it through the power of classes.

What’s the Problem?

The problem I ran into when I was first starting to learn how to use ScriptRunner is that very little is written in a context that a beginner could make use of, and few examples are provided.  A lot of the Jira’s internal functions are interdependent, and someone who is new to both Groovy and Jira

I try to learn something new every day when it comes to Groovy, or at the very least to challenging my understanding of some of the tenets of the syntax that I think I already know.

This morning I was playing around with the assert statement in ScriptRunner, trying to understand the nuances of it.   I was getting frustrated because I wanted the assertion to fail, but then for the script to keep going.  I couldn’t figure out how to capture the error without having the script stop.

I tried a few things.   I know you can include a log statement to the end of an assertion, like so:

 assert (1 == 2) : "Assertion failed"  

 

 java.lang.AssertionError: Assertion failed. Expression: (1 == 2)
	at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:438)
	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
	at Script205.run(Script205.groovy:2) 

But even with the inclusion of the log statement, my script would grind to a halt when the assertion failed.    I also tried putting the assertion in a try/catch statement, but that didn’t work at all.

I then realized that I had no idea what the functional difference between an assertion and a try/catch statement actually was.  Moreover, I didn’t know when it was appropriate to use one or the

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=" +