Creating daily status updates email from JIRA

Context

My client has always preferred reading an email at the start of his day that summarizes all the work his dev team was up to the night before. We call it the status mail. It usually contains the list of tasks we have worked on, containing its full description along with our comments/status on that day. Such reports are favorites among clients who are non-technical or who cannot afford to spend time in JIRA, as they get all updates in one go. But, of course, this is beneficial only for a team size of 5-20 members. Any more than that, the mail itself becomes too long to read.

Background

We adopted JIRA only recently, slightly more than a year ago. During the pre-JIRA dark ages, my predecessors had to perform a laborious task of compiling status mails from tasks, bugs and feature requests sent by the clients over emails. It used to easily take up 30-60 mins on a rainy day, copy pasting updates provided by the team, then mix-matching the formats to make it look good, etc. Moreover, it was too much manual labor to love this part of the job.

Then came JIRA, and life became much more efficient.

Creating Status Mail from JIRA

So, let us get down to the core part of this post. The steps to create a status mail are pretty straightforward. But before we proceed further, go through the checklist below that you may need (to know).

The Secret Ingredients List

  1. JQL (JIRA Query Language) to build your query
  2. JIRA API to fetch the query results programmatically
  3. An application+ to fetch your results using JIRA API
  4. JavaScript and JQuery+ to process the result (final JSON)
  5. JQuery templates+ to build your email content (HTML) from your processed final JSON.

[+]: Choose language or tool of your choice.

JQL

First and foremost, you need to fetch only relevant issues that have been updated that day to be processed and put into the status mail. JQL (JIRA Query Language) is the best way to get that done. A typical example of the query I use is as follows.

project in ("My Project") AND comment ~ "\\[statuscomment\\]"
    AND updated > startOfDay() AND updated < endOfDay()
ORDER BY key DESC

NOTE

  • In our implementation, a comment that needs to go into the status mail must be prefixed with[statuscomment], so that only the relevant comments are grabbed when processing results (later below).
  • Of course, you can use any other keyword instead – just change it in the processing function. If you don’t want any “keyword processing” at all, such that all comments from today should show up, that’s fine by me. Just, again, make sure to change the processing function accordingly.
  • You may save the JQL query as a JIRA Filter, so that you can reuse or reference it later.
  • The JQL above is not perfect, because it may fetch issues that have not been actually updated with a statuscomment on that day. A typical example is, if you added a status comment for an issue yesterday (and not today), but you did update it today (like marked it to done or edited some fields), the task still qualifies for the JQL above. This is because JQL does not support querying individual comments’ updated dates. However, this should not be an issue for us, as we will be filtering out such comments, as you will see later.

JIRA API

To fetch issues from JIRA in to your application, you need to use /search API. You can use either GETor POST method to do so. The JIRA documentation explains all necessary steps clearly, so I’m not covering that. However, here are the key parameters that you should add to fetch all necessary details in one go.

{
    "expand": "names,schema,renderedFields",
    "jql": "our JQL query mentioned above",
    "fields": "summary,description,comment"
}

Sample JQL result-set provided at the bottom. (See gist 03-jira-api-result-sample.json)

There are a few key things to note in this result set:

  • It brought only the fields mentioned in fields parameter above.
  • The startAt and maxResults properties are used for pagination. By default, JIRA API brings only 50 items per query. So, if you have more than 50 issues to process daily, you should tweak your API call accordingly.
  • There is a renderedFields property which contains the actual rendered HTML of the description fields, and not the JIRA wiki syntax as in descriptions in the original fields. The JIRA wiki syntax is useless for us.

Process Result

processStatus(data) function in below 01-jira-task-status-processor.js gist is our public API to building the status HTML. What it essentially does is:

  1. Process result set per issue-item per comment
  2. Check that the comment must begin with [statuscomment] – else do not process
  3. Check that date of comment must be after checkDate passed (checkDate defaults to today) – else do not process
  4. Strip out [statuscomment] tag from comment, as not required anymore
  5. Prepare an object containing issue title, description, filtered comments content, user details of comments, etc.
  6. (Freebie) Encode comments so that we can put it into email replies automatically when Repy >button is clicked. (See further sections below to understand what I’m talking about)
  7. Pass object to JQuery Templates (see 02-jira-status-template.html gist) that builds the final status mail HTML.

