GitHubのissueのフロントエンド向けラベルを精査したので、OctokitでLabel登録を自動化した

# はじめに

最近レビュワーとして案件に入ることが多くなりました。

Slackで箇条書きにして送るなんてやりたくないので、使用するGit環境的に可能であればGitHubのissueで返すようにしています。
その際、「ラベルを貼ると分かりやすかったりする…？」と思いたち、弊社の作業の進め方にあったラベルを考えてみたので、まとめたいと思います。

また、そのラベルを毎回リポジトリごとにﾎﾟﾁﾎﾟﾁ登録するのはそれもまた面倒なので、登録を自動化したいマンの道筋を書き記します。

# フロントエンドで"使える"ラベルを考える

我々のよくある作業フローとしては、こんな感じです。

1. コーダーがコーディングする
1. レビューを頼む
1. レビュワーがレビューする
1. レビュワーがissueをたてる
1. コーダーが修正しレビュワーに戻す
1. レビュワーが再確認する
1. 良ければissue close ダメならコメントし5へ

外部のコントリビュータが入ってきたりというのはほとんどなく、このフローを加味して実際に使われる、使いやすいラベルを決めていきたいと思います。

## とりあえず検索検索:mag:

ざっと調べてみると、巷でおすすめとされるラベルはある程度共通点があるようで、

- Statusは作れ
- Typeは作れ
- Priorityは良いぞ
- Close理由もつけてやれよな

といった感じでした。

出来る限り扱いやすく、運用が続きやすいようにしたいため、出来る限りラベルの数は少なめでいきたいところです。
また、ラベルの付け替えのタイミングも減らしていきたいです。

## ラベルの精査

### Status

issueの現在の状況を指定します。

|Label|Description|ラベルをつける場面|
|:-|:-|:-|
|Status: Resolved|作業完了・解決|フロー7で作業完了しcloseしたとき|
|Status: Design Needed|デザイン中|デザインフェーズまで戻っているとき|
|Status: In Progress|対応中|フロー4のとき|
|Status: On Hold|保留|保留と判断されたとき|
|Status: Review Needed|レビュー待ち|フロー5のとき|
|Status: Confirm Needed|確認が必要|他者の確認が必要なとき|

ほとんど、他の方々が提唱してるものと同じですが、デザイン調整や画像調整などデザインフェーズまで戻っているときのステータスを追加しました。

On HoldとPendingの違いについてですが、同じ『保留』でもPendingは宙ぶらりんみたいな意味合いがあるようです。
よって検討した、しようとしたけど、一旦意思を持って保留と判断したという意味合いでOn Holdにしました。

### Type

issueのタイプを指定します。

|Label|Description|ラベルをつける場面|
|:-|:-|:-|
|Type: Bug|バグ|制作ページの閲覧に際して何かしらの問題があったとき|
|Type: Enhancement|機能追加|機能の追加・強化が求められるとき|
|Type: Improvement|要改善|コードの記述やロジックなどの改善が求められるとき|

### Problem

issueで提示された問題を指定します。
（フロントエンドらしさが出てるのはここくらいかもしれません）

|Label|Description|ラベルをつける場面|
|:-|:-|:-|
|Problem: Design|デザインに問題あり|デザインとの差異が発生していたり、見た目上の問題があるとき|
|Problem: Logic|ロジックに問題がある|JSなどのロジックに問題があるとき|
|Problem: Markup|HTMLマークアップに問題がある|HTMLのマークアップに問題があるとき|

### Priority

issueの解決優先度を指定します。

|Label|Description|ラベルをつける場面|
|:-|:-|:-|
|Priority: Critical|致命的|最も高い優先度のとき|
|Priority: Low|優先度低|作業を急がないと判断したとき|

なぜHighじゃないかというと、HighよりもCriticalの方が**なんかやばそう感**が出ていいかなと思ったからです。
また中間のMediumがありませんが、そこに優劣はなく直したほうが良いぞという意味合いになります。いちいちつけるのも面倒ですからね。

### Close

close系は特に設けません。
理由は、issueをcloseするときに、「 **as not planned** 」で閉じることができるためです。
![スクリーンショット 2022-10-14 17.50.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924363/ccce0016-18fa-4d7d-728e-239a4ce1897b.png)

## 精査した結果

以下のラベルを採用することとしました！
一旦これでしばらく運用してみようと思います。

|Label|Description|Hex Color|
|:-|:-|:-|
|Priority: Critical|致命的|`#b60205`|
|Priority: Low|優先度低|`#0e8a16`|
|Problem: Design|デザインに関する問題がある|`#622094`|
|Problem: Logic|ロジックに問題がある|`#d93f0b`|
|Problem: Markup|HTMLマークアップに問題がある|`#a2eeef`|
|Status: Resolved|作業完了・解決|`#0e8a16`|
|Status: Design Needed|デザイン中|`#bfdadc`|
|Status: In Progress|対応中|`#fef2c0`|
|Status: On Hold|保留|`#cfd3d7`|
|Status: Review Needed|レビュー待ち|`#c2e0c6`|
|Status: Confirm Needed|確認が必要|`#cfd3d7`|
|Type: Bug|バグ|`#b60205`|
|Type: Enhancement|機能追加|`#41a3fb`|
|Type: Improvement|要改善|`#7f5b3d`|

