[TIL] 14. GitHub Actions

各ジョブごとに仮想マシンが用意される
Github hosted runnerは実行環境として、エフェメラルな仮想マシンを提供する。1つのジョブにつき1つの仮想マシンが割り当てられる(ジョブごとにruns-onを指定することもスッキリ理解できる)。
次のワークフロー例には、Run-npm-on-Ubuntu および Run-PSScriptAnalyzer-on-Windows という名前のついた 2 つのジョブがあります。 このワークフローがトリガーされると、GitHub ではジョブごとに新しい仮想マシンをプロビジョニングします。
- Run-npm-on-Ubuntu という名前のジョブは Linux VM で実行されます。これは、ジョブの runs-on: で ubuntu-latest が指定されているためです。
- Run-PSScriptAnalyzer-on-Windows という名前のジョブは Windows VM で実行されます。これは、ジョブの runs-on: で windows-latest が指定されているためです。

各ステップごとに別のシェルプロセスが作成される
以下に示すように、個々のステップごとにシェルプロセスが立ち上がる(ステップごとに起動シェルを変更することが可能なこともスッキリ理解できる)。
name: STEP process check
on: push
jobs:
process-check:
runs-on: ubuntu-latest
steps:
- run: ps
- run: ps # ステップごとに異なるbashプロセスが立ち上がるため出力が異なる
------------------ result ------------------
run ps
PID TTY TIME CMD
~~~~~省略~~~~~
1791 ? 00:00:00 bash
1792 ? 00:00:00 ps
run ps
PID TTY TIME CMD
~~~~~省略~~~~~
1793 ? 00:00:00 bash
1794 ? 00:00:00 ps

actions/checkout とは?
リモートリポジトリの作業対象ブランチを GitHub Hosted Runner 内に複製する操作である。内部的には git fetch + git checkout を実行している。デフォルトではワークフローを実行したブランチをcheckoutしてくるが、refパラメータを利用することで別のブランチをcheckoutすることも可能。

中間環境変数を使おう!
GitHub Actionsではコンテキストという事前定義されたオブジェクトから値を取得可能。
コンテキストによっては特殊文字が含まれるため、シェルコマンドの実行時に意図しない影響を与える可能性がある。そのため、コンテキストは基本的に直接シェルコマンドに埋め込むことはせず、中間環境変数にコンテキストを渡すことで実装する。
「環境変数としてOS上で定義した値をシェルコマンドで使用する」という流れも理解しやすい。
name: Intermediate environment variables
on: push
jobs:
print:
runs-on: ubuntu-latest
env:
ACTOR: ${{ github.actor }} # コンテキストの値を中間環境変数へセット
steps:
- run: echo "${ACTOR}" # 中間環境変数経由でコンテキストのプロパティを参照

ステップ間(別プロセスのシェル間)のデータ共有
各ステップは別プロセスであるため、export TEST = test
のように環境変数を定義しただけではステップ間では値を共有できない。仮想マシン(ジョブ)のファイルに書き出して参照する必要がある。
そこで、GITHUB_OUTPUT
環境変数を利用してステップ間で値を受け渡す。GITHUB_OUTPUT
環境変数はGithub Hosted Runner(仮想マシン)に事前に定義されている環境変数であり、以下に示すような特殊なファイルパスが格納されている。
steps:
- name : CHECK ENVIRONMENT VARIABLES
run : printenv | grep GITHUB_OUTPUT
------------------ result ------------------
GITHUB_OUTPUT=/home/runner/work/_temp/_runner_file_commands/set_output_xxx-xxx-xxx-xxx
受け渡し側ステップでGITHUB_OUTPUT
環境変数(に格納された特殊ファイル)へキーバリュー形式の文字列を書き出し、受け取り側ステップでstepsコンテキストを参照することで、ステップ間での値の受け渡しが可能となっている。steps.[step_id].outputs.KEY
で書き出した値を参照できる。
name: GITHUB_OUTPUT
on: push
jobs:
share:
runs-on: ubuntu-latest
steps:
- id: source # ステップIDを設定
run: echo "result=Hello" >> "${GITHUB_OUTPUT}" # GITHUB_OUTPUTへ書き出し
- env:
RESULT: ${{ steps.source.outputs.result }} # stepsコンテキストから参照
run: echo "${RESULT}"

