Overview

Tempo Planner allows for planning team capacity and schedules within Jira.  However, you may have some need to pull that resource planning information out of the Tempo interface and add it to a ticket.

The Tempo API has some severe limitations, but where there’s a will there’s a way.

Team Info

The first thing we’ll examine is how to get information on all of the teams in Tempo Planner.  According the documentation, this isn’t possible.  Per the API documentation, you can return limited very information about plans and allocations. 

Naturally I found this to be unacceptable, and I figured out a way to have the API return all of the teams.   One of the undocumented API endpoints is a search function: /rest/tempo-teams/3/search.    One of the tricks to using this method is that it’s not a GET, it’s a POST, so we have to supply a search parameter as a payload.  When we POST to this endpoint, we supply some JSON: {“teamSearchString”:”<string>”}.  But here’s the rub: the API will accept an empty search string, and return all of the teams as a result.

Allocation Info

Much like team info, there is no public Tempo API endpoint that will

There may come a day when you’re asked to create a large number of Confluence pages. Rather than doing it by hand, why not script it?

This Python script essentially does two things: it reads the CSV file, and it sends page creation requests to a Confluence server.   

For each row in the CSV file, it assumes the page name should be the value in the first cell of the row.  It then generates an HTML table that is sent as part of the page creation request. 

Rather than generating HTML, this could be useful for setting up a large number of template pages, to be filled in by various departments.  It could also run as a job, and automatically create a certain selection of pages every week or month, to store meeting notes or reports.

Please note that in order to connect to the Confluence server, you’ll need to generate a Personal Access Token.

 

 import csv
import requests
import json
import html
import logging

# Initialize logging
logging.basicConfig(level=logging.ERROR)

api_url = 'https://<url>.com/rest/api/content/'
#What's the URL to your Confluence DC instance?


file_path = "<your CSV file path>"
#where is the file stored locally?

parent_page_id = "<your parent page ID>"

The amount of code required to fetch information from Confluence Cloud and bring it into Jira Cloud is a bit shocking. In a good way.

Here’s the code:

 import org.jsoup.*

def authString = "<authstring>"

def fieldConfigsResult = get("https://<url>.atlassian.net/wiki/rest/api/content/229377?expand=body.storage")
  .header('Content-Type', 'application/json')
  .header("Authorization", "Basic ${authString}")
  .asObject(Map)

def storage = fieldConfigsResult.body.body.storage.value


return storage
 

 

In the end it’s all just REST.  So long as you can authenticate, UNIREST allows us to pretty easily fetch information from other sites.

If you’d like to learn more about authenticating against Jira Cloud, check out my post on the subject.

The request on the Atlassian Forums that caught my eye last night was a request to return all Jira Cloud attachments with a certain extension.   Ordinarily it would be easy enough to cite ScriptRunner as the solution to this, but the user included data residency concerns in his initial post.

My solution to this was to write him a Python script to provide simple reporting on issues with certain extension types.    Most anything that the rest API can accomplish can be accomplished with Python; you don’t HAVE to have ScriptRunner.

The hardest part of working with external scripts is figuring out the authorization scheme. Once you’ve got that figured out, the rest is just the same REST API interaction that you’d get with UniREST and ScriptRunner for cloud.

Authorizing the Script

First, read the instructions: https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/ 

Then:

1. Generate an API token: https://id.atlassian.com/manage-profile/security/api-tokens

2. Go to https://www.base64encode.net/ (or figure out the Python module to do the encoding)

3. Base-64 encode a string in exactly this format: youratlassianlogin:APIToken.   
If your email is john@adaptamist.com and the API token you generated is:

ATATT3xFfGF0nH_KSeZZkb_WbwJgi131SCo9N-ztA3SAySIK5w3qo9hdrxhqHZAZvimLHxbMA7ZmeYRMMNR

 

Then the string you base-64 encode is:

john@adaptamist.com:ATATT3xFfGF0nH_KSeZZkb_WbwJgi131SCo9N-ztA3SAySIK5w3qo9hdrxhqHZAZvimLHxbMA7ZmeYRMMNR

 

Do not forget the colon between the two pieces.

This script takes a list of custom field names, and searches each issue in the instance for places where that custom field has been used (i.e., where it has a value other than null).

In this way, we gain insight into the usage of custom fields within a Jira Cloud instance.

The script is interesting for a number of reasons. First, it’s another instance of us having to parse a rawBody response before we can make use of it. Second, it handles the need for pagination, which we’ve also talked about in recent posts.

My intent for this script is for it to serve as a basis for future custom field work in Jira Cloud. Namely, I’d like to be able to easily rename any field with “migrated” in the title.

 

 import groovy.json.JsonSlurper
 
def stillPaginating = true
//Loop to paginate
 
def startAt = 0
//Start at a pagination value of 0
 
def maxResults = 50
//Increment pagination by 50
 
