GitHub Actions Tips

:::note info
この記事は[ZOZO AdventCalender 2024](https://qiita.com/advent-calendar/2024/zozo)シリーズ5の5日目の記事です。
:::

# GitHub Actions Tips
本記事では筆者がGitHub Actionsでworkflowを実装した際に得た知見を共有します。  
主に[actions/checkout](https://github.com/actions/checkout)に関連した高速化についてのTipsを紹介したいと思います。  

## 大規模リポジトリで使うactions/checkout
GitHub Actionsでworkflowを実装する際には、まずリポジトリをチェックアウトするために`actions/checkout`を使うことが多いと思います。  
`actions/checkout`はGitHub Actionsの公式アクションで、リポジトリをチェックアウトするためのアクションです。  
しかし、大規模なリポジトリでデフォルト設定のまま使うと、チェックアウトに時間がかかりすぎてしまうことがあります。  
なお、デフォルト設定でも`fetch-depth`は1に設定されているため、最新コミットのみを取得するShallowClone自体は有効になっています。

## そのworkflow、本当にリポジトリのファイル全部必要ですか？
workflowの処理がリポジトリのファイル全てを必要としない場合、`sparse-checkout`が有効です。  
`sparse-checkout`は、リポジトリ内の特定のディレクトリやファイルのみを指定してチェックアウトすることができる機能です。  
例えば`.github`ディレクトリと`src`ディレクトリ以下のファイルしか使わない場合、以下のように設定します。
※ この設定自体は[README](https://github.com/actions/checkout?tab=readme-ov-file#fetch-only-the-root-files-and-github-and-src-folder)にも記載されています。
```yaml
- name: Checkout
  uses: actions/checkout@v4
  with:
    sparse-checkout: |
      .github
      src
```
この設定を有効にすることで、workflowで必要なファイルに絞ってチェックアウトすることができるため、チェックアウトにかかる時間を短縮することができます。  
大量のファイルがコミットされているリポジトリで特に有効です。  
筆者の試したリポジトリでは、数分かかっていた処理が数秒で終わるようになりました。

## そのworkflow、そもそもチェックアウト自体必要ですか？
workflowの処理が
- リポジトリ内のファイルは使わない
- 履歴が必要(diffやPRの差分を見るなど)で`fetch:depth: 0`でフルクローンしてる

というケースだと、チェックアウトしているけど時間をかけてダウンロードしたファイルを使っていないという無駄が発生しています。  
こういったケースでは、`actions/checkout`自体使うのをやめましょう。

### ではどうするか？
`actions/checkout`を使わずにdiffやPRの差分を取得したりするには、GitHub APIを使う方法があります。  
GitHub Actionsのデフォルトのランナーには、`GitHub CLI`があらかじめインストールされているため、GitHub CLIを使ってGitHub APIを叩くことができます。  
例えばPullRequestの差分を取得する場合、Pull Request Eventをトリガーとしたworkflowで以下のように差分を取得することができます。
```yaml
on:
  pull_request:
    types: [opened, synchronize, closed]

jobs:
  diff:
    runs-on: ubuntu-latest
    steps:
      - name: Get diff
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh api /repos/${{ github.repository }}/pulls/${{ github.event.number }}/files --jq '.[] | .filename'
```
このようにGitHub APIを使うことで、リポジトリをチェックアウトすることなく、リポジトリの情報を取得することができます。  
workflowの処理がリポジトリのファイル自体を必要としない場合、`actions/checkout`を使わずにGitHub APIを使うことで、チェックアウト自体を省略できるので大幅に処理時間を短縮することができます。  
なお、`gh pr diff`コマンドでPullRequestの差分を取得することもできますが、`gh pr`を使う場合、`.git`ディレクトリが必要なため、`actions/checkout`を使って`.git`ディレクトリだけ先にチェックアウトする必要があります。

## 応用
ここまでで一部のディレクトリ・ファイルだけ必要なケース、ファイルは必要ないけどリポジトリの情報は必要なケースを紹介しましたが、一部のディレクトリ・ファイルだけ必要だけど必要なものはPullRequestの差分を取得しないとわからず、事前にsparse-checkoutの設定をするのが難しいというケースもあるかと思います。  
そういった場合は、はじめにGitHub APIで差分を取得して、sparse-checkoutの設定を動的に行うという方法を紹介したいと思います。
サンプルコードは以下の通りです。  
```yaml
on:
  pull_request:
    branches:
      - main
    types: [opened, synchronize, closed]

jobs:
  get-diff:
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{github.token}}
    steps:
      - name: Get diff
        id: get-diff
        run: |
          diff=$(gh api /repos/${{github.repository}}/pulls/${{github.event.number}}/files --jq '.[] | .filename')
          {
            echo 'diff<<EOF'
            echo "${diff}"
            echo EOF
          } >> "${GITHUB_OUTPUT}"
          echo "${diff}" >> "${GITHUB_STEP_SUMMARY}"
      - name: checkout
        uses: actions/checkout@v4
        with:
          sparse-checkout: |
            ${{ steps.get-diff.outputs.diff }}
      - name: tree
        run: tree >> "${GITHUB_STEP_SUMMARY}"
```
stepごとに解説していきたいと思います。  
1. `Get diff`  
   GitHub APIを使ってPullRequestの差分を取得します。`--jq '.[] | .filename'`でファイル名のみを取得しています。  
   また、`GITHUB_OUTPUT`はそのままでは改行を含んだ文字を出力できませんが、区切り文字で囲って出力することで改行を含んだ文字を出力することができます。  
   参考: https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#multiline-strings
2. `checkout`  
   `Get diff`で取得した差分をsparse-checkoutの設定としています。  
   本記事の例では、差分のファイルパスをそのままsparse-checkoutの設定としていますが、`Get diff`のステップでファイルが含まれるディレクトリを取得して出力することで、ディレクトリを指定してsparse-checkoutの設定をすることも可能です。
3. `tree`
   実際にどういうファイルがチェックアウトされたかを確認するために、`tree`コマンドを実行しています。  
   出力結果は以下の通りです。  
   `A/1/test.txt`と`A/2/test.txt`の2ファイルに修正を加えたPullRequestを作成し、このworkflowを実行した結果です。
   ```bash
   A/1/test.txt
   A/2/test.txt
   
   .
   ├── A
   │   ├── 1
   │   │   └── test.txt
   │   └── 2
   │       └── test.txt
   ├── README.md
   └── test.txt
   
   3 directories, 4 files
   ```
   比較用に、このworkflowを実行したリポジトリのディレクトリ構成を記載します。
   ```bash
   .
   ├── A
   │   ├── 1
   │   │   └── test.txt
   │   ├── 2
   │   │   └── test.txt
   │   └── 3
   │       └── test.txt
   ├── README.md
   └── test.txt
   
   5 directories, 5 files
   ```
   PullRequestに含まれていない`A/3`ディレクトリがチェックアウトされていないことが確認できます。

## まとめ
本記事では、主に`actions/checkout`に関連した高速化についてのTipsを紹介しました。  
`actions/checkout`はよく使われるactonですが、大規模リポジトリで使う場合はworkflowのボトルネックになりがちです。  
workflowを見直してみると、案外使う必要がなかったりするかもしれません。