This script builds upon the previous script.  It authenticates against both Jira Server and Jira Cloud.  It then takes a list of Projects as input, and compares the issue count for the project on the Server and Cloud side.

This would be most useful in the case of an ongoing migration, to validate a successful transfer of data.

 import requests
import json
import base64

projects = []
#Define a list of target projects to compare

server_username = "<server username>"
server_password = "<server password>"
server_url = "<server URL>"
#Define connection parameters for the server side

cloud_username = "<Cloud username"
cloud_token = "<Cloud token>"
cloud_url = "<Cloud URL>"
#Define connection parameters for the Cloud side

server_credentials_string = f'{server_username}:{server_password}'
server_input_bytes = server_credentials_string.encode('utf-8')
server_encoded_bytes = base64.b64encode(server_input_bytes)
server_encoded_string = server_encoded_bytes.decode('utf-8')
#Encode the server credentials

cloud_credentials_string = f'{cloud_username}:{cloud_token}'
cloud_input_bytes = cloud_credentials_string.encode('utf-8')
cloud_encoded_bytes = base64.b64encode(cloud_input_bytes)
cloud_encoded_string = cloud_encoded_bytes.decode('utf-8')
#Encode the Cloud credentials

server_headers = {
    'Authorization': f'Basic {server_encoded_string}',
    'Content-Type': 'application/json',
    'Accept': 'application/json',
}
#Define the headers used to connect to the server

cloud_headers = {
    'Authorization': f'Basic {cloud_encoded_string}',
    'Content-Type': 'application/json',
    'Accept': 'application/json',
}
#Define the headers used to connect to the Cloud

#Iterate through the list of projects
for project in projects:
    server_issue_count_request = requests.get(f'{server_url}/rest/api/2/search?jql=project={project}',
                                            headers=server_headers)
    

Connecting to server and Cloud instances of Jira with Python is accomplished with much the same method and approach. The only differences between the two are that Server uses a username and password, while Cloud uses a username and token.

Generating a token is pretty straightfoward.  I recommend reading the documentation first.

The script below consists of essentially three pieces.   You define the connection parameters,  create the headers used to authenticate against the instance, and return the results of the authentication request.

The script example below returns one page of project results from each instance, just to demonstrate how it works.  If you wanted to actually work with the results, they’d need to be converted to JSON or some other format. 

The process for connecting to Confluence is the same; you need only point the script at a Confluence instance (and switch to returning some Space data or something).

 import requests
import base64

server_username = "<username>"
server_password = "<password>"
server_url = "<url>"
#Define connection parameters for the server side

cloud_username = "<Cloud login email>"
cloud_token = "<Cloud token"
cloud_url = "<Cloud url>"
#Define connection parameters for the Cloud side

server_credentials_string = f'{server_username}:{server_password}'
server_input_bytes = server_credentials_string.encode('utf-8')
server_encoded_bytes = base64.b64encode(server_input_bytes)
server_encoded_string 

Here’s a very basic example of a script to review group membership on Jira Server/DC

By first fetching the groups, and then the users in each group, we take the most efficient path toward only fetching the users who are in a group.

On the other hand, we could also tweak this script to show us users who are NOT in a group, or who are in  X or fewer groups.   That might be interesting, too.

 

 import com.atlassian.jira.component.ComponentAccessor

def groupManager = ComponentAccessor.getGroupManager()
def groups = groupManager.getAllGroups()
def sb = []
//Define a string buffer to hold the results

