📚

GitHub Actionsの共通したアクションを切り出してシンプルに保つ

2022/01/17に公開
2

こんにちは。スターフェスティバル株式会社の ikkitang です。

さて、皆様 GitHub Actions 使ってますか? 弊社では大半のプロジェクトで GitHub Actions が活用されていて、 月 1 回の WinSession で知見が共有されたりしています。

GitHub Actions の素敵な所はイベントの柔軟さだと感じています。それによって GUI での操作が想起できて、直感的にイベントをフックさせて Workflow を作っていくことができます。とはいえ、Workflow が増えてくると別の Workflow をコピペで持ってきて必要な Step を書き換えるなんてことをすることがないでしょうか。

そこで、今回は Workflow の保守性を上げるために Step の共通化について調べてみました。

ちなみに今回説明にあたって GitHub にサンプルを用意してみました。参考にしてみてください。

https://github.com/TakahashiIkki/gha-standardization-sample

サンプルの前提の説明

  • GitHub Pages で静的サイトを管理する
  • 技術スタック
  • 以下、2 つの Workflow を作る
    • 全ての Push イベントにおいて、linter を掛けて build を試みる (普通なら test も動かす)
      • ルール違反があれば NG とする
      • 以下、main.yml として説明
    • main ブランチへの Push(PR のマージ)において、GitHub Pages へデプロイする
      • 以下、deploy.yml として説明

GitHub Actions でよくある構成の一種、Push イベントで test/linter 実行、main ブランチへの Push でデプロイという形式です。

通常時の Workflow 設定

通常の main.yml

すべての Push イベントにおいて、linter を掛ける場合は以下のように書く事が出来ます。
ちなみに、package.json に予め "lint": "next lint""build": "next build" という script の設定がある前提です。
npm ci などで Next.js が動作する環境を整えた後でそれぞれのコマンドを実行する事で期待した動作を得る事が出来ます。

通常の main.yml(クリックで展開)
name: Main

on:
  push:

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v2
        with:
          node-version: "16"

      - name: Install dependencies
        run: npm ci

      - name: Check lint
        run: npm run lint

      - name: Run build
        run: npm run build

通常の deploy.yml

main ブランチへの Push イベントにおいて、デプロイ処理を行う場合は以下のように書く事が出来ます。
Static HTML Export 機能で out ディレクトリに export した後、そのディレクトリをpeaceiris/actions-gh-pagesを使ってデプロイしています。 (前提として、package.json の scripts に"export": "next export"と定義されているとします。)

https://github.com/peaceiris/actions-gh-pages

通常の deploy.yml(クリックで展開)
name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v2
        with:
          node-version: "16"

      - name: Install dependencies
        run: npm ci

      - name: Check lint
        run: npm run lint

      - name: Run build
        run: npm run build

      - name: Static HTML Export
        run: npm run export

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./out

さて、余談ですが、今回 mainブランチへのpushイベント をトリガーに Workflow を発火させました。本来の業務に目を向けると、main ブランチへは PR を作ってレビューが通ればマージする、というフローを取られる事が一般的でしょう。

mainブランチへのPRがマージされた時をトリガーにしたい所ではありますが、実は GitHub Actions では ブランチへのPRがマージされた時をトリガーにする事は出来ません。それをしたい場合は以下のどちらかをやる事になります。

  • パターン1
    • GitHub の Branch protection rules で PullRequest 経由以外の main ブランチへの Push を禁止する。
    • main ブランチの push イベントをトリガーに Workflow を実行する。
  • パターン2
    • main ブランチへの PR がクローズされた時をトリガーに Workflow を実行する(PR のマージでもイベントとしては[closed]として扱われる)
    • 実行時で if: github.event.pull_request.merged == true でマージ以外の時は Workflow を終了させる
パターン 2 の例(クリックで展開)
pull_request:
branches:
  - main
types: [closed]
jobs:
  sample:
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true
    steps:
      - name: checkout
        uses: actions/checkout@v2.4.0

今回はパターン 1 での実装をしました。

共通している部分に目を向ける

上記で示したコードは以下の画像のように、複数の共通な Step があります。

共通部分のdiff画像

npm cinpm run lint / npm run build この辺の Step をまとめて共通化してしまいましょう。(共通化できる部分は実はもっとありますが、後に共通化の制限・注意事項にて補足します。)

コードの共通化

共通部分の切り出し

コードの共通化は Composite action などと呼ばれています。公式ドキュメントとしては以下あたりが参考になります。

yaml ファイルは任意の場所に配置してよい、とドキュメントにありますが、制約として yaml ファイルは action.yml or action.yaml というファイル名にする必要があります。
公式ドキュメントでは、 ./.github/actions/ ディレクトリに 各アクションのディレクトリを用意して配置する方法が紹介されています。
./github/actions/{Action名}/action.yml といった感じでしょうか。