def issueIDs = []
def fieldNames = ["Actual end", "Actual start", "Change risk", "Epic Status"]
def customFieldMap = [: ]
 
 
//Get all the fields in the system
def fieldIDs = get("/rest/api/3/field")
  .header('Content-Type', 'application/json')
  .header('Accept', 'application/json')
  .asBinary()
 
def inputStream = fieldIDs.rawBody

Get All Filters in a Jira System

Here’s the truth: getting all of the filters in a Jira DC instance with ScriptRunner is awkward and fussy.  There’s no method that simply returns all of the filters. 

Instead, we need to first return all of the users in the system.  And then we need to examine all of the filters that each of them owns, as each filter must have an owner.  Here’s an example of some code that does that:

 import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.bc.filter.SearchRequestService
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.jira.bc.user.search.UserSearchParams
import com.atlassian.jira.bc.user.search.UserSearchService
   
   
SearchRequestService searchRequestService = ComponentAccessor.getComponent(SearchRequestService.class)
UserSearchService userSearchService = ComponentAccessor.getComponent(UserSearchService)
def sb = new StringBuffer()
   
UserSearchParams userSearchParams = new UserSearchParams.Builder()
    .allowEmptyQuery(true)
    .includeInactive(false)
    .ignorePermissionCheck(true)
    .build()
//Define the parameters of the query
  
//Iterate over each user's filters
userSearchService.findUsers("", userSearchParams).each{ApplicationUser filter_owner ->
    try {
        searchRequestService.getOwnedFilters(filter_owner).each{SearchRequest filter->
            String jql = filter.getQuery().toString()
            //for each fiilter, get JQL and check if it contains our string
             
                sb.append("Found: ${filter.name}, ${jql}\n" + "<br>")
             
        }
    } catch (Exception e) {
            //if filter is private
           sb.append("Unable to get filters for ${filter_owner.displayName} due to ${e}")
    }
}
 
return sb 

 

Getting a list of filters on Jira Cloud is much simpler, as there’s a REST API that accomplishes this.  If we

Confluence Meta-Macros and the joy of being organized

Let’s talk about meta-macros.  That is, macros that examine other macros.   I just made up the term, so don’t be concerned if you can’t find other examples on the internet.

If you wanted some insight into which pages in your Confluence Instance were using a specific macro, how would you find that information?

You could certainly check each page manually, but that sounds dreadful.

One option to get Macro information is this ScriptRunner script that I wrote, which examines the latest version of each page in each Space for references to the specified macro:

 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"'
  
spaces.each {
  spaceObj ->
    def pages = pageManager.getPages(spaceObj, false)
  pages.each {
    page ->
      if (page.getBodyContent().properties.toString().contains(macroRef) && page.version == page.latestVersion.version) {
        log.warn("'${page.title}' (version ${page.version}) is the latest version of the page, and contains the target macro")
      }
  }
}
 

 

But what if you wanted MORE information?  What if you wanted to know every macro running on every page in the system, and you didn’t have ScriptRunner to do it for you?  In that

Overview

I spoke to someone recently on the subject of learning to use ScriptRunner and Groovy.  One of the questions he had was around the number of results that were returned when he called the API.  That is, he had set maxResults to 500, but only 100 results were returned.   Why?
It’s true that you have some agency over the number of results that are returned by the Jira (and Confluence) REST API.  You can set the maxResults value as part of the request, and receive more than the 50 items that are returned by default.   However, there are limits to this parameter.  The API has a built-in limit, and no matter what you set maxResults to, you cannot exceed that limit.

Let’s look at an example.

If I call /rest/api/3/search?jql=project is not EMPTY, it’ll return every issue in the instance from a project that isn’t empty.  That’s potentially a lot of issues. 
However if we look at the results that are returned, there are some meta attributes in addition to the issues themselves.  In our example, the JSON that is returned starts with this:
 {
"expand": "schema,names",
"startAt": 0,
"maxResults": 50,
"total": 35,
 
From this, we see that

This script searches for Jira filters by name, then by filter ID, and then updates the permissions set applied to them.

It’s useful for updating a bulk list of filter names, but really it’s an exercise in working with filters and the search service.

The script iterates through an array of filter names. For each filter name, it uses the searchRequestManager to find that filter object.  It then uses the filter ID of the object to return a filter object.  This seems redundant, but the object returned by the searchRequestManager isn’t actually a filter. It’s information about the filter. 

When we search for the filter by name using searchRequestManager , it will return any filter that has that name. For that reason, we must treat the results like an array, and iterate through them.  For each filter ID that is returned by the name search, we use the searchRequestManager to search for the filter object.

Once we’ve got a filter object, we apply the new permissions to it and commit the change with updateFilter().

 

 

 import com.atlassian.jira.bc.JiraServiceContextImpl
import com.atlassian.jira.bc.filter.SearchRequestService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.sharing.SharePermissionImpl
import com.atlassian.jira.sharing.SharedEntity
import com.atlassian.jira.sharing.type.ShareType
import com.atlassian.jira.issue.search.*

def searchRequestService = ComponentAccessor.getComponent(SearchRequestService)
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def searchRequestManager 

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