How can I access the list of GH Sprints programmatically (from workflow post-functions)

David _old account_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
April 19, 2012

I'm trying to access the list of GreenHopper Sprints, with associated metadata (name and dates), from workflow post-functions. Can it be done?

What I'm trying to do is simple: automatically set the GH Sprint field when resolving an issue that was not planned (i.e. included in a sprint).

Thanks in advance,

David.

6 answers

1 accepted

5 votes
Answer accepted
David _old account_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
April 24, 2012

All right, I've implemented the solution. I thought I'd share it with the rest of the community:

/**
 *
 */

import org.apache.commons.httpclient.HttpClient
import org.apache.commons.httpclient.methods.GetMethod
import org.apache.commons.httpclient.HttpException
import org.apache.commons.httpclient.HttpStatus
import groovy.json.JsonSlurper
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.util.ImportUtils
import com.atlassian.jira.issue.util.IssueChangeHolder
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.index.IssueIndexManager
import com.atlassian.jira.issue.customfields.view.CustomFieldParams
import com.atlassian.jira.issue.customfields.view.CustomFieldParamsImpl
import com.atlassian.jira.issue.customfields.CustomFieldType
import org.apache.log4j.Category
import com.atlassian.jira.issue.history.ChangeItemBean


//create a class to wrap our post-function, so that it can be debugged in a Groovy debugger
class AddCurrentSprintPostFunctionClass {
  private static Category log = Category.getInstance("com.polyspot.AddCurrentSprintPostFunction")

  static void execute(Map transientVars, Issue issue, IssueChangeHolder changeHolder) {
    ComponentManager componentManager = ComponentManager.getInstance()
    CustomFieldManager customFieldManager = componentManager.getCustomFieldManager()
    IssueIndexManager indexManager = ComponentManager.getInstance().getIndexManager()

    //get the Sprints the isssue is already in
    CustomField cfSource = customFieldManager.getCustomFieldObject("customfield_11200")  //Sprint field id - may need to be adjusted for each Jira instance
    CustomFieldType cfType = cfSource.getCustomFieldType();
    Object sprintsOfIssue = issue.getCustomFieldValue(cfSource)

    // Create an instance of HttpClient.
    HttpClient client = new HttpClient();

    // Create a method instance.
    //We must go through the public URL (not localhost) otherwise, although it works, it generates Crowd WARNings in the logs
    GetMethod method = new GetMethod("https://<URL>/rest/greenhopper/1.0/sprints/14?os_username=<username>&os_password=<password>");

    try {
      // Execute the method.
      int statusCode = client.executeMethod(method);

      if (statusCode != HttpStatus.SC_OK) {
        log.error("REST call failed: " + method.getStatusLine());
        return;
      }

      //parse the result into a "struct"
      JsonSlurper slurp = new JsonSlurper();
      Reader reader = new InputStreamReader(method.getResponseBodyAsStream());
      def result = slurp.parse(reader);

      //look for the (only) active sprint
      for (Object sprint : result.sprints)
        if (!sprint.closed) {
          //we've found the current sprint, add it to the issue
          def sprintSet = [] as Set
          if (sprintsOfIssue != null)
            sprintSet = sprintsOfIssue.collect {it.id}
          sprintSet.add(sprint.id as String)
          def value = sprintSet.join(",")

          //now set the issue field value
          boolean wasIndexing = ImportUtils.isIndexIssues();
          ImportUtils.setIndexIssues(true);

          try {
            //convert from string to Object
            CustomFieldParams fieldParams = new CustomFieldParamsImpl(cfSource, value);
            value = cfType.getValueFromCustomFieldParams(fieldParams);
            cfSource.updateValue(null, issue, new ModifiedValue(issue.getCustomFieldValue(cfSource), value), changeHolder)
          }
          finally {
            indexManager.reIndex(issue);
            ImportUtils.setIndexIssues(wasIndexing);
          }
        }

    } catch (HttpException e) {
      log.error("Fatal protocol violation: " + e.getMessage());
    } catch (IOException e) {
      log.error("Fatal transport error: " + e.getMessage());
    } catch (Exception e) {
      log.error("Error during post-function execution: " + e.getMessage());
    } finally {
      // Release the connection.
      method.releaseConnection();
    }
  }

  //wrapper call to manage history logging
  static void executeWithHistory(Map transientVars, Issue issue) {
    IssueChangeHolder holder = createChangeHolder(transientVars);

    try {
      execute(transientVars, issue, holder);
    } finally {
      releaseChangeHolder(holder, transientVars);
    }

  }

  /**
   * Create new holder with changes from transient vars
   */
  private static IssueChangeHolder createChangeHolder(Map<String, Object> transientVars) {
    List<ChangeItemBean> changeItems = (List<ChangeItemBean>) transientVars.get(CHANGE_ITEMS);

    if (changeItems == null) {
      changeItems = new LinkedList<ChangeItemBean>();
    }

    if (log.isDebugEnabled()) {
      log.debug("Create new holder with items - " + changeItems.toString());
    }

    IssueChangeHolder holder = new DefaultIssueChangeHolder();

    holder.setChangeItems(changeItems);

    return holder;
  }

  /**
   * Release change holder
   */
  private static void releaseChangeHolder(IssueChangeHolder holder, Map<String, Object> transientVars) {
    List<ChangeItemBean> items = holder.getChangeItems();

    if (log.isDebugEnabled()) {
      log.debug("Release holder with items - " + items.toString());
    }

    transientVars.put(CHANGE_ITEMS, items);
  }

  private static final String CHANGE_ITEMS = "changeItems";
};


//call our static method
AddCurrentSprintPostFunctionClass.executeWithHistory(transientVars,issue)

0 votes
David _old account_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
April 21, 2012

Thanks Renjith for the idea.

But how do I consume Jira's REST services from within a Jira plugin - more precisely a workflow function? More specifically, how do I find out about Jira's REST endpoint, and more importantly, how do I handle authentication since I'm already within Jira server-side code?

0 votes
sclowes
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
April 21, 2012

Just to clarify, the Rapid Board allows many Sprints to be active at once, but will not allow Plan mode to be used to start another when one is already active on Work mode.

That said, Renjith is totally right, this REST end point will list all of the Sprints that are active on the work mode of the specified Rapid Board.

0 votes
Renjith Pillai
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
April 21, 2012

Rapidboard allows only one active sprint at a time and the details can be fetched used REST. See a sample url below

http://localhost:8080/rest/greenhopper/1.0/sprints/4

The '4' in the url is the Rapid View Id.

This returns a list of sprints and the last object in the list will indicate the active sprint ("closed":false)

0 votes
David _old account_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
April 20, 2012

I'm talking about the RapidBoard Sprints. But what I'm trying to do is get the list of active Sprints in order to set the Sprint custom field automatically.

0 votes
Renjith Pillai
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
April 20, 2012

You mean rapid board of the regular Greenhopper? If rapidboard this is a custom field.

if it is a regular one it is the fixVersion field.

Suggest an answer

Log in or Sign up to answer