Node.js で Jira の API から課題情報を取得する

30034 ワード

モチベーション

  • Jira の課題には StoryPoint を設定していたが、Jira の画面上だけでは必要な情報を必要な形で取ってこれなかった
  • 今回のスプリントや前回のスプリントでどれだけの StoryPoint を消化できたかを知りたかった(これは Jira のデフォルトの機能にもあるが、色々と絞り込み条件をカスタマイズしたかった)
  • プロジェクトの全体の StoryPoint を消化するためには、あと何スプリント必要なのかを知りたかった(プロジェクトの予測を数値にして報告するため)
  • Jira の課題を Excel に出力するプラグインはあるが、データが複雑なので、 Excel で頑張るよりも、API で取得して TypeScript でいじった方が簡単そうだった

上記の理由から、Jira の API から課題を取得することにしました。

API トークンを作成

まずは API トークンを作成します。

JIRA 画面の右上プロフィールをクリック
  ↓
設定 をクリック
  ↓
アカウントの設定(アカウントの環境設定)をクリック
  ↓
左側の「セキュリティ」タブをクリック
  ↓
API トークンの作成と管理 をクリック
  ↓
API トークンを作成する をクリック

ここで API トークンを作成し、コピーしておきます。

jira-client でアクセス

Jira の API は jira-client を使って叩きます。

npm i dotenv
npm i jira-client
npm i -D @types/jira-client

.env ファイルに以下を用意します。

JIRA_HOST="[会社のアカウント].atlassian.net"
MAIL_ADDRESS="ユーザー名に使っているメールアドレス"
JIRA_API_TOKEN="設定画面から取得したAPIトークン"
PROJECT_NAME="Jiraのプロジェクト名。 課題のJQLでproject = 'XXX'で指定されているやつ"

API から Jira の課題を取得するサンプルは以下のようになります。

import JiraApi, { JsonResponse } from "jira-client";
import dotenv from "dotenv";

dotenv.config();

const host = process.env.JIRA_HOST;
const mailAddress = process.env.MAIL_ADDRESS;
const apiToken = process.env.JIRA_API_TOKEN;
const projectName = process.env.PROJECT_NAME;

let jiraClient: JiraApi | undefined;
function getJiraClient() {
  if (
    host === undefined ||
    mailAddress === undefined ||
    apiToken === undefined
  ) {
    throw new Error("環境変数が設定されていません。");
  }
  if (jiraClient === undefined) {
    jiraClient = new JiraApi({
      protocol: "https",
      host,
      username: mailAddress,
      password: apiToken,
      apiVersion: "2",
      strictSSL: true,
    });
  } else {
    console.log("jiraClient はキャッシュされています。");
  }
  return jiraClient;
}

export async function getAllIssues() {
  const client = getJiraClient();
  if (client === undefined) {
    throw new Error("jiraClient が生成されていません。");
  }

  if (projectName === undefined) {
    throw new Error("プロジェクト名が環境変数に設定されていません。");
  }
  const jql = `project = "${projectName}" ORDER BY created DESC`;

  const jiraResponse: JsonResponse = await client.searchJira(jql, {
    startAt: 0,
    maxResults: 10,
  });

  const jira: Jira = {
    startAt: jiraResponse.startAt,
    maxResults: jiraResponse.maxResults,
    total: jiraResponse.total,
    issues: jiraResponse.issues,
  };

  console.log(jira.total);
}

以下の部分で maxResults を設定しています。
最大が 100 です。

await client.searchJira(jql, {
  startAt: 1,
  maxResults: 10,
});

そのため、100 以上の課題をすべて取得する場合は、 (total / maxResults) + 1 回分 リクエストを投げる必要があります。

Jira のレスポンスのうち、必要なものの型をつけていく

Jira からのレスポンスは any 型で定義されています。

型定義は以下です。

interface JsonResponse {
  [name: string]: any;
}

実際にレスポンスを見ても、以下のような不定形と思われるプロパティが多々あります。

