はじめに
今回、業務で社内改善ツールとしてSlackワークフローからRedmineチケットを自動作成するツールを作成する機会があったので、開発内容を備忘録として記録したいと思います。
完成イメージ
① ユーザアクション:Slackワークフローからチケット情報を入力
② ツール実行:Redmineチケットが自動起票される
③ ツール実行:Redmine URLを含むメッセージをSlackに通知
構成
- Slackワークフロー
- Slackカスタムアプリ
- Incoming WebHooksアプリ
- Userリスト取得アプリ
- Googleスプレッドシート
- Google Apps Script(以下GAS)
- RedmineAPI
カスタムインテグレーションが非推奨となったため、カスタムインテグレーションとしてのOutgoing Webhooks、 Incoming WebHooksを使わないことを前提に構成しました。
今回はSlackカスタムアプリとしてIncoming WebHooksを作成しております。
SlackワークフローのステップからはRedmineに直接アクセスできないため、ワークフローからの「Googleスプレッドシートへのデータ追加」をトリガーに、GAS経由でRedmineAPIを実行しています。
処理の流れ
- Slackワークフローより
- チケット情報を記入
- ワークフローステップからデータをGoogleスプレッドシートに記入
- Googleスプレッドシートより
- 変更を感知してGASを発火
- GASより
- Googleスプレッドシートからデータを取得して整形
- RedmineAPIを実行してチケット作成
- 作成されたチケットURLを記載したメッセージをSlack通知
実装方法
1) データ記入用スプレッドシート作成
新規スプレッドシートを作成します。
「拡張機能」 > 「Apps Script」からGASを開始します。
2) curlでRedmine APIを実行してデータ構造を確認する
以下の記事を参考にRedmine APIを有効にし、APIアクセスキーを取得します。
以下の記事を参考にチケットの情報を確認します。
確認した内容は以降の手順でデータの整形時などに参考にします。
3)スプレッドシートに項目列を追加
手順2で確認したRedmineのチケット情報を参考に、先ほど作成したスプレッドシートの1行目にRedmineへ登録する項目の行を追加します。
4)Slackワークフロー作成
情報を取得するワークフローを作成します。
ステップ2では先ほど作成したスプレッドシートを選択し、「スプレッドシートの対象列」と「フォームで収集する項目」が対応するよう指定します。
5)GASに変更感知のトリガーを設定
GASのトリガーを開きます。
以下のようにトリガーを設定します。
6)Slackカスタムアプリ作成
① 以下の記事を参考にIncoming Webhookアプリを作成し、メッセージを投稿したいチャネルのWebhook URLを取得します。
② 以下の記事を参考にUserリスト取得アプリを作成し、User Tokenを取得します。
7)スクリプトプロパティの設定
手順6で取得したWebhook URLやUser Tokenをスクリプトプロパティに設定します。
「プロジェクトの設定」を開きます。
スクリプトプロパティに以下3つを設定します。
- 手順6で取得したWebhook URL
- 手順6で取得したUser Token
- 手順2で取得したRedmineAPIキー
8)GASの実装
以下5ファイルを作成します。
- main.gs
- forms.gs
- redmine.gs
- slack.gs
- tools.gs
var prop = PropertiesService.getScriptProperties().getProperties();
function main() {
let mySheet = SpreadsheetApp.getActiveSheet();
let sheetName = mySheet.getSheetName();
let data = {};
let slackMessage = {};
let response = {}
// フォーム追加時はここに分岐を追加
response = formMain(sheetName)
data = response.data
slackMessage = response.slackMessage
let ticketUrl = createTicket(data)
post_slack(ticketUrl, slackMessage);
}
function formMain(sheetName) {
let data = srformatData(sheetName)
let slackMessage = srSlackMessage(data)
return {"data": data, "slackMessage": slackMessage}
}
// Redmineチケット作成用フォーマット
function srformatData(sheetName) {
let response = {};
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName(sheetName);
var lastRow = sheet.getLastRow();
// 以下、手順2で確認したRedmineのチケット項目に合わせて任意に変更してください。
// トラッカー
response['tracker'] = 31;
// 期日
response['due_date'] = formatDate(sheet.getRange(lastRow, 1).getValue());
// 対象顧客
response['target_clients'] = sheet.getRange(lastRow, 2).getValue();
// 題名
response['subject'] = sheet.getRange(lastRow, 10).getValue();
// 説明
response['description'] = "♦︎依頼詳細\n" + sheet.getRange(lastRow, 3).getValue()
// 調査依頼内容
response['request_detail'] = sheet.getRange(lastRow, 3).getValue();
// 案件番号
response['case_number'] = sheet.getRange(lastRow, 4).getValue();
// 通知メンバー1
response['notice_1'] = removeAtSymbol(sheet.getRange(lastRow, 5).getValue());
// 通知メンバー2
response['notice_2'] = removeAtSymbol(sheet.getRange(lastRow, 6).getValue());
// 通知メンバー3
response['notice_3'] = removeAtSymbol(sheet.getRange(lastRow, 7).getValue());
// 作成者
response['created_user'] = removeAtSymbol(sheet.getRange(lastRow, 8).getValue());
return response;
}
// Slackメッセージ送信用フォーマット
function srSlackMessage(data) {
let users = getUsers()
let notice_1 = fetchUserIdByDisplayName(users, data['notice_1'])
let notice_2 = fetchUserIdByDisplayName(users, data['notice_2'])
let notice_3 = fetchUserIdByDisplayName(users, data['notice_3'])
let created_user = fetchUserIdByDisplayName(users, data['created_user'])
let notice_3_text = ''
if (notice_3 !== undefined) {
notice_3_text = ' *通知メンバー③* \n' + '<@' + notice_3 + '>'
}
// 以下、Slack通知フォーマットを任意に変更してください。
const json =
{
'text': '<@' + created_user + '>さんからの *Discoveriez調査依頼フォーム* の送信\n\n'
+ ' *期日* \n' + data['due_date'] + '\n\n'
+ ' *概要* \n' + data['subject'] + '\n\n'
+ ' *対象顧客* \n' + data['target_clients'] + '\n\n'
+ ' *調査依頼内容* \n' + data['request_detail'] + '\n\n'
+ ' *通知メンバー①* \n<@' + notice_1 + '>\n\n'
+ ' *通知メンバー②* \n<@' + notice_2 + '>\n\n'
+ notice_3_text
};
return json;
}
function createTicket(data) {
// 以下、手順2で確認したチケット項目に合わせて変更してください。
const payload = {
"issue": {
"project_id": 237
,"tracker_id": data['tracker']
,"status_id": 1
,"priority_id": 2
,"description": data['description']
,"subject": data['subject']
,"due_date": data['due_date']
,"custom_field_values": {
"66": data['case_number']
}
}
};
let redmine_url = 'https://{RedmineURL}/issues.json';
let api_key = prop.REDMINE_API_KEY; // RedmineのAPI key を入力
let headers = {
'X-Redmine-API-Key': api_key,
'Content-Type': 'application/json',
};
let options = {
'method': 'POST',
'contentType': 'application/json',
'headers': headers,
'payload': JSON.stringify(payload),
};
let response = UrlFetchApp.fetch(redmine_url, options);
let id = JSON.parse(response).issue.id;
return 'https://{RedmineURL}/issues/' + id;
}
function post_slack(ticketUrl, json) {
// SlackのWebhookURL
const slack_webhook_url = prop.WEBHOOK_TEST_DM; // webhook url入力
json.text += '\n\n*チケットURL* \n' + ticketUrl
// SlackのWebhook URLに送信するデータをJSONに変換する
const payload = JSON.stringify(json);
// UrlFetchAppで使用するメソッドやコンテントタイプを指定
const options =
{
'method': 'post',
'contentType': 'application/json',
'payload': payload
};
// Slackに送信
UrlFetchApp.fetch(slack_webhook_url, options);
}
function getUsers() {
var slackToken = prop.BOT_USER_OAUTH_TOKEN;
// ユーザーリストを取得するためのSlack APIエンドポイント
var url = "https://slack.com/api/users.list";
// APIリクエストのオプション
var headers = {
'Authorization': 'Bearer '+ slackToken
};
var options = {
"method": "get"
,"headers": headers
};
// Slack APIからユーザーリストを取得
var response = UrlFetchApp.fetch(url, options);
var users = JSON.parse(response.getContentText()).members;
return users;
}
function fetchUserIdByDisplayName(users, displayName) {
// 表示名からユーザーIDを検索
var userId = null;
for (var i = 0; i < users.length; i++) {
if (users[i].profile.display_name === displayName || users[i].name === displayName) {
userId = users[i].id;
break;
}
}
// ユーザーIDが見つからない場合の処理
if (userId === null) {
Logger.log("指定された表示名のユーザーが見つかりません: " + displayName);
return;
}
return userId;
}
function formatDate(date) {
var date = new Date(date);
var format = "yyyy-MM-dd";
var timeZone = Session.getScriptTimeZone();
var formattedDate = Utilities.formatDate(date, timeZone, format);
return formattedDate;
}
function removeAtSymbol(str) {
if (str.charAt(0) === '@') {
return str.substring(1);
}
return str;
}
新規フォームの追加方法
複数のフォームを扱えるように、共通部分は切り出しています。
フォームを追加するときは以下の手順で追加可能です。
① スプレッドシートに新規シートを追加
② Slackワークフローを新規作成し、データの追加先に上記のシートを指定
③ form.gsをもとに該当フォーム用のgsファイルを作成し、項目調整
④ 以下のようにmain.gsに分岐追加
//シート名が「①で追加したシート」であれば「②で追加したgsファイルのmain関数」を実行する
if (sheetName == '見積もり依頼フォーム') {
response = qrMain(sheetName)
} else if(sheetName == '新規機能開発依頼フォーム' ) {
response = drMain(sheetName)
}
※ 関数は全ファイルでユニークになるよう設定してください。
GASの関数はすべてグローバル関数となるので、複数ファイルで同一の名前の関数名をつけてはならないため(泣)
動作確認
Slackワークフローを実行し、Redmineチケット作成とSlack通知が完了すればOK!
ちなみに、GASの実行ログは以下「実行数」で確認できます。
スクリプト内でLogger.log()
を記載すればここにログが出力されるのでデバッグも可能です。
おわりに
GASは初めて触りましたが、簡単にツールを作成することができて楽しかったです。
Notion APIなどでも遊んでみたいと思います!誰かの参考になれば幸いです。
参考記事