Github Actions のレベルをちょっと上げる

## 誰向けなのか

Github Actions をとりあえず導入してみたけど、テストやビルドのシェルを流しているだけ、という人々向けにおすすめの機能を紹介。

Github Actions でどんなことができるのかは↓を見れば大体書かれているんですが、そこそこ量があるので読み切るのは大変です。

https://docs.github.com/ja/actions/writing-workflows/workflow-syntax-for-github-actions

なので、Github Actionsの使用範囲をこれから拡大していく前に知っておくとよりメンテナンスが楽になる機能を逆引き的に紹介していきます。

## サマリー

- たくさん存在するリポジトリで処理を共通化しよう
    - on.workflow_call
- 同じ処理を複数の入力パラメーターで実行しよう
    - jobs.\<job_id\>.strategy.matrix
- 開発環境をちょっと試しに変えてみたい
    - jobs.\<job_id\>.steps[*].uses

## たくさん存在するリポジトリで処理を共通化しよう

### 結論

Github Actions もプログラムと同じように部品化して管理しましょう。

部品側はこんな感じ。

```
name: count-diff
on:
  workflow_call:
    inputs:
      limit:
        required: false
        type: number
        default: 500

jobs:
  check_diff:
    runs-on: ubuntu-latest
    steps:
      # リポジトリをチェックアウト
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # 変更行数を計算
      - name: Calculate changed lines
        id: diff
        run: |
          BASE="${{ github.event.pull_request.base.sha }}"
          if [ "${BASE}" == "" ]; then
            BASE=HEAD^
          fi
          echo "total_lines=$(git diff --shortstat ${BASE} | grep -oP '\d+(?= insertions\(\+\)| changes| deletion)' | awk '{s+=$1} END {print s}')" >> $GITHUB_OUTPUT

      # 500行以上の場合はエラーを出す
      - name: Fail if too many changes
        run: |
          if [ ${{ steps.diff.outputs.total_lines }} -ge ${{ inputs.limit }} ]; then
            echo "Error: Too many changes ($TOTAL_LINES lines)."
            exit 1
          fi
```

呼び出し側はこんな感じ。

```
name: count-diff
on:
  pull_request:

jobs:
  check_diff:
    uses: <owner>/<repo>/.github/workflows/count-diff.yml@develop
    with:
      limit: 100
```

### なぜなのか

部品化しかり、マイクロサービスしかり、業務でアプリケーションを作って行くうえではリポジトリは増えていくものです。

これらのリポジトリで共通使用している処理があれば、それらの処理を共通のGithub Actionsで呼び出した方が良いでしょう。

複雑な処理に関しては効果は一目瞭然ですが、それがたとえ既存のコンテナを呼び出すようなものでも、少し面倒に思うかもしれませんがしておくことをオススメします。
これは以下のような実体験のためです。

- コンテナのバージョンアップの際にすべてのリポジトリをアップデートする羽目になったので
- コンテナのアップデートの非互換により一斉にActionsすべてが動かなくなり、すべてのリポジトリに修正を行う羽目になったので

### 注意点

### その1

on.workflow_call で呼び出しを行った場合でも、
イベントは呼び出し側のものが利用されます。

そのため例えば部品側でリポジトリなどを未指定でパブリックアクションの actions/checkout を呼び出すと、プルされるコンテナは、呼び出し元のリポジトリになります。

同様にsecretなども呼び出し元のものが使用されるのでsecretが必要な場合は on.workflow_call.secert に定義して、呼び出し元から受け渡す必要があります。

### その2

セキュリティの問題上、デフォルトでは外部からGithub Actionsは呼び出せないようにリポジトリは設定されています。
そのため、部品側の設定で 「Code and automation > Actions > General > Access」から「Not accessible」以外の値に変更して上げる必要があります。

## 同じ処理を複数の入力パラメーターで実行しよう

### 結論

こんな感じで strategy.matrix を使用します。

