GitHub Actionsを使ってアプリを開発するときのワークフロー記述の個人的ベストプラクティス

皆さん、あけましておめでとうございます! エンジニアの岡村です。

今年もSynamon's Engineer blogでは皆様の開発の参考になることを願って情報を発信していきます!

GitHub Actions

弊社ではマルチプラットフォーム向けのアプリをビルドすることが多いため、CIは重要です。以前はJenkinsを使っていましたが、SYNMNの開発にあたりGitHub Actionsへの移行を行いました。

開発もある程度軌道に乗り、GitHub Actionsもそこそこ慣れてきたので、現時点で感じているワークフローを運用する際に便利だったことを紹介しておきます。

GITHUB_STEP_SUMMARYに、後から確認出来るようにワークフローのパラメータを書き出しておく

GitHub Actionsのサマリー画面はデフォルトではごく僅かな情報しか載っていませんが、ワークフロー中にワークフローコマンドを実行することで追加情報を載せることが出来ます。

docs.github.com

特にworkflow_dispatchなどでワークフローを手動実行した場合などは、ワークフローの引数を後から確認することが難しい為、そういった情報を出力しておくことで後で混乱せずに済みます。

GITHUB_STEP_SUMMARYはmarkdownが使えるため、以下のように出力しておけば表形式になって視認性が向上します。

        run: |
          echo "|Properties|Value|" >> $GITHUB_STEP_SUMMARY
          echo "|---|---|" >> $GITHUB_STEP_SUMMARY
          echo "|GitHub Ref|\`${{ github.ref }}\`|" >> $GITHUB_STEP_SUMMARY
          echo "|Commit Hash|\`${{ github.sha }}\`|" >> $GITHUB_STEP_SUMMARY
          echo "|Some Workflow Input|${{ inputs.hoge }}|" >> $GITHUB_STEP_SUMMARY

以下の画像は一例ですが、GITHUB_STEP_SUMMARYに記述した内容は、このようにArtifactsの下に表示されます。

ツールを動かすときはProblem Matchersを設定して、エラーの詳細をSummaryに出力するようにする

GitHubにはproblem_matcherという機能があり、設定しておくとログ出力から正規表現で一致するログを検出して、わかりやすくSummeryに出してくれたりインラインコメントを付けてくれます。

github.com

例えばUnityビルドを行う場合、以下のようなMatcherを適用しておくとコンパイルエラーを拾ってくれます。

{
  "problemMatcher": [
    {
      "owner": "Unity",
      "severity": "error",
      "pattern": [
        {
          "regexp": "^(.*\\.cs)\\((\\d+),(\\d+)\\):\\serror\\s(.+):\\s(.+)$",
          "file": 1,
          "line": 2,
          "column": 3,
          "code": 4,
          "message": 5
        }
      ]
    }
  ]
}

Matcherを有効化するには、そのステップの直前に以下のようなコマンドを実行します。

      - run: echo ::add-matcher::.github/workflows/unity-problem-matcher.json

以下の画像はこれまた一例ですが、上の設定をすることで以下のようにコンパイルエラー発生時に検出してくれます。

コンパイルが成功し、テストが失敗した場合はテスト結果ファイルを解析するAction(こちらなど)に流す事が出来るのですが、テスト実行以前のエラーに関しては、通常はステップの中までログを見に行かなければならないので、Matcherを設定しておくことで時間が短縮できます。

あと、上記画像では何故か同じエラーが4つも出力されていますが、まあ何も出ないよりはマシという事で一旦放置しています……。

workflow_callでトリガーと処理を分割する

一つのワークフローを、ワークフローの実行条件等を記述する呼び出し側と、ビルドなどの実際の処理を書く側に分割したところ、見通しが良くなりました。

背景として、GitHub Actionsはyamlベースで記述し、基本的には問題なく使えるのですが、JenkinsのGroovy Scriptに比べてコードが重複しやすく、特にマルチプラットフォームで微妙にmatrix化し辛いような処理を書いているとすぐに同じコードだらけになってしまっていました。

JenkinsのGroovy Scriptなら関数を使って同じ処理を纏めることが出来るのですが、Actionsではそれが出来ません。yamlにはアンカーやエイリアスといった、定義したブロックを使いまわせる機能があるのですが、GitHub Actionsではそれが動作しない(2023年1月時点)ためです。

github.com

workflowは基本的にGitHub上でしか動かない(ローカルで検証しづらい)為、ローカルで叩いても問題ないような処理単位は外部スクリプトにまとめた方が良いのですが、キャッシュや成果物のアップロード、シークレット情報の管理などのGitHub Actions上で動かすためのステップに関しても中々の量になります。

