【GitHub Actions】Composite ActionのTipsと注意点
- 概要
- 動作環境
- 用語
- 使用するプロジェクト
- Composite Actions の実装
- Composite Actionの細かな仕様
- Actionの補足
- Composite Actionの注意点
- Reusing Workflows との違い
- 余談、筆者が遭遇した事象
- サンプルプロジェクト
- 雑感
概要
今回はGitHub Actionsの機能の一つである "Composite Action" について紹介します。
今回の記事は、GitHub Actionsに多少知見がある人向けの記事になります。
Composite Actionはいわゆる再利用性のあるステップをyamlファイルに集約して再利用可能にする機能です。
テンプレート的な機能、もしくはプログラミングにおける関数的なものと考えてもらっても良いと思います。
Composite Actionは便利ですが、注意点もあるため紹介しようと思います。
ついでにPrivate Action (Privateなリポジトリに作成したAction) の使用方法も合わせて紹介します。
記事の最後にサンプルリポジトリも記載しておきます。
動作環境
- GitHub Actions + ubuntu-latest
- デバッグモード有効化 Enabling debug logging を参照
用語
- Public Action
- PublicなリポジトリにあるAction (例、 actions/checkout, actions/upload-artifactなど)
- Private Action
使用するプロジェクト
本題では有りませんが、ビルドのサンプル用に以下を入れています。
- C# プロジェクトのビルド用サンプルプロジェクト
Lottery
という実行するたびにtrue or falseを返すだけのプログラムLotteryTests
はUnitTest
Composite Actions の実装
Composite Actions を組み込むワークフロー
まずは Composite Actionなしのworkflowを例にしたいと思います。
# .github/workflows/build-dotnet-without-composite-actions.yml name: "Build Dotnet without Composite Actions" on: workflow_dispatch: {} jobs: build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: lfs: true - uses: actions/cache@v3 with: path: ./Lottery/obj key: dotnet-${{ runner.os }}-${{ github.ref_name }} - uses: actions/setup-dotnet@v2 with: dotnet-version: '6.0.x' include-prerelease: false - name: Restore Packages shell: bash run: dotnet restore ./GitHubActionsTestbed.sln - name: Build Projects shell: bash run: dotnet build ./GitHubActionsTestbed.sln --configuration Release - name: Test Projects shell: bash run: dotnet test ./GitHubActionsTestbed.sln --blame - uses: actions/upload-artifact@v3 with: name: Lottery path: ./Lottery/bin/Release/net6.0 retention-days: 3
ワークフローの詳細
- リポジトリのcheckout (actions/checkout)
- .Net 6.0のビルド環境の構築 (actions/setup-dotnet)
- dotnet コマンドを使ったビルド (パッケージの取得、テストを含む)
- Artifactとしてアップロード
実行結果は以下です。
https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117273807
Composite Actions 対応
Composite Actionのファイル構成
以下のようなファイル構成を取ります。
<path to action>/<composite action name>/action.yml
<composite action name>
はフォルダで、 ステップは action.yml
に定義します。
フォルダ名はステップの流れがわかる名前にすると良いです。
例えば、「.Netのビルドの一連の流れを集約」するComposite Actionを作りたい場合は以下のようにします。
.github/composite/dotnet-build/action.yml
自分はリポジトリ専用のComposite Actionは .github/composite
に置くようにしていますが、
別に.github/composite
以下でなくとも問題ありません。
そして呼び出すときは以下のように uses
にフォルダを指定します
- uses: ./.github/composite/dotnet-build
後述しますが、with
により入力(inputs
)を与えることも可能です。
- uses: ./.github/composite/upload-artifact with: name: Lottery path: ./Lottery/bin/Release/net6.0
Composite Actionの組込方針
Composite Action は個人的には以下の活用方法があると考えています。
- 複数のステップを1つに集約
- 入力のデフォルト値を独自に定義
.Netのビルドの一連の流れを集約 (複数のステップを1つに集約)
.NetのビルドをComposite Action対応します。
フォルダ構成は固定として入力(inputs
)はありません。
# .github/composite/dotnet-build/action.yml name: 'Dotnet Build' description: 'Restore packages, Build and Test' runs: using: "composite" steps: - uses: actions/setup-dotnet@v2 with: dotnet-version: '6.0.x' include-prerelease: false - name: Restore Packages shell: bash run: dotnet restore ./GitHubActionsTestbed.sln - name: Build Projects shell: bash run: dotnet build ./GitHubActionsTestbed.sln --configuration Release - name: Test Projects shell: bash run: dotnet test ./GitHubActionsTestbed.sln --blame
upload-artifactの有効日数3日をデフォルト化 (入力のデフォルト値を独自に定義)
公式の actions/upload-artifact@v3
ですが、デフォルトが90日となかなか長いです。
Composite Actionsは独自の入力 (inputs
) を定義することが可能です。
ArtifactはPrivateなリポジトリの場合、使いすぎると従量課金の対象となるためデフォルトで3日ぐらいにしたい場合などは、
Composite Actionの inputs
を使用することで独自のデフォルト値を定義できます。
# .github/composite/upload-artifact/action.yml name: 'Upload Artifact' description: 'An action to create a artifact' inputs: name: required: true default: 'Artifact' path: required: true retention-days: required: false default: 3 runs: using: "composite" steps: - uses: actions/upload-artifact@v3 with: name: ${{ inputs.name }} path: ${{ inputs.path }} retention-days: ${{ inputs.retention-days }}
name
(Artifact名)のデフォルトを"Artifact"retention-days
(有効期限)をデフォルトを3 (3日)path
(対象のファイル群)はデフォルトなしで指定を必須化
required
一応指定しておきましょう。(ただ、個人的にはComposite Actionだと微妙にrequired機能していない印象です)
Composite Action を使用
改めて「Composite Actions を使用していないワークフロー」を改修したいと思います。
# .github/workflows/build-dotnet.yml name: "Build Dotnet" on: workflow_dispatch: {} jobs: build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: lfs: true - uses: actions/cache@v3 with: path: ./Lottery/obj key: dotnet-${{ runner.os }}-${{ github.ref_name }} - uses: ./.github/composite/dotnet-build - uses: ./.github/composite/upload-artifact with: name: Lottery path: ./Lottery/bin/Release/net6.0
上記のようにビルドの流れがスッキリした見た目になりました。
また、upload-artifact
は有効期限を指定していなくてもデフォルトの3日が設定されるようになっています。
実行結果は以下です。
https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117435297
Composite Actionの細かな仕様
以下に記載されています。
デフォルトのshellは指定できないなど、細かい仕様が書いてあります。
Actionの補足
通常のActionとComposite Actionは構成自体は同じ
実はComposite Actionのファイル構成 (<path to action>/<composite action name>/action.yml
) ですが、
特殊に見えて、実は通常のActionと同じ構成になっています。
例えば、公式の actions/checkout
のルートのファイルを見ると action.yml
が存在しています。
つまりGitHub Actionsで使用されるActionは、必ずaction.ymlを持ったファイル群となっています。
Actionはcheckoutしてからフォルダを指定しても実行可能
実はActionは特定のフォルダにcheckoutして、uses
に指定しても使用可能です。
例えば actions/upload-artifact
は一旦 ./.github/repos/actions/upload-artifact
というフォルダにcheckoutして、
usesでそのフォルダを指定する形をとっても、同様の機能を得ることができます。
- uses: actions/upload-artifact@v3 with: name: Lottery path: ./Lottery/bin/Release/net6.0 retention-days: 3
↓
- uses: actions/checkout@v3 with: repository: 'actions/upload-artifact' ref: v3.1.0 path: ./.github/repos/actions/upload-artifact - uses: ./.github/repos/actions/upload-artifact with: name: Lottery path: ./Lottery/bin/Release/net6.0 retention-days: 3
PrivateリポジトリのActionもcheckoutしてフォルダを指定すれば実行可能
前項と同じ原理でPrivateリポジトリもcheckoutして実行が可能です。
社内専用のActionを作って使用したい場合などにご活用ください。
- name: Checkout tsgcpp/upload-artifact-private uses: actions/checkout@v3 with: # actions/upload-artifact をコピーしてPrivate化したリポジトリ repository: 'tsgcpp/upload-artifact-private' ref: main path: ./.github/repos/tsgcpp/upload-artifact-private token: ${{ secrets.PAT_TOKEN }} - uses: ./.github/repos/tsgcpp/upload-artifact-private with: name: Lottery path: ./Lottery/bin/Release/net6.0 retention-days: 3
対象のリポジトリにアクセス可能なPersonal Access Tokenを作成してsecretsに登録して使用する必要があるなど、多少手間があります。
Private Actionを直接 uses
に指定できない理由
GitHub Actionsのワークフローでデフォルトで発行される GITHUB_TOKEN
があるのですが、
GITHUB_TOKEN
は ワークフローを実行したリポジトリのみアクセス可能なトークンなので他のリポジトリにはアクセスできません。
そのため、Private Actionの場合はアクセス可能なトークンを使ってcheckoutしてから、uses
に指定する必要があります。
GitHub様、Private Actionに特化したトークンの機能つくってほしいなー
ダウンロード済みのActionは再利用される
全く同じバージョンやSHAのActionがダウンロード済みの場合は、ダウンロード済みのものが再利用されます。
ダウンロード済みのActionはComposite Actionなどの外部yamlでも共有されます。
- name: Cache actions/cache uses: actions/checkout@v3 with: repository: 'actions/cache' ref: v3.0.8 path: ${{ inputs.pathRoot }}/actions/cache - uses: ./.github/composite/checkout-actions
# .github/composite/checkout-actions # Compsite Action側 - name: Cache actions/upload-artifact uses: actions/checkout@v3 # ダウンロード済みの `actions/checkout` を使用 with: repository: 'actions/upload-artifact' ref: v3.1.0 path: ${{ inputs.pathRoot }}/actions/upload-artifact
ちなみにデバッグモードを有効化すると、以下のログで再利用されていることが確認できます。
Getting action download info ##[debug]Action 'actions/upload-artifact@v3' already downloaded at '/home/runner/work/_actions/actions/upload-artifact/v3'.
https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276928/jobs/5055819436#step:7:9
Composite Actionの注意点
Composite Action自体のcheckoutが必要
ワークフロー実行時はリポジトリの内容はcheckoutされていません。
Composite Actionは外部yamlに定義する関係であらかじめcheckoutで他ソースと一緒に取得する必要があります。
... steps: - uses: actions/checkout@v3 with: lfs: true ... # actions/checkoutで取得したComposite Actionを使用 - uses: ./.github/composite/dotnet-build
Publicリポジトリの場合はリポジトリ指定で実行可能
Publicなリポジトリに配置されたComposite Actionであれば、以下の様に指定できます。
- uses: <org>/<repository>/<path to action directory>@<ref(tag or branch)>
以下は指定例です。
- uses: tsgcpp/GitHubActionsTestbed/.github/composite/dotnet-build@main
ワークフロー本体のyamlのuses
で指定されたActionは事前ダウンロードされる
ワークフロー本体のyaml内の uses
に定義したActionですが、 ワークフローの最初 (Set up job
) で事前ダウンロードされます。
こちらもデバッグモードを有効化すると確認できます。
Getting action download info Download action repository 'actions/upload-artifact@v3' (SHA:3cea5372237819ed00197afe530f5a7ea3e805c8) ##[debug]Download 'https://api.github.com/repos/actions/upload-artifact/tarball/3cea5372237819ed00197afe530f5a7ea3e805c8' to '/home/runner/work/_actions/_temp_e6a3dde4-ca19-4d00-af3a-9a6c772ea0ec/241095c2-ea32-481b-83fe-d1b6af6915ac.tar.gz'
https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276928/jobs/5055819436#step:1:45
外部yamlに定義されたActionはステップ実行時に遅延ダウンロードされる
本記事の本題といっても過言ではありません!
Composite Actionを含む外部yaml内のActionは実行されるタイミングでダウンロードされます!
つまり、外部yamlのActionは遅延処理的な性質があります。
.github/workflows/build-dotnet.yml
を例に取ると
actions/cache
はワークフローのはじめにダウンロードされる- ワークフロー本体のyaml内で定義されているため
actions/setup-dotnet
とactions/upload-artifact
は各ステップ実行時にダウンロードされる- Composite Actionのyaml内に定義されているため
# withは省略 - uses: actions/cache@v3 ... # 内部で uses: actions/setup-dotnet@v2 - uses: ./.github/composite/dotnet-build # 内部で uses: actions/upload-artifact@v3 - uses: ./.github/composite/upload-artifact ...
Actionのログを見てみると、Set up job
でactions/cache
はダウンロードされていますが、
actions/setup-dotnet
とactions/upload-artifact
はダウンロードされていないことがわかります。
actions/setup-dotnet
とactions/upload-artifact
は各種ステップの実行時にダウンロードされています。
ログの全体は以下です。
遅延ダウンロードの何が問題なのか?
「大した問題じゃなくね?」って思った方もいると思いますし、実際大した問題にならないパターンも多いです。
問題になりやすい例として、完了に長時間を要するワークフローがあります。
例えば以下のようなワークフローです。
- 5時間かかるアプリのビルド実行
- ビルド完了後に Composite Actionを使ってアプリをストアへアップロード
- Composite Action内でアプリのストアアップロード用Actionを取得して使用
ワークフロー開始時にはGitHubは正常だったのに、
5時間後のビルド時にGitHubのAPIが一部死んでいてストア用のActionのダウンロード(checkout)が失敗してビルドがパーになっちゃうパターンです。
ストア側のAPIは問題がなかった場合、予めストア用のActionをダウンロードできていれば回避できた問題ですね。。。
昨今クラウドベンダー(AWSなど)の一時インスタンスでビルドすることも多くなっていて、ビルド成果物をどこかに退避していないとサルベージも困難だったりします。
対策1 あらかじめ使用するActionすべてのcheckoutを済ませる (オススメ)
事前ダウンロードされてないなら、明示的に事前ダウンロードしてしまおうという発想です。
1例として、以下のようなセットアップ用Composite Actionを用いる方法があります。
name: 'Set Up Actions' inputs: pathRoot: required: true description: 'Relative path the actions will be into' default: ./.github/repos patToken: required: true description: 'GitHub Personal Access Token to checkout private repositories.' runs: using: "composite" steps: - name: Cache actions/checkout uses: actions/checkout@v3 with: repository: 'actions/checkout' ref: v3.0.2 path: actions/checkout@v3 - name: Cache actions/cache uses: actions/checkout@v3 with: repository: 'actions/cache' ref: v3.0.8 path: ${{ inputs.pathRoot }}/actions/cache - name: Cache actions/upload-artifact uses: actions/checkout@v3 with: repository: 'actions/upload-artifact' ref: v3.1.0 path: ${{ inputs.pathRoot }}/actions/upload-artifact - name: Cache actions/download-artifact uses: actions/checkout@v3 with: repository: 'actions/download-artifact' ref: v3.0.0 path: ${{ inputs.pathRoot }}/actions/download-artifact - name: Cache actions/setup-dotnet uses: actions/checkout@v3 with: repository: 'actions/setup-dotnet' ref: v2.1.0 path: ${{ inputs.pathRoot }}/actions/setup-dotnet - name: Checkout tsgcpp/upload-artifact-private uses: actions/checkout@v3 with: # Same with actions/upload-artifact repository: 'tsgcpp/upload-artifact-private' ref: main path: ${{ inputs.pathRoot }}/tsgcpp/upload-artifact-private token: ${{ inputs.patToken }}
後は、uses
にダウンロード済みのActionを指定するだけです。
name: "Build Dotnet with Set Up Actions" on: workflow_dispatch: {} jobs: build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: lfs: true - uses: ./.github/composite/setup-actions with: patToken: ${{ secrets.PAT_TOKEN }} - uses: actions/cache@v3 with: path: ./Lottery/obj key: dotnet-${{ runner.os }}-${{ github.ref_name }} - uses: ./.github/composite/dotnet-build-with-setup-actions - uses: ./.github/composite/upload-artifact-with-setup-actions with: name: Lottery path: ./Lottery/bin/Release/net6.0
# .github/composite/dotnet-build-with-setup-actions/action.yml name: 'Dotnet Build with Set Up Actions' description: 'Restore packages, Build and Test' runs: using: "composite" steps: - uses: ./.github/repos/actions/setup-dotnet with: dotnet-version: '6.0.x' include-prerelease: false - name: Restore Packages shell: bash run: dotnet restore ./GitHubActionsTestbed.sln - name: Build Projects shell: bash run: dotnet build ./GitHubActionsTestbed.sln --configuration Release - name: Test Projects shell: bash run: dotnet test ./GitHubActionsTestbed.sln --blame
# .github/composite/upload-artifact-with-setup-actions/action.yml name: 'Upload Artifact with Set Up Actions' description: 'An action to create a artifact' inputs: name: required: true default: 'Artifact' path: required: true retention-days: required: false default: 3 runs: using: "composite" steps: - uses: ./.github/repos/actions/upload-artifact with: name: ${{ inputs.name }} path: ${{ inputs.path }} retention-days: ${{ inputs.retention-days }}
このやり方の利点は以下があると思っています。
- 複数のWorkflow間で使用するActionのバージョンを統一できる
- 特に同じActionを使う場合でも、v2とv3で指定を間違えるなども回避しやすい
- Public Action, Private Actionどちらも使用時の
uses
への指定方法が統一される- どちらもダウンロード済みのActionになっているため
対策2 usesで使用するActionを宣言 (非推奨)
「ダウンロード済みのActionは再利用される」の性質を利用したやり方ですね。
ただ、このやり方は公式ドキュメントにはないやり方で、仕様の裏をついたやり方なので非推奨です。
また、uses
で宣言した限りステップ自体は実行されてしまうため、Actionによっては予期しない副作用が発生する可能性もあります。
事前ダウンロード時に失敗してもワークフローを継続させるために continue-on-error: true
を宣言しています。
jobs: build: name: Build runs-on: ubuntu-latest steps: # actions/upload-artifactを事前ダウンロードさせる - uses: actions/upload-artifact@v3 continue-on-error: true # actions/setup-dotnetを事前ダウンロードさせる - uses: actions/setup-dotnet@v2 continue-on-error: true with: dotnet-version: '6.0.x' include-prerelease: false - uses: actions/checkout@v3 with: lfs: true - uses: actions/cache@v3 with: path: ./Lottery/obj key: dotnet-${{ runner.os }}-${{ github.ref_name }} # ダウンロード済みのactions/setup-dotnetが使用される - uses: ./.github/composite/dotnet-build # ダウンロード済みのactions/upload-artifactが使用される - uses: ./.github/composite/upload-artifact with: name: Lottery path: ./Lottery/bin/Release/net6.0
GitHub Actions側に事前ダウンロード機能として、usesの pre-download
的なオプションを要望として出しても良さそうな気はしてます。
Reusing Workflows との違い
GitHub ActionsにはReusing Workflowsという機能があります。
こちらは名前の通りワークフロー全体を再利用する形になります。
一方でComposite Actionは数ステップを集約して、ワークフロー(ジョブ)にステップとして組み込む機能になります。
もしワークフロー全体を再利用する場合は、Composite ActionではなくReusing Workflowsのほうが最適と言えます。
余談、筆者が遭遇した事象
「外部yamlに定義されたActionはステップ実行時に遅延ダウンロードされる」の仕様を認識するきっかけになった事象がありました。
Composite Actionを使ったCIのワークフローを運用していて、半年以上問題が発生していなかったのですが、
ある日大事な提出でビルドマシンがいつも以上にガンガン回っているときでした。
初回のcheckoutは問題なく実行されましたが、数時間のビルドを終わらせた後のComposite Action内でActionのダウンロード(checkout)が発生したとき、
なぜか急にUnauthorizedになってcheckout不可になる現象が発生するようになりました。
checkout対象はPublic Actionの uses: actions/upload-artifact
だったので、なぜUnauthorizedになったのかは本当に謎でした。。。
ただ、今回の現象に関わらず外部APIにアクセスできなくなる可能性は十分に考えられます。
一番の問題は失敗時の時間的損失が大きかったことのため、
外部APIなどが関係するステップをワークフローの初回に集約し、失敗しても時間的損失を極力回避できるように組み直しました。
今回紹介した setup-actions
がその一例となります。
完全なネットワーク障害などに対応しきれるわけではありませんが、あらかじめActionをダウンロードしておくことは一部ワークフローでは有効かと思いますため、
良かったら参考にしていただければと!
サンプルプロジェクト
雑感
直近はかなりドタバタしていたので、これまた久々の記事です。。。
最近はXR、非XRに限らずインフラはやはり重要だなと痛烈に感じています。
XRアプリの開発もどんどん規模が大きくなっているため、開発基盤の重要性もかなり上がっています。
Unityに限らずAndroid, iOS, Dockerを含むサーバーサイドのCI/CDを経験してきた身としては、
インフラをもっと強化していきたいと常に考えるようになりました。
そういえば「すぎしーのXRと3DCG」というブログ名ですが、そろそろ改名を考えています。
XR開発はインタラクションやグラフィックスももちろん重要ですが、それに負けないくらいクリエイターが開発に注力できる環境を用意することも大事だと思います。
これからもよろしくです!
それでは~