前編：GAS初心者がBacklog-JIRA連携してみた　〜チケットの連携をGASで一生懸命がんばったら外部ツールで一瞬でできた話〜

このエントリーは、GMOアドマーケティング Advent Calendar 2018【12/6】 の記事です。
GMOアドマーケティングとしては初のAdvent Calendar参戦です。

# 背景
社内ではBacklogとJIRAというふたつのチケット管理ツールを使っています。
Backlogはとても使いやすいチケット管理ツールですが、唯一つの欠点は、時間管理機能が貧弱ということ・・・。
日毎の時間集計をすることができないんです。ある日に、誰が何をしていたか、工数集計をすることができません。

かといって、チームメンバーにいちいちふたつのチケット管理ツールを開いて、JIRAに時間を入力させるのもちょっとなぁ、と思って時間連携ツールを作ることにしました。

# やりたいこと
前提：Backlogに当日の作業時間が入力されていること
1. Backlogから時間を取得する
2. 対応するJIRAチケットを検索する（Backlogの課題キーをJIRAのカスタムフィールド「Backlog課題番号」に持っている前提）
3. 検索にヒットしたら、対応するJIRAのチケットに時間を記録する
4. 検索にヒットしない場合、（Backlogの課題キーをJIRAのカスタムフィールド「Backlog課題番号」にセットしてチケットを作成

# 実装
それでは、GASで実装していきます。 **と記載されているところは、環境ごとに設定してください。

## Main.gs
メインとなるクラス。ここで上記の「やりたいこと」に書いてある処理を順次呼んでいきます。

```javascript:Main.gs
/*
 * Google Spread Sheet上の実行ボタン押すと実行される。メンバーには帰りがけにポチッと押して貰う想定。
 * （Google Spread Sheetのwebhook機能は、社内のIP制限でつかえなかった）
 */
function main() {
    try {
        // 対象の課題一覧をBacklogから取得する
        var issuelist = getBacklog_Issues();

        for (var i = 0; i < issuelist.length; i++) {
            // JIRAの連携チケットを検索して、（Backlog-keyをキーに）
            var jira_key = findJIRA(issuelist[i]);

            if (jira_key === undefined) {
                // チケットがない場合は作成する。
                jira_key = createJIRA(issuelist[i]);
            }
            // 時間を記載
            addWorklog(jira_key, issuelist[i]);
        }
    } catch (e) {
        var error = e;
        Logger.log("message:" + error.message + "\nfileName:" + error.fileName + "\nlineNumber:" + error.lineNumber + "\nstack:" + error.stack);
    }
}
```

## GetBacklog.gs
Backlogから課題を取得します。

```javascript:GetBacklog.gs
// backlog関連パラメタ
var BACKLOG_URL = "https://*****.backlog.jp";
var API_KEY_PARAM = "?apiKey=******************";
var COUNT = 100;

/**
 * Backlog課題一覧の取得
 * @returns {undefined}
 */
function getBacklog_Issues() {

    // 当日を検索対象
    var start_date = new Date();
    var until_date = new Date();

    // シートで選ばれた人を処理対象
    var spreadsheet = SpreadsheetApp.openById('**********');
    var sheet = spreadsheet.getSheetByName('**');
    var value = sheet.getRange("**").getValue();
    var backlog_user_id = value.substring(value.indexOf(":") + 1, value.length);

    // APIキーでBacklog認証&取得(課題一覧)
    // 抽出条件：直近１日で更新、かつ開発チームメンバーが更新したもの
    var result = UrlFetchApp.fetch(BACKLOG_URL + "/api/v2/issues"
        + API_KEY_PARAM
        + "&projectId[]=*****"
        + "&updatedSince=" + Utilities.formatDate(start_date, 'Asia/Tokyo', 'yyyy-MM-dd')
        + "&updatedUntil=" + Utilities.formatDate(until_date, 'Asia/Tokyo', 'yyyy-MM-dd')
        + "&assigneeId[]=" + backlog_user_id
        + "&count=" + COUNT);

    if (result.getResponseCode() !== 200) {
        return false;
    }
    return JSON.parse(result.getContentText());
}
```

## ProcessJira.gs
JIRAをBacklog課題キーをつかって検索し、ない場合はJIRAのチケット作成、ある場合はBacklogに記入された時間をJIRAに連携します。

