10分でできる Jira チケット作成 Chatwork Chatbot


Chatwork Chatbot × Jira へのお誘い

仕事のやりとりの中心にチームコミュニケーションツールがありますよね。
Slack, Chatwork, Discord ... とかとか

そして仕事のタスクはチケット管理システムを導入していることもあるでしょう。
Backlog, Redmine, Jira ... とかとか

Chatwork × Jira Cloud の環境で仕事をしていて地味にフラストレーションがたまるのが
Chatworkでのコミュニケーションから Jira へ記録を残したり、Jira を参照することです。
Jira は良くも悪くもリッチになりすぎて、目的の場所にいろいろなやり方で到達できる、つまりどうやって目的のページに行くかを沢山のボタンの中からみつけださないといけない、そしてどのボタンを押しても恐ろしくボタンのついたサブダイアログがでるか、ゆっくりとしたページ遷移で気を持たせるにもかかわらず大概は目的のページじゃないところに飛ばされます。
わたしは Atlassian が考えるほど優秀なエンジニアではないので、とてもフラストレーションがたまってしまう世界です。
それを少しカイゼンしたいと思い、Chatwork というシンプルなツールを軸にして Jira を操作できるようにしたいとおもいました。

そこで、Chatwork Chatbot に Jira API を連携し、退屈な Jira 操作を Chatbot に集約することで Chatwork から動かずに Jira を操作してやるのです!

今回必要なもの

  • Chatbot 用の Chatwork アカウント
  • Chatbot の Chatwork API Token
  • Jira アカウント
  • Jira API のトークン

Chatwork Chatbot を作る

基本的な仕組みは Chatwork Webhook を使ってメンション時にリクエストを受け取り、その中で Jira チケットを作るだけです。
Chatwork Webhook については下記も参照して頂ければ。
chatwork で自分への TO を探せるようにする

では実際に設定したり、GAS のコードを書いてみましょう。

Chatbot 用 GAS スクリプト

/**
* chatwork WebHook から TO メッセージ情報を受け取る
* @param {Object} e - TO メッセージ情報 see. http://developer.chatwork.com/ja/webhook.html#eventType
*/
function doPost(e) {

  var json = JSON.parse(e.postData.contents);
  var message = json.webhook_event.body;
  var roomId = json.webhook_event.room_id;
  var fromAccountId = json.webhook_event.from_account_id;
  var messageId = json.webhook_event.message_id;
  var sendTime = json.webhook_event.send_time;

  // Jira API の設定値
  var jiraDomain = 'demo';
  var jiraApiUser = '[email protected]';
  var jiraApiToken = 'AAAAAAAAAAAAAAAAAAA';

  var jiraApiClient = new JiraApiClient(jiraDomain, jiraApiUser, jiraApiToken);

  // チケットを作るための変数
  // 名前で指定したほうが直感的に使えるのでIDではなく名前を積極的に使う
  var projectKey = 'DEMO';
  var issueTypeName = 'Task';
  var reporterName = 'lain';
  var summary = 'TEST';

  // 課題タイプ名は重複がありえるので ID をもってくる
  var issueTypeId = this.getProjectIssueTypeIdByIssueTypeName(jiraApiClient, projectKey, issueTypeName);

  // チケット作成
  var request = new CreateIssueRequest(projectKey, issueTypeId, reporterName, summary);
  var result = jiraApiClient.createIssue(request);

  // Chatbot の Chatwork API Token
  var api_token = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
  var client = createClient_(api_token);
  // 情報を整形して投稿する
  client.sendMessage(
    {
      room_id: roomId,
      body: Utilities.formatString("Jira チケットを作成しました。https://%s.atlassian.net/browse/%s", jiraDomain, result['key'])
    }
  );
}

function createClient_(api_token)
{
  return ChatWorkClient.factory(
    {
      'token': api_token,
    }
  );
}

function getProjectIssueTypeIdByIssueTypeName(jiraApiClient, projectKey, issueTypeName) {
  var projectModel = jiraApiClient.getProjectByProjectIdOrKey(projectKey);
  var issueTypes = projectModel['issueTypes'];
  var issueTypeId = null;
  issueTypes.some(function (issueType) {
    if (issueType['name'] === issueTypeName) {
      issueTypeId = issueType['id'];
      return true;
    }
  });
  return issueTypeId;
};

/**
 * JIRA API クライアント
 * @link https://developer.atlassian.com/cloud/jira/platform/rest/v3/
 */
