概要
今回はGitHub Actionsの機能の一つである "Composite Action" について紹介します。
今回の記事は、GitHub Actionsに多少知見がある人向けの記事になります。
Composite Actionはいわゆる再利用性のあるステップをyamlファイルに集約して再利用可能にする機能です。
テンプレート的な機能、もしくはプログラミングにおける関数的なものと考えてもらっても良いと思います。
docs.github.com
Composite Actionは便利ですが、注意点もあるため紹介しようと思います。
ついでにPrivate Action (Privateなリポジトリに作成したAction) の使用方法も合わせて紹介します。
記事の最後にサンプルリポジトリも記載しておきます。
動作環境
用語
- Public Action
- PublicなリポジトリにあるAction (例、 actions/checkout, actions/upload-artifactなど)
- Private Action
- PrivateなリポジトリにあるAction
- 非公開のリポジトリにあるアクセスが制限されたAction
使用するプロジェクト
本題では有りませんが、ビルドのサンプル用に以下を入れています。
- C# プロジェクトのビルド用サンプルプロジェクト
Lottery という実行するたびにtrue or falseを返すだけのプログラム
LotteryTests はUnitTest
Composite Actions の実装
Composite Actions を組み込むワークフロー
まずは Composite Actionなしのworkflowを例にしたいと思います。
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)はありません。
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 を使用することで独自のデフォルト値を定義できます。
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 を使用していないワークフロー」を改修したいと思います。
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の細かな仕様
以下に記載されています。
github.com
デフォルトのshellは指定できないなど、細かい仕様が書いてあります。
Actionの補足
通常のActionとComposite Actionは構成自体は同じ
実はComposite Actionのファイル構成 (<path to action>/<composite action name>/action.yml) ですが、
特殊に見えて、実は通常のActionと同じ構成になっています。
例えば、公式の actions/checkout のルートのファイルを見ると action.ymlが存在しています。
github.com
つまり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:
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
- name: Cache actions/upload-artifact
uses: actions/checkout@v3
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
...
- uses: ./.github/composite/dotnet-build
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はワークフローのはじめにダウンロードされる
actions/setup-dotnetとactions/upload-artifactは各ステップ実行時にダウンロードされる
- Composite Actionのyaml内に定義されているため
- uses: actions/cache@v3
...
- uses: ./.github/composite/dotnet-build
- uses: ./.github/composite/upload-artifact
...
Actionのログを見てみると、Set up job でactions/cacheはダウンロードされていますが、
actions/setup-dotnetとactions/upload-artifactはダウンロードされていないことがわかります。

actions/setup-dotnetとactions/upload-artifactは各種ステップの実行時にダウンロードされています。


ログの全体は以下です。
github.com
遅延ダウンロードの何が問題なのか?
「大した問題じゃなくね?」って思った方もいると思いますし、実際大した問題にならないパターンも多いです。
問題になりやすい例として、完了に長時間を要するワークフローがあります。
例えば以下のようなワークフローです。
- 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:
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
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
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:
- uses: actions/upload-artifact@v3
continue-on-error: true
- 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 }}
- uses: ./.github/composite/dotnet-build
- 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という機能があります。
こちらは名前の通りワークフロー全体を再利用する形になります。
docs.github.com
一方で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をダウンロードしておくことは一部ワークフローでは有効かと思いますため、
良かったら参考にしていただければと!
サンプルプロジェクト
github.com
雑感
直近はかなりドタバタしていたので、これまた久々の記事です。。。
最近はXR、非XRに限らずインフラはやはり重要だなと痛烈に感じています。
XRアプリの開発もどんどん規模が大きくなっているため、開発基盤の重要性もかなり上がっています。
Unityに限らずAndroid, iOS, Dockerを含むサーバーサイドのCI/CDを経験してきた身としては、
インフラをもっと強化していきたいと常に考えるようになりました。
そういえば「すぎしーのXRと3DCG」というブログ名ですが、そろそろ改名を考えています。
XR開発はインタラクションやグラフィックスももちろん重要ですが、それに負けないくらいクリエイターが開発に注力できる環境を用意することも大事だと思います。
これからもよろしくです!
それでは~