[JIRA] Dev – Where are OSWorkflow Transition Screens?

Found a piece that isn’t well documented. But as usual, I’m explaining my steps and then the solution. It’s no real big deal anyway. 🙂

Here’s the background:
Mission was to write a Groovy script that returns all the transition screens used across all workflows of a given JIRA workflow scheme (for documentation purposes). This looked pretty easier, until a collection of ‘com.opensymphony.workflow.loader.ActionDescriptor’ came up without any linked JavaDocs. That looked like a dead end.

Then, with a little bit of googling I came across ActionDescriptor source code, which wasn’t helpful either. getView() method looked promising because ‘Transition View’ is a synonym used in JIRA UI for ‘Transition Screen’. But disappointingly it was just returning string ‘fieldscreen’ for every transition. Aw.. snap! 😦

Next attempt is called the ‘Bruteforce method’ (not java.lang.reflect.Method of course 😉 ).

Since I was too lazy to read across the ActionDescriptor’s source code:

// workflow is type of com.atlassian.jira.workflow.JiraWorkflow
// I'm not explaining all my code here
workflow.getAllActions().each {
  it.class.getDeclaredMethods().grep {
    it.name.matches( "^get.*" )
  }.each {
    log.debug ( it.name )
  }
}
getAutoExecute
getConditionalResults
getPreFunctions
getUnconditionalResult
getMetaAttributes
getValidators
getPostFunctions
getRestriction
getView
getName

It didn’t took much of time. With a bit of guessing I made my first attempt with getter method ‘getMetaAttributes()’. And it had the eye-catching piece I was looking for!

workflow.getAllActions().each {
  log.debug ( it.getMetaAttributes().toString() )
}
[opsbar-sequence:10, jira.description:, jira.fieldscreen.id:10000]
[opsbar-sequence:20, jira.description:, jira.fieldscreen.id:20000]
[opsbar-sequence:40, jira.description:, jira.fieldscreen.id:30000]
[opsbar-sequence:60, jira.description:, jira.fieldscreen.id:30000]

getMetaAttributes() returns a HashMap. So, finally…

def screens = [:]
workflow.getAllActions().each {
  def screenId = it.getMetaAttributes()["jira.fieldscreen.id"]
  if ( screenId != "" ) {
    def workflowAction = workflow.name + " - " + it.name
    def workflowActions = []
    if ( screens[screenId] ) workflowActions = screens[screenId]
    workflowActions.add ( workflowAction )
    screens[screenId] = workflowActions
  }
}
for ( e in screens ) {
  log.debug ( "Transition Screen: " + e.key )
  e.value.each {
    log.debug ( "  Workflow action: " + it )
  }
}

Nice! This HashMap (‘screens’) is all what I wanted. 🙂 Each transition screen, and then where it has been used.

So one last thing… I also wanted to have a one last peek into ActionDescriptor’s source code on GitHub. Only the writeXML() method gave an indication that metaAttributes is actually a HashMap. There was absolutely no indication that screen association information is kept as a ‘meta attribute‘.

Bottom line(s):

  1. If you are a JIRA developer and whenever you want to find screens associated to a workflow, don’t use getView(). Use getMetaAttributes() and then look for the attribute with key “jira.fieldscreen.id”. In other words, transition screen is stored as a meta attribute in each transition. Screen is referred by its numeric ID as a string.
  2. Since OpenSymphony doesn’t seem to go away from JIRA, and some already claim that OpenSymphony is dead, it would be worthwhile if Atlassian maintains their own repository of OSWorkflow docs and special remarks.
Advertisements

JIRA project migration with Google Spreadsheets

JIRA to JIRA project migration — most of the time this has been a headache when it comes to customer negotiations and compromises.

In my experience, usually it starts with a discussion round to identify what customer exactly needs. Some customers expect everything, including issue change history, comments, votes, etc. In this case we have to use the XML project import method. With status, resolution, priorities and custom field mapping this is a headache. In the near-perfect JIRA project migrations I’ve performed, I remember I used Groovy scripts to merge field values, convert additional issue types to labels, and etcetera. If you choose XML import, it needs a lot of technical work and beforehand testing when you have a demanding customer and a JIRA instance that isn’t expected to change much.

The other type of customers are simple-living minimalist people. 😉 They keep things simple, needs are simple and they just need the contents to be brought over. They are okay with basic things such as issue summary, description, type and status.

The method I’m going to discuss today is for the customers who lie in-between the two types of customers above. In my particular case I met a customer who needs comments and one of their custom fields to be brought over. File attachments and issue change history were not necessary for them. In this case downloading JIRA issues from Issue Navigator into Excel and using it as a CSV source won’t do the perfect import.

This led me to develop a custom Google Spreadsheets script. It uses JIRA REST API and fetches project issues into the spreadsheet. Then, it can be downloaded as CSV and imported into JIRA. Here’s a reduced version of the code I wrote – just to demonstrate:

var jql = encodeURIComponent("project = ABC ORDER BY key ASC");
var rows = [];
var s = 0;
while (true) {
  var response = fetchJIRA (baseurl, "/rest/api/2/search?jql=" + jql + "&maxResults=200&fields=*all%2Ccomment&startAt=" + s);
  // fetchJIRA () is a custom function I wrote 
  // to call JIRA REST API and return JSON.
  // It is not explained in this article.
  var max = response.maxResults;
  var c = response.issues.length;
  var t = response.total;
  if (c == 0) break;

  response.issues.forEach (function (i) {
    var row = [];
    row.push (i.key);
    row.push (i.fields.summary);
    // .. Include other fields
    rows.push (row);
    s = response.startAt + c;
  });
}
rows.unshift (["Key", "Summary", ...]);