customfield_10128: null,
customfield_10007: null,
customfield_10129: null,

これらのフィールド名は、「自分のプロジェクト」だけでなく、「社内の他のプロジェクトで設定したフィールド」も含まれています。

つまり、社内のプロジェクト間で Jira の使い方が統一されていない場合は、自分のプロジェクトにとっては不要な(他のプロジェクトで作成した)カスタムフィールドが大量に紛れ込んでくるということです。

自分のプロジェクトで使っていないフィールドの値は null になっていました。

その他にも issues の中に fields があって、 その fields が持っている parentissue と同じ型になっていて使いづらかったり、色々とフィールドが入れ子になっていたりと、そのままだと非常に使いにくいように感じました。

そんなわけで、型を自分で定義して、レスポンスを型にはめていけば、API から取得したデータの利用が楽になります。

業務的なの情報が入らないようにコードを抜粋すると、こんな感じに型を作ります。

lib/types/index.ts

export type Jira = {
  startAt: number
  maxResults: number
  total: number
  issues: Issue[]
}

export type Issue = {
  id: string
  key: string
  fields: Fields
}

export type Fields = {
  parent: Parent
  summary: string
  status: Status
  targetServices: TargetService[] // ここは会社特有のもの。fields.customfield_xxxx の値を入れる
  sprints: Sprint[]
  storyPoint: number  // fields.customfield_yyyy の値を入れる
  created: string
  updated: string
  description: string
  statuscategorychangedate: string
}

上の型をつける作業は、プロパティ(フィールド)が不定であるがゆえに、レスポンスの値を見ながら各自が頑張って設定することになります。

頑張って型をつけるサンプルは以下のようなイメージです。

lib/issues.ts
// サンプルです
function otharfunc() {
  const jira: Jira = {
    startAt: jiraResponse.startAt,
    maxResults: jiraResponse.maxResults,
    total: jiraResponse.total,
    issues: jiraResponse.issues,
  }

  // 型をつける関数を呼んでる
  const typedJira = getTypedJira(jira)
}



function getTypedJira(jira: any): Jira {
  const typedIssues: Issue[] = jira.issues.map((issue: Issue) => {
    const fields: any = issue.fields
    const sprints = fields.customfield_xxxx.map((sprint: any) => {
      return {
        name: sprint.name,
        state: sprint.state,
        startDate: sprint.startDate,
        endDate: sprint.endDate,
      }
    })
    const typedFields: Fields = {
      parent: {
        summary: fields.parent.fields.summary,
      },
      summary: fields.summary,
      status: fields.status,
      targetServices: fields.customfield_xxxx,
      sprints,
      storyPoint: fields.customfield_yyy,
      created: fields.created,
      updated: fields.updated,
      description: fields.description,
      statuscategorychangedate: fields.statuscategorychangedate,
    }
    return {
      id: issue.id,
      key: issue.key,
      fields: typedFields,
    }
  })
  const typedJira: Jira = {
    startAt: jira.startAt,
    maxResults: jira.maxResults,
    total: jira.total,
    issues: typedIssues,
  }
  return typedJira
}

上の例で型をつけた typedJira は元々の Jira のレスポンスが反映された形になっているので、ネストが多くて若干使いづらいです。

そのため、自分は typedJiraissues[n].fields から本当にほしい情報を取ってきて、別のオブジェクトに入れて使っています(文章だと伝わりにくいですね)

コード例を記事にしたいところですが、ここまでいくと仕事に寄りすぎてしまうので、泣く泣く割愛します。

各自いい感じに型をつけて Jira のレスポンスを使うのが良いと思います。

あとは個人的な工夫点として、型をつけた Jira のレスポンスはキャッシュしておく、JSON ファイルに日付をつけて保存しておくようにしました。

何度も Jira にリクエストを投げてもそんなに頻繁には結果は変わらないからです。

同じ日のレスポンスはキャッシュから、キャッシュがないときはローカルの JSON ファイルを読み込んで返すようにしました。