```
name: table-sample
on:
  pull_request:

jobs:
  parameters:
    runs-on: ubuntu-latest
    outputs:
      targets: ${{ steps.targets.outputs.targets }}
    steps:
      - name: Check out
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: List targets
        id: targets
        run: echo "targets=$(git diff --name-only ${{ github.event.pull_request.base.sha }} | jq -R . | jq -s -c .)" >> $GITHUB_OUTPUT

  todo:
    if: needs.parameters.outputs.targets != '[""]'
    runs-on: sisco-runner-container
    needs: [parameters]
    strategy:
      fail-fast: false
      max-parallel: 3
      matrix:
        target: ${{ fromJson(needs.parameters.outputs.targets) }}
    steps:
      - name: To Do
        run: echo ${{ matrix.target }}
```

### なぜなのか

言うまでもなく、DRY(Don't Repeat Yourself)の原則に従うためです。
ただ、matrixは少し癖があるので、うまく使いこなそうとすると少し工夫が必要です。

その中でも知っておいたほうがいいのは以下の点。

#### デフォルトでは並列のいずれかが停止すると全ての並列処理がキャンセルされる

strategy.fail-fast というパラメーターで並列時のエラー処理は制御されています。
これはデフォルトではtrueであり、並列処理のいずれかがエラーになると、それ以外の並列処理をキャンセルし、エラーを可能な限り早く検知して処理を中断します。

その点は良いのですが、問題は停止方法です。
停止時は処理途中でも単にキャンセルされるだけなので、キャンセルされた場合を考慮した実装が必要になります。
なので以下のような場合はstrategy.fail-fastをfalseにしておいたほうが結果的には楽になるでしょう。

- どこでもキャンセルされて良いような作りを検討するのが大変
- 稀に通信エラーなどで失敗することが想定され、リトライすれば成功する可能性が高い

#### 動的な並列処理をしようとするとJsonの変換を挟む必要がある

サンプルのように動的なもの作成しようとした場合、Github Actionsでは直接配列を引き渡すことはできないので、一度Jsonの文字列化して、それを元に戻す、といった作り込みが必要になります。

簡単に以下のワンライナーを解説すると以下では

```
echo "targets=$(git diff --name-only ${{ github.event.pull_request.base.sha }} | jq -R . | jq -s -c .)" >> $GITHUB_OUTPUT
```

1. `git diff --name-only ${{ github.event.pull_request.base.sha }}` でプルリクエストのベースブランチからの差分があるファイルの一覧を取得する
1. 出力結果を `jq -R .` で行ごとにダブルクォートで囲む
1. 出力結果を `jq -s -c .` で1行のJsonの配列形式にする
1. `echo "targets=$(...)" >> $GITHUB_OUTPUT` でstepの出力に設定する

といったことをしています。

そして以下の `fromJson` により再度Json化して配列として扱うようにしています。

```
target: ${{ fromJson(needs.parameters.outputs.targets) }}
```

## 開発環境をちょっと試しに変えてみたい

### 結論

こんな感じで uses に直接、Dockerfile を直接指定することができます。

```
name: dockerfile-sample
on:
  pull_request:

jobs:
  sample:
    runs-on: ubuntu-latest
    steps:
      - name: Check out
        uses: ./.github/workflows/actions/sample
```

実行するDockerfileは `./.github/workflows/actions/sample/Dockerfile` に配置しておきます。

### なぜなのか

通常であればコンテナビルドをイメージしてそこを参照するべきですが、色々試しながら作成したいときに毎回コンテナイメージをPushするのが面倒くさかったり、そもそもレジストリを用意するのが面倒くさかったり。
そういったときにちょっと試すには便利です。

ただ、毎回ビルドが走るため、実際に運用が乗った際にはやはり正しいレジストリを用意するのが良いでしょう。

ということでちょくちょく使っていたんですが、構文の jobs.\<job_id\>.steps[*].uses を確認したんですが、どうにも指定できるという明示はないです。
なので色々隠された条件があったり、いつの間にか使えなくなったりするかもしれませんので、あくまでも検証の範囲での使用をおすすめします。

## 最後に

Github Actions、便利なのでみんな使い倒そうね。