Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

GitHubカスタムアクションの作り方(JavaScript編)

こんにちは、最近娘が妻の料理の手伝いをするようになってきたのですが、卵を割ることを「こんこんぱっする」と言っていて音の表現が秀逸で可愛いなと思った木村です。

さて、今回はGitHubカスタムアクションとその作り方についてお話ししたいと思います。

GitHubカスタムアクションとは

GitHubカスタムアクションとは、GitHub Actionsのワークフロー内で実行できるアクションを、独自に(カスタムで)作成したもののことです。
例えばワークフロー中でリポジトリをチェックアウトするときに

      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

と書いたりしますが、これはactions/checkoutというアクションの、v2タグの付いたバージョンを使い、その実行時パラメータとしてfetch-depthを0にして実行する、という意味です。
GitHubアクションはマーケットプレイスで多数公開されており、自分に必要な機能を持ったアクションを探して使うことができます。

しかし、自分に必要な機能を持ったアクションが見つからない時もあります。その場合、ワークフロー内で必要なツールをインストールしてからシェルコマンドを実行したり、ツールをインストールしたコンテナでワークフローを実行するということも可能です。しかし、可読性が悪くなり、再利用性も低くなるので、ワークフローのメンテナンスも大変になります。

もし同様の機能を複数のワークフローで使うことがある場合は、独自のカスタムアクションを作ると良いです。
今回は、GitHubカスタムアクションを作成する方法を紹介します。

カスタムアクションについての公式ドキュメントは以下のURLになります。

docs.github.com

カスタムアクションの種類

カスタムアクションには、以下の種類があります。

  • 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で必須と定義しているので、requiredtrueにしています。なお、入力値が指定されていなかった場合にはエラーとなり例外が発生します。 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です。

ワークフロー実行結果(1)

GitHub Actionsでのテスト2

次に、リポジトリ内のアクションではなく、公開されたアクションとして実行してみましょう。
タグやブランチ名を使うこともできますが、ひとまずコミットのSHAを使ってみましょう。

現在のコミットのSHAを以下のコマンドで取得します。

$ git show --format='%H' --no-patch
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

action.ymlusesを以下のように変更します。

(省略)
        - 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にプッシュしてワークフローを実行してみます。
以下のように、チェックアウトステップがなく、指定されたコミットのアクションが実行されていることがわかります。

ワークフロー実行結果(2)

パッケージをリポジトリに含めたくない場合

先述のように、カスタムアクションは、実行に必要なパッケージもリポジトリに含める必要があります。アクションの実行では、package.jsonなどに従って実行環境のセットアップなどは行わず、「ダウンロードしてきたものをそのまま」実行するからです。

しかし、外部のパッケージを自分のリポジトリに含めるのは、あまり好ましくない場合もあります。
その際は、1つの方法としてはvercel/nccのような、Node.jsのパッケージを1つのJSファイルにコンパイルしてくれるツールを使うという方法があります。
手順としては

  1. npm i @vercel/nccでパッケージをインストール
  2. npx ncc build index.js -o distでJSファイルを生成
  3. node_modulesディレクトリをリポジトリに含めず、dist/index.jsを含める

となります。
なお、利用しているパッケージをリポジトリに含める場合も、@vercel/ncc等を使う場合も、パッケージのライセンスには十分ご注意ください。

まとめ

今回は、GitHubカスタムアクションについて説明し、JavaScriptでのカスタムアクションの作り方を説明しました。
カスタムアクションの作り方ではaction.ymlの記載方法、入力値と出力値の取り扱い方、エラー時の処理、ローカルでの動作検証方法、リポジトリ内アクションの実行方法、公開アクションの実行方法と一通りのことを説明しましたので、あとは皆さん実際に手を動かしてJavaScript(ないしはTypeScript)で必要な処理を書いて、カスタムアクションを作ってみてください。

そして、便利なアクションが作れたら是非マーケットプレイスに公開してみてください。
マーケットプレイスへの公開方法については、Dockerコンテナアクションの作り方を説明する次回の記事で併せて説明したいと思います。

皆さんのお役に立てば幸いです。