LoginSignup
0
3

More than 3 years have passed since last update.

バーンダウンチャートを送信するSlackBotの作成

Last updated at Posted at 2021-02-21

概要

近年、スクラムを導入した開発チームが増えつつあります。
スクラムチームの質を向上させるため、プロジェクト管理者だけではなくて、チームメンバーにも開発状況を把握しやすくすることが重要だと思います。

今回は、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}

こんな感じのレスポンスで問題ないかと思います。
image.png

SlackBot を作成する

Botの作り方はワークスペースで利用するボットの作成を参照してください。
現在、元宵節を迎えることになりますので、MagicalYuanxiao(中国語:神奇小元宵)と名付けました。
slack-app.png
次に、アプリにスコープを設定するように(chat:writefiles:write)の権限を追加しました。
設定後:
image.png
さらに、作成済みのAppを送信したいChannelに追加しました。
addapp.png

SlackBot 送信テスト

HTTPクライアントのインストール

$ npm install axios

メッセージを送信する

sendMsg.js
// 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

テスト結果
image.png

SlackBot 設計と実装

ここからバーンダウンチャートを送信するSlackBot MagicalYuanxiaoのワークフローや実装部分を簡単に説明します。

ワークフロー

image.png

ライブラリの導入

・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)

スプリントデータを保存する

app.js
// 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);
    }
  });

チャートデータを用意する

app.js
 // 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)
    );
  }

バーンダウンチャートを作成する

app.js
 // 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にバーンダウンチャートを送信する

app.js
// 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のスクリーンショット
burndown.png
以上でnodejsの実装部分は完成です。

定期実行する方法

Node.jsのコードを定期実行する(crontabを利用) ←おすすめ方法

例:平日18時に自動投稿を実行したい

$ crontab -e

00 18 * * 1-5 {nodeのフルパス} {実行ファイルのパス}

$ crontab -l
00 18 * * 1-5 {nodeのフルパス} {実行ファイルのパス}

DockerでNode.jsアプリを起動する

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によるチャート作成

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3