特定のリポジトリのIssueを全てクローズするスクリプト

GitHubでまとめてIssueを閉じたりする場合、数が少ないなら手動でポチポチ行えば良いです。
ただ、Issueが数百ある場合は面倒なので工夫をしたいです。
今回使われなくなった社内のリポジトリをアーカイブする作業があり、その前にIssueを全て閉じる作業を行うためのスクリプトを作成してみました。

https://github.com/Umekawa/issue_closer

## アクセストークンの準備

まずスクリプトなどを利用してGitHubの操作を行う場合、アクセストークンを発行する必要があります。
GitHubにアクセス・ログインし、今回は右上の自分のアイコンをクリック -> `Settings` -> `Developer Settings` -> `Personal access tokens` -> `Fine-grained tokens` と辿っていきます。
`Fine-grained personal access tokens`のページに到達したら、`Generate new token`をクリックし、トークンの作成画面にジャンプします。

トークンの作成画面では、以下を記入・変更します。

- Token name
    - 用途に適した名前を記入する
- Expiration
    - お好みで選択
- Description
    - 書きたいことがあれば記入
- Repository access
    - `All repositories`か`Only select repositories`にする
    - `Only select repositories`にした場合は変更対象のリポジトリを選択する
- Permissions
    - Issuesを`Read and write`にする
        - `Repository access`の設定がデフォルトの`Public Repositories (read-only)`だと出ないので注意

作成されたアクセストークンは一旦保管しておきます。

## gemのインストール

今回はGemfileを利用して2つのgemをインストールしています。

```ruby:Gemfile
# frozen_string_literal: true

source 'https://rubygems.org'

gem 'dotenv'
gem 'faraday-retry' # インストールしないと警告が出てうるさいので追加
gem 'octokit'
```

メインは`.env`ファイルから環境変数を取得するためのdotenv、GitHubのAPIラッパーである[octokit.rb](https://github.com/octokit/octokit.rb)です。faraday-retryは後述するプログラムを実行した際、以下のメッセージが出てしまうため素直に従っています。

```sh
To use retry middleware with Faraday v2.0+, install `faraday-retry` gem
```

## 環境変数の設定
`.env`ファイルを作成し、環境変数を埋めていきます。
https://github.com/Umekawa/issue_closer には.env.sampleが存在するのでコピーするのが良いでしょう。

以下は https://github.com/Umekawa/issue_closer のissueを全てクローズする場合の例です。
リポジトリの設定やクエリは適宜変更してください。
ちなみにIssueの取得で使うPER_PAGEは、30がデフォルトなのでそのまま仮置きしています。([参考](https://docs.github.com/ja/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests))

```sh:.env
GITHUB_TOKEN=先ほど取得したGitHubのアクセストークン
GITHUB_ID=Umekawa
REPOSITORY_NAME=issue_closer
SEARCH_QUERY='is:issue is:open'
PER_PAGE=30
```

## スクリプトの作成

それでは実際にスクリプトを作成します。

```ruby:main.rb
# frozen_string_literal: true

require 'octokit'
require 'dotenv'

Dotenv.load

client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])

per_page = ENV['PER_PAGE'] || 30

search_result = nil
while search_result.nil? || search_result.items.size.positive?
  search_result = client.search_issues("repo:#{ENV['GITHUB_ID']}/#{ENV['REPOSITORY_NAME']} #{ENV['SEARCH_QUERY']}",
                                       page: 1, per_page:)
  search_result.items.each do |issue|
    client.close_issue("#{ENV['GITHUB_ID']}/#{ENV['REPOSITORY_NAME']}", issue.number)
    puts "#{issue.title} (Issue ##{issue.number}) has been closed."
    sleep 1
  end
  sleep 1
end

```

構成としてはすごくシンプルで、以下のようになっています。

- アクセストークンによる認証
- Issueの取得
    - 今回の環境変数の例ではクエリが`repo:Umekawa/issue_closer is:issue is:open`)で https://github.com/Umekawa/issue_closer の全てのOpenしているIssueを取得します
- IssueのClose
    - リポジトリとIssueの番号（ID）を指定することでcloseすることができるので、第一引数にリポジトリ名、第二引数にissue.numberを引数に入れています
    - GitHubのサーバーに負荷をかけすぎないよう、1秒のsleepを挟むようにしています
- Issueの再取得
    - 0件になるまでループさせています
    - pageが1固定なのは、1ページを全てcloseするので、それまでの2ページ目が１ページ目に変わるためです

このプログラムを実際に`PER_PAGE`を変えたりして、https://github.com/Umekawa/issue_closer/ で試した限り、問題なく動作することを確認できました。

![スクリーンショット 2024-05-28 12.32.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359721/d515e4d0-4b8f-550e-4907-3624599f3e48.png)

## まとめ

今回、octokit gemを利用することで簡単にGitHubの操作を行うことができました。
GitHubのAPIラッパーは他の言語でも幅広く提供されているので、GUIだと辛いなという作業がある場合は好きな言語で挑戦してみるのはいかがでしょうか？

## REF

- [検索用 REST API エンドポイント - GitHub Docs](https://docs.github.com/ja/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests)
- [octokit/octokit.rb: Ruby toolkit for the GitHub API](https://github.com/octokit/octokit.rb)