Introduction

Well, it finally happened.  I finally had to start learning JavaScript.

It’s actually not that bad, I probably should have learned a while ago.  My use case for it is writing Confluence Macros and plugins for both Confluence and Jira. I started with the plugins, for simplicity’s sake.  
My inspiration came from a post on the Atlassian Community Forums. Someone had requested a way to essentially mirror the setup of a macro. But they wanted to mirror the most recent child page, of a parent page.
I think that without pretty strong knowledge of Confluence and the REST API, I’d have struggled to complete this.  It enough work to learn JavaScript’s basic tenets as I went.

 

Digging Into The Problem

Okay so what do we actually need the script to do? We need it to:

  •  Figure out the most recently updated child page of a parent page
  •  Fetch the macro setup of the child page
  • Update the parent page accordingly

These are the three high-level functions that the macro needs to accomplish. 

Figuring out the most recently updated child page wasn’t hard.  You can make a call to baseURL + pageID + “/child/page?limit=1000&expand=history.lastUpdated. This returns a list

As is often the case, the point of this blog isn’t so much to explain how to do something complicated. The point is that I’m trying to explain something simple that should be easy to find an answer for, but was not. In this case my question was “what on earth is a UserTemplate (User Template) in Confluence”?

On the surface, it seems like creating a new user in Confluence should be a pretty straightforward process.   There’s a UserAccessor class, and that class has a createUser() method.   However, the expected inputs to that method are a User Template object and a Credentials object. From the class documentation:

  • createUser

     ConfluenceUser createUser(com.atlassian.user.User userTemplate,
                              com.atlassian.user.security.password.Credential password)

The import required to work with Credential is spelled out for us, but the userTemplate is a different story.   There’s virtually no documentation on what that means, and no amount of Googling “Confluence User Template”, “UserTemplate”, “Confluence Create User Template” will actually tell you what to do. Part of the issue is that “template” means several different things in the context of Confluence, so that muddies the waters.

Let’s cut to the chase. Here’s the code that I eventually came up with:

 import com.atlassian.confluence.api.model.people.User
import com.atlassian.sal.api.component.ComponentLocator
import 

Introduction

In a bid to contribute more to the Atlassian Community, I took a look at the most recent requests and questions on the Forums.  One that caught my eye was a request for a Confluence Macro that would:

“…display on the restricted page exactly who has access (including a breakout of all group members, not just the group name) to create transparency and build confidence in the selected user group that they are posting in the appropriately restricted area.”

I’d never created a Confluence Macro before, and this seemed like a challenge I could meet.

Please note that this isn’t a how-to on creating Macros, but really just an accounting of my experience learning the tool.

Getting Started

The first thing I did was check to see what Atlassian has to say on the subject. Confluence Macros are written in Apache Velocity, which is quite different from the Groovy that I’m used to working with.

All of the functional lines in Velocity start with a #, which makes a Velocity script look like one big page of commented-out code.  The truth is that Velocity is very old and pretty clunky.  The last news update to the project was

This script returns a list of pages in a Confluence instance that use a specified macro.

There are a number of references to “MacroManager” in the Confluence API documentation, but none of the implementations seemed to work for me.

For that reason, our best bet for checking on Macro usage is to examine the body content of each page, and look for a specific reference to the macro in question.

We also need to check that the page in question is the latest version of the page. Otherwise the script checks all versions of all pages.

 

 import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.pages.Page
def pageManager = ComponentLocator.getComponent(PageManager)
def spaceManager = ComponentLocator.getComponent(SpaceManager)
def spaces = spaceManager.getAllSpaces()
  
def macroRef = 'ac:name="info"'
//Replace "info" with the name of the macro you want to assess
  
spaces.each{  spaceObj ->
//Get all Spaces in the instance
  def pages = pageManager.getPages(spaceObj, false)
  pages.each{    page ->
//Get all pages in the instances
      if (page.getBodyContent().properties.toString().contains(macroRef) && page.version == page.latestVersion.version) {
      //Check if the page contains the macro, then check to see if it's the most current version of the page
        log.warn("'${page.title}' (version ${page.version}) is the latest version of the page, and contains the target macro")
      }
  
There’s nothing terribly complicated about what I’m posting today, but it’s been a while since I put out an update.
The challenge today was to create a sort of interlocking series of issues, using a listener. That is, the listener listens for an issue to be created. It creates a bug and an epic within the same project. It then sets the epic link of the original issue and bug to be the newly created epic. It was also an exercise in balancing efficiency with readability.  Certainly, there are much more efficient ways this script could have been written. But I think as an example, it’s fairly readable, and that has value too.   
 //Example Listener for Jira Cloud - Create Bug and Epic Automatically
 
//Author: Ken McClean / kmcclean@daptavist.com
 
//Script functions:
// - Script runs as a listener
// - On issue create, script creates a new epic
// - It also creates a new bug
// - The script then sets the epic link of the original issue and the new bug to be the newly created epic
 
//References:
//https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post
//https://library.adaptavist.com/entity/create-subtasks-when-issue-created
 
//BEGIN DECLARATIONS:
final bugTypeID = "10008"
final epicTypeID = "10000"
final epicNameField = "customfield_10011"
final epicLinkField 

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