ZAICOでは、Android・iOS・Rubyエンジニアを絶賛募集中です! 詳しくは、採用ページをご覧ください。
はじめに
こんにちは。ZAICO開発チームです。
先日弊社で行われた社内ハッカソンにて、定期的にJIRAのissueを集計してSlackに結果を通知する、という処理をGoogle App Script(GAS)で実装したので紹介したいと思います。
背景
弊社ではタスク管理ツールとしてJIRAを使っていて、締め切り日(due date)を入力するようにしていますが、時々入力を忘れてしまいます。そこで毎週水曜日、入力を忘れている人にSlackでメンションしてくれるスクリプトをGASで作りました。
できたもの
リンクをクリックするとJIRAのページが開き、start date/due dateが入力されていないチケットを一覧できます。

実装について
Google SpreadSheetにJIRAのユーザーIDとSlackのユーザーIDをセットにしたものを記入しておきます。
JIRAのIssueのAssigneeになっている人にSlackで通知するため、JIRAのユーザーとSlackのユーザーをセットにするためにこれらを使います。
このGoogle SpreadSheetで開いたGASで、下記のような実装をしました。
// notify_over_due_date_issues.gs
// due dateが過ぎているissueについてSlackでAssigneeに通知する
function notifyOverDueDateIssues() {
var issues = getOverDueDateIssues();
if (issues.length == 0) {
Logger.log("issueがありませんでした。");
exit;
}
// AssigneeごとにissueのKeyをまとめる
var keysByAssigneeId = extractIssueKeysByAssigneeId(issues);
// 通知内容を作成
var message = createMessageForOverDueDate(keysByAssigneeId);
if (message == "") {
Logger.log("通知対象がいませんでした。");
return;
}
// Slackに通知
postToChannel(message);
}
// due dateが過ぎているissueを取得する。
// @return [Array<Object>] Assigneeとissueのリストのペアになったリスト。
function getOverDueDateIssues() {
// JQLを作成
var jql = "(status = \"In Progress\" or status = ToDo) and assignee IS NOT Empty and duedate < now() and updated >= endOfMonth(-3)";
// JQLを実行
var issues = fetchByJql(jql);
return issues
}
// Slackに通知する内容をAssigneeとIssueのセットから生成する。
// @return [String]
function createMessageForOverDueDate(keysByAssigneeId) {
var message = "";
// 冒頭の文
message += "Due dateを過ぎているJIRAチケットがあります。Due dateの更新をお願いします🙇🏼\n";
// メンション対象の人数
var mentionedAssigneesCount = 0;
// アクティブなシートを取得
var sheet = SpreadsheetApp.getActiveSheet();
var db = new DB(sheet);
for (var assigneeId in keysByAssigneeId) {
// assigneeからslackIDに変換
var slackId = db.convertJiraUserIdToSlackId(assigneeId);
if (!slackId) {
continue;
}
// assigneeにメンション
message += "<@" + slackId + ">";
// issueのkeyからjqlのurlを作成
var url = createIssuesUrl(keysByAssigneeId[assigneeId]);
// 件数をurlリンクで貼り付け
message += " <" + url + "|" + keysByAssigneeId[assigneeId].length + "件お願いします。>\n";
mentionedAssigneesCount++;
}
if (mentionedAssigneesCount == 0) {
return "";
}
return message;
}
JIRAでissueを検索する際に使用したJQLは下記です。3ヶ月以上更新がないissueは取得しないようにしています。
(status = "In Progress" or status = ToDo) and assignee IS NOT Empty and duedate < now() and updated >= endOfMonth(-3)
ユーティリティメソッドを書いたファイルは下記になります。
// common.gs
var JIRA_USER_NAME = "hoge@example.com";
var API_TOKEN = PropertiesService.getScriptProperties().getProperty("JIRA_API_TOKEN")
var encCred = Utilities.base64Encode(JIRA_USER_NAME + ":" + API_TOKEN);
var WEBHOOK_URL_TO_SLACK = PropertiesService.getScriptProperties().getProperty("WEBHOOK_URL_TO_SLACK");
// 引数のissueを変換し、AssigneeのユーザーIDごとのIssueのKeyの配列を返す。
// @return [Obj] AssigneeのユーザーIDをキーとして、Issueのkeyの配列を格納した連想配列
function extractIssueKeysByAssigneeId(issues) {
var keysByAssigneeId = {}
for (var i = 0; i < issues.length; i++) {
var issue = issues[i];
var assignee = issue.fields.assignee;
if (!assignee){
continue;
}
var keys = keysByAssigneeId[assignee.accountId];
if (!keys) {
keysByAssigneeId[assignee.accountId] = [issue.key];
} else {
keys.push(issue.key);
}
}
return keysByAssigneeId;
}
// 渡されたissueのKey全てを表示するURLを生成する。
function createIssuesUrl(issues) {
var url = "https://hogehoge.atlassian.net/issues/?jql=";
for (var i = 0; i < issues.length; i++) {
var issue = issues[i];
if (i > 0) {
url += " or ";
}
url += "key = " + issue;
}
url = encodeURI(url);
return url;
}
// JIRA APIにアクセスしてJQLでissueを取得する。
function fetchByJql(jql) {
var baseURL = "https://hogehoge.atlassian.net/rest/api/3/search";
var url = encodeURI(baseURL + "?jql=" + jql + "&fields=*all");
var data = fetchFromJiraApi(url);
var issues = [];
for (var id in data["issues"]) {
if (data["issues"][id] && data["issues"][id].fields) {
issues.push(data["issues"][id])
}
}
return issues;
}
// JIRA APIにアクセスしてレスポンスを取得する。
function fetchFromJiraApi(url) {
var fetchArgs = {
contentType: "application/json",
headers: { "Authorization": "Basic " + encCred },
muteHttpExceptions: true
};
var httpResponse = UrlFetchApp.fetch(url, fetchArgs);
if (httpResponse) {
var rspns = httpResponse.getResponseCode();
switch (rspns) {
case 200:
var data = JSON.parse(httpResponse.getContentText());
return data;
case 404:
Logger.log("Response error, No item found");
exit();
default:
Logger.log("Error: " + data.errorMessages.join(","));
exit();
}
} else {
Logger.log("Jira Error", "Unable to make requests to Jira!");
exit();
}
}
// Slackのチャネルに投稿する
// @param [String] message メッセージ
function postToChannel(message) {
postToSlack(message, WEBHOOK_URL_TO_SLACK);
}
// SlackにメッセージをPOSTする。
// @param [String] message メッセージ
// @param [String] webhookUrl Webhookの送るURL
function postToSlack(message, webhookUrl) {
var data = { text: message };
var payload = JSON.stringify(data);
var options = {
method: "post",
contentType: "application/json",
payload: payload
};
UrlFetchApp.fetch(webhookUrl, options);
}
// シートの内容を扱いやすいクラスにしたもの
class DB {
constructor(sheet) {
this.rows = [];
this.sheet = sheet;
this.parseSheet();
}
parseSheet() {
this.rows = [];
for (var row = 2; row < 50; row++) {
const name = this.sheet.getRange(`B${row}`).getValue();
if (name == '') continue;
const rowData = {
row: row,
name: name,
jiraUserId: this.sheet.getRange(`B${row}`).getValue(),
slackId: this.sheet.getRange(`C${row}`).getValue()
};
this.rows.push(rowData);
}
}
// rowsをフィルタリングする
// @param [Function] filterFunc フィルタリングする関数。 `Array.filter` へ渡す。
// @return [Array<Map>] フィルタリングした結果
filter(filterFunc) {
if (typeof filterFunc !== 'function') {
throw new Error("filterには関数を指定してください。");
}
return this.rows.filter(filterFunc);
}
// jira User IDからSlack IDに変換する
// @return [String] Slack ID
convertJiraUserIdToSlackId(userId) {
var person = this.filter((r) => r.jiraUserId == userId)[0];
if (!person) {
return null;
}
return person.slackId;
}
}
JIRA APIにアクセスするためにはJIRAのユーザーIDとアクセストークンが必要です。取得方法はJIRAの公式ヘルプをご確認ください。
ユーザーIDとアクセストークンをコードに書き込んでもいいですが、今回はGASの「スクリプト プロパティ」に設定して、下記のようにコードから取得するようにしました。
PropertiesService.getScriptProperties().getProperty()
スクリプト プロパティは環境変数のようなもので、GASに設定することができます。
今回、GASのコードをgithubにアップロードしたかったので、JIRAのアクセストークンなどをコードに含めないために、スクリプト プロパティを使いました。
GASのコードをgithubにアップロードするためにGoogle Apps Script GitHub アシスタントを使いました。
メソッドgetOverDueDateIssuesを定期的に実行されるようGASで設定すれば、定期的にSlackに通知されるようになります。
最後に
今回はDue dateが過ぎているチケットだけ通知対象としましたが、必要な入力が抜けているなどさらに応用ができそうです。
今後も改善していきたいと思います。

