
こんにちは、カミナシPMのGTOです。
みなさんは開発の動きはどのように確認していますか?
弊社では、PMチームが作業状況をJira上のコメントを適宜確認し、動きがないか人力ウォッチしていました。
これでは、チーム間でコミュニケーションのミスは大小限らず各所で発生しますし、何よりも非効率的ですよね。
さらにコミュニケーションのミスは関係者間で認識を再度揃え直すのにもコストがかかるし、そもそもあの人がやったやらないの議論って本質的じゃない。なんとか出来ないかと頭を悩ませていました。
この問題を解決する解決策として、GASとSlack Botを使うといい感じにできそうだったので、今回作ってみました!
作ったツール
以下のスクショのように、作業完了したJiraチケットをEpic単位でまとめてSlackへ通知してくれるBotを作成しました。(🌾は自分が所属するチームのチームアイコンです)

今回はこのツールの作り方について紹介します!
作り方サマリ
以下3つの手順を踏むことで、Jira上のチケットの情報を元にSlackへ通知することが出来ます。
- 作業完了チケットを表示するシートを作成
- プロジェクト内にあるJiraチケットを一括で取得
- GASを使って2で取得したチケット情報をSlackに通知
それぞれ順を追って紹介していきます。
1. 作業完了チケットを表示するシートを作成

作業完了したチケットを表示するためのシートを作成します。このシートには主に3つの区分があります。
- 通知対象チケットの表示欄
- チケットのうち「通知対象の絞り込み欄」で指定した条件で絞り込んで表示する欄
- 通知対象の絞り込み欄
- 通知対象のEpic Name、通知対象のEpic ID、開始日、終了日で絞り込みを行う欄
- 各種事前設定シート欄
- 1,2の欄内に情報を表示する項目の関数内で利用するために必要な設定群をまとめたシート欄
作業完了したチケットはシート上で管理することで、通知対象のチケットも一覧性高く確認出来るような設計にしています。
今回のシートのサンプルを以下に載せたので、こちらからダウンロードしてお使いください。
このシートのうち、「JIRA Master」のシートにはプロジェクト内にあるJiraチケット群を一括で取得する必要があります。
2. Jiraプロジェクト内にあるチケットを一括で取得
今回Google Workspace内にある以下の拡張機能を用いて、Jiraチケット群を一括で取得していきます。
こちらの拡張機能を入れることにより、Jira上のチケットを定期的に処理を走らせデータを最新の状態にすることが出来ます。
対象のJiraチケットを読み込むには以下3つの設定を加える必要があります。
1/ Jira上の取り込み対象のプロジェクトの設定

メニュー内のISSUES > Projectsから、チケット群を一括で取得したい対象のプロジェクトを選択します。
この時のプロジェクトは複数選択することも可能なので、プロジェクト横断的に関わっている方でも設定するプロジェクトを追加することで他プロジェクトのチケット群を一括で取得出来ます。
2/ チケットの取り込み列の指定

メニュー内のFIELDSから、取り込みを行う必要のある項目を設定します。
今回はチケットの完了時間を利用して通知を飛ばす仕組みを実装する予定です。
そこで今回は作業完了時間を表す項目である「Resolved」を取り込み対象の項目として設定しています。
また、Slackへ通知時にチケットと共に通知を行いたい項目もこのタイミングで一緒に選択しておきます。
3/ 取り込みを行う頻度の設定

