Jira Cloud REST APIとGASを使ったチケット起票のしくみについて

# はじめに

こんにちは。前年[エンジニアリング･トライアル](https://qiita.com/tsugumiri/items/f043d1df1c9382baa879)について記事を書きました@tsugumiriです。
今年は、前年に引き続き[審査の業務](https://qiita.com/sanshi/items/c7b16779e77c858afe7d)に従事しつつ、エンジニアリング･トライアル2年生として、牛歩の歩みながら知見を深めて参りました。

今回は、取り組みを通じ制作した、Jira Cloud REST APIとGASを使ったチケット起票の仕組みについて、頭を悩ませた結果、[エンジニアさん](https://qiita.com/omomukumamani_piroshi)にアドバイスいただき、解決に至ったものがありましたので、紹介させていただければと思います。

# Jira Software Cloudへの移行

今年経験した弊社の大きなイベントとして、Jira Software Cloudへの移行がありました。
Jira Software Cloudとは、チケット管理を基本とした、バグトラッキングや課題管理、プロジェクト管理に用いられるAtlassian社の製品です。
品質管理の業務に従事されている方にとっては、おなじみのツールではないかと思います。

審査業務においては、QA業務ほど多くのチケット起票は発生しないものの、できることへの工数削減として、移管とともに、Google Apps Script (以降GASと記載します)を利用した、チケット起票の自動化に取り組むことになりました。

# Jira Cloud REST APIとGASを使ったチケット起票

チケット起票の自動化については、公式ドキュメントとともにQiita内の以下記事を参考とさせていただきました。

- [GASを使ってJira課題を作成する](https://qiita.com/meowmeowcats/items/c7e77264e65581e625ad)
- [GASでJIRA Cloud REST APIを呼び出す](https://qiita.com/yu_grf/items/736063d92e5549be2cb1)

今回は、チケット起票に関する基本的な内容はすでに記事があることから、複数名で仕組みを利用することを前提とした上で、個人のアクセストークンをどのように管理するか、制作にあたり頭を悩ませた結果、[エンジニアさん](https://qiita.com/omomukumamani_piroshi)にアドバイスいただいた内容について、後述いたします。

# 個人のアクセストークンをどのように保管するか

今回、ツールを利用する際のAPI トークンは、最終的に、個人で発行可能なものを利用する方法としました。

- [Atlassian アカウントの API トークンを管理する](https://support.atlassian.com/ja/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/)

この方法に行き着くまでには、共通アカウントの利用なども検討しましたが、審査業務は、社内の多くのプロジェクトに横断的に関わることから、共通アカウントを利用するよりは、セキュリティの側面からも、すでにJiraのプロジェクトに対し、必要な範囲の権限を所持している、個人のアカウントで投稿を行えたほうがよいのではないかと、アドバイスをいただきました。

また、個人のトークンを、ツールを利用する他者に閲覧されることなく、登録する仕組みについてアドバイスをいただいた方法が、Class PropertiesServiceのUserPropertyに登録できる仕組みとする点でした。

- [プロパティ サービス](https://developers.google.com/apps-script/guides/properties?hl=ja)

# アクセストークンを登録･削除するためのコード

Jiraのアクセストークンを登録･削除する仕組みは、以下のようなコードで構築しました。
``` js
//以下をスプレッドシート起動時のトリガーとして設定することでUIにメニューを表示する
function createUi()
{
  SpreadsheetApp.getUi()
    .createMenu('メニュー')
      .addItem('アクセストークンを登録する', 'setJiraToken')
      .addItem('アクセストークンを削除する', 'deleteJiraToken')
      .addToUi()
}

var userProperties = PropertiesService.getUserProperties();
var jira_user_key = getUserKey();

//UserKeyは任意の接頭語とメールアドレスで生成する
function getUserKey()
{
  var jira_user_name = Session.getActiveUser().getEmail();
  var jira_user_key = 'JIRA_TOKEN_' + jira_user_name;
  return jira_user_key;
}

//JIRAトークンをUserPropertiesに登録する
function setJiraToken()
{
  //すでにトークンが登録されているかのチェックを行う。
  if (hasJiraToken(jira_user_key)) {
    if (Browser.msgBox("すでに登録が完了しています。既存の登録内容を上書きしますか?", Browser.Buttons.OK_CANCEL) != 'ok') {
      Browser.msgBox('登録を中止しました。終了します');
      return
    }else{
      userProperties.deleteProperty(jira_user_key);
    }
  }
  var input = Browser.inputBox('アクセストークンを入力してください\\n※アクセストークンは他の人へ共有しないでください', Browser.Buttons.OK_CANCEL) 

  if (input != '' || 'cancel') {
    var token = input
    userProperties.setProperty(jira_user_key, token);
      Browser.msgBox('アクセストークンを登録しました。終了します');
      return
  }else{
    Browser.msgBox('登録を中止しました。終了します');
  }
}

//user_keyに紐づくトークンがすでに登録されているかを確認する
function hasJiraToken(user_key)
{
  keys =  userProperties.getKeys();
  var ret = false
  keys.forEach(function(col){
    if (col == user_key) {
      ret = true
    }
  })
  return ret
}

//トークンの削除を行う仕組み
function deleteJiraToken()
{
  if (hasJiraToken(jira_user_key)) {
    if (Browser.msgBox("登録済みのアクセストークンを削除しますか?", Browser.Buttons.OK_CANCEL) != 'ok') {
      Browser.msgBox('中止しました。終了します');
      return
    }

    userProperties.deleteProperty(jira_user_key);
    Browser.msgBox('削除を行いました。終了します');
    return
  }else{
    Browser.msgBox('登録されているアクセストークンはありません。終了します');
  }
}

```

登録が行えているかの確認については、以下のコードで行いました。

``` js
function showUserProperties()
{
  Logger.log(userProperties.getProperties())
}
```

登録を行ったアカウントでの出力結果は以下となりました。
``` js
{JIRA_TOKEN_XXXXXX@XXXX.net=XXXXX}
```

登録を行ったアカウント以外での出力結果は以下となりました。

``` js
{}
```
確認の結果からも、個人のアクセストークンを、ツールを利用する他者に閲覧されることなく、登録することができていることが確認できました。

# チケット起票を行うコード

上記で登録を行ったアクセストークンを利用したチケット起票の仕組みは、以下のようなコードで構築しました。
なお、今回、JSON部分は、仮のものとして記載しております。実際の制作時は、以下のドキュメントを参考とし、スプレッドシートから取得したデータをもとに、構築いたしました。

- [JSON を使用した詳細なフィールド編集](https://support.atlassian.com/ja/cloud-automation/docs/advanced-field-editing-using-json/)

``` js
var scriptProperties = PropertiesService.getScriptProperties();
var userProperties = PropertiesService.getUserProperties();

function createJiraIssue()
{
  //JSON部分は仮の内容として作成
  var json = createJSON();
  
  try{
    //投稿結果(戻り値)は後続の結果をスプレッドシートに記録する動作などに用いる
    var result = postJiraIssue(json)
  }catch(e){
    console.error('エラーが発生しました:' + e["message"]);
  }
}

function createJSON()
{
  var ret = {
    "fields": {
      "summary": 'summary',
      "project": {
        "key": 'project_name'
      },
      "description":'XXXXX',
      "issuetype": {
        "id": 'XXXXX'
      }
    }
  }
  return JSON.stringify(ret);
}

function postJiraIssue(json)
{  
  //呼び出し先のAPIのURLを設定。なおJIRA_BASE_URLはスクリプトプロパティに登録
  var script_prop = scriptProperties.getProperties();
  var request_url = script_prop["JIRA_BASE_URL"] + 'rest/api/2/issue/';

  //optionsに含める情報構築のためメールアドレスを取得する
  var jira_user_name = Session.getActiveUser().getEmail();
  var jira_user_key = getUserKey();
  
  var user_prop = userProperties.getProperties();
  var token = Utilities.base64Encode(jira_user_name + ':' + user_prop[jira_user_key], Utilities.Charset.UTF_8);

  var options = {
    method: 'post',
    payload: json,
    contentType: 'application/json',
    headers: {'Authorization': ' Basic ' + token}, 
    muteHttpExceptions:true,
  }

  var response = UrlFetchApp.fetch(request_url, options);

  var response_code = response.getResponseCode();
  var response_body = response.getContentText();

  if (response_code != 201) {
    // 戻り値より取得したresponse_codeが201以外の場合はエラーの扱いとする
    throw new Error(`code:${response_code} responseBody:${response_body}`);
  }else{
    return JSON.parse(response_body);
  }
}

//UserKeyは任意の接頭語とメールアドレスで生成する
function getUserKey()
{
  var jira_user_name = Session.getActiveUser().getEmail();
  var jira_user_key = 'JIRA_TOKEN_' + jira_user_name;
  return jira_user_key;
}
```

# 取り組みを通じ得られた知見

今回アドバイスをいただいたUserPropertyを利用する仕組みは、その性質上、同一シートを複数名で利用するシーンが多いGoogle スプレッドシートを利用し、他の仕組みへ投稿を行う際に、セキュリティに配慮した方法でサービスに対し、アクセスに必要な情報を管理できる方法でした。

情報の管理に対する知識や意識付けは、社内研修で学ぶ機会があります。また、エンジニアリング･トライアルでは、ツールの設計や、コードの記述方法をアドバイスいただくことが多いですが、今回は、加えて、機密情報の管理に対する知識とその方法について、学ぶことができました。

セキュリティと言う言葉を聞く機会は多いですが、いざ、自分がツールを作成しているときに、特に初学者の自分が、どれだけセキュリティに配慮した設計とできているかについては、自己評価が難しいと感じます。今回は、制作の初期の段階で、アクセストークンの管理方法のアドバイスをいただけたことで、その重要さについて、改めて認識しました。

# さいごに
GASは身近なツールであり、Google スプレッドシートと組み合わせることで、多くの自動化が実現できます。
今年もエンジニアリング･トライアルを通じ、いくつかのツール制作に取り組ませていただくことで、できる｢自動化｣の方法をより増やすことができました。

牛の歩みも千里を心に、来年も知見を深めたいと考えています。
最後までお読みいただきありがとうございました。