NOTE

  •  jiraAPIResponseProcessor() is just a wrapper function.
  • You may need to modify these gists for your use.
  • I will try to create an API/library for you in the near future, so that you do not have to deal with these boiler plate codes.

Final Output

Combined together with the JIRA API result-set, you may generate an HTML which finally looks like the following image. Of course, this is a very crude representation below without any styling whatsoever. So, please do stylize the output as you like, before sending it to your boss. 😉

status-emails-01-final-html.png

Here is the gist

function jiraAPIResponseProcessor(data) {
/**
* This is the API to be exposed which creates the status HTML from given `status` json.
* @param {json} status – Your json from JIRA API
* @param {string} checkDate – The date for which the status has to be created,
* like if you want to create status for comments from 2 days ago, etc.
* Format: yyyy-MM-dd. Default: today's date
* @return {string} Final status HTML
*/
function processStatus(status, checkDate) {
if (!status) return;
checkDate = checkDate ? new Date(checkDate) : new Date();
checkDate.setHours(0, 0, 0, 0);
//initialize final status object after processing `status` and cleaning it up
var finalStatus = {
issues: []
};
for (var i = 0; i < status.issues.length; i++) {
//loop through each JIRA issue that came up in my JQL
var processedIssueDetails = _processIssue(status.issues[i], checkDate);
if (processedIssueDetails) finalStatus.issues.push(processedIssueDetails);
}
//generate HTML for all processed JIRA issues, including issue description and comments
var statusDiv = $('<div></div>');
$('#jira-status').tmpl(finalStatus).appendTo(statusDiv);
return statusDiv.html();
}
function _processIssue(item, checkDate) {
//initialize and get basic details required for your status
var processedIssueDetails = {
title: item.fields.summary,
link: getMyIssueLink(item.key),
description: item.renderedFields.description,
comments: [] //array to accomodate multiple comments on same issue on the same day
};
for (var j = 0; j < item.renderedFields.comment.comments.length; j++) {
//loop through each JIRA issue's comments
var processedComment = _processIssueComment(item.fields.comment.comments[j], item.renderedFields.comment.comments[j], checkDate);
//add to comments if valid
if (processedComment) {
processedComment.mailToSubject = escape(processedIssueDetails.title);
processedIssueDetails.comments.push(processedComment);
}
}
//if no comments at all, do not send anything
return processedIssueDetails.comments.length ? processedIssueDetails : null;
}
function _processIssueComment(issueComment, renderedIssueComment, checkDate) {
//collect relevant issue properties together
var processedComment = {
html: renderedIssueComment.body,
author: issueComment.author && issueComment.author.displayName,
date: issueComment.created,
mailToSubject: '', //we'll fill this in parent function
mailToBody: ''
};
//your status prefix to be detected in comments
var regexStatus = /\&#91;statuscomment\&#93;/i;
//comments validations
if (!regexStatus.test(processedComment.html))
return null; //comment should start with "[statuscomment]"
var createdDate = new Date(processedComment.date);
if (createdDate < checkDate) return null; //comment should be of today
//regex to detect and remove the `statuscomment` to remove it from final HTML
var regexCommentCleanup = /<span class=\"error\">\&#91;statuscomment\&#93;<\/span>(<br\/>)?/ig;
//comment cleanup and add to data
processedComment.html = processedComment.html.replace(regexCommentCleanup, '');
processedComment.date = ''; //not required anymore
//additional reply-to feature for quick reply to issues in status mail
processedComment.mailToBody = $(processedComment.html).text();
if (processedComment.mailToBody) {
processedComment.mailToBody = ' \n\n____________________\n\n'
+ processedComment.author + ' comment: '
+ processedComment.mailToBody;
processedComment.mailToBody = escape(processedComment.mailToBody);
}
return processedComment;
}
function getMyIssueLink(key) {
return "https://myproject.atlassian.cloud/browse/&quot; + key;
}
function htmlEncode(value) {
// Explanation: Create an in-memory div, set it's inner text (which jQuery automatically encodes)
// then grab the encoded contents back out. The div never exists on the page.
return $('<div/>').text(value).html();
}
//run your code and get your final processed status html
return processStatus(data, "2016-02-11"));
//NOTE: date parameter is optional. Above date is as per `03-jira-api-result-sample.json`,
//so that when you run the script, it builds you _that day's_ status HTML, accordingly.
}