メニュー内のSCHEDULEから、データ取り込みの頻度を設定します。
データ取り込みのタイミングは、作業完了するタイミングと合わせるために、夜遅くの時間を設定しています。
上記3つの手順でJiraのチケットを定期的にアップデートするための仕組みを作ります。 最後にGASの実装を紹介します。
3. GASを使って通知対象のチケット情報をSlackに通知
最後にGASを使ってチケットの情報をSlackへ通知していきます。 以下コードを自由にご利用ください。
// slackのIncoming-Webhook URLの取得。取得方法は以下を参照。
// https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8
const postUrl = 'SLACK_WEB_HOOK_URL';
// Jiraへのリンクを作成するためのHTTP URLを作成。((DOMAIN_NAME))の欄は自社用に書き換えてご利用ください。
const jiraBaseURL = "https://((DOMAIN_NAME))/browse/";
const username = 'チケットの完了逃さないマン';
const icon = ':surfer:';
let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("通知用シート");
/**
スプレットシートから受け取った情報から、以下のオブジェクトに加工しチケットの情報を取得。
{
epicID: {
epicName: EPIC_NAME,
tickets: [
{
id: TICKET_ID,
summary: TICKET_SUMMARY,
assignee: TICKET_ASSIGNEE,
status: TICKET_STATUS,
}
]
}
}
*/
function getTicketInfos() {
let ticketInfos = {};
const fValues = sheet.getRange('F:F').getValues();
const lastRow = fValues.filter(String).length+1;
const rangeValues = sheet.getRange(`A3:G${lastRow}`).getValues();
rangeValues.forEach((rangeValue) => {
if (rangeValue[3] !== "" && rangeValue[3] !== "Epic Link") {
const ticket = {id: rangeValue[0], summary: rangeValue[1], assignee: rangeValue[2], status: rangeValue[4]};
if (ticketInfos[rangeValue[3]] === undefined) {
ticketInfos[rangeValue[3]] = { epicName: rangeValue[6], tickets: [ticket] }
} else {
ticketInfos[rangeValue[3]].tickets.push(ticket);
}
}
})
return ticketInfos;
}
// 通知用のメッセージを取得。Epic単位でメッセージをグルーピング。
function getMessages() {
const ticketInfos = getTicketInfos();
let messages = [];
Object.keys(ticketInfos).forEach((key) => {
let value = "";
ticketInfos[key].tickets.forEach((ticket) => {
value = `:ticket: <${jiraBaseURL}${ticket.id}|: ${ticket.summary}> :people_holding_hands: ${ticket.assignee} \n ${value}`
})
let message = {
color: "#27B6F2", // 縦線の色
title: ticketInfos[key].epicName,
fields: [{
value: value,
short: false
}],
};
messages.push(message);
});
return messages;
}
// 通知を実行
function notice() {
const today = new Date();
if (isWorkday (today) === false) return;
const messages = getMessages();
const workDate = Utilities.formatDate(sheet.getRange(3,11).getValue(), 'Asia/Tokyo', 'yyyy年MM月dd日');
const jsonData = messages.length > 0 ?
{
username: username,
icon_emoji: icon,
text: `${workDate}にチーム内で以下のチケットを完了にしたよ〜`,
attachments: messages
} :
{
username: username,
icon_emoji: icon,
text: `${workDate}の完了チケットは0件だったよ`
};
const payload = JSON.stringify(jsonData);
const options =
{
"method" : "post",
"contentType" : "application/json",
"payload" : payload,
};
UrlFetchApp.fetch(postUrl, options);
}
// 祝日判定を行う。以下記事を参考に作成させていただきました。
// https://dev.classmethod.jp/articles/202001-workday-only-gas/
function isWorkday(targetDate) {
const rest_or_work = ["REST","mon","tue","wed","thu","fri","REST"];
if ( rest_or_work [targetDate.getDay ()] === "REST" ) return false;
const calJpHolidayUrl = "ja.japanese#holiday@group.v.calendar.google.com";
const calJpHoliday = CalendarApp.getCalendarById (calJpHolidayUrl);
if (calJpHoliday.getEventsForDay (targetDate).length !== 0) return false;
return true;
}
実装は大きくは3つのメソッドで構成されており、
getTicketInfos(): 「通知対象チケットの表示欄」で表示されているチケット情報を加工getMessages(): 通知を行う際のメッセージを加工notice(): SlackへWeb Hook URLを用いてメッセージを通知
の上記3つの構成を用いて通知を飛ばしています。また、Slackへ定期的にデータを飛ばすに、以下3つの設定を加えています。
- SlackのWeb Hook URLの取得
- こちら記事を参考に、Slack上でIncoming Webhook URLを取得
- Incoming Webhook URL取得後、postUrlの値を書き換え
- JiraのURLを入力
- JiraのbaseURLに書き換え。独自のbase URLを設定している場合は、その設定に則り値を書き換えを行う。
- GAS側での定期実行の処理の設定を追加
- GAS上で 左メニュー > トリガー > 「トリガーを追加ボタン」から、以下写真のようなトリガーを作成し、保存

そうすると、以下のような通知をSlack上で飛ばすことができます🎉

運用してみての感想
早速上記ツールを今PMチーム内で導入してみて、1週間ほど運用してみて以下のようなメリットがありました。
- 開発チーム内でのチケットの動きが分かり作業の透明度が上がったが故に、「今このチケット終わってますかね?」のような不要なコミュニケーションが減る
- 1日毎に完了チケットを追いかけられるので、過去作業完了分の後追いのコストが減る
この記事の冒頭に話していた、コミュニケーションの齟齬の問題は解消しつつ、副次的な効果として開発の作業完了の履歴を残せるのが大きなメリットでした。
「開発検証環境版リリースノート」を1日ごとにみてる感覚 でここは使い勝手良いなと思いました。
開発が検証環境へアップロードした時のチケットがリアルタイムに把握出来るので、自分が関心が強い実装に先取りして検証を行い、開発とのコミュニケーションコストをグッと下げられるのが良い感じです!
まとめ
今回、開発の作業完了タイミングを漏れなくキャッチアップするためにGASを作りました。
リモートが前提としてチームを動かす際に、コミュニケーションロスによって問題が起こった時、その修復にはそれなりの労力とコストがかかってしまいます。
なるべく互いのチームが自律的に動き、お互いが最大速度で仕事を行うためにも、コミュニケーション齟齬が起こりやすいポイントには早めに対処をする必要があります。
こういったツールを駆使して、なるべく効率的に正しい情報を最速でキャッチアップ出来る状態にチームをしていきたいですね。
これからもチームが最大速度で動けるよう、いらない仕事はがつっとGASで消していきたい所存です!
最後に宣伝です!
開発生産性を爆上げしていきたい、価値あるプロダクト作りやっていきたいPM、UXD、PD、Engineerの方いましたら、全方位採用しているので、是非以下のリンクからポチッと応募よろしくお願いします〜
🔈カミナシは全方位的に募集しています🔈
カミナシPMのEntrance Bookはこちら
noiseless-fold-f68.notion.site
ではまた〜👋