sb.add("<br>Group Name, Active User Count, Inactive User Count, Total User Count")
//Add a header to the buffer
groups.each{ group ->

 def activeUsers = 0
 def inactiveUsers = 0
 Each time we iterate over a new group, the count of active/inactive users gets set back to zero
 def groupMembers = groupManager.getUsersInGroup(group)
 //For each group, fetch the members of the group
    
    groupMembers.each{ member ->
    //Process each member of each group
        
    def memberDetails = ComponentAccessor.getUserManager().getUserByName(member.name)
    //We have to fetch the full user object, using the *name* attribute of the group member
        
        if(memberDetails.isActive()){
            activeUsers += 1 
        }else{
            inactiveUsers += 1
        }
    }//Increment the count of 

There’s a simple way to return a list of field configurations and field configuration schemes in Jira DC/Jira Server.  However, in order to find that information you have to know that Jira once referred to these as field layouts

Using the FieldLayoutManager class, this script returns a list of field layouts:

 import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.fields.layout.field.FieldLayoutManager

def layoutManager = ComponentAccessor.getFieldLayoutManager()
def fieldLayouts = layoutManager.getEditableFieldLayouts()
def sb = []

fieldLayouts.each{ fieldLayout ->

    sb.add("<br> ${fieldLayout.name}")

}

return sb 

 

This script returns the field layout schemes, with a simple change of the method:

 import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.fields.layout.field.FieldLayoutManager

def layoutManager = ComponentAccessor.getFieldLayoutManager()
def layoutSchemes = layoutManager.getFieldLayoutSchemes()
def sb = []

layoutSchemes.each{ layoutScheme ->

    sb.add("<br> ${layoutScheme.name}")

}

return sb 

This simple script fetches all projects, then fetches each issue in the project.  For each issue, it counts the number of attachments and adds it to a running tally for that project.

 import com.atlassian.jira.component.ComponentAccessor

def projectManager = ComponentAccessor.getProjectManager()
def projects = projectManager.getProjectObjects()
def issueManager = ComponentAccessor.getIssueManager()

projects.each{ project ->

  def attachmentsTotal = 0
  def issues = ComponentAccessor.getIssueManager().getIssueIdsForProject(project.id)

  issues.each{ issueID ->

    def issue = ComponentAccessor.getIssueManager().getIssueObject(issueID)
    def attachmentManager = ComponentAccessor.getAttachmentManager().getAttachments(issue).size()
    attachmentsTotal += attachmentManager

  }
  log.warn(project.key + " - " + attachmentsTotal)
} 

 

This script fetches all of the projects in a Jira Cloud instance. It then fetches all of the project roles for that project, and finally fetches all of the users in that role for that project. In this way, it iterates through the projects and returns information about the users in the project roles.

 

 import groovy.json.JsonSlurper

def sb = []
//Define a string buffer to hold the results

def getUsers = get("/rest/api/2/project")
  .header('Content-Type', 'application/json')
  .asJson()
//Get the list of projects in the instance

def content = getUsers.properties.rawBody
//Get the raw body contents of the HTTP response

def scanner = new java.util.Scanner(content).useDelimiter("\\A")
String rawBody = scanner.hasNext() ? scanner.next() : ""
def json = new JsonSlurper().parseText(rawBody)
//Turn the raw body contents into JSON

json.each{ project ->
//Iterate through the projects
  
  sb.add("$project.name")

  def getRoles = get("/rest/api/2/project/$project.id/role")
    .header('Content-Type', 'application/json')
    .asObject(Map)
//For each project, get the list of roles


  getRoles.body.each{ projectRole ->
  //Iterate through the project roles

      def getRoleMembers = get("$projectRole.value")
      .header('Content-Type', 'application/json')
      .asObject(Map)
      //Return the details about each role

    getRoleMembers.body.actors.each{ roleMember ->
    //Get all the actors (users) in that role

        sb.add("$getRoleMembers.body.name:   $roleMember.displayName")
    }
  }
}

return sb
//Return the results


 

Mitigating CORS Errors With Custom Jira REST API Endpoints

If you dive into the world of REST requests and APIs, you may encounter a CORS error that prevents your request from completing. CORS stands for Cross-Origin Resource Sharing.  Same-origin is a security feature in browsers that prevents requests coming from one place (origin) to access resources in a different domain.  CORS allows web pages to access resources on a different network by providing a standard for safely allowing cross-origin requests.

Let’s talk about the example that I encountered.  I wrote a JavaScript macro for Confluence Server, and I was trying to access a third-party API using that macro.  However, Confluence macros run in the browser when the page loads, rather than running on the back-end Confluence server itself.   Thus, while the Confluence server may be set up to address CORS, your browser almost certainly is not, and the request gets blocked.

We can address this by creating a custom REST API endpoint in Confluence (or Jira).   In this way, we have the server making the request to the third party API, and the macro makes the request to the internal API.

In other words, the custom REST API endpoint acts

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

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.