はじめに
こんにちは Bugfire です。
早いものでクラウドワークスにジョインして1年がすぎました。
やりたかったことはできているでしょうか。
クラウドワークス Advent Calendar 2020 の4日目になりました。
目的
表題の通りです。Github Actions では、action を外部レポジトリから、もしくはローカルファイルから実行できます。
でも、表題のやつが欲しいのです。action を指定するところを何度も同じものを書きたくないのです。フラグを大量に用意して大部分を DRY に書くことはできそうですが、平易に書き下したいのです。if をいっぱい書きたくないのです。
ソースを見れば察する、という人は GitHub bugfire/gh-actions-test をどうぞ。
さてどうするか
Template のツールはいくつもありますね。自分で作っても良いですが、今はコードを増やしたくないです。(前職では JSON Schema から API コード生成をしていたので作りました)
JSON Schema の $ref 記法 - JSON Reference がツールの選択肢も広そうです。その線で検索しました。dictknife を使ってみることにしました。
dictknife を試してみる
環境を作る
python を直接使える環境で、pip install pyyaml dictknife
をするなら、それで良いです。
自分はローカル環境にはあまりインストールしないので、Docker で環境を作ります。といっても小さいです。
FROM python:3
RUN apt-get update && pip install --upgrade pip && pip install pyyaml dictknife
VOLUME /work
WORKDIR /work
小さいですね。dictknife に入っている jsonknife を使います。(jsonknife って OSS がありますが、関係ないです)
$ docker build -t dictknife .
# 略
$ docker run --rm dictknife jsonknife
usage: jsonknife [-h]
[--log {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}]
[-q] [--debug]
{cut,deref,select,bundle,separate,examples} ...
# 略
起動しました。
試してみる
こんな感じで書いてみます。
foo:
bar: 1
ref_local:
$ref: "#/sub_item"
ref_remote:
$ref: "test_remote.yml#/remote_item/umu"
sub_item:
dayo: ne
remote_item:
umu:
homu: 9
見れば分かりますね。$ref
に、<URI>
, #<JSON Path>
, <URI>#<JSON Path>
を書くだけです。<URI>
がなければ同じファイル、#<JSON Path>
がなければファイル全体を指します。
実行してみましょう
$ docker run --rm -v $(PWD):/work dictknife jsonknife select --src src/test.yml
foo:
bar: 1
ref_local:
dayo: ne
ref_remote:
homu: 9
sub_item:
dayo: ne
すばらしいですね。
GitHub Actions で使ってみる
GitHub Actions 用のファイルをどこにおくべきか困ったのですが、.github
の中に workflow
以外のディレクトリを作って良いのか判断に迷ったので、.github-actions-src
を作ります。
(公式ドキュメントでは、.github/actions
にローカル Action を設置するようなサンプルはありますが)
ファイルを置くルールを決めます。深い理由はないです。
-
.github-actions-src/
スクリプトを置く-
src/
テンプレートが展開されるファイルを置く-
ref/
参照のみされるファイルを置く
-
-
.github-actions-src/src
の直下のファイルを jsonknife で .github/workflow
にビルドする形ですね。以下の様なスクリプトを書きました。(source コマンド経由では動きません)
#!/bin/sh
OUT_DIR=../../.github/workflows/
cd `dirname $0`/src
for i in *.yml
do
docker run --rm -v $PWD:/work dictknife jsonknife select --src $i > $OUT_DIR/$i
done
統合実行のバリデーション
.github/workflow
そのものは、workflow から起動されるため、github actions で生成はできません。手動で実行して commit しておく必要があります。
しかし、手動だと忘れるのは世の常。CI で統合を行い、正しい workflow がコミットされているか確かめます。
name: workflow settings validation
'on': [ push ]
jobs:
validation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
# https://github.com/actions/cache/blob/master/examples.md#using-a-script-to-get-cache-location
- name: Get pip cache dir
id: pip-cache
run: 'echo "::set-output name=dir::$(pip cache dir)"'
- uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip-dictknife
restore-keys: |
${{ runner.os }}-pip-
- name: Install dictknife
run: |
python -m pip install --upgrade pip
pip install pyyaml dictknife
- name: Validation
run: |
cp -R .github/workflows .github/workflows.orig
.github-actions-src/build-local.sh
diff -ur .github/workflows .github/workflows.orig || ( echo '.github/workflows に差分があります' && exit 1 )
rm -R .github/workflows.orig
echo 'validation complete'
注意点
- key 名で
on
を使うと、なぜか true になります。明示的に文字列の'on'
にします。 - コメントは綺麗さっぱり消えます。
テストを書いてみる
フロントエンド側のワークフローを考えてみましょう。
並列化できるところは並列化しておきたいので、こんな感じですかね?
しかし、個々のプロセスが小さいなら Job を大量に作るより、一つにまとめた方が立ち上がりの時間の合計と比較すると早そうです。しかし、気がついたら肥大化しているのがプロジェクトです。
ともあれ、中身の定義は適当ですが、図のフローをそのまま workflow に起こしてみます。
name: Node env
'on': [ push ]
env:
CACHE_VERSION: 'test-node'
TARGET_PATH: '.'
ARTIFACT_NAME: 'test-node'
ARTIFACT_PATH: './build'
# setup
# |
# +-------+--------+
# | | |
# lint unit_test build
# | | |
# | | e2e_test
# | | |
# +-------+--------+
# |
# deploy
jobs:
setup:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- $ref: 'ref/node-env-common.yml#/checkout'
- $ref: 'ref/node-env-common.yml#/setup'
- $ref: 'ref/node-env-common.yml#/cache'
- $ref: 'ref/node-env-common.yml#/install'
lint:
needs: [ setup ]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- $ref: 'ref/node-env-common.yml#/checkout'
- $ref: 'ref/node-env-common.yml#/setup'
- $ref: 'ref/node-env-common.yml#/cache'
- $ref: 'ref/node-env-common.yml#/install'
- $ref: 'ref/node-env-common.yml#/lint'
unit_test:
needs: [ setup ]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- $ref: 'ref/node-env-common.yml#/checkout'
- $ref: 'ref/node-env-common.yml#/setup'
- $ref: 'ref/node-env-common.yml#/cache'
- $ref: 'ref/node-env-common.yml#/install'
- $ref: 'ref/node-env-common.yml#/unit_test'
build:
needs: [ setup ]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- $ref: 'ref/node-env-common.yml#/checkout'
- $ref: 'ref/node-env-common.yml#/setup'
- $ref: 'ref/node-env-common.yml#/cache'
- $ref: 'ref/node-env-common.yml#/install'
- $ref: 'ref/node-env-common.yml#/build'
- $ref: 'ref/node-env-common.yml#/store_artifact'
e2e_test:
needs: [ build ]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- $ref: 'ref/node-env-common.yml#/checkout'
- $ref: 'ref/node-env-common.yml#/setup'
- $ref: 'ref/node-env-common.yml#/cache'
- $ref: 'ref/node-env-common.yml#/install'
- $ref: 'ref/node-env-common.yml#/restore_artifact'
- $ref: 'ref/node-env-common.yml#/e2e_test'
deploy:
needs: [ lint, unit_test, e2e_test ]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- $ref: 'ref/node-env-common.yml#/checkout'
- $ref: 'ref/node-env-common.yml#/setup'
- $ref: 'ref/node-env-common.yml#/cache'
- $ref: 'ref/node-env-common.yml#/install'
- $ref: 'ref/node-env-common.yml#/restore_artifact'
- $ref: 'ref/node-env-common.yml#/deploy'
# 使用 env
# TARGET_PATH: package.json のあるディレクトリ
# CACHE_VERSION: cache 用の prefix
# ARTIFACT_NAME: artifact 保存用の名前
# ARTIFACT_PATH: artifact を保存するディレクトリ
#
checkout:
uses: actions/checkout@v2
setup:
uses: actions/setup-node@v2.1.2
with:
node-version: 12.x
check-latest: true
cache:
uses: actions/cache@v2
with:
path: ${{ env.TARGET_PATH }}/node_modules
key: ${{ env.CACHE_VERSION }}-${{ runner.os }}-v12-${{ hashFiles(format('{0}/package-lock.json', env.TARGET_PATH)) }}
restore-keys: |
${{ env.CACHE_VERSION }}-${{ runner.os }}-v12-
install:
run: |
cd ${{ env.TARGET_PATH }}
cp package-lock.json package-lock.json.back
npm install
diff -u package-lock.json package-lock.json.back || ( echo '${{ env.TARGET_PATH }}/package-lock.json に差分があります' && exit 1)
store_artifact:
uses: actions/upload-artifact@v2
with:
name: ${{ env.ARTIFACT_NAME }}
path: ${{ env.ARTIFACT_PATH }}
restore_artifact:
uses: actions/download-artifact@v2
with:
name: ${{ env.ARTIFACT_NAME }}
path: ${{ env.ARTIFACT_PATH }}
lint:
name: Lint
run: npm run --silent lint
unit_test:
name: Test
run: npm run --silent test
build:
name: Build
run: npm run --silent build
e2e_test:
name: E2E Test
run: |
echo 'E2E Test dummy'
ls ${{ env.ARTIFACT_PATH }}
deploy:
name: Deploy
run: |
echo 'Deploy dummy'
ls ${{ env.ARTIFACT_PATH }}
凄まじいコピペ感を感じる...!!!
JSON Reference は Object のマージができないので、複数 step (配列要素) をまとめることは出来ないですが、直接記述するよりは全然よいです。
グローバル変数を使った引数渡しの関数呼び出しみたいですね...。
一応 env は、Global, job, job.step で設定できるので、job 単位のスコープにはできます。
まとめ
We're hiring!
dictknife 便利!
以上のサンプルは GitHub bugfire/gh-actions-test に置いてあります。