JIRAの自動化機能の料金体系が変更されて上限にあたったので対応した話

この記事は [SmartHR Advent Calendar 2023 シリーズ1](https://qiita.com/advent-calendar/2023/smarthr) の24日目の記事です。
この記事では、JIRAの自動化機能の料金体系が変更されて利用上限にあたった際の対応内容を紹介します。

忙しい方のために先に結論を書いておくと、JIRAの自動化機能で行っていたプロセスを、Webhookを外部に飛ばし、JIRA APIをコールして同じプロセスを実現したという話です。

## 背景
SmartHRでは、多くの開発チームでJIRAを使用してタスク管理を行っており、いくつかのプロダクトでLeSS(Large Scale Scrum)という開発手法を使っています。
複数のチームに割り振られたタスクを1つのJIRAボードで管理し、各チームごとにラベルで絞り込みを行いチームのタスクを管理しています。

スプリントプランニングでタスクを詳細化する過程で、タスクをサブタスクに分割しています。作成されたサブタスクには親タスクの情報はコピーされないため、サブタスクを同じチームのボードで管理するためには、サブタスクの作成のたびにチームのラベルを付ける必要があります。毎回そんなことをするのは大変なので、JIRAの自動化機能を使って自動付与していました。
その他にも細かいオートメーションを実行しており、全体で約3,000回/月ほど実行されていました。

## 料金体系の変更で起こったこと
JIRAの自動化機能は、もともとはマルチプロジェクトとグローバルのオートメーションのみが制限対象で、実行上限はStandardプランで500回でした。今回の変更で全てのオートメーションが制限対象となり、その代わりに実行上限が1,700回になっています。
今まで利用していたオートメーションは全て単一プロジェクトのオートメーションだったため上限が無く、気兼ねなく使っていたところ、今回の変更で突然上限にあたってしまい、プランを上げるか実行回数を半分近く削るという対応を余儀なくされました…

詳細な変更内容は公式のお知らせを参照ください。
[Jira Cloud における自動化機能の使用体系が新しくなります | Atlassian Japan 公式ブログ | アトラシアン株式会社](https://www.atlassian.com/ja/blog/introducing-our-new-packaging-model-for-jira-cloud-automation)

## 対応方法
実行回数の多くを占めていた「サブタスクに親タスクのラベルをコピーする」自動化機能を以下の仕組みに置き換えました。

- タスクが作成された際にWebhookを飛ばす
- Webhookを受け取り、対象がサブタスクの場合は親タスクのラベルを取得し同じものをサブタスクにも付けるようAPIで変更を加える

これでオートメーションの実行回数にはカウントされず、同様の機能を実現することができます。


### サンプルコード
検証のために作成したGoogle Apps Sciriptのコードです。
実際は社内で動いているRailsサーバーで同じ仕組みを稼働させています。

```js
const JIRA_API_ENDPOINT = 'https://sample.atlassian.net/rest/api/3/issue';
const JIRA_USERNAME = 'JIRA_USERNAME';
const JIRA_API_TOKEN = 'JIRA_API_TOKEN';

function doPost(e) {
  const requestBody = JSON.parse(e.postData.contents);
  if (isSubTaskCreationEvent(requestBody)) {
    const subTaskKey = requestBody.issue.key;
    const parentTaskKey = requestBody.issue.fields.parent.key;

    const labels = getLabelsFromParentTask(parentTaskKey);

    if (labels.length > 0) {
      addLabelsToIssue(subTaskKey, labels);
    }
  }

  return ContentService.createTextOutput('success');
}

// Webhookの内容がサブタスクの作成であるかを確認する
function isSubTaskCreationEvent(requestBody) {
  return requestBody.webhookEvent === 'jira:issue_created' && requestBody.issue.fields.issuetype.subtask;
}

// 親タスクからラベルを取得
function getLabelsFromParentTask(parentTaskKey) {
  const url = `${JIRA_API_ENDPOINT}/${parentTaskKey}`;
  const headers = {
    'Authorization': 'Basic ' + Utilities.base64Encode(`${JIRA_USERNAME}:${JIRA_API_TOKEN}`)
  };

  const response = UrlFetchApp.fetch(url, { headers: headers });
  const parentTaskData = JSON.parse(response.getContentText());

  return parentTaskData.fields.labels || [];
}

// サブタスクにラベルを追加
function addLabelsToIssue(issueKey, labels) {
  const url = `${JIRA_API_ENDPOINT}/${issueKey}`;
  const headers = {
    'Authorization': 'Basic ' + Utilities.base64Encode(`${JIRA_USERNAME}:${JIRA_API_TOKEN}`),
    'Content-Type': 'application/json'
  };
  
  const labelsParam = labels.map(function(label) {
    return { add: label };
  });
  const payload = JSON.stringify({ update: { labels: labelsParam } });
  UrlFetchApp.fetch(url, { method: 'put', headers: headers, payload: payload });
}

```

## おわりに
対応してみれば大したことは行っていないのですが、回避策を思いつくまで頭を悩ませていました。もし同じ問題にぶつかって困っている人がいたら、解決のヒントになれば幸いです。