var JiraApiClient = /** @class */ (function () {
    /**
     * @param hostname
     * @param email
     * @param apiToken
     */
    function JiraApiClient(hostname, email, apiToken) {
        this.hostname = hostname;
        this.email = email;
        this.apiToken = apiToken;
    }
    JiraApiClient.prototype.createApiUrl = function (resourceName) {
        return Utilities.formatString('https://%s.atlassian.net/%s/%s', this.hostname, JiraApiClient.BASE_API_PATH, resourceName);
    };
    /**
     * 全てのプロジェクト情報を取得する
     * @link https://developer.atlassian.com/cloud/jira/platform/rest/v3/?utm_source=%2Fcloud%2Fjira%2Fplatform%2Frest&utm_medium=302#api-rest-api-3-project-get
     */
    JiraApiClient.prototype.getAllProjects = function () {
        var url = this.createApiUrl('project');
        var response = this.requestApi(url, 'get');
        return JSON.parse(response.getContentText());
    };
    /**
     * 特定のプロジェクト情報を取得する
     * @link https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-project-projectIdOrKey-get
     */
    JiraApiClient.prototype.getProjectByProjectIdOrKey = function (projectIdOrKey) {
        var url = this.createApiUrl(Utilities.formatString('project/%s', projectIdOrKey));
        var response = this.requestApi(url, 'get');
        return JSON.parse(response.getContentText());
    };
    /**
     * ユーザーが1つ以上のプロジェクトに対するプロジェクトの参照プロジェクト権限を持っている場合、ユーザーが参照する権限を持つプロジェクトに関連付けられている課題タイプが返されます。
     * @link https://developer.atlassian.com/cloud/jira/platform/rest/v3/?utm_source=%2Fcloud%2Fjira%2Fplatform%2Frest&utm_medium=302#api-rest-api-3-issuetype-get
     */
    JiraApiClient.prototype.getAllIssueTypesForUser = function () {
        var url = this.createApiUrl('issuetype');
        var response = this.requestApi(url, 'get');
        return JSON.parse(response.getContentText());
    };
    /**
     * チケットを参照する
     * @link https://developer.atlassian.com/cloud/jira/platform/rest/v3/?utm_source=%2Fcloud%2Fjira%2Fplatform%2Frest&utm_medium=302#api-rest-api-3-issue-issueIdOrKey-get
     *
     * @param issueIdOrKey
     * @param fields
     */
    JiraApiClient.prototype.getIssueByIssueIdOrKey = function (issueIdOrKey, fields) {
        var url = this.createApiUrl(Utilities.formatString('issue/%s', issueIdOrKey));
        var response = this.requestApi(url, 'get');
        return JSON.parse(response.getContentText());
    };
    /**
     * チケットを作成する
     * @link https://developer.atlassian.com/cloud/jira/platform/rest/v3/?utm_source=%2Fcloud%2Fjira%2Fplatform%2Frest&utm_medium=302#api-rest-api-3-issue-post
     *
     * @param request
     * @return Object
     */
    JiraApiClient.prototype.createIssue = function (request) {
        var url = this.createApiUrl('issue');
        var response = this.requestApi(url, 'post', request.toPayload());
        Logger.log(response.getResponseCode());
        if (response.getResponseCode() == 201) {
            return JSON.parse(response.getContentText());
        }
        // エラー
        Logger.log(Utilities.formatString('Error createIssue. %s, %s', response.getResponseCode(), response.getContentText()));
        return null;
    };
    /**
    * @returns object
    */
    JiraApiClient.prototype.requestApi = function (url, method, payload) {
        var header = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': Utilities.formatString('Basic %s', Utilities.base64Encode(this.email + ":" + this.apiToken))
        };
        var options = {
            'method': method,
            'headers': header,
            'muteHttpExceptions': true
        };
        if (payload) {
            options.payload = JSON.stringify(payload);
        }
        return UrlFetchApp.fetch(url, options);
    };
    JiraApiClient.BASE_API_PATH = 'rest/api/3';
    return JiraApiClient;
}());

/**
 * チケット作成リクエストオブジェクト
 */
var CreateIssueRequest = /** @class */ (function () {
    /**
     * @param projectKey プロジェクトのキー名
     * @param issueTypeId 課題タイプID(課題タイプ名は重複できる。IDを指定する必要がある)
     * @param reporterName 報告者名
     * @param summary 要約
     * @param priority 優先度
     * @param labels ラベル
     * @param description 説明
     */
    function CreateIssueRequest(projectKey, issueTypeId, reporterName, summary, priority, labels, description) {
        this.projectKey = projectKey;
        this.issueTypeId = issueTypeId;
        this.reporterName = reporterName;
        this.summary = summary;
        this.priority = priority;
        this.labels = labels;
        this.description = description;
    }
    CreateIssueRequest.prototype.toPayload = function () {
        var fields = {
            'project': {
                'key': this.projectKey
            },
            'issuetype': {
                'id': this.issueTypeId
            },
            'reporter': {
                'name': this.reporterName
            },
            'summary': this.summary
        };
        if (this.priority) {
            fields['priority'] = {
                'name': this.priority
            };
        }
        if (this.labels) {
            fields['labels'] = this.labels;
        }
        if (this.description) {
            fields['description'] = this.description;
        }
        return {
            'update': {},
            'fields': fields
        };
    };
    return CreateIssueRequest;
}());


ウェブアプリケーションとして公開する

ウェブアプリケーションの URLはメモっておきましょう

Chatbot 用の Chatwork アカウントで Webhook の設定をする

Chatbot 用の Chatwork アカウントにログインして、Webhook の設定をしましょう

動作テスト

Chatbot にメンション付きで話しかけてみましょう。
Jiraチケットを作成してくれるはずです。

きちんとチケット作成してくれましたね 👼

もっと実用的にする

チャットメッセージのフォーマットを決めて、チャットメッセージの内容からチケットのタイトルや説明を作るようにすれば、現場でも便利に使えるかと思います。
Jira API はさまざまなことができますので、こんな Chatbot を作ったら便利だったよ! など教えてもらえたらうれしいです😊