概要
近年、スクラムを導入した開発チームが増えつつあります。
スクラムチームの質を向上させるため、プロジェクト管理者だけではなくて、チームメンバーにも開発状況を把握しやすくすることが重要だと思います。
今回は、JIRA REST APIを使って、最新のスプリント情報を取得すると、バーンダウンチャートを生成して、日常的にSlackのChannelに送信するBotを紹介します。
要求仕様
node 12.20.1
npm 6.14.10
JIRA REST API テスト
まずはChrome API Tester Toolを使って、JIRA Agile REST APIのテストを行う。(スクラムボードのみ支持)
METHOD: GET
URL: http://{your-domain}.atlassian.net/rest/greenhopper/latest/rapid/charts/sprintreport?rapidViewId={rapidViewId}&sprintId={sprintId}
SlackBot を作成する
Botの作り方はワークスペースで利用するボットの作成を参照してください。
現在、元宵節を迎えることになりますので、MagicalYuanxiao
(中国語:神奇小元宵)と名付けました。
次に、アプリにスコープを設定するように( chat:write
、files:write
)の権限を追加しました。
設定後:
さらに、作成済みのAppを送信したいChannelに追加しました。
SlackBot 送信テスト
HTTPクライアントのインストール
$ npm install axios
メッセージを送信する
// test file
// Run "node sendMsg.js" to test access slack token
// If send message to channel is successful, the token is OK
const axios = require("axios");
const MSG_URL = "https://slack.com/api/chat.postMessage";
const slackToken = "Your slack token"; // *Slack Bot User OAuth Access Token*
async function run() {
const res = await axios.post(
MSG_URL,
{
channel: "#test", // *send to target channel*
text: "Hello, I am magical yuanxiao!", // message content
},
{
headers: {
authorization: `Bearer ${slackToken}`,
},
}
);
console.log("Done", res.data);
}
run().catch((err) => console.log(err));
実行コマンド
$ node sendMsg.js
SlackBot 設計と実装
ここからバーンダウンチャートを送信するSlackBot MagicalYuanxiao
のワークフローや実装部分を簡単に説明します。
ワークフロー
ライブラリの導入
・JIRAアクセス(https://www.npmjs.com/package/jira-client)
・ファイルとディレクトリの読み取りと書き込み(https://www.npmjs.com/package/fs)
・HTTPリクエスト(https://www.npmjs.com/package/request)
・時間データの操作(https://www.npmjs.com/package/moment)
・グラフ描画(https://github.com/SeanSobey/ChartjsNodeCanvas)
・環境変数の設定(https://www.npmjs.com/package/dotenv)
スプリントデータを保存する
// jira API options
const jira = new JiraApi({
protocol: "https",
host: jiraHost,
username: jiraUsername,
password: jiraPassword,
apiVersion: "3",
strictSSL: true,
});
// today's date
const YYYYMMDD = moment().format("YYYYMMDD");
// step1: get sprint info to generate json from jira
let sprint = {};
await jira
.getSprintIssues(rapidViewId, sprintId)
.then(function (issues) {
sprint["id"] = issues.sprint.id; // スプリントID
sprint["name"] = issues.sprint.name; // スプリント名
sprint["goal"] = issues.sprint.goal ? issues.sprint.goal : 0; // 目標点数
sprint["startDate"] = moment(issues.sprint.isoStartDate).format(
"YYYYMMDD"
); // 開始日
sprint["endDate"] = moment(issues.sprint.isoEndDate).format("YYYYMMDD"); // 終了日
sprint["issuesPointSum"] = issues.contents.completedIssuesEstimateSum
.value
? issues.contents.completedIssuesEstimateSum.value
: 0 + issues.contents.issuesNotCompletedEstimateSum.value
? issues.contents.issuesNotCompletedEstimateSum.value
: 0; // ストーリー点数合計
sprint["notCompletedIssuesPointSum"] = issues.contents
.issuesNotCompletedEstimateSum.value
? issues.contents.issuesNotCompletedEstimateSum.value
: 0; // 未完了ストーリー点数合計(include: todo, doing, review...)
})
.catch(function (err) {
console.error(err);
});
// json data to be written
let jsonData = {
code: 0,
data: sprint,
updateDate: moment().format("YYYY/MM/DD HH:mm:ss"),
msg: "success",
};
// format json
let text = JSON.stringify(jsonData);
// params: directory and file name
let file = path.join("./output/", YYYYMMDD + "_sprint_data.json");
// write into json
await fs.writeFile(file, text, function (err) {
if (err) {
console.log(err);
} else {
console.log("File was successfully created: " + file);
}
});
チャートデータを用意する
// step2: get data for line chart
var startAndEndDateDiff = moment(sprint.endDate).diff(
moment(sprint.startDate),
"days"
);
// X axis labels for line chart
var xLabels = [];
for (let i = 0; i <= startAndEndDateDiff; i++) {
xLabels[i] = moment(sprint.startDate).add(i, "days").format("MM/DD");
}
// Y axis values for line chart
var yValues = [];
// from 0 to date difference(from today to startDate)
var dateDifferenceArray = [];
for (let i = 0; i <= moment().diff(moment(sprint.startDate), "days"); i++) {
dateDifferenceArray.push(i);
}
var jsonArray = dateDifferenceArray.reverse().map(getJsonAsync);
await Promise.all(jsonArray)
.then(function (jsonData) {
// 本スプリントの日別残ポイントデータを埋め込む
yValues = jsonData.map((s) => s.data.notCompletedIssuesPointSum);
})
.catch(function (err) {
console.error(err);
});
// guideline Y axis values
var guidelineValues = [];
for (let i = 0; i <= startAndEndDateDiff; i++) {
guidelineValues.push(
sprint.goal - Math.floor((sprint.goal * i) / startAndEndDateDiff)
);
}
バーンダウンチャートを作成する
// step3: create burn down chart image
const height = 400;
const width = 700;
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height });
(async () => {
const configuration = {
type: "line",
data: {
labels: xLabels,
datasets: [
{
label: "Story Points remaining",
borderColor: "rgba(255, 100, 100, 1)",
data: yValues,
fill: false,
tension: 0, // straight line
},
{
label: "Guideline",
borderColor: "rgba(122, 122, 122, 1)",
borderDash: [10, 3], // dotted line
data: guidelineValues,
fill: false,
borderWidth: 1,
tension: 0,
},
],
},
options: {
scales: {
yAxes: [
{
ticks: {
beginAtZero: true, // set Y min value to 0
},
},
],
},
elements: {
point: {
radius: 0, // do not show points
},
},
title: {
display: true,
fontSize: 16,
text:
sprint.name +
"(" +
moment(sprint.startDate).format("MM/DD") +
"~" +
moment(sprint.endDate).format("MM/DD") +
")", // chart title
},
},
};
const dataUrl = await chartJSNodeCanvas.renderToDataURL(configuration);
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
await fs.writeFile(
"./output/" + YYYYMMDD + "_burn_down_chart.png",
base64Data,
"base64",
function (err) {
if (err) {
console.log(err);
}
}
);
})();
Slackにバーンダウンチャートを送信する
// step4: upload created chart to slack
request.post(
{
url: UPLOAD_URL,
formData: {
file: fs.createReadStream(
"./output/" + YYYYMMDD + "_burn_down_chart.png"
),
token: slackToken,
filetype: "png",
filename: YYYYMMDD + "_burn_down_chart.png",
channels: slackChannel, // send to XXX channel
title: YYYYMMDD + "_burn_down_chart.png", // show this name in slack
},
},
function (error, response, body) {
if (error) {
console.log(error);
} else {
console.log("Send burn down chart to slack at " + moment().format("YYYY/MM/DD HH:mm:ss"));
}
}
);
実行結果
実行コマンド
$ node app.js
Slackのスクリーンショット
以上でnodejsの実装部分は完成です。
定期実行する方法
・Node.jsのコードを定期実行する(crontabを利用) ←おすすめ方法
例:平日18時に自動投稿を実行したい
$ crontab -e
00 18 * * 1-5 {nodeのフルパス} {実行ファイルのパス}
$ crontab -l
00 18 * * 1-5 {nodeのフルパス} {実行ファイルのパス}
Source Code (GitHub)
今後の課題
・ガイドラインについて、祝日と休日の判定処理
・JSON読み取りについて、本スプリント先日のデータが存在していないエラーの対処方法(期間中のみ)
・カスタム看板に対して戻り値が正しいかどうか検証する
まとめ
そもそも、本課題の解決策として、既存製品Standuply、Trooprなどは無料ではなくて、更にJIRAの管理権限が持っていない状況で、Slack自体のリマインド機能(/remind)を使って、基本的なJIRAリンクを送るリマインダー機能を実現できますけど、実感がないと思います。なので、既存製品のようなSlackBotを開発する必要があります。
参照サイト
Python編記事-Slackに定期的にバーンダウンチャートを投稿するBotの作成方法
Jira Software でバーンダウンチャートを使用する方法
JIRA Agile REST API Reference - JIRA Agile 7.0.4
JIRA REST APIで課題情報を取得(jira-client-npmを使用)
Working with the Slack API in Node.js
node.jsでファイルの入出力操作
Chart.jsによるチャート作成