Alternative Architecture DOJO

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

GitHubカスタムアクションの作り方(Dockerコンテナ編)

こんにちは、先日どういう話の流れだったか、娘が「世の中には色んな人がいるのは良いと言うことを学んだ」と言ってるの聞いて、多様性って大事だけどそんな言い回しをどこで覚えてきたのかと感心した木村です。

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

GitHubカスタムアクションならびにJavaScriptカスタムアクションについては前回の記事で紹介していますので、合わせてご覧ください。

Dockerコンテナカスタムアクションの作成

GitHubカスタムアクションならびにDockerコンテナカスタムアクションの概要については前回のブログで説明していますので、早速作成していきます。
なお、Dockerコンテナを作成するための、docker環境のセットアップについてもここでは割愛します。

リポジトリの作成

以下のコマンドを実行します。

$ mkdir sample-action-docker
$ cd sample-action-docker
$ git init
$ git add .
$ git commit -m 'initial commit'

action.ymlの作成

JavaScriptカスタムアクションと同様に、action.ymlを作成します。アクションの動作は前回と同じく、2つの入力値を受け取り、2つの出力値を返すものとします。

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: 'docker'
  image: 'Dockerfile' # 実行するコンテナのDockerfile
  env:
    ID_OF_INPUT: ${{ inputs.id_of_input }} # 入力値を環境変数に渡す
    ID_OF_INPUT2: ${{ inputs.id_of_input2 }} # 入力値を環境変数に渡す

runs以外の要素はJavaScriptアクションと一緒です。
runs.usingdockerとし、runs.imageに実行するコンテナのDockerfileを指定します。そして、入力はaction.ymlの中ではinputs.入力値のIDという形で参照できますので、これをruns.envで環境変数にマップしてコンテナに渡してあげます。

Dockerfileの作成

次に、処理の本体となるコンテナのDockerfileを作成します。まずは前回のJavaScriptカスタムアクションと同じく、受け取った2つの値をそのまま2つの出力に引き渡すだけのコンテナを作成してみます。

FROM ubuntu:22.04
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

コンテナで実行されるentrypoint.shを作成します。JavaScriptカスタムアクションではcore.setFailed()を使ってエラーを発生させていましたが、Dockerコンテナでは最後にENTRYPOINTなどで実行されるコマンドの終了コードに応じてエラー終了または正常終了が判定されます。今回の場合、必須の入力値が設定されていない場合はexit 1と0でない戻り値でシェルスクリプトを終了させることでエラーとしています。

そして、出力については${GITHUB_OUTPUT}環境変数で示されるファイルに

ID_OF_OUTPUT=VALUE_OF_OUTPUT
ID_OF_OUTPUT2=VALUE_OF_OUTPUT2

という形で、出力1つにつき1行になるように記載してあげればOKです。

最後に、デバッグのために出力内容を標準出力に出しています。これはワークフローの実行結果の所に表示されます。

#!/bin/bash
if [ -z "ID_OF_INPUT" ]
then
  echo "No id_of_input environment variable supplied"
  exit 1
fi
if [ -z "ID_OF_INPUT2" ]
then
  echo "No id_of_input2 environment variable supplied"
  exit 1
fi

echo "id_of_output=${ID_OF_INPUT}" >> "${GITHUB_OUTPUT}"
echo "id_of_output2=${ID_OF_INPUT2}" >> "${GITHUB_OUTPUT}"
cat "${GITHUB_OUTPUT}"

出力が複数行になる場合は適切にエスケープして1行にすることでも対応可能ですが、以下のようにdelimiterを使ってあげると良いです。
delimiterを使った$GITHUB_OUTPUTファイルは以下のような形になります。

ID_OF_OUTPUT<<__DELIMITER1__
VALUE_OF_OUTPUT_LINE1
VALUE_OF_OUTPUT_LINE2
__DELIMITER1__
ID_OF_OUTPUT2<<__DELIMITER2__
VALUE_OF_OUTPUT2_LINE1
VALUE_OF_OUTPUT2_LINE2
__DELIMITER2__

delimiterの値は固定にならないようにランダムにするのがセキュリティ上望ましいとされていますので、シェルスクリプト内では以下のようにすると良いでしょう。

#!/bin/bash
if [ -z "ID_OF_INPUT" ]
then
  echo "No id_of_input environment variable supplied"
  exit 1
fi
if [ -z "ID_OF_INPUT2" ]
then
  echo "No id_of_input2 environment variable supplied"
  exit 1
fi

delimiter="$(openssl rand -hex 8)"
echo "id_of_output<<${delimiter}" >> "${GITHUB_OUTPUT}"
echo "${ID_OF_INPUT}" >> "${GITHUB_OUTPUT}"
echo "${delimiter}" >> "${GITHUB_OUTPUT}"

delimiter="$(openssl rand -hex 8)"
echo "id_of_output2<<${delimiter}" >> "${GITHUB_OUTPUT}"
echo "${ID_OF_INPUT}" >> "${GITHUB_OUTPUT}"
echo "${delimiter} >> "${GITHUB_OUTPUT}"
cat "${GITHUB_OUTPUT}"

entrypoint.shを実行可能にします。

$ git add entrypoint.sh
$ git update-index --chmod=+x entrypoint.sh

ローカルでのテスト

では、ローカルでテストしてみましょう。ローカルで実行する場合は${GITHUB_OUTPUT}は設定されていないので適当な値を設定して実行します。

$ docker build -t sample-action-docker:latest ./
$ docker run --rm --name sample-aciton-docker  -e ID_OF_INPUT=value_of_input -e ID_OF_INPUT2=value_of_input2 -e GITHUB_OUTPUT=/output.txt sample-action-docker:latest

以下のように出力されれば成功です。

id_of_output=value_of_input
id_of_output2=value_of_input2

GitHub Actionsでのテスト

今回はリポジトリのワークフローからの呼び出しテストを省略して、いきなり公開されたアクションとして呼んでみましょう。

リポジトリにコミットしてからハッシュタグを取得します。

$ git add .
$ git commit -m '[add] add a container action'
$ git show --format='%H' --no-patch

.github/workflows/action.ymlを作成します。xxxxxの所は、上記で取得したハッシュに置き換えてください。

on:
    push:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
        - name: test action
          id: test
          uses: your-github-user-name/sample-container-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にプッシュして、GitHubでActionsの結果を確認します。

ワークフローの実行結果

無事に動いて、出力も取得できていますね!

まとめ

今回は、Dockerコンテナカスタムアクション作り方を説明しました。 この程度の内容であればワークフローファイルに直接書いても大差ありませんが、例えば特定のバイナリファイルを実行する必要があったり、使い慣れた言語でアクションを書きたいといった場合にはDockerコンテナカスタムアクションを作ると良いでしょう。

GitHubマーケットプレイスへの公開についてはまた次回のブログで紹介しようと思います。

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