Jira Cloud REST APIとGASを使ってJQLで指定した条件にマッチする課題を取得する(OAuth版)

この記事は MicroAd Advent Calendar 2023 の8日目の記事です。

https://qiita.com/advent-calendar/2023/microad

# 背景と目的
以前、APIトークンを使用してBasic認証でJira Cloud REST APIを実行し、Jiraの課題を取得する方法を紹介しました。

https://qiita.com/cemaimai/items/ecb716654a2fc1c45a72

その後、Googleから公開されているGASのOAuthライブラリを使用してOAuthの処理を行う仕組みに改良しましたので、今回はその内容を紹介しようと思います。

# 参考ドキュメント
今回の改良を行う上で以下の公式ドキュメントや記事を参考にさせていただきました。
ありがとうございました！

https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/

https://www.pre-practice.net/2022/12/jira-api-oauth20add-offlineaccess-to.html

https://qiita.com/TakeshiNickOsanai/items/62810b0e96bf37bd0eca

https://tonari-it.com/gas-oauth2-get-access-token/

# 全体の流れ
今回の大まかな流れです。

- 取得した情報を書き出すGoogleスプレッドシートとGASプロジェクトの用意
- Atlassian DeveloperサイトでOAuth 2.0 Integrationアプリの設定
- GASにスクリプト プロパティを設定
- GASにOAuthライブラリを追加
- GASのコード作成
- OAuth処理の実行
- JQLで指定した条件にマッチするJira課題の取得

# 作業詳細

## GoogleスプレッドシートとGASプロジェクトの用意
1. 新規でGoogleスプレッドシートを作成し、任意の名前を設定します。
今回は「無題のスプレッドシート」のままにしています。
1. rawDataとconfigというシートを作成します。
rawDataシートは取得したJira課題を書き出すのに使用します。
configシートはJQLを指定するために使用します。
1. [拡張機能] > [Apps Script]をクリックしてGASプロジェクトを開き、任意の名前を設定します。
今回は「無題のプロジェクト」のままにしています。
1. ⚙️([プロジェクトの設定])アイコンをクリックし、 `スクリプトID` をコピーします。
`スクリプトID`は次項のコールバック関数の設定で使用します。
![スクリプトIDのコピー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/0c9550c1-b755-b415-01f0-92661e88be14.png)


## OAuth 2.0 Integrationアプリの設定