<script id="jira-comments" type="x-jquery-tmpl">
<table border="0" cellpadding="1" cellspacing="1" class="issue-article" style="width: 100%;">
<tr>
<td>{{html description}}</td>
</tr>
{{each comments}}
<tr>
<td class="comment">
{{tmpl($value) "#jira-comment"}}
</td>
</tr>
{{/each}}
</table>
</script>
<script id="jira-comment" type="x-jquery-tmpl">
<table class="comment-head" width="100%">
<tr>
<td align="left"><span class="title" style="font-weight: 600; float:left;">Comment:</span></td>
<td align="right">
<span class="author"> (By ${author})</span>&nbsp;
<a class="reply-to" href="mailto:[ReplyToEmail]?Subject=RE:%20${mailToSubject}&Body=${mailToBody}">Reply &gt;</a>
</td>
</tr>
</table>
<div>{{html html}}</div>
</script>
<script id="jira-status" type="x-jquery-tmpl">
<html>
<body>
{{each issues}}
<a href="${link}">${title}</a><br />
{{tmpl($value) "#jira-comments"}}
{{/each}}
</body>
</html>
</script>

{
"expand": "schema,names",
"startAt": 0,
"maxResults": 50,
"total": 1,
"issues": [{
"expand": "operations,versionedRepresentations,editmeta,changelog,transitions,renderedFields",
"id": "22116",
"self": "https://myproject.atlassian.cloud/rest/api/latest/issue/22116",
"key": "PROJ-1340",
"fields": {
"summary": "Reports – Drip Reports Oddity – Super watch",
"description": "*Task one* This is the task description of our drip reports oddity super watch issue.",
"comment": {
"startAt": 0,
"maxResults": 1,
"total": 1,
"comments": [{
"self": "https://myproject.atlassian.cloud/rest/api/2/issue/22116/comment/22645",
"id": "22645",
"author": {
"self": "https://myproject.atlassian.cloud/rest/api/2/user?username=Krishnan",
"name": "Krishnan",
"key": "krishnan",
"emailAddress": "krishnan@myproject.com",
"displayName": "Krishnan Mudaliar",
"active": true,
"timeZone": "Asia/Kolkata"
},
"body": "[statuscomment]\r\nHello, this is my first status comment today, but second overall",
"created": "2016-02-19T16:40:42.177+0530"
}, {
"self": "https://myproject.atlassian.cloud/rest/api/2/issue/22116/comment/22645",
"id": "22640",
"author": {
"self": "https://myproject.atlassian.cloud/rest/api/2/user?username=Krishnan",
"name": "Krishnan",
"key": "krishnan",
"emailAddress": "krishnan@myproject.com",
"displayName": "Krishnan Mudaliar",
"active": true,
"timeZone": "Asia/Kolkata"
},
"body": "[statuscomment]\r\nHello, this is my _first status comment_ *ever*.",
"created": "2016-02-18T16:40:42.177+0530"
}]
}
},
"renderedFields": {
"summary": null,
"description": "<strong>Task one</strong> This is the task description of our drip reports oddity super watch issue.",
"comment": {
"startAt": 0,
"maxResults": 1,
"total": 1,
"comments": [{
"self": "https://myproject.atlassian.cloud/rest/api/2/issue/22116/comment/22645",
"id": "22645",
"author": {
"self": "https://myproject.atlassian.cloud/rest/api/2/user?username=Krishnan",
"name": "Krishnan",
"key": "krishnan",
"emailAddress": "krishnan@myproject.com",
"displayName": "Krishnan Mudaliar",
"active": true,
"timeZone": "Asia/Kolkata"
},
"body": "<p><span class=\"error\">&#91;statuscomment&#93;</span></p>\n\n<p>Hi, this is my first status comment today, but second overall.</p>",
"created": "2016-02-19T16:40:42.177+0530"
}, {
"self": "https://myproject.atlassian.cloud/rest/api/2/issue/22116/comment/22645",
"id": "22640",
"author": {
"self": "https://myproject.atlassian.cloud/rest/api/2/user?username=Krishnan",
"name": "Krishnan",
"key": "krishnan",
"emailAddress": "krishnan@myproject.com",
"displayName": "Krishnan Mudaliar",
"active": true,
"timeZone": "Asia/Kolkata"
},
"body": "<p><span class=\"error\">&#91;statuscomment&#93;</span></p>\n\n<p>Hi, and this is my <i>first status comment</i> <b>ever</b>.</p>",
"created": "2016-02-18T16:40:42.177+0530"
}]
}
}
}],
"names": {
"summary": "Summary",
"description": "Description",
"comment": "Comment"
},
"schema": {
"summary": {
"type": "string",
"system": "summary"
},
"description": {
"type": "string",
"system": "description"
},
"comment": {
"type": "comments-page",
"system": "comment"
}
}
}

