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が過ぎているチケットだけ通知対象としましたが、必要な入力が抜けているなどさらに応用ができそうです。
今後も改善していきたいと思います。