tagpr で比較的高い頻度で 'Secondary rate limit' となる問題を解決したい

問題
この Issue と同様の現象が割と頻繁に発生する(その日の初回実行時でも発生する)
エラーメッセージ
403 You have exceeded a secondary rate limit. Please wait a few minutes before you try again.
直前になにも実行していないにも関わらず Rate Limit と言われることが多々ある

ワークアラウンド
Private な GitHub App を用意して、その App の Token を利用することで動作が安定したように見える
「create-github-app-token で Token を生成してみる」へ
一旦これで様子を見る

調査のまとめ
わかったこと
- Issue の Search を API で実行したときに該当エラーが発生している
-
GITHUB_TOKEN
の生成元によって挙動が変わっていそう- 問題が出ているケース
-
secrets.GITHUB_TOKEN
を使用している
-
- 問題が出ないケース
- private な GitHub App を用意し、
actions/create-github-app-token
を使って Token を生成している
- private な GitHub App を用意し、
- 問題が出ているケース
-
git-pr-release
でも同様の状況は発生していた
わかっていないこと
- 他の類似ツールでは問題が出ていない?
- 類似ツール
git-pr-release
- 類似ツール
-
secrets.GITHUB_TOKEN
の制限なのか? - GitHub App や Personal Access Token を使う以外の解決方法はないのか?

修正がマージされた
以下で修正されたので close

ローカルで試す
debug 用にログを追加し response のヘッダ情報を出力してみた
検証用リポジトリ をローカルにcloneして tagpr
を実行してみる
:
::debug [Search.Issues] x-ratelimit-reset=1726714496
::debug [Search.Issues] x-ratelimit-resource=search
::debug [Search.Issues] x-github-request-id=E7A1:1BDD16:DAE272:E75AD0:66EB925B
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-remaining=26
::debug [Search.Issues] x-ratelimit-used=4
::debug [Search.Issues] x-ratelimit-reset=1726714496
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-remaining=25
::debug [Search.Issues] x-ratelimit-resource=search
::debug [Search.Issues] x-github-request-id=E7A1:1BDD16:DAE2BB:E75B20:66EB925F
::debug [Search.Issues] x-ratelimit-used=5
:
::debug [Search.Issues] x-ratelimit-remaining=24
::debug [Search.Issues] x-ratelimit-resource=search
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-github-request-id=E7AD:3D430D:DEF9BF:EB7383:66EB9273
::debug [Search.Issues] x-ratelimit-reset=1726714496
::debug [Search.Issues] x-ratelimit-used=6
::debug [Search.Issues] x-github-request-id=E7AD:3D430D:DEF9FC:EB73C2:66EB9275
::debug [Search.Issues] x-ratelimit-remaining=23
::debug [Search.Issues] x-ratelimit-used=7
::debug [Search.Issues] x-ratelimit-reset=1726714496
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-resource=search
:
::debug [Search.Issues] x-github-request-id=E7B9:3E9C50:DA4E64:E6C828:66EB9288
::debug [Search.Issues] x-ratelimit-used=1
::debug [Search.Issues] x-ratelimit-remaining=29
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-reset=1726714568
::debug [Search.Issues] x-ratelimit-resource=search
::debug [Search.Issues] x-ratelimit-used=2
::debug [Search.Issues] x-ratelimit-reset=1726714568
::debug [Search.Issues] x-github-request-id=E7B9:3E9C50:DA4EE4:E6C899:66EB928C
::debug [Search.Issues] x-ratelimit-remaining=28
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-resource=search

連続で実行してみたが、 x-ratelimit-remaining
の数値は比較的すぐに回復している
x-ratelimit-remaining=26
x-ratelimit-remaining=25
x-ratelimit-remaining=24
x-ratelimit-remaining=23
x-ratelimit-remaining=29 <-- 回復している
x-ratelimit-remaining=28

debug を仕込む

go install
でインストールできるようにコードを修正 ( Songmu
-> snaka
) する

修正をブランチに push
go install
で参照するために tag を打つ (例えば debug-20240919-1613
)
git tag --no-sign debug-20240919-1613
tag を push する
git push origin debug-20240919-1613

修正された tagpr を利用する側
インストール対象は以下のような意味となっている
- リポジトリ
github.com/snaka/tagpr
- 対象コマンド(mainパッケージ)のPath
/cmd/tagpr
- バージョン(tag)
@debug-20240919-1613

公式の workflow と自分の workflow を比較する
GITHUB_TOKEN
の取得方法が異なる
自分の workflow
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
公式(tagpr) の workflow
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- name: tagpr
run: |
make install
echo '::echo::on'
tagpr
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

create-github-app-token で Token を生成してみる
やること
- Register new GitHub App
-
Store the App's ID in your repository environment variables (example:
APP_ID
) -
Store the App's private key in your repository secrets (example:
PRIVATE_KEY
)

