github actions の push イベントワークフロー改善
23時に寝て6時に起きた。今週はサービスインで凸凹していて疲れた。
ストレッチ⌗
今日の開脚幅は開始前161cmで、ストレッチ後164cmだった。ちょっと数値がよくなった。一昨日の日本酒イベントで4時間ほど立ち呑みをしていた疲労で腰に張りが少しあった。それ以外はとくに問題はなくて調子がよかった。右股関節の詰まりも2-3週間前よりもよくなっている気がする。これはトレーナーさんも注意を払って詰まりを取り除くようにストレッチのメニューを組んでくれているのでその成果が徐々に出始めている気がする。以前よりも可動領域が広がってきている気がして安心感を得た。
github actions の push イベントワークフロー改善⌗
午後からビルド・デプロイの最適化のために github actions のワークフローを改善作業をしていた。
今週はサービスインにより、本番環境への緊急リリースを何回もやっているのを傍からみていて、ビルド・デプロイが速くなればなるほど、その回数を増やせるし、修正後の検証に時間を多く割ける。あるリポジトリが7つのモジュールをまとめてビルド・デプロイしている。これはあるモジュールの微修正の反映には向かないので改善することにした。結果として最大で50%ぐらいのビルド時間の削減、モジュールに依っては、具体的には10分かかっていたものを4分台でビルドできるようにした。
ワークフロー改善のためのデバッグしている間はビルド・デプロイが出来なくなることから開発者が使っていない時間帯が望ましい。必然的にサービス休日出勤して github actions のワークフローを改善していた。デバッグと動作の検証も兼ねて午後から半日以上やっていたので休日にやったのは正解だと言えるだろう。
push イベントの github コンテキストの github.event.commits
にコミット情報が入っていて、そこにコミットのリビジョンがある。例えば、次のようなオブジェクトで id
がコミットのリビジョンに相当する。
[
{
"author": {
"email": "...",
"name": "...",
"username": "t2y"
},
"committer": {
"email": "...",
"name": "...",
"username": "t2y"
},
"distinct": true,
"id": "f8df1f77ffec9ef234e7321b2e237b663256b01c",
"message": "コミットログのメッセージ",
"timestamp": "2022-07-08T12:32:33+09:00",
"tree_id": "37b066734e58779c5d2c687d40b4cc43af177cb2",
"url": "https://github.com/OWNER/REPO/commit/f8df1f77ffec9ef234e7321b2e237b663256b01c"
},
...
]
このリビジョンを使って github rest api からファイル情報を取得できる。
$ gh api -H "Accept: application/vnd.github+json" /repos/OWNER/REPO/commits/f8df1f77ffec9ef234e7321b2e237b663256b01c
いろんなデータが返ってくるけど、ここでは変更したファイルのパスを知りたい。
{
"sha": "...",
"node_id": "...",
"commit": {
...
},
...
"files": [
{
"sha": "...",
"filename": "module1/path/to/src",
...
},
{
"sha": "...",
"filename": "module2/path/to/src",
...
}
]
}
この filename
のトップディレクトリがモジュール名と同じなのでここだけ取り出して、管理対象のモジュールかどうかを比較する。bash でも =~
をサブ文字列のマッチングができる。
$ targets=(mymodule1 mymodule2)
$ echo " ${targets[@]} "
mymodule1 mymodule2
$ [[ " ${targets[@]} " =~ " mymodule1 " ]] && echo "match"
match
$ [[ " ${targets[@]} " =~ " mymodule2 " ]] && echo "match"
match
さらにマッチしたモジュールを github actions の expressions で制御しやすいように json の array に変換する。
$ jq --compact-output --null-input '$ARGS.positional' --args -- ${targets[@]}
["mymodule1","mymodule2"]
これを step の outputs として格納する。
echo "::set-output name=modules::${json_array}"
例えば、後続の job で実行条件としてモジュールの有無を調べたいときは expressions を使って次のように記述できる。if 文は ${{ ... }}
のブラケットを省略できるようだけど、ここだけ省略すると返って混乱するかなと思って私は記述するようにしている。その方が統合性があってコードが読みやすいように私は考えている。
mymodule1-job:
if: ${{ contains(fromJSON(needs.build.outputs.mymodule_target.modules), 'mymodule1') }}
needs:
- build
ちなみに workflow レベルの env は job の if 文には使えない。outputs を使って動的な値を扱うようにしている。どうも workflow レベルの env はいろいろ問題があるみたいでなかなか issue がクローズされないのをみると取り扱い注意なのかもしれない。
- Workflow level env does not work properly in all fields. #480
- workflow level env. is unrecognised on job level’s ‘if’-expression when calling reusable workflow #1661
最終的なモジュールを判別するための step は次のようなものになった。
build:
outputs:
mymodule_target: ${{ steps.mymodule-target.outputs.modules }}
steps:
- name: コミットログからビルド対象のモジュールを設定
id: mymodule-target
run: |
declare -A modules
target_modules=${{ env.TARGET_MODULES }}
revisions=$(jq --raw-output '.[].id' <<< '${{ env.COMMITS }}')
for revision in ${revisions}
do
names=$(gh api -H "${{ env.ACCEPT_HEADER }}" ${{ env.COMMITS_PATH }}/${revision} | jq --raw-output '.files[].filename' | cut -d"/" -f1 | sort -u)
for name in $names
do
if [[ " ${target_modules[@]} " =~ " ${name} " ]]; then
modules[${name}]=true
fi
done
done
targets=$(jq --compact-output --null-input '$ARGS.positional' --args -- "${!modules[@]}")
echo "::set-output name=modules::${targets}"
env:
TARGET_MODULES: |
(
'mymodule1'
'mymodule2'
)
ACCEPT_HEADER: "Accept: application/vnd.github+json"
COMMITS_PATH: /repos/${{ github.repository }}/commits
COMMITS: ${{ toJSON(github.event.commits) }}
処理も理屈も簡単なんだけど、実際の運用コードはもう少しだけ複雑なものの、デバッグは github actions を実行しないといけない。ちょっとした typo のために実は2時間ほどはまっていたのは内緒。シェルの配列や連想配列を使うと、記号が多くてわかりにくい。配列の閉じブラケットを忘れていたがために EOF のエラーが発生していた。閉じブラケットのミスだけにエラーが発生しているところと実際のコードがズレていて、それに気付くのに少しずつコードを足したり消したりしてデバッグするみたいな原始的なやり方で些細な typo を気付くのに時間がかかった。
xxx.sh: line 47: unexpected EOF while looking for matching `"'