さて、実際の yaml についてですが、ある程度指定された書き方があります。
通常の Workflow では jobs.[*].stepsで各 Step を記述する所をruns.[*].stepsという記法で step を記述します。
また、 using: "composite" という記述と 各 Step にある shell の指定は必須です。
shell の指定が必須な理由は見つけられませんでした。 どの OS で動いてるかは呼び出し元の Workflows が知ってる事であって Composite action は知る術が無く、指定が必要なのだと思っています。

runs:
  using: "Composite"
  steps:
    - name: Install dependencies
      run: npm ci
      shell: bash

    - name: Check lint
      run: npm run lint
      shell: bash

    - name: Run build
      run: npm run build
      shell: bash

共通部分の読み込み

読み込みはレポジトリルートからの相対パス指定で 先程 action.yml を配置したディレクトリの位置を指定することで読み込みがされます。
規定された方法で指定をしないと actions/checkout@v2 とかと同じように扱われてしまって、外部の third party アクションを探しにいくので注意です。

  • main.yml
name: Main
on:
  push:
jobs:
  build_and_test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v2
        with:
          node-version: "16"

      - name: Initialize
        uses: ./.github/actions/build
  • deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v2
        with:
          node-version: "16"

      - name: Initialize
        uses: ./.github/actions/build

      - name: Static HTML Export
        run: npm run export

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./out

ある程度の共通化の結果が得られました。

共通化の制約

if 構文のサポート

便利な Composite action 機能ですが、制約としては if構文 がサポートされていません。
ぶっちゃけ、自分は if 構文が無くて困る程使いこなせてないですが、サポートがあれば内部のスクリプトの微調整という点で役立つでしょう。

2021-01-18 追記

コメントで補足頂きましたが、現時点で既に if 構文 がサポートされているようです。 Shunsuke Suzuki さんありがとうございました。
Shunsuke Suzuki さんのコメント

公式ドキュメント

共通化出来ない所

- uses: actions/checkout@v2 は共通化として切り出す事は出来ません。
切り出された Composite action は checkout アクションによって取得されるから、という理由だそうです。

use 構文のサポート

https://github.blog/changelog/2021-08-25-github-actions-reduce-duplication-with-action-composition/

去年の 8 月にサポートされました。 今回、 setup-node は共通化に含めませんでしたが、こんな感じで Node.js のセットアップも Composite action に含める事も出来ます。

runs:
  using: "composite"
  steps:
    - uses: actions/setup-node@v2
      with:
        node-version: "16"

    - name: Install dependencies
      run: npm ci
      shell: bash

    - name: Check lint
      run: npm run lint
      shell: bash

    - name: Run build
      run: npm run build
      shell: bash

引数のサポート

inputs という形式で Composite action に変数を渡す事も出来ます。例えばこのような感じです。

inputs:
  node_version:
    description: "setup node-version"
    required: true
runs:
  using: "composite"
  steps:
    - uses: actions/setup-node@v2
      with:
        node-version: ${{inputs.node_version}}

Context のサポート

https://docs.github.com/ja/actions/learn-github-actions/contexts

githubコンテキスト とか envコンテキスト はもちろんサポートしてます。

まとめ

  • GitHub Actions では共通した step をまとめて Composite action として外部に定義できる。
  • uses 構文を使い、レポジトリの任意のディレクトリをレポジトリルートからの相対パスで読み込みが可能。
    • ただし、ファイル名は action.yml としないといけない。
    • 例) ./github/actions/{Action名}/action.yml
  • 基本的に通常の Workflow と同様のコンテキストをサポートする

この記事が読まれた方のより良い GitHub Actions ライフにつながれば幸いです。
以上、スターフェスティバルのエンジニア ikkitang がお送りしました!

[PR] エンジニア募集中

私達スターフェスティバルではエンジニアを絶賛募集しております。 という我々の生活と切っては切り離せない関係の領域の問題について技術で問題解決していきませんか?
詳しくはこちら〜!

https://stafes.notion.site/stafes/d0996a00d77d418280982797c7e16001

スタフェステックブログ

Discussion

Shunsuke SuzukiShunsuke Suzuki

便利な Composite action 機能ですが、制約としては if構文 がサポートされていません。

以前はサポートされていませんでしたが、既に(この記事の公開日時 2022.01.17 時点で)サポートされているかと思います。
https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsif

ikkitangikkitang

Shunsuke Suzuki さん

コメントありがとうございます!
めちゃくちゃ、ドキュメントに書いてある〜〜w

本文追記させて頂きました。
補足ありがとうございます。