GitHub App を登録する



App に権限を付与する

Appへリポジトリのアクセスを許可する

pem がダウンロードされるので控えておく

APP ID と Private Key を secret に設定しておく

workflow の中で create-github-app-token を呼び出し Token を生成する
diff --git a/.github/workflows/run_tagpr.yml b/.github/workflows/run_tagpr.yml
index f1e6ca8..3eb5c61 100644
--- a/.github/workflows/run_tagpr.yml
+++ b/.github/workflows/run_tagpr.yml
@@ -18,6 +18,12 @@ jobs:
with:
go-version: '1.21'
- run: go install github.com/snaka/tagpr/cmd/tagpr@debug-20240919-1708
+ - name: Generate token
+ id: generate_token
+ uses: actions/create-github-app-token@v1
+ with:
+ app-id: ${{ secrets.TAGPR_APP_ID }}
+ private-key: ${{ secrets.TAGPR_PRIVATE_KEY }}
- run: tagpr
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

secrets.GITHUB_TOKEN
使ってたときに比べて、こちらのほうが安定している。
何度か実行してみても Secondary Rate Limit のエラーは出てこない。

git-pr-release
を調べる
類似ツール:

git-pr-release
を使っているProjectの workflow 実行ログ見ると同様のエラーが出ているのを見つけた
workflow log
/opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/response/raise_error.rb:14:in `on_complete': GET https://api.github.com/search/issues?q=repo%3Axxxxxxxxxxxxxxx+is%3Apr+is%3Aclosed+a703494: 403 - You have exceeded a secondary rate limit. Please wait a few minutes before you try again. If you reach out to GitHub Support for help, please include the request ID 4BC0:A2B2B:1E37741:375C2A4:669F762E. // See: https://docs.github.com/free-pro-team@latest/rest/overview/rate-limits-for-the-rest-api#about-secondary-rate-limits (Octokit::TooManyRequests)
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/middleware.rb:60:in `block in call'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/response.rb:42:in `on_complete'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/middleware.rb:59:in `call'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/middleware/follow_redirects.rb:73:in `perform_with_redirection'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/middleware/follow_redirects.rb:61:in `call'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/rack_builder.rb:152:in `build_response'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/connection.rb:444:in `run_request'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/connection.rb:200:in `get'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/sawyer-0.9.2/lib/sawyer/agent.rb:99:in `call'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/connection.rb:156:in `request'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/connection.rb:84:in `paginate'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/client/search.rb:99:in `search'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/client/search.rb:49:in `search_issues'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:145:in `search_issue_numbers'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:179:in `fetch_squash_merged_pr_numbers_from_github'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:101:in `fetch_merged_prs'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:46:in `start'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:12:in `start'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/exe/git-pr-release:5:in `<top (required)>'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/bin/git-pr-release:23:in `load'
from /opt/hostedtoolcache/Ruby/3.0.1/x64/bin/git-pr-release:23:in `<main>'
backtrace 見た感じ issue に対する search を実行している箇所で発生しているので、同じ状況と思われる
from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:145:in `search_issue_numbers'
しかし、workflow 自体は失敗していないので、リトライ機構が用意されているのかもしれない?

git-pr-release
の該当箇所

Octokit の search_issues
search
の実装は以下

Octokit 自身には Retry 機構は入って無さそうだけど、 faraday-retry
が何かやっていそう?

Octokit では特に Retry してなさそう... と思ったら、 workflow 側で 以下のようにやっていて、エラーを無視して進めていた
git-pr-release --squashed || echo 'Done.'

go-github-ratelimit の使い方を調べる
READMEより
rateLimiter, err := github_ratelimit.NewRateLimitWaiterClient(nil)
if err != nil {
panic(err)
}
client := github.NewClient(rateLimiter).WithAuthToken("your personal access token")
RateLimitWaiterClient
を github.NewClient()
の第一引数(httpClient) として渡す

NewRateLimitWaiterClient(nil)
http.Client{ Transport: waiter }
とは?
Gemini に訊いてみると以下のような回答
Transport
フィールドは、http.Client
が実際に HTTP リクエストを送信する方法を制御するインターフェースです。waiter
はSecondaryRateLimitWaiter
型であり、これはおそらく API の二次的なレート制限に対処するためのカスタムのhttp.RoundTripper
実装です。http.RoundTripper
インターフェースは、HTTP リクエストを処理し、レスポンスを返すためのメソッドRoundTrip
を定義します。

http.RoundTripper
について
現状の tagpr では以下のように oauth2.Client
が Transport
として渡されるようになっている
前述のようにgithub_ratelimit.NewRateLimitWaiterClient()
の第一引数が RoundTripper
( Transport
プロパティ ) を受け取るようになっているので、そこに oauthClient
を渡すと良さそう?