こんにちは、最近娘が妻の料理の手伝いをするようになってきたのですが、卵を割ることを「こんこんぱっする」と言っていて音の表現が秀逸で可愛いなと思った木村です。
さて、今回はGitHubカスタムアクションとその作り方についてお話ししたいと思います。
GitHubカスタムアクションとは
GitHubカスタムアクションとは、GitHub Actionsのワークフロー内で実行できるアクションを、独自に(カスタムで)作成したもののことです。
例えばワークフロー中でリポジトリをチェックアウトするときに
- name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0
と書いたりしますが、これはactions/checkoutというアクションの、v2
タグの付いたバージョンを使い、その実行時パラメータとしてfetch-depth
を0にして実行する、という意味です。
GitHubアクションはマーケットプレイスで多数公開されており、自分に必要な機能を持ったアクションを探して使うことができます。
しかし、自分に必要な機能を持ったアクションが見つからない時もあります。その場合、ワークフロー内で必要なツールをインストールしてからシェルコマンドを実行したり、ツールをインストールしたコンテナでワークフローを実行するということも可能です。しかし、可読性が悪くなり、再利用性も低くなるので、ワークフローのメンテナンスも大変になります。
もし同様の機能を複数のワークフローで使うことがある場合は、独自のカスタムアクションを作ると良いです。
今回は、GitHubカスタムアクションを作成する方法を紹介します。
カスタムアクションについての公式ドキュメントは以下のURLになります。
カスタムアクションの種類
カスタムアクションには、以下の種類があります。
- JavaScript
- Dockerコンテナ
- Composite
それぞれについて説明します。
JavaScript
JavaScriptアクションは、JavaScriptで書かれたアクションです。ワークフローを実行するコンテナの上で、Node.jsで実行されます。
author/action-name@tag
というアクションを実行する場合、 https://github.com/author/action-name/ というリポジトリのtag
というタグのついたバージョンをチェックアウトし、そこに含まれるaction.yml
で指定されたJavaScriptファイルを実行します。
このため、後述するDockerよりも実行速度が速いですが、特定のバイナリに依存するような処理を実行することはできません。
Dockerコンテナ
Dockerコンテナアクションは、Dockerコンテナで書かれたアクションです。アクションは指定されたコンテナ上で実行されます。コンテナ上で実行できる処理であれば何でも実行できます。
このため、JavaScriptアクションよりも開発が容易ですが、コンテナイメージのメンテナンスコストが必要なのと、実行速度がJavaScriptアクションよりも遅いというデメリットがあります。
Composite
Compositeは複数のワークフローを1つのアクションに集約するものとなります。今回はCompositeについては省略します。
JavaScriptカスタムアクションの作成
それでは早速JavaScriptアクションを作成してみます。Dockerコンテナアクションの作成方法は、次回の記事で説明します。
また、gitやNode.jsのインストールについては割愛します。
リポジトリの作成
以下のコマンドを実行します。
$ mkdir sample-action $ cd sample-action $ npm init -y $ git init $ git add . $ git commit -m 'initial commit'
action.yml
の作成
カスタムアクションを作成する場合、まずaction.yml
というファイルを準備します。このファイルには、アクションのメタデータを記述します。
以下は、JavaScriptアクションのaction.yml
の例です。
name: 'アクションの名前' description: 'アクションの説明' branding: icon: upload-cloud color: blue inputs: id_of_input: # 入力値のID description: '入力値の説明' required: true # 必須かどうか default: 'default value' # デフォルト値 id_of_input2: # 入力値のID description: '入力値の説明' required: true # 必須かどうか default: 'default value 2' # デフォルト値 outputs: id_of_output: # 出力値のID description: '出力値の説明' id_of_output2: # 出力値のID description: '出力値の説明' runs: using: 'node16' main: 'index.js' # 実行するJavaScriptファイル
このアクションは、2つの入力値を受け取り、2つの出力値を返します。
実際にこのアクションが実行されるときは、runs.main
で指定されたindex.js
が、ワークフローを実行しているコンテナ内でnode index.js
という形で実行されます。
branding
は、アクションの実行には影響ないので動かすだけであれば不要です。マーケットプレイスに登録する場合は、ここでマーケットプレイスに表示されるアイコンや色を指定します。
なお、入力名には-
(ハイフン)などは使わない方が良いです。この後説明しますが、ローカルで動作検証をする際に環境変数として入力値を設定しないといけないので、環境変数名に使いにくい文字は避けて置いた方が無難です。
index.js
の作成
次に、処理の本体となるindex.js
を作成します。index.js
では、入力値を受け取り、出力値を返す処理を記述します。
入力値・出力値の取り扱いは@actions/core
というツールキットパッケージを使います。
ツールキットはhttps://github.com/actions/toolkitで公開されているので、適宜必要なものを利用します。
以下のコマンドで@actions/core
パッケージをインストールします。
npm install @actions/core
index.js
は以下のようになります。
const core = require('@actions/core'); try { const input1 = core.getInput('id_of_input', { required: true}); const input2 = core.getInput('id_of_input2', { required: true}); core.setOutput('id_of_output', input1); core.setOutput('id_of_output2', input2); } catch (error) { core.setFailed(error.message); }
core.getInput()
で入力値を取得します。第1引数には、action.yml
で指定した入力値のIDを指定します。第2引数には、オプションとして、required
を指定します。今回はいずれの入力値もaction.yml
で必須と定義しているので、required
をtrue
にしています。なお、入力値が指定されていなかった場合にはエラーとなり例外が発生します。
core.getInput()
戻り値の型はstring
です。必要に応じて適切な型に変換する必要があります。
core.setOutput
で出力値を設定します。第1引数には、action.yml
で指定した出力値のIDを指定します。第2引数には、出力する値を指定します。
そして、例外が発生した場合には、アクションを失敗させるためにcore.setFailed()
を実行します。第1引数には、失敗時のエラーメッセージを指定します。このエラーメッセージは、ワークフローのログに出力されます。
ローカルでのテスト
では、ローカルでテストしてみましょう。
$ node .\index.js ::error::Input required and not supplied: id_of_input
必須のオプションであるid-of-input
が指定されていないため、エラーとなりました。実際にGitHub Actionsのワークフロー内でこのアクションを呼び出すと、ワークフローが失敗し、このメッセージがログに出力されます。
ローカルでテストする際に入力値を渡すには、INPUT_ID_OF_INPUT
と、action.yml
で指定した入力値のIDにINPUT_
を付けて全て大文字にした環境変数を設定します。
Linux/Macなどでbashを使っているのであれば以下のようにします。
$ export INPUT_ID_OF_INPUT='input value 1' $ export INPUT_ID_OF_INPUT2='input value 2'
WindowsでPowerShellであれば以下のようにします。
$ $env:INPUT_ID_OF_INPUT='input value 1' $ $env:INPUT_ID_OF_INPUT2='input value 2'
再度実行してみましょう。
$ node .\index.js ::set-output name=id_of_output::input value 1 ::set-output name=id_of_output2::input value 2
無事動いていますね!
GitHub Actionsでのテスト1
では、GitHub Actionsでテストしてみましょう。まずはこのリポジトリのワークフローから呼んで正しく動くか確認します。
.github/workflows/action.yml
を以下の内容で作成します。
on: push: jobs: test: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v2 - name: test action id: test uses: ./ with: id_of_input: 'input value 1' id_of_input2: 'input value 2' - name: view result run: | echo ${{ env.id_of_output }} echo ${{ env.id_of_output2 }} env: id_of_output: ${{ steps.test.outputs.id_of_output }} id_of_output2: ${{ steps.test.outputs.id_of_output2 }}
test action
ステップで、uses: ./
とすることで、checkout
ステップでチェックアウトしたこのリポジトリのアクションを使うようにしています(action.yml
の位置を指定している)。
view result
で、test action
ステップの出力を表示しています。ステップ内で直接参照はできないので、env:
を使って環境変数として渡します。
実際にGitHubで動かしてみましょう。リポジトリをプッシュするとき、「実行に必要なパッケージ(node_modules
以下)」も全てリポジトリに含める必要があるので注意してください。
GitHubのリポジトリのページの「Actions」タブを確認し、リポジトリのプッシュで起動されたワークフローの実行が以下のように成功していればOKです。
GitHub Actionsでのテスト2
次に、リポジトリ内のアクションではなく、公開されたアクションとして実行してみましょう。
タグやブランチ名を使うこともできますが、ひとまずコミットのSHAを使ってみましょう。
現在のコミットのSHAを以下のコマンドで取得します。
$ git show --format='%H' --no-patch xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
action.yml
のuses
を以下のように変更します。
(省略) - name: test action id: test uses: your-github-user-name/sample-action@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (省略)
先頭の./
を外し、プッシュしたGitHubユーザ名ならびにリポジトリ名の後ろに、@
を付けて、コミットのSHAを指定します。
また、公開されたアクションを使うので(今回は実体として同じリポジトリではありますが)、最初のチェックアウトステップは不要です。
最終的にワークフローファイルは以下のようになります。
on: push: jobs: test: runs-on: ubuntu-latest steps: - name: test action id: test uses: your-github-user-name/sample-action@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx with: id_of_input: 'input value 1' id_of_input2: 'input value 2' - name: view result run: | echo ${{ env.id_of_output }} echo ${{ env.id_of_output2 }} env: id_of_output: ${{ steps.test.outputs.id_of_output }} id_of_output2: ${{ steps.test.outputs.id_of_output2 }}
では、この修正をGitHubにプッシュしてワークフローを実行してみます。
以下のように、チェックアウトステップがなく、指定されたコミットのアクションが実行されていることがわかります。
パッケージをリポジトリに含めたくない場合
先述のように、カスタムアクションは、実行に必要なパッケージもリポジトリに含める必要があります。アクションの実行では、package.json
などに従って実行環境のセットアップなどは行わず、「ダウンロードしてきたものをそのまま」実行するからです。
しかし、外部のパッケージを自分のリポジトリに含めるのは、あまり好ましくない場合もあります。
その際は、1つの方法としてはvercel/nccのような、Node.jsのパッケージを1つのJSファイルにコンパイルしてくれるツールを使うという方法があります。
手順としては
npm i @vercel/ncc
でパッケージをインストールnpx ncc build index.js -o dist
でJSファイルを生成node_modules
ディレクトリをリポジトリに含めず、dist/index.js
を含める
となります。
なお、利用しているパッケージをリポジトリに含める場合も、@vercel/ncc
等を使う場合も、パッケージのライセンスには十分ご注意ください。
まとめ
今回は、GitHubカスタムアクションについて説明し、JavaScriptでのカスタムアクションの作り方を説明しました。
カスタムアクションの作り方ではaction.yml
の記載方法、入力値と出力値の取り扱い方、エラー時の処理、ローカルでの動作検証方法、リポジトリ内アクションの実行方法、公開アクションの実行方法と一通りのことを説明しましたので、あとは皆さん実際に手を動かしてJavaScript(ないしはTypeScript)で必要な処理を書いて、カスタムアクションを作ってみてください。
そして、便利なアクションが作れたら是非マーケットプレイスに公開してみてください。
マーケットプレイスへの公開方法については、Dockerコンテナアクションの作り方を説明する次回の記事で併せて説明したいと思います。
皆さんのお役に立てば幸いです。