GitHub Actionsで外部ファイルの定義を環境変数に設定する

# 背景

背景としては、元々命名規則が曖昧だったり、クラウドサービスのリソース名などの設定文字数の制限などに抵触して
一定のルールで書くことができなかったので、ほとんど同じ処理内容ながら環境別にワークフロー定義を作成していました。

中身がほとんど同じなので、何かの修正をするときに複数のファイルを修正するのが面倒だなぁと思っていたのですが、比較的スッキリとした対応方法を思いついたのでメモしておきます。

# やりたいこと・ゴール

- 環境別に分かれていたワークフロー定義を１つのファイルに集約する
- 環境差異（リソース名）などの定義はワークフロー定義には含めず外部ファイルで定義する

# 前提・事前準備

環境毎にブランチを作成して、ブランチ名をキーにした以下のような環境定義ファイルを用意します。
以下、YAMLファイルで定義していますが、JSONでも大丈夫です。
（個人的に定義ファイル系は見やすい・書きやすい・コメントが書けるという観点からYAMLの方が適していると考えています）

```yaml:environments.yml
# 環境定義（キーはブランチ名とすること）
develop:
  RESOURCE_GROUP: devgrp
  DB_HOSTNAME: dev01.xxx.yyy.zzz
  DB_NAME: hogehoge_system_dev
  DB_USERNAME: dev_user
  ENCRYPT_SERVICE: xxxxx-secrets-dev
testing:
  RESOURCE_GROUP: test-group
  DB_HOSTNAME: test01.xxx.yyy.zzz
  DB_NAME: testdb
  DB_USERNAME: test_user
  ENCRYPT_SERVICE: yyyyy-secrets-test
production:
  RESOURCE_GROUP: production-group
  DB_HOSTNAME: prod.xxx.yyy.zzz
  DB_NAME: production_db
  DB_USERNAME: prod_user
  ENCRYPT_SERVICE: zzzzz-secrets-prod
```

上の定義はサンプルですが、実施にはもっと環境別に異なる命名なってました。。。

※DBパスワードはクラウドの暗号化系サービス(AWSのKMSやAzureのKey Vault)に格納してあるので実際にはワークフローのジョブで取得するようにしているため、取得するためのサービスのリソース名を定義しています。

# ワークフロー定義

先に結論。

```yaml:build.json
name: Set environment variable from environments.yml

on:
  push:
    branches:
      - deveop
      - testing
      - production
  workflow_dispatch:

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      # チェックアウト
      - name: checkout
        uses: actions/checkout@main

      # イベントの対象ブランチ名を取得して環境変数TARGET_BRANCH_NAMEに設定する
      - name: Extract branch name
        shell: bash
        run: echo "TARGET_BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV

      # environments.ymlをyqでロード。yqだけだとループがややこしいのでjqにパイプさせてbash変数展開で name=valueに変換する
      - name: Set environment variables
        run: |
          for s in $(yq e ".${{ env.TARGET_BRANCH_NAME }}" -o=j environments.yml | jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]"); do
           echo $s >> $GITHUB_ENV
          done

      # 設定されているかを確認
      - name: Print environment variable
        run: printenv | grep -E '^DB|^ENC|^RESOURCE' | sort
```

実行すると"Print environment variable"ジョブのログには以下のように出力されます。（productionの場合の抜粋）

```text:log
Run printenv | grep -E '^DB|^ENC|^RESOURCE' | sort
  printenv | grep -E '^DB|^ENC|^RESOURCE' | sort
  shell: /usr/bin/bash -e {0}
  env:
    TARGET_BRANCH_NAME: production
    RESOURCE_GROUP: production-group
    DB_HOSTNAME: prod.xxx.yyy.zzz
    DB_NAME: production_db
    DB_USERNAME: prod_user
    ENCRYPT_SERVICE: zzzzz-secrets-prod
DB_HOSTNAME=prod.xxx.yyy.zzz
DB_NAME=production_db
DB_USERNAME=prod_user
ENCRYPT_SERVICE=zzzzz-secrets-prod
RESOURCE_GROUP=production-group
```

あとは、 `${{env.xxxxx}}` で他の環境変数定義と同じように参照することができるようになります。

# 解説

## ブランチ名の取得

ブランチ名を取得するのはトリガーされるイベント毎に異なります。
全部を把握しているわけではないので、知っていることだけを書きます。

### pushとworkflow_dispath(手動実行)の場合

pushとworkflow_dispathの場合は `GITHUB_REF` という環境変数に `refs/heads/xxxxx` 形式で格納されているため、xxxxxの部分をbashの変数展開を利用して抽出します。
前述の"Extract branch name"のジョブでは取得したブランチ名を環境変数 TARGET_BRANCH_NAME に設定していますが、以下のようにoutputで定義して後述のジョブで参照させることも可能です。

```yml
  steps:
    - name: Extract branch name
      shell: bash
      run: echo "::set-output name=branch::${GITHUB_REF#refs/heads/}"
      id: extract_branch
    
    - name: Print branch name
      run: echo ${{ steps.extract_branch.outputs.branch }}
```

※workflow_dispathにおけるブランチは実行時に選択できる以下のリストから選択したブランチ名となります。

### pull_requestの場合

私のプロジェクトではpull_requestをトリガーする運用にしていないので上のワークフロー定義には含まれていませんが、以下の変数からマージ元、マージ先のブランチ名をそれぞれ取得できます。

- ターゲット: `${{github.base_ref}}"`
- ソース： `${{github.head_ref}}"`

## 環境定義ファイルをロードして環境変数に設定する

ジョブ"Set environment variables"の部分の処理です。
環境定義ファイルはリポジトリの管理対象としてしておき、checkoutジョブでチェックアウトしたものをyqコマンドでロードさせます。
GitHubActionsのUbuntu-latestに標準でインストールされているため、特殊なことをせずに利用できます。
あとは、bashの変数展開などを駆使して、 `name=value` 形式にしたものを `$GIHUB_ENV` という変数（実態はファイル）にリダイレクトさせることで、後続のジョブにおいて環境変数として `${{ env.DB_HOSTNAME }}` のように参照して利用することができるようになります。

# 余談？

前述の通り、`$GITHUB_ENV` というところにリダイレクトすることで環境変数を設定してくれるわけなのですが、
ではこの環境変数のスコープ？はというと、そのジョブの中だけみたいです。
なので、以下のように事前ジョブで環境変数を設定して、needsでこの事前ジョブを指定した後続ジョブで設定した環境変数は参照できませんでした。

例）

```yaml:confirm-env-scope.yml
name: confirm-scope

on:
  workflow_dispatch:

jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - name: Set environment variable
        run: echo "NEW_ENV_VAR=HOGEHOGE" >> $GITHUB_ENV

      - name: Show NEW_ENV_VAR
        run: printenv NEW_ENV_VAR
  job2:
    needs:
      - job1
    runs-on: ubuntu-latest
    steps:
      - name: Show NEW_ENV_VAR
        run: printenv NEW_ENV_VAR || echo "Not defined"
```

結果は、以下の通りです。残念。。。

```
Run printenv NEW_ENV_VAR || echo "Not defined"
  printenv NEW_ENV_VAR || echo "Not defined"
  shell: /usr/bin/bash -e {0}
Not defined
```

ちょっと調べてみたら、ドキュメントで記述が曖昧？矛盾？しているって指摘受けて修正されていました。

- [Fixes Inconsistent description of scope of env set via GITHUB_ENV & core.exportVariable](https://github.com/github/docs/pull/8392)