GitHub Actions の JavaScript Action を TypeScript で書いた
GitHub Action を TypeScript で作成したので,覚え書きがてらどうやって作ったかについて書きます. github-action-benchmark という Action をつくりました.
紹介記事:継続的にベンチマークを取るための GitHub Action をつくった
Action とは
今年9月に GitHub Action v2 がリリースされました.GitHub Action は GitHub が提供する CI/CD サービスです. 既存のサービスと大きく違う点は,処理を汎用的に Action として切り出して再利用できることです. 例えば,GitHub からのリポジトリのクローン actions/fetch や Node.js のセットアップ actions/setup-node などの基本的な実行ステップも Action として実装されています.
Action の種類
GitHub Action には JavaScript Action と Docker Action があります.下記のページに公式の解説があります.
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-actions
- Docker Action: Docker コンテナを書くと,そのコンテナが GitHub Action のステップで実行されます
- Docker コンテナとして実行されるので,実行環境は Linux オンリーです
- ちょっとしたシェルコマンドをまとめた Action ならシェルスクリプトで書いて,簡単な Dockerfile を書いてエントリポイントから実行するだけで実現できます
- Docker コンテナのイメージに任意のツールや言語のランタイムを入れておけば,好きなツールや言語で Action がつくれます
- Action の入力は環境変数,出力は標準出力で行います
- 公式ヘルプ: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-a-docker-container-action
- JavaScript Action: Node.js のパッケージを書くと,そのパッケージが GitHub Action のステップとして実行されます
- Node.js で実行されるので,好きな npm パッケージがそのまま使えます
- 公式の npm パッケージ群があって,Action の入出力はライブラリ
@actions/*
で行えます.TypeScript に公式で対応しています - コンテナで実行されないので,Docker Action に比べて高速らしいです
- 公式ヘルプ: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-a-javascript-action
この記事では後者のみに絞ります.
Hello world
まず JavaScript で雛形を書きます.
Creating a JavaScript action を見て順番に試していけば良いので,ここで解説することは特にありません.
action.yml
を置き,npm init
して @actions/*
パッケージを入れ,index.js
を書きます.
次に npm install --save-dev typescript
して TypeScript コンパイラを入れ,index.js
を index.ts
に書き換え,tsconfig.json
を用意します.
{ "compilerOptions": { "module": "commonjs", "moduleResolution": "node", "preserveConstEnums": true, "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "noEmitOnError": true, "strictNullChecks": true, "target": "es2019", "sourceMap": true, "esModuleInterop": true, "resolveJsonModule": true }, "files": [ "index.ts" ] }
もしくは公式のテンプレートを使っても良いです.
公式の @actions/*
パッケージは TypeScript で書かれていて型定義を持っているのでこのままでほぼ問題ありませんが,残念ながら一発ではコンパイルが通りません.
@actions/github
が使っている @octokit/graphql
は v4 から TypeScript に対応しているのですが,@actions/github
が使っているのは v2 なので型定義がありません.
少し調べてみると,@actions/github
は @octokit/graphql
の型定義を自前で抱えていることが分かったので,v4 に対応するまではそこのものをコピーしてくるか,下記のように回避するための型定義ファイルを書いて自分のプロジェクトに含めておく必要がありました.
// Workaround for @actions/github until @octokit/graphql v4 is used declare module '@octokit/graphql' { type GraphQlQueryResponse = unknown; type Variables = unknown; }
最後にテスト用の workflow ファイルを更新します.TypeScript のコンパイルが必要になるので,リポジトリを checkout してきて node をセットアップし,npm install
して tsc
でコンパイルします.
実行する Action を ./
で始まる相対パスにしておくとローカルのアクションを実行してくれます.
ちなみに .
ではダメで ./
にする必要があって,少しハマりました.
runs-on: ubuntu-latest name: A job to say hello steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + - run: npm install + - run: ./node_modules/.bin/tsc -p . + - name: Hello world action step id: hello - uses: actions/hello-world-javascript-action@master + uses: ./ with: who-to-greet: 'Mona the Octocat' # Use the output from the `hello` step
後は自分の好きなように入力を定義し,処理を書いて結果を出力するだけなので,基本的に下記のドキュメントを見て進めれば OK です.
action.yml
の書き方: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions.workflow
ファイルの書き方: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions- actions/toolkit リポジトリの README
この記事では今回 github-action-benchmark を作成する際に引っかかったポイントや考えたポイントだけ書きます.
Action の入力
Action への入力は workflow ファイルでは with:
に key: value
で複数していします.JavaScript Action では @actions/core
パッケージの getInput(name: string): string
で値を取得できます.
少し注意する必要があるのは,入力は文字列型オンリーな点です.この入力には array や map が使えません.なので,例えば下記のようなリスト形式やキーバリュー形式の入力を定義することはできません.
uses: ./ with: awesome-array-option: - one - two - three awesome-map-option: key: value key2: value2
これは入力が Docker Action で環境変数 $INPUT_*
にマッピングされていることに由来していそうですが,理由は不明です.
リストを使いたい場合はどうすれば良いのかなといくつか公式 Action を調べてみると,
with: awesome-array-option: | one two three
のように改行区切りの文字列で指定させて Action 側で処理しているものがありました.
Action のデバッグ
GitHub Action の実行環境と同じ環境変数(例えば入力 foo-bar
は $INPUT_FOO_BAR
)を与えて node index.js
で実行するか,Action をプッシュしてテスト用の workflow の実行結果を見てデバッグすることになります.簡単なものであれば前者だけで事足りますが,最終的に GitHub Action で実行して結果を確認することになると思います.
@actions/core
の core.debug()
でデバッグ情報を埋め込んでおくのですが,これは(デバッグ情報なので当然ですが)デフォルトでは表示されません.
デバッグ情報は特定の secret に true
をセットすることで初めて表示されます.secret は本来リポジトリ固有の秘密の情報(API トークンとか)を保存する場所ですが,デバッグ情報の設定にも利用されています.リポジトリページ → Settings → Secrets タブ で設定できます.
ACTIONS_RUNNER_DEBUG
: workflow を実行する Runner の実行ログACTIONS_STEP_DEBUG
: 各ステップの実行ログ
Action のデバッグ情報を見るには後者が必要です.
Action のテスト
単体テスト
普通の npm パッケージと同じようにテストすれば OK です. GitHub Action と同じ環境変数を与えてテストを実行すれば OK です.
github-action-benchmark では単体テスト中に git
コマンドを実行されると困るのと,@actions/core
の実装のロジックに依存するのが嫌だったので @actions/core
を mock-require で mock してしまってます.
真面目にやるならテスト用の Git リポジトリを作ってその中でテストケースを走らせるべきですが,その辺はサボってます.
E2Eテスト
実際に Action を走らせて期待した動作になっているかは GitHub Action で走らせて結果を見るのが一番良いと思います. github-action-benchmark では CI 用の workflow の中で action をビルドして実行し,実行後に期待した状態になっているかをスクリプトでチェックしています.
これだと「正しく fail するか」をテストすることができない(action が fail するとその時点で workflow が止まってしまうので)のですが,どうやれば良いかはまだよく分かっていないです…(誰かご存知なら教えてもらえると嬉しいです)
Action の公開
Action は GitHub の公開リポジトリに置いておくと即使えるようになります.例えば user/foo-action
というリポジトリをつくると
name: user/foo-action@ref
のように書くだけで使えるようになります.ここで ref
は Git リポジトリの ref で,ブランチ名やタグ名,コミットハッシュなどが使えます.
なので,この ref
で Action のバージョンを制御するのですが,セマンティックバージョニングをサポートするような仕組みは無いので,自前でルールを定める必要があります.
github-action-benchmark ではセマンティックバージョニングに従ってリリースすることを決めました.
rhysd/github-action-benchmark@v1
は v1.x.y の最新を表すrhysd/github-action-benchmark@v1.1.0
は v1.0.0 に固定することを表す- マイナーバージョンまでの固定や範囲指定はサポートしない(必要性を感じなかったので)
としました.
また,JavaScript Action は Action のリポジトリを checkout した時点で即 node .
で実行できるようになっていないといけないので,TypeScript や babel を使っている場合は事前にコンパイルした JavaScript コードをリポジトリに置いておく必要があります.
また,Action 実行時に npm install
も行わないので node_modules
も丸ごと置いておく必要があります.native extension を利用しているパッケージが node_modules
に含まれている時にどうすれば良いのかはよく分かりません…
master
ブランチにビルドした生成物と依存パッケージを直置きしたくなかったので,下記のように運用することにしました
- 各メジャーバージョンごとに orphan ブランチを切る.例えば v1.x.y 向けは
v1
ブランチ - 新しいバージョンのリリースは対応するメジャーバージョンのブランチで行う.例えば現在の
master
ブランチをv1.0.2
にリリースする場合は,まずはmaster
ブランチで TypeScript コードを JavaScript にコンパイルし,v1
ブランチに切り替えてそれらを add して commit し,v1.0.2
タグをつけて push します - 次のメジャーバージョン(例えば
v2
)をリリースする時は前バージョンの開発用ブランチ(例えばdev/v1
)を作っておいて v1.x.y 向けの修正はこちらからv1
ブランチに入れる
ちなみに公式の推奨では release/v1
ブランチをつくって v1
タグを毎回最新に張り替えることを推奨しています.これはリリース前に release/v1
上でテストを行う前提なんだと思いますが,毎回張り替えないといけないのと,master
でテストしてから v1
ブランチに成果物を置く前提だったのでブランチ名を v1
としています.
ちなみに master
→ v1
へは簡単なスクリプトを使って成果物を置いてます.
https://github.com/rhysd/github-action-benchmark/blob/master/scripts/prepare-release.sh
Action で発行される GitHub API トークンの注意
GitHub Action では実行ごとに GitHub API のトークンが自動生成され secrets.GITHUB_TOKEN
に格納されます.基本的には普通の API トークンのように使えるのですが,どうやらパーソナルアクセストークンとは異なりイベントを起こす権限を持っていないようです.
例えば GitHub Pages のブランチに push することはできますが,GitHub Pages のデプロイをすることはできません.
この制限のため,github-action-benchmark ではパーソナルアクセストークンが必要になってしまっています.
ちなみにこの制限はなぜか public repo のみで,private repo では問題なく GitHub Pages のデプロイができました.