Jira: Epics, Sub-Epics, and Story Points

I love Jira, probably more than I should, but the difficulty in tracking progress and breaking down work to mirror my teams kills me. The new Greenhopper 6.2 and the “edit in place” fields made a big difference, but I still needed a couple of things:

  • A way to groups a bunch of Epics together into a larger trackable Epic
  • A way to track Total Story Points/Open Story points for my Epics

With this, I could get automated Gantt charts from my system that kept my corporate overlords happy, and didn’t bog myself or my team with extra tracking.

I used the jPlugs Gantt chart plugin. While not free, it was affordable, flexible, and did what I needed. Best of all, it also has a Confluence plugin, so my project status pages could be updated via Jira.

Next, I needed to make some custom fields, so I installed JIRA Misc Custom Field. This free swiss army knife of Jira custom fields makes up for tricky implementation by super-flexibility and a dedicated following of users who post all sorts of tips and howtos, which you will need because the documentation is sparse.

Next I created a Calculated Number Field, using the following Code:

The sum of story points for children tasks
<!-- @@Formula: 
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.issue.CustomFieldManager;
import com.atlassian.jira.issue.link.IssueLink;
import com.atlassian.jira.issue.link.IssueLinkManager;
import com.atlassian.jira.issue.issuetype.IssueType;
import java.util.Arrays;
import java.util.ArrayList;
import java.lang.Double;
import java.util.List;

// SWAP THIS WITH THE FIELD WHERE YOU STORE STORY POINTS
private static long STORY_POINT_FIELD = 10007;

private static String STORY_NAME = "Story";
private static String EPIC_NAME = "Epic";

// I ONLY WANT TO INCLUDE CHILDREN THAT ARE STORIES
// OR ARE DEPENDENT EPICS
private static String[] LEGAL_SUB_EPIC_LINKS = {"Dependency", "Epic-Story Link"};

// TRACK ALL THE EPICS CHECKED TO AVOID FOLLOWING CIRCULAR REFS
private List checkedEpics = new ArrayList();

/**
@return the number of story points for a story
**/
int getStoryPointsForStory(Issue theIssue) {
 int rv = 0;
CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager();
 CustomField storyCF = customFieldManager.getCustomFieldObject(STORY_POINT_FIELD);
if(theIssue.getCustomFieldValue(storyCF) != null) {
 try {
 rv = Double.valueOf(theIssue.getCustomFieldValue(storyCF)).intValue();
 } catch (Exception e) {
 }
 }
return rv;
}

/**
@return the sum of story points for the given issue
**/
int sumChildrenStoryPoints(Issue theIssue) { int rv = 0; String issueTypeName = theIssue.getIssueTypeObject().getName();

 // IF THE ISSUE IS A STORY, RETURN THE STORY POINTS
 if (STORY_NAME.equals(issueTypeName)) {
 rv = getStoryPointsForStory(theIssue);
// BAIL EARLY ON NON-EPICS
 } else if (!EPIC_NAME.equals(issueTypeName)) {
 rv = 0;
// IF IT IS AN EPIC, MAKE SURE WE HAVE NOT CHECK THIS EPIC BEFORE
 } else if (!checkedEpics.contains(theIssue.getId())){
// REMEMBER TO AVOID CIRCULAR DEPENDENCIES
 checkedEpics.add(theIssue.getId());

 IssueLinkManager issueLinkManager = ComponentAccessor.getIssueLinkManager();
 List legalLinks = Arrays.asList(LEGAL_SUB_EPIC_LINKS);
for(IssueLink link : issueLinkManager.getOutwardLinks(theIssue.getId())) {
if(legalLinks.contains(link.getIssueLinkType().getName())) {
 rv = rv + sumChildrenStoryPoints(link.getDestinationObject());
 }
 }
} 

 return rv;
}
sumChildrenStoryPoints(issue.getIssueObject());
 -->
<!-- @@Format: numberTool.format(value) -->

To get a sum of the open points, I copied and pasted ( know…lame) a second calculated number field with the same code. I added a filter to the function getStoryPointsForStory:

// BAIL EARLY WITH 0 FOR CLOSED STORIES
List closedStatuses = Arrays.asList({"Closed", "Resolved"});
if (closedStatuses.contains(theIssue.getStatusObject().getName())) {
return 0;
}

This returns them with zero points against the open total. Finally, I made a progress field as anotherĀ calculated number field with:

Progress as a percentage of closed story points/total story points
<!--
customfield_10336 = sum of open children story points
customfield_10344 = sum of all children story points
-->
<!-- @@Formula: 
(
((issue.get("customfield_10334") != null && issue.get("customfield_10336") != null)? ((issue.get("customfield_10334") - issue.get("customfield_10336"))/issue.get("customfield_10334")*100) : 0)
).intValue()
 -->
<!-- @@Format: numberTool.format(value) -->

You will have to update the customfield numbers for your system. No I set my jPlugs progress to track this final custom field. As people close their stories, I watch my Gantt chart update.

2 Comments Add yours

  1. Andreas says:

    Very cool – it works perfectly! But I have an issue withe the Filter for the Bail out of Closed Issues… can’t get it work.

    where to I have to put that code exactly?

    1. helloscriptkitty says:

      I put it at the beginning of the function sumChildrenStoryPoints. It should see that the story is in a closed state and return a 0 immediately.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s