var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.insertSheet("Issues");
var iR = sheet.getRange(1, 1, rows.length, rows[0].length);
iR.setValues(rows);

Actually it does more!

  • Status/ issue type/ resolution/ user account/ select-list custom field value mapping:
    Using a function like this maps statuses on-the-fly fetching, so we don’t have to worry about creating a workflow.

    function statusMapper (param) {
    var mapping = {
      "Open":"Backlog",
      "Done":"Closed",
      "Resolved":"Closed",
      "Validate":"In Verification",
      "Planned":"Defined"
    };
    var ret = mapping[param];
    if (ret == undefined) ret = param;
    return ret;
    }
    row.push (statusMapper (i.fields.status.name));
    row.push (typeMapper (i.fields.issuetype.name));
    row.push (i.assignee ? userMapper (i.assignee.name) : "shaakunthala");
    // ^^ Handy if target project doesn't allow unassigned issues.
    // It maps user accounts on-the-fly, 
    // while assigning all unassigned issue to a default user account.
  • Linking parent and sub-task on-the-fly. The script will do what’s asked in the Atlassian’s official documentation. After fetching issues, ParentIDs will be added using VLOOKUP spreadsheet formula applied by the script.
    var issueid_col = sheet.getLastColumn();
    var newCol = issueid_col + 1;
    var parentkey_cell1 = sheet.getRange(2, issueid_col - 1, 1, 1).getA1Notation();
    var formula ="=IFERROR(VLOOKUP(" + parentkey_cell1 + "," + issuesRange.getA1Notation().replace (/[0-9]/g, "") + "," + issueid_col + ",FALSE), \"\")";
    sheet.getRange(1, newCol, 1, 1).setValue("Parent ID");
    sheet.getRange(2, newCol, 1, 1).setFormula(formula)
         .copyTo(sheet.getRange(3, newCol, sheet.getLastRow() - 2, 1));
  • Mapping additional custom fields to labels. This is useful when you want to avoid creating any custom fields.
    if (i.fields.customfield_30000 != null)
      labels_array.push (i.fields.customfield_30000.value);
  • Comments, Affects Versions, Fix Versions, Labels, etc. can be fetched into different sheets of the same spreadsheet to be imported separately. The reason for this is to avoid large and much complicated CSV files that aren’t much human-readable.
    • In this case, still, target project doesn’t have to be empty. If target project is different, relationship between multiple CSV imports can be handled with issue key inserted to a text custom field as a reference ID.

So, here’s the pros and cons.

Pros:

  1. What’s being transferred over are in human-readable format in the spreadsheet. It may be useful reference. This gives you a better way to verify any mapping and additional labeling you have done.
  2. Google Spreadsheets will take care of the CSV syntax and escaping. You don’t have to worry a thing about CSV errors. Just code what needs to sit in each column.
  3. Less time consuming compared to XML import, but brings over more data compared to minimal CSV import.

Cons:

  1. Limitations of Google Spreadsheets: Google will terminate the script if it runs for more than 6 minutes. This can happen with larger projects. In this case you can use a Python script instead. But you’ll need to write additional code to map parents and sub-tasks.

Reference: 

Official documentation by Atlassian: https://confluence.atlassian.com/adminjiraserver071/importing-data-from-csv-802592885.html

Notes:

There could be third-party add-ons that can perform hassle-free project migrations. If you can purchase such an add-on, this kind of approaches may not be necessary.

There is a method to import JSON instead of CSV. This syntax appears to be different from the issue object returned by the REST API. Therefore you’ll still need a custom script to perform JSON import.

[JIRA] Broken Permission Schemes?

Edit (24 May): Spoiler alert! There’s a better way.

I came across a corrupted JIRA database with all projects lost their permission schemes.

From the database, I believe this happens when corresponding records were deleted from the NODEASSOCIATION table. Whatever the cause is, you’ll notice the following log entry in atlassian-jira.log when this happens:

2017-05-23 02:57:18,363 WARN [permission.DefaultPermissionSchemeManager]: No permission scheme is associated with project ''

The problem is, (in JIRA 7.1.x) when you navigate to Project Administration –> Permissions to associate it with a scheme, you’ll see an empty page. For a newly created project Default Permission Scheme should show up, but now it absolutely has no permission scheme. It’s lost.

So, here’s the expensive but easy solution. Use the Script Runner add-on. Execute this Groovy code in the script console and it will fix everything in a minute!

import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.ComponentManager;
import com.atlassian.jira.bc.project.ProjectService;
import com.atlassian.jira.bc.project.ProjectService.UpdateProjectSchemesValidationResult;

def projectService = ComponentManager.getComponent(ProjectService.class);
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser();

Long permissionSchemeId = 10000; // ID of the permission scheme that you wish to associate to each project
Long notificationSchemeId = null;
Long issueSecuritySchemeId = null;
ProjectService.UpdateProjectSchemesValidationResult validationResult = projectService.validateUpdateProjectSchemes(currentUser, permissionSchemeId, notificationSchemeId, issueSecuritySchemeId);

ComponentAccessor.getProjectManager().getProjectObjects().each {
  projectService.updateProjectSchemes(validationResult, it);
}

As it can be understood from the code, you can fix Notification Schemes and Issue Security Schemes using the same way.