GitHub Actionsのワークフローを実行するとき、リポジトリのcheckoutはなるべく浅いdepthのシャロークローンをすることでビルド時間を節約することができます。
通常はビルドをするだけであればdepth = 1としてビルド対象のコミットだけをfetchするだけで十分です。actions/checkoutもデフォルトでfetch-depth: 1
としてgitリポジトリをcheckoutするようになっています。
しかし、PRの自動レビューのためにDangerを使う場合は、fetch-depth: 1
ではgitの履歴が足りずDanger実行時にDangerが追加でgit fetchを実行してしまいます。DangerがfetchするときはPRのmerge-branchが見つかるまでdepth = 20でfetch、depth = 54でfetch...とexpornential backoffでfetchを繰り返すため無駄なコミットまでfetchしてしまいます。
Dangerがgit fetchをする必要のない最低限のdepthでfetchする設定を解説します。
環境
- danger 9.5.3 時点で調査しています
結論: fetch depth数を計算しDangerのbaseオプションを指定する
GitHub Actions Workflowは以下のように設定します。
PRのmerge commitをcheckoutするとき:
name: test
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- id: get_fetch_depth
run: |
# PR merge commitをcheckoutするときはPRコミット数+2をcheckoutする
# depth = {PR commits} + {PR merge commmit: 1} + {PR merge-base commit: 1}
echo "depth=$((${{ github.event.pull_request.commits }} + 2))" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
with:
fetch-depth: ${{ steps.get_fetch_depth.outputs.depth }}
...
- name: Danger
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if ! git rev-parse -q --verify "$BASE_SHA^{commit}" > /dev/null; then
# Dangerが起動時に追加fetchすることを防ぐためbase branchのcommitがなければfetchしておく
git fetch --depth=1 origin "$BASE_SHA"
fi
# GitHub APIでリモートのgit merge-baseを取得する
merge_base_sha=$(
curl -s \
-H "Authorization: token $DANGER_GITHUB_API_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/compare/$BASE_SHA...$HEAD_SHA" \
| jq -r ".merge_base_commit.sha"
)
# Dangerを
# base = {merge-base commit}, head = {github.event.pull_request.head.sha}
# として実行させる(headはデフォルトでOK)
bundle exec danger --base="$merge_base_sha"
PRのhead commitをcheckoutするとき:
name: test
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- id: get_fetch_depth
run: |
# PR head commitをcheckoutするときはPRコミット数+1をcheckoutする
# depth = {PR commits} + {PR merge-base commit: 1}
echo "depth=$((${{ github.event.pull_request.commits }} + 1))" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: ${{ steps.get_fetch_depth.outputs.depth }}
...
... Danger stepは同様のため省略 ...
解説
例として、gitの履歴が以下の形であるとして説明します。
pr1
branchからmain
へPRを出している状況です。
* 9232604 (pull/1/merge) Merge commit pr1 into main
|\
| * 88ecb70 (HEAD, pr1) commit F (<- head ref / sha)
| * c527208 commit E
* | edb6e03 (main) commit D (<- base ref / sha)
* | 45d4528 commit C
|/
* e2bb795 commit B (<- merge-base)
* 412a87d commit A
この構成の時、GitHub Actions pull_requestイベントではそれぞれ以下のcommitを指しています
- github.ref =
pull/1/merge
(Merge commit pr1 into main) - github.event.pull_request.head.ref =
pr1
- github.event.pull_request.head.sha =
88ecb70
(commit F) - github.event.pull_request.base.ref =
main
- github.event.pull_request.base.sha =
edb6e03
(commit D)
PRの分岐元commitはgit merge-base main pr1
で取得でき、e2bb795
(commit B)です。
Dangerはmerge-base commitを必要とする
Dangerは実行時に以下の3つのcommitが存在することをチェックします。
- base branchが指すcommit
- デフォルトでgithub.event.pull_request.base.sha
- head branchが指すcommit
- デフォルトでgithub.event.pull_request.head.sha
- baseとheadの分岐元であるmerge-base commit
Dangerはmerge-baseをgit merge-base {base} {head}
コマンドを実行することで探索します。
merge-base commitはbase commitとhead commitが存在し、その分岐元のコミットまでつながった状態の履歴がfetch済みであれば見つけることができますが、その十分な履歴がローカルになければ見つけることはできません。
例の構成ではgit merge-base main pr1
コマンドが成功するための最低限の履歴は以下の通りです。以下のコミットが一つでも欠けていれば見つけることはできません。
* 88ecb70 (HEAD, pr1) commit F (<- head ref / sha)
* c527208 commit E
* | edb6e03 (main) commit D (<- base ref / sha)
* | 45d4528 commit C
|/
* e2bb795 commit B (<- merge-base)
base branch commit, head branch commit, merge-base commitの3つのうちいずれかをローカルで見つけることができなければDangerは該当コミットを見つけられるようになるまでgit fetch --depth=N {base} {head}
を繰り返します。
Dangerに--base={merge-base}を渡したい
そもそもDangerはPRの差分を知るためにmerge-baseとhead commitを必要としています。
base commitはmerge-baseを探すために使用しますが、merge-baseを見つけた後ならbase commitは不要となります。
Dangerにmarge-baseをbase branchとして扱うように設定できればbase branchの履歴は不要となります。
つまり、以下の履歴があればDangerがgit fetchなしで動作できるようになります。
* 88ecb70 (HEAD, pr1) commit F (<- head ref / sha)
* c527208 commit E
|
|
/
* e2bb795 commit B (<- merge-baseをbaseとみなす)
checkout depthの計算
上記の最低限の履歴はdepthの計算により取得可能です。
pull_requestのpull/1/merge
コミットをcheckoutするときは${{ github.event.pull_request.commits }} + 2
を取得すると以下の履歴となります。
例ではdepth = 2 + 2 = 4
です。
depthを指定してfetchすると、fetch対象のコミットから辿れる親コミットをすべて辿るため今回はbase branch側のコミットも偶然すべて取得されました。しかし、base branchのコミット数がPRコミット数よりも多くなっている場合はbase branch側の履歴はbase commitからmerge-baseまで繋がっていない中途半端な履歴となる点に気をつけてください。
* 9232604 (pull/1/merge) Merge commit pr1 into main
|\
| * 88ecb70 (HEAD, pr1) commit F (<- head ref / sha)
| * c527208 commit E
* | edb6e03 (main) commit D (<- base ref / sha)
* | 45d4528 commit C
|/
* e2bb795 commit B (<- merge-base)
action/checkout
にref: ${{ github.event.pull_request.head.sha }}
を指定し、pull_requestのhead
コミットをcheckoutするときは${{ github.event.pull_request.commits }} + 1
を取得すると以下の履歴となります。
例ではdepth = 2 + 1 = 3
です。
* 88ecb70 (HEAD, pr1) commit F (<- head ref / sha)
* c527208 commit E
|
|
/
* e2bb795 commit B (<- merge-base)
GitHub Actions compare APIでmerge-baseを取得できる
depthを計算することでmerge-base commitまでfetchすることができましたが、PR branchにさらに他のbranchのmergeコミットが含まれていたりするとhead commitから複数の親コミットに分岐してしまうため、ローカルのcommit履歴だけではbase refとhead refのmerge-branch commitを正確に突き止めることはできません。
GitHub Actions compare APIを使えば、ローカルに十分な履歴がなくともbaseとheadのmerge-baseを取得できます。
Dangerに--base={merge-base}を渡す
ここまでで、Dangerが動作するために必要最低限のコミット履歴をfetchし、merge-baseコミットも特定できるようになりました。
--base
引数でDangerにmerge-base commitを渡しましょう。
ただし、pull_requestのheadコミットをcheckoutしている場合にはもともとのbase sha commitがローカルに存在しません。Dangerに--base
を指定しても、github.event.pull_request.base.sha
が存在するかは起動時にチェックしてしまうようなので、base shaをfetchしておきます。
base shaのfetch、merge-baseコミットの特定、Dangerの起動までまとめると以下のstepとなります。
- name: Danger
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if ! git rev-parse -q --verify "$BASE_SHA^{commit}" > /dev/null; then
git fetch --depth=1 origin "$BASE_SHA"
fi
merge_base_sha=$(
curl -s \
-H "Authorization: token $DANGER_GITHUB_API_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/compare/$BASE_SHA...$HEAD_SHA" \
| jq -r ".merge_base_commit.sha"
)
bundle exec danger --base="$merge_base_sha"
Appendix
Dangerが起動時にbase, head, merge-baseが見つかるまでfetchしようとする
Dangerに--base
を渡していたとしても、起動時の処理でpull_request.base.shaとpull_request.head.shaを探し、見つからなければgit fetchを実行します。
続いて、起動処理の中でgit merge-base
コマンドによりmerge-base commitを見つけられるまでgit fetchを実行します。こちらは--base
で指定したbaseを使ってmerge-baseを探します。
pull_request.base.sha, pull_request.head.sha, merge-baseの3つのコミットがgit fetchなしで見つけられればgit fetchは実行されません。
Dangerはexpornential backoffでgit fetchを繰り返す
3..6
でMath.exp()
を計算し、git fetchを繰り返しています。
つまり、depth = 20, 54, 148, 403と試し、それでもbase shaとhead shaが見つからなければ最後にgit fetch 1000000
を実行します。base branchとhead branchはgit fetchで明示的に取得しているため、一般的な状況であればすぐにbase shaとhead shaを見つけることができるはずですが、Workflow実行中にbase branchやhead branchがforce pushされるとどんなにfetchしてもbase shaとhead shaを見つけられないということがあるかもしれません。
同様に、merge-baseの探索もdepth = 20, 54, 148, 403を試しますが、git fetch 1000000
は試しません。base branchとhead branchが403コミット以上離れてしまったPRだとmerge-baseが見つからないエラーとなるのかもしれません。
参考
以下の記事を参考にしました。
-
https://zenn.dev/catatsuy/articles/effad41943a435
- GitHub compare APIからリモートのgit merge-base結果を得る