# ラベル登録の自動化

続いて、登録を自動化していきます。

世の中にはやはり便利なものがあるもので、GitHubにもGitHub APIをお手軽に使わせてくれる**Octokit**というラッパーライブラリが存在します。

今回はREST APIを使用したい、また、JavaScriptで書いていきたいと思いますので、以下を使わせていただこうと思います。

https://github.com/octokit/rest.js/

## Octokit Rest.jsを追加

何はともあれインストール。

```bash
$ yarn add @octokit/rest
```

```js:index.mjs
import { Octokit } from "@octokit/rest";
```

## Octokitインスタンスを作成

### Personal access tokensを発行しておく

Publicリポジトリの読み取りであれば不要ですが、いずれ書き込みも必要ですので、Personal Access Tokenを発行します。

GitHubの*Settings*→*Developer settings*→*Personal access tokens*から発行できます。
repoスコープにチェックを付ければOKです。

### インスタンス生成時にTokenを指定する

Octokitクラスのインスタンスを生成する際、コンストラクタでauthにTokenを指定します。

```js:index.mjs
const octokit = new Octokit({ auth: 'ghp_xxxxxxxxxxxxxxxxxxxxx' });
```

## CRUDを実装する

全部は使わないのでいらないのですが、なんとなーく揃ってると気持ちいい（主観）のでCRUDを作っておきます。

```js:index.mjs
class Labels {
  constructor(owner, repo, name = '', description = '', color = '') {
    this.owner = owner;
    this.repo = repo;
    this.name = name;
    this.description = description;
    this.color = color;

    this.handleError = (err) => {
      console.error(`[ERROR OCCURRED] ${err}`);
    };
  }

  async getLabel() {
    try {
      const gottenLabels = await octokit.request('GET /repos/{owner}/{repo}/labels/{name}', {
        owner: this.owner,
        repo: this.repo,
        name: this.name
      });
      console.log(gottenLabels.data);
    } catch (err) {
      this.handleError(err);
    }
  }

  async createLabel() {
    await octokit.request('POST /repos/{owner}/{repo}/labels', {
      owner: this.owner,
      repo: this.repo,
      name: this.name,
      description: this.description,
      color: this.color,
    }).catch(err => this.handleError(err));
  }

  async updateLabel() {
    await octokit.request('PATCH /repos/{owner}/{repo}/labels/{name}', {
      owner: this.owner,
      repo: this.repo,
      name: this.name,
      description: this.description,
      color: this.color,
    }).catch(err => this.handleError(err));
  }

  async deleteLabel() {
    await octokit.request('DELETE /repos/{owner}/{repo}/labels/{name}', {
      owner: this.owner,
      repo: this.repo,
      name: this.name,
    }).catch(err => this.handleError(err));
  }
}
```

## 複数追加できるようにする

このままだと１件ずつ追加することになり、手動でﾎﾟﾁﾎﾟﾁとなんら変わらないので、追加したいlabelを一覧にしたJSONを渡して複数追加出来るようにします。

```diff_javascript:index.mjs
+ import labelData from './labelDatas.json' assert { type: 'json' };

class Labels {
+ constructor(owner, repo, name, description = '', color = '', json = labelData) {
    this.owner = owner;
    this.repo = repo;
    this.name = name;
    this.description = description;
    this.color = color;
+    this.json = json;

    this.handleError = (err) => {
      console.log(`ERROR OCCURRED: ${err}`);
    };
  }

+  async batchCreateLabel() {
+    await Promise.all(this.json.map(async (element) => {
+      octokit.request('POST /repos/{owner}/{repo}/labels', {
+        owner: this.owner,
+        repo: this.repo,
+        name: element.name,
+        description: element.description,
+        color: element.color,
+      }).catch(err => this.handleError(err));
+    }));
+  }

}
```

あとは、追加登録したいラベルのJSONを作っておきます。
```json:labelData.json
[
  {
    "name": "Priority: Critical",
    "description": "致命的",
    "color": "b60205"
  },
  {
    "name": "Priority: Low",
    "description": "優先度低",
    "color": "0e8a16"
  },
  {
    "name": "Problem: Design",
    "description": "デザインに関する問題がある",
    "color": "622094"
  },
  {
    "name": "Problem: Logic",
    "description": "ロジックに問題がある",
    "color": "d93f0b"
  },
  {
    "name": "Problem: Markup",
    "description": "HTMLマークアップに問題がある",
    "color": "a2eeef"
  },
  {
    "name": "Status: Resolved",
    "description": "作業完了・解決",
    "color": "0e8a16"
  },
  {
    "name": "Status: Design Needed",
    "description": "デザイン中",
    "color": "bfdadc"
  },
  {
    "name": "Status: In Progress",
    "description": "対応中",
    "color": "fef2c0"
  },
  {
    "name": "Status: On Hold",
    "description": "保留",
    "color": "cfd3d7"
  },
  {
    "name": "Status: Review Needed",
    "description": "レビュー待ち",
    "color": "c2e0c6"
  },
  {
    "name": "Status: Confirm Needed",
    "description": "確認が必要",
    "color": "cfd3d7"
  },
  {
    "name": "Type: Bug",
    "description": "バグ",
    "color": "b60205"
  },
  {
    "name": "Type: Enhancement",
    "description": "機能追加",
    "color": "41a3fb"
  },
  {
    "name": "Type: Improvement",
    "description": "要改善",
    "color": "7f5b3d"
  }
]
```

