14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-10-14

はじめに

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

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

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

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

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

  1. コーダーがコーディングする
  2. レビューを頼む
  3. レビュワーがレビューする
  4. レビュワーがissueをたてる
  5. コーダーが修正しレビュワーに戻す
  6. レビュワーが再確認する
  7. 良ければ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

精査した結果

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

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で書いていきたいと思いますので、以下を使わせていただこうと思います。

Octokit Rest.jsを追加

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

$ yarn add @octokit/rest
index.mjs
import { Octokit } from "@octokit/rest";

Octokitインスタンスを作成

Personal access tokensを発行しておく

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

GitHubのSettingsDeveloper settingsPersonal access tokensから発行できます。
repoスコープにチェックを付ければOKです。

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

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

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

CRUDを実装する

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

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));
  }
}

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

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

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を作っておきます。

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"
  }
]

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

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

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

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

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

完成

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:

$ node index.mjs owner repository

まとめ

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

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

参考文献

14
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?