0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub ActionsでDangerのために必要な最小限のシャロークローンをする

Posted at

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する設定を解説します。

環境

結論: 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/checkoutref: ${{ 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を実行します。

image.png

続いて、起動処理の中でgit merge-baseコマンドによりmerge-base commitを見つけられるまでgit fetchを実行します。こちらは--baseで指定したbaseを使ってmerge-baseを探します。

image.png

pull_request.base.sha, pull_request.head.sha, merge-baseの3つのコミットがgit fetchなしで見つけられればgit fetchは実行されません。

Dangerはexpornential backoffでgit fetchを繰り返す

3..6Math.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を見つけられないということがあるかもしれません。

image.png

image.png

同様に、merge-baseの探索もdepth = 20, 54, 148, 403を試しますが、git fetch 1000000は試しません。base branchとhead branchが403コミット以上離れてしまったPRだとmerge-baseが見つからないエラーとなるのかもしれません。

image.png

参考

以下の記事を参考にしました。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?