```javascript:ProcessJira.gs

/**
 * BacklogUserID : JIRAUserIDのマッピング
 */
function get_jira_userid(issue) {
    var userMapping =
        {
            '***backlogのユーザID***': '**JIRAのユーザID***',
            // 人数分設定してください。
        };

    for (var key in userMapping) {
        if (key === issue["assignee"]["mailAddress"]) {
            jira_usrid = userMapping[key];
        }
    }
    return jira_usrid;
}

/**
 * ユーザ認証情報を返す
 * @returns token
 */
function get_jira_user_token(jira_usrid) {
    // https://ja.confluence.atlassian.com/cloud/api-tokens-938839638.html
    if (jira_usrid === 'admin') { //adminユーザだけ、token生成時はIDをメールアドレスに詰め替え
        jira_usrid = '**JIRAのユーザID**';
        var pw = '***JIRAのpwトークン****';
　　// ユーザ数だけ追加が必要
    } else if (jira_usrid === '**JIRAのユーザID2**') {
        var pw = '***JIRAのpwトークン2****';
    } 

　　// ユーザIDがユーザID+ドメインだったので、ここで付加しています
    var token = Utilities.base64Encode(jira_usrid + '@**メールアドレスのドメイン**' + ':' + pw);
    return token;
}

/*
 * JIRA検索
 * @param {type} issue
 * @returns jira_key
 */
function findJIRA(issue) {
    var id = '****';  // botユーザ
    var pw = '****';
    var token = Utilities.base64Encode(id + ':' + pw);

    var options = {
        contentType: "application/json",
        headers: {"Authorization": " Basic " + token}
    };

    // ここでは一例として「Backlog課題番号」というカスタムフィールドに、Backlog課題キーをセットしている前提です。
    var url = 'https://*****.atlassian.net/rest/api/2/search/?jql=Backlog課題番号 ~' + issue["issueKey"];
    var response = UrlFetchApp.fetch(url, options);
    var jobj = JSON.parse(response);

    if (jobj['total'] !== 0) {
        var jira_key = jobj['issues'][0]['key'];
        Logger.log('found JIRA key:' + jira_key);
    }
    return jira_key;
}

/*
 * JIRA作成
 * @param {type} issue
 * @returns key
 */
function createJIRA(backlog_issue) {
    var id = '****';  // botユーザ
    var pw = '****';
    var token = Utilities.base64Encode(id + ':' + pw);

    var jira_usrid = get_jira_userid(backlog_issue);

    var data = {
        project: {key: '***'},
        issuetype: {name: 'タスク'},
        priority: {name: '中'},
        summary: backlog_issue["summary"],
        description: backlog_issue["description"],
        timetracking: {originalEstimate: backlog_issue["estimatedHours"]},
        customfield_*****: backlog_issue["issueKey"],
        assignee: {name: jira_usrid},
    };
    var fields = {fields: data};
    var payload = JSON.stringify(fields);

    var options = {
        method: 'post',
        payload: payload,
        contentType: 'application/json',
        headers: {'Authorization': ' Basic ' + token},
    };
    var response = UrlFetchApp.fetch('https://*****.atlassian.net/rest/api/2/issue/', options);

    var jobj = JSON.parse(response);
    var key = jobj['key'];
    Logger.log('created JIRA key:' + key);

    return key;
}

/**
 * 作業時間をJIRAに追記
 * @param {type} jira_key
 * @param {type} bakclog_issue
 * @returns none
 */
function addWorklog(jira_key, bakclog_issue) {
    var jira_usrid = get_jira_userid(bakclog_issue);
    var token = get_jira_user_token(jira_usrid);

    if (bakclog_issue["actualHours"]) {
        var dt = new Date();
        // JIRAが受け取れる形式にTimestampを整形してあげる。時差も修正する
        dt.setHours(dt.getHours() + 9);
        var t_started = dt.toISOString().slice(0, -1) + "+0900";

        var data = {
            // きちんと時間単位もつける(h=hours)
            timeSpent: bakclog_issue["actualHours"] + "h",
            started: t_started,
        };

        var payload = JSON.stringify(data);
        var options = {
            method: "post",
            payload: payload,
            contentType: "application/json",
            headers: {"Authorization": " Basic " + token},
        };

        // 当日の実績登録
        UrlFetchApp.fetch('https://*****.atlassian.net/rest/api/2/issue/' + jira_key + '/worklog'
            , options);

        Logger.log('worklog add completed. target JIRA key:' + jira_key);
    }
}
```

# 地味にハマったこと
- JIRAのREST-APIはv2とv3があります。v3のほうはいまだベータ版であり、エラー・メッセージが間違っていたり、ざっくりしすぎていることがあります。僕の場合自分のリクエストがどう間違っているかわからずとてもハマりました。

- JIRAのapiドキュメントはときどきリンクが間違っていて、v2のドキュメントを見ているはずが、v3のドキュメントにリンクされていたりします。注意してください。URLで見分けましょう。こちらはv2　https://developer.atlassian.com/cloud/jira/platform/rest/v2/

- JIRAのapiドキュメントには、Exampleという項目がありrequestパラメータ例が書いてありますが、フォーマット定義から自動生成されています。そのままrequest投げても動きません。あくまでもイメージです（笑） 

しかしこのときの僕は知りませんでした。あんなに楽な方法があったなんて。。。
[次回](https://qiita.com/yossy_adm/private/ca154c28ff36ecb91c8d)に続く。

#次回の[Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gmo-am)
明日は、【@yossy_adm】さんの【Zapierで同じことやってみた】についてのお話です。
お楽しみに。

クリスマスまで続くGMOアドマーケティング [Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gmo-am)
ぜひ今後も投稿をウォッチしてください！