<script src="http://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script>
<script type="text/javascript">
//NOTE: jiraAPIResponse is your JIRA API Result-set, like `03-jira-api-result-sample.json`
var finalHTML = jiraAPIResponseProcessor(jiraAPIResponse);
$('#div-final-html').html(finalHTML); //this fills the div shown below
</script>
<div id="div-final-html">
<a href="https://myproject.atlassian.cloud/browse/PROJ-1340">Reports – Drip Reports Oddity – LandWatch</a><br>
<table border="1" cellpadding="1" cellspacing="1" class="issue-article" style="width: 100%;">
<tbody>
<tr>
<td><strong>Task one</strong> This is your task description</td>
</tr>
<tr>
<td class="comment">
<table class="comment-head" width="100%">
<tbody>
<tr>
<td align="left"><span class="title" style="font-weight: 600; float:left;">Comment:</span></td>
<td align="right"> <span class="author"> (By Krishnan Mudaliar)</span>&nbsp; <a class="reply-to" href="mailto:[ReplyToEmail]?Subject=RE:%20Reports%20-%20Drip%20Reports%20Oddity%20-%20LandWatch&amp;Body=%20%0A%0A____________________%0A%0AKrishnan%20Mudaliar%20comment%3A%20%0A%0AHi%2C%20this%20is%20my%20first%20status%20comment%20today%2C%20but%20second%20overall.">Reply &gt;</a> </td>
</tr>
</tbody>
</table>
<div>
<p></p>
<p>Hi, this is my first status comment today, but second overall.</p>
</div>
</td>
</tr>
<tr>
<td class="comment">
<table class="comment-head" width="100%">
<tbody>
<tr>
<td align="left"><span class="title" style="font-weight: 600; float:left;">Comment:</span></td>
<td align="right"> <span class="author"> (By Krishnan Mudaliar)</span>&nbsp; <a class="reply-to" href="mailto:[ReplyToEmail]?Subject=RE:%20Reports%20-%20Drip%20Reports%20Oddity%20-%20LandWatch&amp;Body=%20%0A%0A____________________%0A%0AKrishnan%20Mudaliar%20comment%3A%20%0A%0AHi%2C%20and%20this%20is%20my%20first%20status%20comment%20ever.">Reply &gt;</a> </td>
</tr>
</tbody>
</table>
<div>
<p></p>
<p>Hi, and this is my <i>first status comment</i> <b>ever</b>.</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>

2 thoughts on “Creating daily status updates email from JIRA

  1. Amit June 14, 2016 / 5:09 pm

    Have you tried activity stream?

    Like

  2. Nilesh Padwal June 21, 2016 / 9:42 am

    Great hacks. Useful information.

    Like

Leave a Reply to Amit Cancel 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