この記事は トレタ Advent Calendar 2019 の9日目です。
三度の飯より寿司が好き、QAエンジニアの林です。気付けばトレタに入社して半年経っていました。
今回は、入社1ヶ月目くらいのときに作って以来ちまちま改良しているTrelloのカード移動をSlackにメンション付きで通知するスクリプトについて書いていきます。
背景
社内の一部のチームではTrelloを使用し、ボードに以下のようなリストを作成してカードでタスクを管理しています。
よくあるカンバン方式ですね。
開発担当者が Doing
にあるタスクを実装し終わったらカードを QA
に移動し、QA担当者がテストを実施して再修正が必要な場合は BugFix
へ移動させて再び開発者ボールに、問題なければ Close
へ…という運用です。
リスト名 | ステータス |
---|---|
ToDo | 未着手 |
Doing | 実装中 |
BugFix | QA中に手戻りが発生し、修正中 |
Code Review | 実装が完了し、レビュー中 |
QA | QA実施中 |
Close | 完了 |
公式のTrelloアプリをSlackに追加することでSlackのチャンネルにカードの動きを通知することはできます (下記画像のような通知がチャンネルに投稿されます)。
しかし、これだけだと通知を見逃しがちです。
開発担当者からQA担当者へ、またはその逆へボールが渡されたことに気付かないとタスクが停滞してしまいます。
Slack上で「実装完了し、staging環境にデプロイしました」「QA実施し、不具合ありましたので再修正お願いします」など直接メンション付きでリマインドすれば良いかもしれませんが、カード移動のたびに毎回するのは手間ですし、それ自体も忘れる可能性があります。
どうせならその辺を自動化しちゃおう、と考えたのが発端です。
やりたいこと
- Trello上でQAリストにカードが移動されたらQA担当者にSlack上でメンション付き通知
- QA担当者は主に1名
- 仮に担当者が増えた場合も、そのうちの誰かがQA実施という形になるので全員に通知したい
- Trello上でBugFixリストにカードが移動されたら開発担当者にSlack上でメンション付き通知
- 開発担当者は複数存在し、各タスクにアサインされる
- そのタスクを担当している人物だけに通知したい
使ったもの
- Google Apps Script (以下GAS)
- Trello API
実装方法
まずはGASプロジェクトを新規に作成し、以下の順で code.gs
にガリガリ書いていきます。
各プロパティ (APIキーやボードIDなど) はスクリプトプロパティで扱うこととしました。
1. Incoming WebhookをSlackに追加し、Webhook URLを取得
このページ でアプリを検索し、Incoming Webhookを追加します。
追加後、 インテグレーションの設定
で各設定を実施します。
- チャンネルへの投稿: 通知をPOSTするチャンネルを選択
- Webhook URL: スクリプトで使用するのでコピーしておく
- 説明ラベル: 任意。アプリの用途など説明を書いておく
- 名前をカスタマイズ: 任意。Slack上で表示されるアプリのユーザー名になる
- アイコン: 任意。同じくアプリのアイコンになる
2. TrelloのAPIキー・トークンを取得
Trelloにログインしている状態でこちらにアクセスするとAPIキーが表示されます。
トークンは、同ページ内の トークン
リンクから遷移したページで 許可
を選択すると表示されます。
どちらもスクリプトで使用するのでコピーしておきます。
3. TrelloのボードID・リストIDの取得
以下を参考に、ボードIDとリストIDを取得します。
- ボード一覧取得API
- 一覧の中から監視対象とするボードのIDをコピーしておく
- リスト一覧取得API
- 一覧の中からQAリストとBugFixリストのIDをコピーしておく
各パラメータには以下を当てはめます。
id
- ボード一覧取得APIの場合: Trelloのユーザー名 (プロフィール画面から確認可能)
- リスト一覧取得APIの場合: ボード一覧取得APIで取得した、監視対象とするボードのID
key
: 2. で取得したAPIキーtoken
: 2. で取得したトークン
4. Callback URLの取得
GASプロジェクトの 公開 > ウェブアプリケーションとして導入...
を選択し、ダイアログを開きます。
以下の設定に変更し、公開
を選択するとURLが発行されるのでコピーしておきます。これがCallback URLとなります。
- アプリケーションにアクセスできるユーザー: 自分のみ
- プロジェクトバージョン: New
5. 各プロパティの設定
GASプロジェクトの ファイル > プロジェクトのプロパティ > スクリプトのプロパティ
で各プロパティを設定します。
今回は以下のプロパティを作成しました。
key | 内容 |
---|---|
CALLBACK_URL | 4. で発行されたCallback URL |
TRELLO_API_KEY | 2. で取得したAPIキー |
TRELLO_TOKEN | 2. で取得したトークン |
BOARD_ID | 3. で取得したボードID |
LIST_ID_QA | 3. で取得したリストID (QA) |
LIST_ID_BUGFIX | 3. で取得したリストID (BugFix) |
SLACK_WEBHOOK_URL | 1. で取得したWebhook URL |
6. Webhook登録処理の実装
WebhookAPI(POST)を使用してWebhookを登録します。callBackURL
はpayloadの中に詰め込みます。
また、5. で設定したプロパティ群は
PropertiesService.getScriptProperties().getProperties()
を使用して参照します。
function createWebhook(){ var scriptProp = PropertiesService.getScriptProperties().getProperties(); var trelloKey = scriptProp.TRELLO_API_KEY; var trelloToken = scriptProp.TRELLO_TOKEN; var callbackUrl = scriptProp.CALLBACK_URL; var boardId = scriptProp.BOARD_ID; var requestUrl = 'https://api.trello.com/1/tokens/' + trelloToken + '/webhooks/?key=' + trelloKey; var options = { 'method' : 'post', 'payload' : { 'description': 'Webhook of Trello', 'callbackURL': callbackUrl, 'idModel': boardId } } Logger.log(UrlFetchApp.fetch(requestUrl, options)); }
7. Webhookの登録
実行 > 関数を実行 > createWebhook
を選択し、6. で作成した関数を実行します。
ログに表示されたレスポンスが正常であればOKです。
8. 通知処理の実装
doPost()
Trelloのカードに対し何かしらのアクション (移動、作成、コメントなど) が行われた際に呼び出される doPost
関数を実装していきます。
まず、目的のアクション (QAリストまたはBugFixリストへのカード移動) 以外だった場合はすべてreturnします。
QA担当者は固定なのでそのままです。
開発担当者は getDevMember()
を呼び出してTrelloIDを取得し、それをkeyにして DEV_MEMBERS
からSlackIDを取得し、Slack上でメンションが飛ぶようにします。
ここでDEV_MEMBERS
でIDを紐付けしていないTrelloIDが返ってきた場合は、メンションの代わりに専用のメッセージを出力します。
なお、SlackIDは表示名 (@hogehoge
など) をそのまま埋め込んでもテキストとして表示されるだけで、メンションになりません。
こちらを参考にしてSlack上から メンバーID
をコピーする必要があります。
getDevMember()
カードオブジェクトにはアクションの履歴が保存されています。
1つのカードにつき1人だけオーナーを設定できるのであればここまでする必要はないのですが、Trelloはカードのメンバーを複数設定できる仕様のため、履歴から開発担当者 (の可能性が高いユーザー) を特定しています。
具体的には、フィルタを利用して履歴の中から「カードを移動した」というアクションのみを取得し、その中で一番最後にQAリストにカードを移動したユーザーを取得
というロジックを組んでいます。
(カードの移動を行うのは、今現在そのカードを担当している人物であることはほぼ確実であるため)
/* メイン処理 Trello上で何かしらの変更が発生した時に毎回実行され、特定の条件下でSlackへ通知する */ function doPost(e){ var contents = JSON.parse(e.postData.contents); var actionType = contents.action.type; var destinationList = contents.action.data.listAfter; // カードが目的のリストに移動された場合のみ通知し、それ以外の場合は終了 if(actionType !== 'updateCard' || !destinationList) { return; } var scriptProp = PropertiesService.getScriptProperties().getProperties(); var qaListId = scriptProp.LIST_ID_QA; var bugfixListId = scriptProp.LIST_ID_BUGFIX; var movedUser = contents.action.memberCreator.fullName; var mentions = ''; // key=TrelloID, value=SlackID // dev memberがjoinした際はここにIDを追加する var DEV_MEMBERS = { '5xxxxxxxxxxxxxxxxxxxxxxxxx': '<@UAAAAAAAA>', // @hogehoge '5yyyyyyyyyyyyyyyyyyyyyyyy': '<@UBBBBBBBB>', // @fugafuga '5zzzzzzzzzzzzzzzzzzzzzzzzz': '<@UCCCCCCCC>' // @piyopiyo }; // QA担当者のSlackID var QA_MEMBERS = '<@UDDDDDDDD>'; // @hayashi switch (destinationList.id) { case qaListId: mentions = QA_MEMBERS; break; case bugfixListId: var devMember = getDevMember(contents.action.data.card.id); // TrelloとSlackのIDがまだ紐付いていないユーザーが指定された場合に表示するメッセージ var msgUserNotFound = '@' + devMember.fullName + ' (TrelloIDとSlackIDを紐付けてください: TrelloID=' + devMember.id + ')\n'; mentions = !DEV_MEMBERS[devMember.id] ? msgUserNotFound : DEV_MEMBERS[devMember.id]; break; default: return; } var postUrl = scriptProp.SLACK_WEBHOOK_URL; var cardName = contents.action.data.card.name; var listName = destinationList.name; var shortLink = 'https://trello.com/c/' + contents.action.data.card.shortLink; var message = mentions + ' カード *<' + shortLink + '|' + cardName +'>* が ' + movedUser + ' さんにより *' + listName + '* リストに移動されました。ご確認をお願いします。'; var jsonData = { "text" : message }; var payload = JSON.stringify(jsonData); var options = { "method" : "post", "contentType" : "application/json", "payload" : payload }; UrlFetchApp.fetch(postUrl, options); } /* QAリストからBugFixリストにカードが移動した際、 Slack上でメンションを飛ばすために開発担当者を特定する */ function getDevMember(cardId) { var scriptProp = PropertiesService.getScriptProperties().getProperties(); var trelloKey = scriptProp.TRELLO_API_KEY; var trelloToken = scriptProp.TRELLO_TOKEN; var qaListId = scriptProp.LIST_ID_QA; var requestUrl = "https://api.trello.com/1/cards/" + cardId + "/actions?key=" + trelloKey + "&token=" + trelloToken + "&filter=updateCard:idList"; var options = { 'method' : 'get' } var response = UrlFetchApp.fetch(requestUrl, options); var cardActions = JSON.parse(response.getContentText()); // 必ずカードにメンバーとして追加されているとは限らないので、 // 「最後にQAリストにカードを移動したユーザー」を開発担当者とする for (var i = 0; i < cardActions.length; i++) { if (cardActions[i].data.listAfter.id === qaListId) { Logger.log(cardActions[i].memberCreator); return cardActions[i].memberCreator; } } return; }
9. 公開
4.
と同様、再度 公開 > ウェブアプリケーションとして導入...
を選択し、ダイアログを開きます。
設定を以下のように変更し、更新
を選択します。
- アプリケーションにアクセスできるユーザー: 全員(匿名ユーザーを含む)
- プロジェクトバージョン: New
ここで「アプリケーションにアクセスできるユーザー」を変更しないとうまく動きません。ちょっとハマりました…。
結果
▼ Code Review → QA にカードを移動させた場合の通知
▼ QA → BugFix にカードを移動させた場合の通知 (開発担当者のID紐付け済み)
▼ QA → BugFix にカードを移動させた場合の通知 (開発担当者のID未紐付け)
ここで表示されたユーザー名とTrelloIDを参考にし、コード上でSlackIDと紐付けます
これでもうカードの移動を見逃さない!
おわりに
開発→QAに転向してから仕事でコードをあまり書かなくなったので、良いリハビリになりました。
GASがES6に対応する時代はまだでしょうか…。
かなり拙いコードとは思いますが、似たような仕組みを作りたい方にとって少しでも参考になれば幸いです。
また、実装・執筆に当たり以下の記事を参考にさせていただきました。ありがとうございます!