すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

【GitHub Actions】Composite ActionのTipsと注意点

概要

今回は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

使用するプロジェクト

本題では有りませんが、ビルドのサンプル用に以下を入れています。

  • 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の細かな仕様

以下に記載されています。

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:
          # 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

ワークフロー本体のyamlusesで指定された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-dotnetactions/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 jobactions/cacheはダウンロードされていますが、 actions/setup-dotnetactions/upload-artifactはダウンロードされていないことがわかります。

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

ログの全体は以下です。

github.com

遅延ダウンロードの何が問題なのか?

「大した問題じゃなくね?」って思った方もいると思いますし、実際大した問題にならないパターンも多いです。

問題になりやすい例として、完了に長時間を要するワークフローがあります

例えば以下のようなワークフローです。

  • 5時間かかるアプリのビルド実行
  • ビルド完了後に Composite Actionを使ってアプリをストアへアップロード
    • Composite Action内でアプリのストアアップロード用Actionを取得して使用

ワークフロー開始時にはGitHubは正常だったのに、
5時間後のビルド時にGitHubAPIが一部死んでいてストア用の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という機能があります。
こちらは名前の通りワークフロー全体を再利用する形になります。

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開発はインタラクションやグラフィックスももちろん重要ですが、それに負けないくらいクリエイターが開発に注力できる環境を用意することも大事だと思います。

これからもよろしくです!

それでは~