GitHubのラベルを定義ファイルで管理したい

GitHubのIssueやプルリクエストにラベルを付けると、便利ですよね。
ラベルの種類はリポジトリ作成時にデフォルトで用意されますが、カスタマイズすることも多いかと思います。

私は大抵のプロジェクトで共通した最低限用意しておきたいラベル群があるのですが、
それなりな量があり、リポジトリを新しく作成する度に、Web画面上でポチポチ追加するのが大変手間です。

GitHub Organazation のデフォルトラベルの機能 [^1] を利用するのも良いのですが、Organizationオーナー権限が必要であったり、と、少し大ごとです。
[^1]: "Organization 内のリポジトリのためのデフォルトラベルを管理する" https://docs.github.com/ja/github/setting-up-and-managing-organizations-and-teams/managing-default-labels-for-repositories-in-your-organization

![img-01-01_intro.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700291/796d7f02-7dde-d198-b801-4d7e65143791.jpeg)

この投稿では、私が現在用いている、定義ファイルとGitHub Actionを用いたラベル管理の方法をご紹介します。
特別な環境や権限等は不要で、気軽に始められるので、個人的に気に入っています。

誰かのお役に立てれば幸いです。


## やりたいこと

* GitHubリポジトリのラベル設定を、自リポジトリ内に配置したラベル定義ファイルによって管理
* 手動での変更（GitHubのWeb画面上など）にも追従できる・しやすいように
* 特別な権限・環境は必要としない


## 方法

* GitHub Actionで、以下の処理を行います。
    * ラベル定義ファイルの変更時: 定義ファイルからリポジトリのラベル設定に反映
    * Web UIなどからラベル設定を変更した時: （変更後の）既存リポジトリのラベル設定から、定義ファイルを出力（プルリクエストを作成）
    * GitHub Actionの手動実行時: 既存リポジトリのラベル設定を、定義ファイルを出力（プルリクエストを作成）
    