ジョブ間のデータ共有
ジョブ間のデータ共有にはoutputs
キーを利用する。
受け渡し側ジョブで渡したい値をoutputs
キーに指定する。受け取り側ジョブではneeds
キーでジョブの依存関係を定義した後に、needs
コンテキストを経由してデータを参照する。
name: Share job data
on: push
jobs:
before:
runs-on: ubuntu-latest
steps:
- id: generate # ステップのID
run: echo "result=Hello" >> "${GITHUB_OUTPUT}" # ステップレベルの出力値
outputs:
result: ${{ steps.generate.outputs.result }} # ジョブレベルの出力値
after:
runs-on: ubuntu-latest
needs: [before] # 依存するジョブIDの指定
steps:
- env:
RESULT: ${{ needs.before.outputs.result }} # 依存ジョブの出力値を参照
run: echo "${RESULT}"

OpenID Connectによる認証
OpenID Connectの仕組みを下図に示す。GitHub OIDC Providerから渡されたJWTトークンの認証は2ステップに分かれている。以下の2工程の認証を経て、一時的な認証情報が与えられる。
- 署名認証により、Token生成元がGithub OIDC Providerであることを確認
-
AssumeRole
のCondition
を使用したJWTクレーム認証により、アカウント情報やリポジトリ情報が意図したものであることを確認
aws-actions/configure-aws-credentials
を利用するだけで、上図1~5の全ての作業を完結することができる(引数で指定したIAM Roleで定義されている権限を有する一時的な認証情報を取得)。
- name : CONFIGURE AWS CREDENTIALS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.ROLE_ARN }}
role-session-name: ${{ env.SESSION_NAME }}
aws-region: ap-northeast-1

Pull Requestのマージをワークフロー実行トリガーにする
Pull Requestがマージされた際に、CICDワークフローを実行したいケースは多い。だがGithub ActionsではPull Requestがマージされたとき用のイベントは用意されていない。
Pull Requestはマージされると自動的にクローズされることを利用して、以下のように設定する。
on:
pull_request:
types:
- closed
jobs:
if_merged:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- run: echo "The PR was merged"

Pull Requestのマージは統合先へのPushと同義であるため、pushイベントを利用してプルリクエストのマージ時にワークフローを実行することも可能(但し、直接Pushした場合にもトリガーされることに注意が必要)。

dorny/paths-filterを用いた「ジョブ単位でのpathフィルター」
on.**.paths
フィルタを用いることで「ワークフロー単位でのpathフィルター」を実装できる。以下の例では、backend_app配下の任意のgoファイルがPushされた場合にワークフローが実行される。
on:
push:
paths:
- 'backend_app/**/*.go'
だが、複雑なワークフローを組む場合には、より詳細にジョブ単位で実行トリガーを制御したい。
これを実現するために、dorny/paths-filter
を利用する。
# フィルタ結果がBool型で出力される
jobs:
job_change_check:
runs-on: ubuntu-latest
outputs:
app1: ${{ steps.filter.outputs.app1 }}
app2: ${{ steps.filter.outputs.app2 }}
steps:
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
app1:
- 'src/app1/**'
app2:
- 'src/app2/**'
job_app1:
needs: job_change_check
if: ${{ needs.job_change_check.outputs.app1 == 'true' }}
runs-on: ubuntu-latest
steps:
- run: echo "src/app1 folder was changed!!"
job_app2:
needs: job_change_check
if: ${{ needs.job_change_check.outputs.app2 == 'true' }}
runs-on: ubuntu-latest
steps:
- run: echo "src/app2 folder was changed!!"