そこで、最低限の分割として、Workflow callという機能を使い、トリガー専用のworkflowと処理専用のworkflowを分けてみました。

これによりdevelopブランチならテストとビルドを両方実施、featureブランチならテストだけ実施というように、実行タイミングの制御も各ワークフローに分散してしまうことがなくなりました。

サンプル

上で紹介したような機能を使い、現状クライアントアプリ用のリポジトリ内のActionsは以下のようになっています(見せられない所が多いので大幅にカットしていますが……)全体の形はこうなるという参考程度にしていただければ幸いです。

トリガー用ワークフロー

name: on-develop

on:
  push:
    branches:
      - "develop"
    paths-ignore:
      - "**.md"
  workflow_dispatch:

jobs:
  build:
    uses: ./.github/workflows/build.yml
    secrets: inherit
    with:
      environment-context: Development
      custom-build-number: ${{ github.run_number }}
      development-build: true
  deploy:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
    needs: build
    steps:
      # 成果物をテスト環境にデプロイする処理など

処理用ワークフロー

name: build

on:
  workflow_call:
    inputs:
      environment-context:
        required: true
        type: string
      custom-build-number:
        required: false
        type: string
        default: "0"
      development-build:
        required: false
        type: boolean
        default: false
      include-platforms:
        required: false
        type: string
        default: "Windows, macOS, Android, iOS"

#workflow_dispatchを設定しておくとbuild単体を手動で実行できるのでおススメ
  workflow_dispatch: 
    inputs:
      environment-context:
        description: Environment Context
        required: true
        type: choice
        options:
          - Release
          - Development
      custom-build-number:
        description: Custom Build Number
        required: false
        type: string
        default: ""
      include-platforms:
        description: Include Platforms
        required: false
        type: string
        default: "Windows, macOS, Android, iOS"

# run-nameを設定しておくと手動実行した際にGitHub上の実行履歴で表示される名前をカスタマイズできるのでおススメ
# workflow_callで実行された時は無視される
run-name: Build as ${{ inputs.context }} on ${{ github.ref_type }} ${{ github.ref_name }} [${{ github.sha }}] 

jobs:
  dump-properties:
    runs-on: ubuntu-latest
    steps:
      - name: Build arguments summary
        run: |
          echo "|Build Properties|Value|" >> $GITHUB_STEP_SUMMARY
          echo "|---|---|" >> $GITHUB_STEP_SUMMARY
          echo "|GitHub Ref|\`${{ github.ref }}\`|" >> $GITHUB_STEP_SUMMARY
          echo "|Commit Hash|\`${{ github.sha }}\`|" >> $GITHUB_STEP_SUMMARY
          echo "|Environment Context|${{ inputs.environment-context }}|" >> $GITHUB_STEP_SUMMARY
          echo "|Custom Build Number|${{ inputs.custom-build-number }}|" >> $GITHUB_STEP_SUMMARY
          echo "|Include Platforms|${{ inputs.include-platforms }}|" >> $GITHUB_STEP_SUMMARY

  windows-build:
    runs-on: [self-hosted, windows, x64]
    if: contains(inputs.include-platforms, 'Windows')
    timeout-minutes: 90
    steps:
      # Windowsプラットフォーム向けのビルド処理

  android-build:
    runs-on: [self-hosted, windows, x64]
    if: contains(inputs.include-platforms, 'Android')
    timeout-minutes: 90
    steps:
      # Androidプラットフォーム向けのビルド処理

  ios-build:
    runs-on: [self-hosted, macOS, x64]
    if: contains(inputs.include-platforms, 'iOS')
    timeout-minutes: 90
    steps:
      # iOSプラットフォーム向けのビルド処理

  macos-build:
    runs-on: [self-hosted, macOS, x64]
    if: contains(inputs.include-platforms, 'macOS')
    timeout-minutes: 90
    steps:
      # macOSプラットフォーム向けのビルド処理

以上

GitHub Actionsは去年あたりからだいぶ使いやすくなってきたように感じます。github-hosted runnerの選択肢や日本語ドキュメントの量、まだ欲しい機能などは色々あるのですが、現状でも他のCIサービスに引けを取らない充実度だと思います。ネット上の記事も充実してきたので、今年からGitHub Actionsを触り始めて見るのも良いのではないでしょうか。また何か面白い使い方などを発見したら記事にしようと思います。

記事を読んでいただきありがとうございました!今年もSynamonとSYNMNを宜しくお願いいたします!

twitter.com

twitter.com