* ラベル管理のための主要な処理は、pythonツール・ライブラリ [hackebrot / labels](https://github.com/hackebrot/labels) を利用させてもらっています。

* ファイル構成はこのようになります。

    ```
    .github/labels.toml  ← ラベル定義ファイル
    .github/workflows/config-gh-labels.yml  ← ラベル管理のGitHub Actionワークフロー定義ファイル
    ```

### 導入手順

* GitHubリポジトリに`.github/workflows/config-gh-labels.yml`を設置

    ```yaml:.github/workflows/config-gh-labels.yml
    name: Configure GitHub labels

    on:
      pull_request:
        branches:
          - master
        paths:
          - .github/labels.toml
      push:
        branches:
          - master
        paths:
          - .github/labels.toml
      label:
        types:
          - created
          - edited
          - deleted
      workflow_dispatch:

    jobs:
      configure:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v2

          # https://docs.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#using-a-specific-python-version
          - name: Set up Python
            uses: actions/setup-python@v2
            with:
              python-version: '3.x'

          # https://pypi.org/project/labels/
          - name: Install Python dependencies
            run: pip install labels

          ###
          # WHEN: pull request (for default branch)
          # WHEN: push (for default branch)
          ###
          - name: Diff labels
            if: contains(github.event_name, 'pull_request') || contains(github.event_name, 'push')
            run: labels sync --dryrun -f .github/labels.toml
            env:
              LABELS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
              LABELS_USERNAME: ${{ github.repository_owner }}

          ###
          # WHEN: push (for default branch)
          ###
          - name: Sync labels
            if: contains(github.event_name, 'push')
            run: labels --verbose sync -f .github/labels.toml
            env:
              LABELS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
              LABELS_USERNAME: ${{ github.repository_owner }}

          - name: Commit definition file after fetch
            if: contains(github.event_name, 'push')
            uses: stefanzweifel/git-auto-commit-action@v4
            with:
              commit_message: "Sync definition file with existing labels"
              file_pattern: .github/labels.toml

          ###
          # WHEN: Detected manual change or manual trigger
          ###
          - name: Fetch labels
            if: contains(github.event_name, 'label') || contains(github.event_name, 'workflow_dispatch')
            run: labels fetch -f .github/labels.toml
            env:
              LABELS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
              LABELS_USERNAME: ${{ github.repository_owner }}

          - name: Create Pull Request
            if: contains(github.event_name, 'label') || contains(github.event_name, 'workflow_dispatch')
            uses: peter-evans/create-pull-request@v3
            with:
              token: ${{ secrets.GITHUB_TOKEN }}
              commit-message: "Fetch definition file from existing labels"
              title: "Sync definition file with existing labels"
              body: |
                Current definition file and existing labels don't match.
              branch: sync-labels
              branch-suffix: ""
    ```

    * 設置後、ワークフロー "Configure GitHub labels" が登録されます

以上で、導入は完了です。

### 使い方

1. リポジトリのラベル設定から、定義ファイルを生成 : ワークフロー "Configure GitHub labels"を手動実行します
    * リポジトリのラベル設定から、定義ファイル `.github/labels.toml` を生成し、
      プルリクエスト "Sync definition file with existing labels" ("sync-labels" ブランチ) が作成されます。
        * ※その後のマージは手動で行ってください
    * ※定義ファイルをイチから作成して利用することも可能です。その場合は当手順は不要です。
    * ※手動実行は、いつ、何度も実行しても問題ありません。
        * プルリクエストをマージする前に再度手動実行した場合でも、見るべきプルリクエストは一つだけです。（単一の"sync-labels" ブランチに変更が挙がってきます）
        * 定義ファイルが設置済みの状態で手動実行した場合、リポジトリのラベル設定と定義ファイルとの差異があれば、プルリクエストが作成されます。差異がなければ、何もされません（プルリクエストは作成されません）。
2. 定義ファイルからリポジトリのラベル設定に反映 : 定義ファイル `.github/labels.toml` を変更し、プルリクエスト＆マージ（もしくは直接、Push）
    * 定義ファイルの内容については後述します（「ラベル定義ファイルについて」）
    * プルリクエスト時に実行される "Configure GitHub labels" ワークフローで、定義ファイルと既存のラベル設定との差分が出力されます。
      それを確認してからマージすると安全です。
    * マージ（もしくは直接Push）時に実行される "Configure GitHub labels" ワークフローで、定義ファイルからリポジトリのラベル設定に反映されます
        * ラベル設定を反映時に、定義ファイルが変更となる場合があります（後述「既存のラべルの名称(name)を変更する場合」）
            その場合には、変更となった定義ファイルをコミットします。
3. GitHubのWeb UIなどからラベル設定が変更された場合 : 自動作成されているプルリクエストをマージ
    * 自動生成されているプルリクエスト "Sync definition file with existing labels" ("sync-labels" ブランチ) を確認し、マージします。
        * ラベル設定が定義ファイル以外から変更された場合には、ワークフローがトリガーされ、リポジトリのラベル設定から定義ファイルを生成し、
            プルリクエストが自動で作成されます。
    * ※マージする前に、何度もラベル設定変更が行われた場合でも、見るべきプルリクエストは一つだけです。（単一の"sync-labels" ブランチに変更が挙がってきます）


#### ラベル定義ファイルについて

```toml:（例）.github/labels.toml
[bug]
color = "d73a4a"
name = "bug"
description = "Something isn't working"

[documentation]
color = "0075ca"
name = "documentation"
description = "Improvements or additions to documentation"

...略...
```

[hackebrot / labels](https://github.com/hackebrot/labels) に依るモノになりますので、詳細はツール・ライブラリのサイトをご参照ください。
とはいえ、至ってシンプルですので、直感的に作成していけるかと思います。
手動でワークフローを実行すれば、リポジトリの既存のラベル設定から定義ファイルを生成できますので、そこから始めるのも良いかと思います。
また、この定義ファイル上には、反映したい先のリポジトリなどの情報は不要ですので、他のリポジトリで使っていたファイルを持ってきてそのまま使うことも可能です。

**※既存ラべルの名称(name)変更を行う場合は、定義ファイルの記述に要注意です（次項にて）**

#### 既存ラべルの名称(name)変更における注意点

たとえば「"bug"ラベルを"bug-edited"に名称を変更したい」場合です。

* 下記のように定義ファイルを変更した場合、思ったような結果となりません。

    ```diff:NG
    - [bug]
    + [bug-edited]
      color = "d73a4a"
    - name = "bug"
    + name = "bug-edited"
      description = "Something isn't working"
    ```

    * 既存の"bug"ラベルは **削除** され、新たな"bug-edited"ラベルが **追加** されます。変更とはなりません。
    * これをやってしまうと、もし"bug"ラベルを付けていたIssueやプルリクエストがあった場合、 **付けていたラベルは消えてしまいます** 。
* **既存ラベルのnameを変更する場合には、下記のように定義ファイルを変更します。**

    ```diff:OK
      [bug]
      color = "d73a4a"
    - name = "bug"
    + name = "bug-edited"
      description = "Something isn't working"
    ```

    * `[bug]`の部分は変えない点がポイントです。
    * これで、既存の"bug"ラベルのnameが"bug-edited"に **変更** されます。
    * しかし、この定義ファイルのまま、再度リポジトリにラベル設定を反映すると、「"bug"ラベルが無いので、nameを変更できない」といったエラーになります。
        * [hackebrot / labels](https://github.com/hackebrot/labels) は気が利いていて、
          この定義ファイルで`labels sync`(リポジトリのラベル設定へ反映) を行うと、
          再度定義ファイルを実態に合わせて再出力してくれます。
          `labels sync`を実行後、定義ファイルは、以下のように変更されています。

            ```diff:
            - [bug]
            + [bug-edited]
              color = "d73a4a"
              name = "bug-edited"
              description = "Something isn't working"
            ```

            * この定義ファイルであれば、再度リポジトリにラベル設定を反映しても、エラーとはならず、正常に動作します。今後も定義ファイルとして利用していきたいのは、この状態の定義ファイルです。
            * そのため、ワークフローにて、`labels sync`実行後に出力された定義ファイルをコミットするようにしています。
                これにより、その後の定義ファイルの変更時にトラブルとならないようにできました。
* こういったトラブルを防ぐためにも、定義ファイルを変更する際は、直接Pushするのではなく、プルリクエストを介し、どんな変更が行われるかをチェックしてから行う方が安全です。
* 心配であれば「ラベル名称の変更は、定義ファイルから行わず、WebUIから行う」というのも良いでしょう。


## その他解説・補足など 

### pythonツール・ライブラリ [hackebrot / labels] について

定義ファイルからのラベル設定、既存のラベル設定から定義ファイルの生成などは、
[hackebrot / labels](https://github.com/hackebrot/labels) 
を用いています。詳細はツール・ライブラリのサイトをご参照頂ければと思います。


下記は作成したワークフローの抜粋（定義ファイルからリポジトリにラベル設定を反映）ですが、
とても簡単にやりたいことを実現でき、助かりました。

```yaml:(抜粋).github/workflows/config-gh-labels.yml
      - name: Sync labels
        if: contains(github.event_name, 'push')
        run: labels --verbose sync -f .github/labels.toml
        env:
          LABELS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          LABELS_USERNAME: ${{ github.repository_owner }}
```

また、今回の根幹となる処理を、GitHub Actionsの機構に依存しないツール・ライブラリで行えたのは嬉しい点です。
もし今後、手元のマシンからラベル操作を行いたい場合でも、根幹は同じノウハウが使えるのは助かります。

### GitHub Actionのワークフロー定義ファイル について

素晴らしいツール・ライブラリやアクションが存在したことで、特段複雑なことをする必要もなく、シンプルに書けました。
ポイントとしましては、

* 自リポジトリに対してのラベル設定操作であるため、追加のsecret設定は不要
    * [hackebrot / labels](https://github.com/hackebrot/labels) は、GitHubのAPIトークンとユーザー名が必要
    * GitHub Actionsにおいては、ワークフロー実行時に自動で設定されている `secrets.GITHUB_TOKEN` `github.repository_owner` を用いることで、自リポジトリへの操作が行える

* `on.workflow_dispatch` により手動実行をトリガーにできた
    * 任意のタイミングでリポジトリの既存のラベル設定から定義ファイルを生成できるように

* `on.label` で、Web UIなどからのラベル設定変更時をトリガーにできた
    * 定義ファイル以外からの変更もハンドルでき、定義ファイルが実態と合わなくなる状況も回避

* commitやプルリクエストの発行も、公開されている素晴らしい(GitHub)アクションのおかげで、ラクに実現できた（感謝しかないです。。）
    * [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action)
        * commit & pushしてくれるactionです。
        * ファイル差異がなければ、（デフォルトでは）何も行われず、正常終了します → 便利！
    * [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request)
        * ブランチ作成、commit & push、プルリクエストしてくれるactionです。
        * ファイル差異がなければ、（デフォルトでは）何も行われず、正常終了します → 便利！

* 🤔定義ファイル外からのラベル変更時など、自動でプルリクエストされる仕組みにしたが、その後マージするのが手間な気も、、
    * 即コミット＆Pushできれば良いのだが、短い間に何度もラベル変更された場合にうまくいかず、、、
        * 変更タイミングのそれぞれでPush時のワークフローがトリガーされ、並列で複数実行されてしまうので、うまくいかない。
    * ひとまず諦めました。（私の場合）この仕様でも我慢できないほどでは無いですので。。

## まとめ

* 素晴らしいツール・ライブラリ、(GitHub)アクションが公開されているおかげで、やりたいことが手軽に実現できました。
* GitHub Actions、便利！！