## デフォルトのラベルを削除する

デフォルトで登録されているラベルを削除して、スッキリさせます。

:::note warn
2022/10/14現在で登録されているデフォルトラベルを元にしています。
:::

```diff_javascript:index.mjs
async batchDeleteDefaultLabels() {
  const defaultLabels = ['bug', 'documentation', 'duplicate', 'enhancement', 'good first issue', 'help wanted', 'invalid', 'question', 'wontfix'];
  await Promise.all(defaultLabels.map(async (element) => {
    octokit.request('DELETE /repos/{owner}/{repo}/labels/{name}', {
      owner: this.owner,
      repo: this.repo,
      name: element,
    }).catch(err => this.handleError(err));
  }));
}
```

## コマンドライン引数で値を渡せるようにする

CRUD作りはしましたが、普段使うのは「デフォルトラベルを削除して、ラベルを登録する」だけだと思うので、コマンドライン引数でこれらに必要な情報、つまりリポジトリのオーナー名と、リポジトリ名をコマンドライン引数で指定できるようにしてしまいます。

```js:index.mjs
const labels = new Labels(process.argv[2], process.argv[3]);
labels.batchDeleteDefaultLabels();
labels.batchCreateLabels();
```

## 完成

```js:index.mjs
import { Octokit } from "@octokit/rest";
import labelData from './labelDatas.json' assert { type: 'json' };

const octokit = new Octokit({ auth: 'ghp_xxxxxxxxxxxxxxxxxxx' });

class Labels {
  constructor(owner, repo, name = '', description = '', color = '', json = labelData) {
    this.owner = owner;
    this.repo = repo;
    this.name = name;
    this.description = description;
    this.color = color;
    this.json = json;

    this.handleError = (err) => {
      console.error(`ERROR OCCURRED: ${err}`);
    };
  }

  async getLabel() {
    try {
      const gottenLabels = await octokit.request('GET /repos/{owner}/{repo}/labels/{name}', {
        owner: this.owner,
        repo: this.repo,
        name: this.name
      });
      console.log(gottenLabels.data);
    } catch (err) {
      this.handleError(err);
    }
  }

  async createLabel() {
    await octokit.request('POST /repos/{owner}/{repo}/labels', {
      owner: this.owner,
      repo: this.repo,
      name: this.name,
      description: this.description,
      color: this.color,
    }).catch(err => this.handleError(err));
  }

  async batchCreateLabel() {
    await Promise.all(this.json.map(async (element) => {
      octokit.request('POST /repos/{owner}/{repo}/labels', {
        owner: this.owner,
        repo: this.repo,
        name: element.name,
        description: element.description,
        color: element.color,
      }).catch(err => this.handleError(err));
    }));
  }

  async updateLabel() {
    await octokit.request('PATCH /repos/{owner}/{repo}/labels/{name}', {
      owner: this.owner,
      repo: this.repo,
      name: this.name,
      description: this.description,
      color: this.color,
    }).catch(err => this.handleError(err));
  }

  async deleteLabel() {
    await octokit.request('DELETE /repos/{owner}/{repo}/labels/{name}', {
      owner: this.owner,
      repo: this.repo,
      name: this.name,
    }).catch(err => this.handleError(err));
  }

  async batchDeleteDefaultLabels() {
    const defaultLabels = ['bug', 'documentation', 'duplicate', 'enhancement', 'good first issue', 'help wanted', 'invalid', 'question', 'wontfix'];
    await Promise.all(defaultLabels.map(async (element) => {
      octokit.request('DELETE /repos/{owner}/{repo}/labels/{name}', {
        owner: this.owner,
        repo: this.repo,
        name: element,
      }).catch(err => this.handleError(err));
    }));
  }
}

const labels = new Labels(process.argv[2], process.argv[3]);
labels.batchDeleteDefaultLabels();
labels.batchCreateLabel();
```

あとはNode.jsで動かします。できた！:relaxed:

```bash
$ node index.mjs owner repository
```

# まとめ

私があると便利だなと思うラベルを考えて、一覧を作ってみました。
また、突貫工事かもしれませんが、とりあえずjsonの内容をlabel追加するところまで出来ました。

これからも内容を精査しつつ、適切にlabelを使ってレビューしていきたいと思います。

# 参考文献

https://medium.com/@dave_lunny/sane-github-labels-c5d2e6004b63

https://qiita.com/shun_tak/items/d363b7c5d9e8fa19dc6b

https://britecharts.github.io/britecharts/github-labels.html

https://dalezak.medium.com/github-pro-tips-for-your-development-team-9fe0f0a48afb