[Atlassian Developer](https://developer.atlassian.com/)サイトの`developer console`でアプリの設定を行います。
1. [developer console](https://developer.atlassian.com/console/myapps/)にアクセスします。

1. [Create] > [OAuth 2.0 integration]をクリックします。
![OAuthアプリの作成1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/50344382-7f62-7732-e336-2f2c098b1247.png)

1. [Name]に任意の名前を入力し、「I agree to be bound by Atlassian's developer terms.」のチェックボックスにチェックを入れ、[Create]をクリックします。
![OAuthアプリの作成2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/712619fe-b88e-9db0-b221-08c659482c79.png)

1. 自分以外にもこのアプリを共有する場合、[Distribution] > [Edit]をクリックして必要な設定を行います。
![Distributionの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/4f1b6925-7748-7c2d-cbcd-ebb67271dae2.png)

1. [Permissions]をクリックし、使用したいAPIの[Add]をクリックします。
今回はJira Cloudの[api-rest-api-3-search-post](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-post)を使用したいので、Jira APIの[Add]をクリックします。
![Permissionsの設定1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/b108b994-fd6e-0565-d3e6-aa0a1a9b694b.png)

1. 表示が[Add]から[Configure]に変わったら[Configure]をクリックします。
![Permissionsの設定2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/bb1d41ca-f6ec-b47c-9985-202e4671810f.png)

1. [Classic scopes]タブまたは[Granular scopes]タブをクリックした後、[Edit Scopes]をクリックします。
今回は[Granular scopes]でスコープを設定するので[Granular scopes]ダブをクリックした後に[Edit Scopes]をクリックします。
![Permissionsの設定3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/253d2e86-a02e-d592-c6e7-7156c532af61.png)

1. 使用したいAPIで要求されるスコープにチェックを入れ、[Save]をクリックします。
今回は[Granular scopes]で `read:issue-details:jira` `read:field.default-value:jira` `read:field.option:jira` `read:field:jira` `read:group:jira` をチェックします。
![Permissionsの設定4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/7a23f7ec-ea47-c5e1-c52b-c862e0ec00a0.png)

1. [Authorization] > [Add]をクリックします。
![Authorizationの設定1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/f918448a-6e0b-bb0c-a992-24becbb8aba8.png)

1. [Callback URL]に`https://script.google.com/macros/d/{GASのスクリプトID}/usercallback`を入力し、[Save changes]をクリックします。
`{GASのスクリプトID}`には前項でコピーしておいたGASの`スクリプトID`を設定します。
![Authorizationの設定2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/55c9ab31-bb7a-2809-6a40-b1a41a54bd1f.png)

1. [Settings]をクリックし、`Client ID`と`Secret`をコピーします。
`Client ID`と`Secret`はOAuthライブラリで使用します。

以上でアプリの設定は完了です。

## スクリプト プロパティの設定
OAuth処理に必要な情報をGASのスクリプト プロパティに追加します。
今回はプロパティ名を`KEYS`とし、値に以下のような内容を設定します。
```json
{
    "serviceName": "Atlassian",
    "clientId": "前項でコピーしたClient ID",
    "secret": "前項でコピーしたSecret",
    "authUrl": "https://auth.atlassian.com/authorize",
    "scope": "前項で指定したスコープ",
    "tokenUrl": "https://auth.atlassian.com/oauth/token",
    "redirectUrl": "前項で設定したCallback URL"
}
```

- serviceName: 任意のサービス名
- clientId: 前項でコピーした`Client ID`
- secret: 前項でコピーした`Secret`
- scope: 前項で指定したスコープをスペース区切りで設定
`"read:issue-details:jira read:field.default-value:jira read:field.option:jira read:field:jira read:group:jira"`
    - リフレッシュトークンを利用したい場合は上記に`offline_access`も追加してください。アプリ側の設定は不要です。
- redirectUrl: 前項で設定したCallback URL
`https://script.google.com/macros/d/{GASのスクリプトID}/usercallback`

1. GASの⚙️([プロジェクトの設定])アイコンをクリックします。
1. [スクリプト プロパティを追加]をクリックします。
1. プロパティと値を入力し、[スクリプト プロパティを保存]をクリックします。
![スクリプト プロパティの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/afa7d371-9267-35c3-f568-c7df7923017b.png)

## GASへのOAuthライブラリ追加
GoogleがGitHubで配布している[googleworkspace/apps-script-oauth](https://github.com/googleworkspace/apps-script-oauth2)を使用します。
ライブラリの使用方法について[README.md](https://github.com/googleworkspace/apps-script-oauth2/blob/main/README.md)を参照しています。

:::note info
前項で設定しているコールバック関数もREADME.mdを参照して設定しています。
:::

1. GASの`< >`([エディタ])アイコンをクリックします。
1. [ライブラリ]の`+`ボタンをクリックします。
![ライブラリの追加1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/09acd1a1-94a1-9454-e89f-7f90e6f17e33.png)

1. [スクリプト ID]に`1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
`をコピペし、[検索]をクリックします。
![ライブラリの追加2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/7d426bc5-51fa-6cb0-524e-7421b2032121.png)

1. [バージョン]を最新にし、[追加]をクリックします。

以上でOAuthライブラリの追加は完了です。

## GASのコード作成
必要な処理のコードを作成します。
以下、今回のために作成したサンプルコードを紹介し、処理について解説します。

### サンプルコード
<details><summary>展開してサンプルコードを表示する</summary>

```javascript:サンプルコード
const KEYS = JSON.parse(PropertiesService.getScriptProperties().getProperty('KEYS'));

/**
 * サービスを生成する
 */
function getService() {
  const service = OAuth2.createService(KEYS.serviceName)
    .setAuthorizationBaseUrl(KEYS.authUrl)
    .setTokenUrl(KEYS.tokenUrl)
    .setClientId(KEYS.clientId)
    .setClientSecret(KEYS.secret)
    .setCallbackFunction('authCallback')
    .setPropertyStore(PropertiesService.getUserProperties())
    .setScope(KEYS.scope)
    .setParam('response_type', 'code')
    .setParam('audience', 'api.atlassian.com')
    .setParam('prompt', 'consent');
  return service;
}

/**
 * サービスをリセットする
 */
function resetService() {
  OAuth2.createService(KEYS.serviceName)
    .setPropertyStore(PropertiesService.getUserProperties())
    .reset();
}

/**
 * コールバック関数
 */
function authCallback(request) {
  const service = getService();
  const isAuthorized = service.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('処理に成功しました。このタブを閉じてください。');
  } else {
    return HtmlService.createHtmlOutput('アクセスが拒否されました。このタブを閉じてください。');
  }
}

/**
 * 認可処理を実行する
 */
function getAuthorization() {
  const service = getService();
  if (service.hasAccess()){
    return Browser.msgBox('認可済みです。');
  }
  const authraizationUrl = service.getAuthorizationUrl();
  const html = '<a href="' + authraizationUrl + '" target="_blank">アプリの認可処理を行う</a><p>処理が完了したら閉じてください。</p>' ;
  const hyperLink = HtmlService.createHtmlOutput(html);
  return SpreadsheetApp.getUi().showModelessDialog(hyperLink, 'API連携');
}

/**
 * 認可をリセットする
 */
function resetAuthorization() {
  resetService();
  return Browser.msgBox('リセットしました。');
}

/**
 * アクセストークンの取得
 */
function getAccessToken() {
  const service = getService();
  return service.getAccessToken();
}

/**
 * リクエストを送信
 */
function sendRequest(method, resource, params) {
  const accessToken = getAccessToken();
  let options = {
    'method': method,
    'headers': {
      'Accept': 'application/json',
      'Authorization': 'Bearer ' + accessToken
    },
    'muteHttpExceptions': true
  };

  let url = resource;
  switch(method){
    case 'GET':
      if (params!==null){
        url += '?' + Object.keys(params).map(function(key) {
          return key + '=' + params[key];
        }).join('&');
      }
      break;

    case 'POST':
      options['headers']['Content-Type'] = 'application/json';
      options['payload'] = params;
      break;
  }

  const response = UrlFetchApp.fetch(url, options);
  const responseCode = response.getResponseCode();
  const responseBodyJson = response.getContentText();

  if ([200, 201].indexOf(responseCode) < 0){
    const error = responseCode + ' : ' + responseBodyJson;
    throw new Error(error);
  }

  if (responseBodyJson===""){
    return {'code': responseCode};
  } else {
    return JSON.parse(responseBodyJson);
  }
}

/**
 * cloudidの取得 
 */
function getAccessibleResources() {
  const method = 'GET';
  const resource = 'https://api.atlassian.com/oauth/token/accessible-resources';
  const response = sendRequest(method, resource, null);
  return response;
}

/**
 * JQLで指定した条件にマッチするJira課題を取得
 */
function getIssuesbySearch(jql) {
  const method = 'POST';
  const resource = 'https://api.atlassian.com/ex/jira/' + (getAccessibleResources())[0].id + '/rest/api/3/search';
  const maxResults = 100;
  let startAt = 0;
  let total = 0;
  try{
    if (jql==""){
      throw new Error("jqlが設定されていません。設定して実行してください。");
    }

    let issues = [];
    do {
      let params = {
        "maxResults" : maxResults, // 1回に最大100件まで取得可能
        "startAt" : startAt,       // オフセット
        "jql" : jql                // JQL
      };
      const response = sendRequest(method, resource, JSON.stringify(params));
      issues = issues.concat(response.issues);
      total = response.total;
      startAt += maxResults;
    } while(total>startAt)

    return issues;
  } catch(e) {
    return {"error": e.message};
  }
}

/**
 * JQLで指定した条件にマッチするJira課題の取得とrawDataシートへの書き込み
 */
function updateIssuesSheet() {
  const jql = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('config').getRange('A2').getValue();
  const issues = getIssuesbySearch(jql);
  let records = [];

  for (const issue of issues){
    const key = '=HYPERLINK("https://your-domain.atlassian.net/browse/' + issue.key + '","' + issue.key + '")';
    const issueType = issue.fields.issuetype.name;
    const summary = issue.fields.summary;
    const reporter = '=HYPERLINK("https://your-domain.atlassian.net/jira/people/' + issue.fields.reporter.accountId + '","' + issue.fields.reporter.displayName + '")';
    const assignee = '=HYPERLINK("https://your-domain.atlassian.net/jira/people/' + issue.fields.assignee.accountId + '","' + issue.fields.assignee.displayName + '")';
    const priority = issue.fields.priority.name;
    const dueDate = issue.fields.duedate;
    const status = issue.fields.status.name;
    const created = Utilities.formatDate(new Date(issue.fields.created), 'JST', 'yyyy-MM-dd');
    let resolutionDate = '';
    if (issue.fields.resolutiondate!=null){
      resolutionDate = Utilities.formatDate(new Date(issue.fields.resolutiondate), 'JST', 'yyyy-MM-dd');
    }
    records.push([key, issueType, summary, reporter, assignee, priority, dueDate, status, created, resolutionDate]);
  }
  records.unshift(['key', 'issuetype', 'summary', 'reporter', 'assignee', 'priority', 'duedate', 'status', 'created', 'resolutiondate']);

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('rawData');
  sheet.getDataRange().clearContent();
  sheet.getRange(1,1,records.length,records[0].length).setValues(records);
}

/**
 * メニューの追加
 */
function onOpen() {
  let ui = SpreadsheetApp.getUi();
  ui.createMenu('Atlassian')
    .addSubMenu(
      ui.createMenu('API連携')
      .addItem('認可処理を実行する', 'getAuthorization')
      .addItem('リセットする', 'resetAuthorization')
    )
    .addSubMenu(
      ui.createMenu('シート管理')
      .addItem('rawDataシートを最新にする', 'updateIssuesSheet')
    )
  .addToUi();
}
```
</details>

### 処理の解説

#### 追加したスクリプト プロパティの取得
以下でスクリプト プロパティに追加した`KEYS`の値を取得します。
```javascript
const KEYS = JSON.parse(PropertiesService.getScriptProperties().getProperty('KEYS'));`
```

#### サービスの生成
OAuth処理に必要な値を設定し、サービスを生成します。
```javascript
function getService() {
  const service = OAuth2.createService(KEYS.serviceName)
    .setAuthorizationBaseUrl(KEYS.authUrl)
    .setTokenUrl(KEYS.tokenUrl)
    .setClientId(KEYS.clientId)
    .setClientSecret(KEYS.secret)
    .setCallbackFunction('authCallback')
    .setPropertyStore(PropertiesService.getUserProperties())
    .setScope(KEYS.scope)
    .setParam('response_type', 'code')
    .setParam('audience', 'api.atlassian.com')
    .setParam('prompt', 'consent');
  return service;
}
```

`setCallbackFunction`に設定している`authCallback`は、サンプルコード内のコールバック関数の名前になります。

```javascript
function authCallback(request) {
  const service = getService();
  const isAuthorized = service.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('処理に成功しました。このタブを閉じてください。');
  } else {
    return HtmlService.createHtmlOutput('アクセスが拒否されました。このタブを閉じてください。');
  }
}
```


また、[公式ドキュメント](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#:~:text=The%20query%20parameters%20for%20the%20authorization%20URL%20are%20described%20below%3A)の以下の記述に従い、3つのパラメーターを`setParam`で設定しています。

> `response_type`: (_required_) Set to `code` as you are requesting an authorization code (not a token).
> `audience`: (_required_) Set this to `api.atlassian.com`.
> `prompt`: (_required_) Set to `consent` so that the screen prompting the user to grant access will display.


また、以下のような記述があり、`state`はOAuthライブラリで自動生成されるため設定していません。

> `state`: (_required for security_) Set this to a value that is associated with the user you are directing to the authorization URL, for example, a hash of the user's session ID. Make sure that this is a value that cannot be guessed. You may be able to generate and validate this value automatically, if you are using an OAuth 2.0 client library or an authentication library with OAuth 2.0 support.


#### 認可処理を行うためのリンクを表示

```javascript
function getAuthorization() {
  const service = getService();
  if (service.hasAccess()){
    return Browser.msgBox('認可済みです。');
  }
  const authraizationUrl = service.getAuthorizationUrl();
  const html = '<a href="' + authraizationUrl + '" target="_blank">アプリの認可処理を行う</a><p>処理が完了したら閉じてください。</p>' ;
  const hyperLink = HtmlService.createHtmlOutput(html);
  return SpreadsheetApp.getUi().showModelessDialog(hyperLink, 'API連携');
}
```

認可処理用URLを取得し、そのURLへのリンクを含んだダイアログをGoogleスプレッドシートに表示させます。

#### cloudidの取得
OAuthでJira Cloudの[api-rest-api-3-search-post](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-post)を実行する場合、
`https://your-domain.atlassian.net/rest/api/3/search`
ではなく
`https://api.atlassian.com/ex/jira/{cloudid}/rest/api/3/search`
というURLになり、`cloudid`が必要になります。

`cloudid`を取得するための処理が`getAccessibleResources`になります。

```javascript
function getAccessibleResources() {
  const method = 'GET';
  const resource = 'https://api.atlassian.com/oauth/token/accessible-resources';
  const response = sendRequest(method, resource, null);
  return response;
}
```

サンプルコードでは`(getAccessibleResources())[0].id`が`cloudid`になります。

#### JQLで指定した条件にマッチするJira課題の取得
Basic認証版では[api-rest-api-3-search-get](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-get)(GET)を使用していましたが、今回は[api-rest-api-3-search-post](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-post)(POST)を使用しています。

これは

> If the JQL query expression is too large to be encoded as a query parameter, use the POST version of this resource.

という記述に基づき、長いJQLにも対応できるようにPOSTのAPIに変更しました。

```javascript
function getIssuesbySearch(jql) {
  const method = 'POST';
  const resource = 'https://api.atlassian.com/ex/jira/' + (getAccessibleResources())[0].id + '/rest/api/3/search';
  const maxResults = 100;
  let startAt = 0;
  let total = 0;
  try{
    if (jql==""){
      throw new Error("jqlが設定されていません。設定して実行してください。");
    }

    let issues = [];
    do {
      let params = {
        "maxResults" : maxResults, // 1回に最大100件まで取得可能
        "startAt" : startAt,       // オフセット
        "jql" : jql                // JQL
      };
      const response = sendRequest(method, resource, JSON.stringify(params));
      issues = issues.concat(response.issues);
      total = response.total;
      startAt += maxResults;
    } while(total>startAt)

    return issues;
  } catch(e) {
    return {"error": e.message};
  }
}
```

1回のリクエストで取得できる最大件数が100件のため、マッチしたJira課題が100件以上でも全て取得できるように do-while で処理を繰り返しています。


#### JQLで指定した条件にマッチするJira課題の取得とrawDataシートへの書き込み
```javascript
function updateIssuesSheet() {
  const jql = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('config').getRange('A2').getValue();
  const issues = getIssuesbySearch(jql);
  let records = [];

  for (const issue of issues){
    const key = '=HYPERLINK("https://your-domain.atlassian.net/browse/' + issue.key + '","' + issue.key + '")';
    const issueType = issue.fields.issuetype.name;
    const summary = issue.fields.summary;
    const reporter = '=HYPERLINK("https://your-domain.atlassian.net/jira/people/' + issue.fields.reporter.accountId + '","' + issue.fields.reporter.displayName + '")';
    const assignee = '=HYPERLINK("https://your-domain.atlassian.net/jira/people/' + issue.fields.assignee.accountId + '","' + issue.fields.assignee.displayName + '")';
    const priority = issue.fields.priority.name;
    const dueDate = issue.fields.duedate;
    const status = issue.fields.status.name;
    const created = Utilities.formatDate(new Date(issue.fields.created), 'JST', 'yyyy-MM-dd');
    let resolutionDate = '';
    if (issue.fields.resolutiondate!=null){
      resolutionDate = Utilities.formatDate(new Date(issue.fields.resolutiondate), 'JST', 'yyyy-MM-dd');
    }
    records.push([key, issueType, summary, reporter, assignee, priority, dueDate, status, created, resolutionDate]);
  }
  records.unshift(['key', 'issuetype', 'summary', 'reporter', 'assignee', 'priority', 'duedate', 'status', 'created', 'resolutiondate']);

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('rawData');
  sheet.getDataRange().clearContent();
  sheet.getRange(1,1,records.length,records[0].length).setValues(records);
}
```

GoogleスプレッドシートのconfigシートA2セルに入力されている内容をJQLとして取得するようにしています。

`updateIssuesSheet`関数内の`your-domain`はご自身のドメインに書き換えてください。

サンプルコードでは取得したJira課題の以下のフィールドの値をrawDataシートに書き込むようにしていますので、必要に応じて適宜カスタマイズしてください。
- キー
- 課題タイプ
- 要約
- 報告者
- 担当者
- 優先度
- 期日
- ステータス
- 作成日
- 解決日

#### メニューの追加

```javascript
function onOpen() {
  let ui = SpreadsheetApp.getUi();
  ui.createMenu('Atlassian')
    .addSubMenu(
      ui.createMenu('API連携')
      .addItem('認可処理を実行する', 'getAuthorization')
      .addItem('リセットする', 'resetAuthorization')
    )
    .addSubMenu(
      ui.createMenu('シート管理')
      .addItem('rawDataシートを最新にする', 'updateIssuesSheet')
    )
  .addToUi();
}
}
```

Googleスプレッドシートに[Atlassian]という名前でメニューを追加しています。

## OAuth処理を実行
1. Googleスプレッドシートに追加された[Atlassian]メニュー > [API連携] > [認可処理を実行する] をクリックします。
![認可処理を実行する1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/c9be5c97-5fc0-dc46-c660-dd3d69d007f1.png)


1. **(初回のみ)** 「認証が必要です」という画面が表示されますので[OK]をクリックして画面に従って認証を行い、認証後に再度[Atlassian]メニュー > [API連携] > [認可処理を実行する] をクリックします。
![認証する1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/1c500910-288a-cf8c-91d1-6b5d79f991c2.png)
![認証する2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/bb12413d-e981-0352-bf73-2bac2cefb657.png)
![認証する3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/b4a745b5-f5a2-d179-b093-7e0757d8239e.png)

1. [アプリの認可処理を行う]をクリックします。
![認可処理を実行する2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/1bd50e89-dec6-e38b-f762-9aa09a031efc.png)

1. 新しいタブでアプリの認可画面が開きますので内容を確認して[Accept]をクリックします。
![認可処理を実行する3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/f4433cac-7fe9-87a8-7fc5-48c08a7c2319.png)

1. 「処理に成功しました。このタブを閉じてください。」と表示されたら☓ボタンをクリックしてタブを閉じます。
1. 3の画面を右上の☓ボタンをクリックして閉じます。

以上で認可処理は完了です。


## JQLで指定した条件にマッチするJira課題の取得
今回はJQLとして以下を入力します。
```SQL
assignee = currentUser() AND created >= -30d order by created DESC
```

1. GoogleスプレッドシートのconfigシートA2セルにJQLを入力します。 
![JQLの入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/792eaa57-19fa-d65f-0f49-25f4b4fcf872.png)

1. Googleスプレッドシートの[Atlassian]メニュー > [シート管理] > [rawDataシートを最新にする] をクリックします。
![rawDataシートを最新にする1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/824821b8-c8ed-cc52-ebd5-a89ba428d280.png)

1. 処理が完了するとrawDataシートに取得したJira課題の情報が書き込まれます。
![rawDataシートを最新にする2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234372/e7117b81-d456-816b-8662-ff024cd0bfed.png)

:::note warn
「Error: Access not granted or expired.」になる場合は認可処理を再度実行してください。
:::

以上ですべての作業が完了です。


# まとめ
今回はOAuthでJira Cloud REST APIを実行する方法を紹介させていただきました。
ブログを書きながら「OAuthと認証と認可が適切に使い分けできていないなぁ。。。」と感じていますので、今後理解を深めたいと思います。

私の所属部署ではJira課題の取得以外にも

- Atlassianユーザーグループのメンバー一括更新(追加・削除)
- Confluenceスペースと権限のリスト化
- Jiraフィールド情報のリスト化

などの機能を追加して業務ツールとして利用しています。
おすすめの利用方法があれば是非教えてください！

最後まで読んでいただきありがとうございました！