✍️

【Elixir】compile, lint, formatの結果をGitHub ActionのJob Summaryに表示する

2022/12/14に公開

GitHub Actionsを利用してElixirプロジェクトのlint, formatのチェックをPRごとに行うことでコードの品質を担保できます。

この記事ではlintやformat, testの実行結果をWorkflowのSummary画面にmarkdown形式のコンテンツとして生成する方法を解説します。

Job Summaryとは

GitHub Actionsにはworkflowのsummaryページに、markdown形式のコンテンツを追加できる機能があります。

https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/

次の2つの方法でサマリーを生成できます。

  • $GITHUB_STEP_SUMMARY という環境変数にコンテンツを出力する
  • @actions/core パッケージで提供されている core.summary クラスのメソッドを使う

この記事では後者の @action/core を使った実装をしています。後者の場合は addTableaddDetails などマークダウン形式のコンテンツを作成するためのメソッドが提供されており、実装がしやすいためオススメです。

ElixirのCI例

早速CIのサンプルです。以下のfly.ioのブログ記事のCIをベースにしていますので、合わせて参照してください。

https://fly.io/phoenix-files/github-actions-for-elixir-ci/

name: CI

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

env:
  MIX_ENV: test

permissions:
  pull-requests: write
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Elixir
        uses: erlef/setup-beam@v1
        with:
          otp-version: 25.1.2
          elixir-version: 1.14.2

      - name: Cache deps
        id: cache-deps
        uses: actions/cache@v3
        with:
          path: deps
          key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ runner.os }}-mix-

      - name: Cache compiled build
        id: cache-build
        uses: actions/cache@v3
        with:
          path: _build
          key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ runner.os }}-mix-

      - name: Install dependencies
        run: mix deps.get

      - name: Compile
        id: compile
        # 1. continue-on-error: trueを設定して、mixコマンドの終了ステータスが0ではない場合でもjobを続行させる
        continue-on-error: true
        # 2. `&>` を指定して標準出力と標準エラー出力をtmpファイルに出力する
        run: mix compile --warnings-as-errors &> /tmp/result

      - name: Report compile result
        # 3. tmpファイルに書き出した内容をGitHubのsummaryに出力する
        # summaryを出力する部分は使いまわせるため、composite actionとして実装
        uses: ./.github/actions/command-summary
        with:
          body: /tmp/result
          header: Compile

      - name: Compile status
        # 4. mixコマンドの終了ステータスが0でない場合はこのタイミングでjobを停止させる
        if: steps.compile.outcome == 'failure'
        run: exit 1

      - name: Format
        id: format
        continue-on-error: true
        run: mix format --check-formatted &> /tmp/result

      - name: Report format result
        uses: ./.github/actions/command-summary
        with:
          body: /tmp/result
          header: Format

      - name: Format status
        if: steps.format.outcome == 'failure'
        run: exit 1

      - name: Credo
        id: credo
        continue-on-error: true
        run: mix credo > /tmp/result

      - name: Report credo result
        uses: ./.github/actions/command-summary
        with:
          body: /tmp/result
          header: Credo

      - name: Credo status
        if: steps.credo.outcome == 'failure'
        run: exit 1

      - name: Test
        id: test
        continue-on-error: true
        run: mix test > /tmp/result

      - name: Report test result
        uses: ./.github/actions/command-summary
        with:
          body: /tmp/result
          header: Test

      - name: Test status
        if: steps.test.outcome == 'failure'
        run: exit 1
.github/actions/command-summary/action.yaml
name: Command Summary
description: create job summary
inputs:
  header:
    description: header
    required: true
  body:
    description: filepath where the command execution result is saved
    required: true
runs:
  using: "composite"
  steps:
    - uses: actions/github-script@v6
      with:
        script: |
          const fs = require('fs');
          const body = await fs.readFileSync("${{ inputs.body }}");
          await core.summary
            .addHeading("${{ inputs.header }}", 2)
            .addCodeBlock(body)
            .write();

compileの部分にコメントを書いています。他のmixコマンドも同様の構成です。ポイントは次のとおりです。

  1. mixコマンドの終了ステータスが0ではない場合にそのタイミングでjobが終了してしまうとJob Summaryへの出力ができません。そのため、continue-on-error: true を設定してjobを継続させるようにします。

  2. mixコマンドの結果をtmpファイルに書き出して後続のsummary作成時に参照します。

  1. command-summary/action.yaml というcomposite actionにheaderとtmpファイルのパスを渡しています。composite actionの中では actions/github-script を使用し、core.summary クラスを利用してサマリーを作成しています。

  2. 1. の処理で continue-on-error: true としていた分をsummary作成後に回収します。stepの結果を steps.<id>.outcome で取得し、 failure であればjobを終了させます。
    formatやcredoでエラーになってもtestまで回したいということであればここは割愛してもOKです。

実際の使用例

このyamlの状態でmainブランチに対してPRを出すなりしてCIを回すと、次の画像のようにjobのsummary画面にMarkdownのコンテンツが生成できます。

59de4ec8-906d-4601-b270-6edee53d5792.png

さらにアレンジする

このworkflowをベースに、次のようなアレンジが考えられそうです。

  • 成功✅、失敗❌の絵文字などを追加してよりリッチにする
  • compile, testなどログが長く生成されうるものは <details> で括る
    • core.summary クラスには addDetails のようなメソッドもあるので容易に実装できます
  • コンパイルの時間やテストの実行時間を表形式で出力する
    • これも addTable メソッドが提供されているので実装しやすいです
  • job sumamryではなくPRに対してコメントをする運用にする
    • この記事のテーマと外れますが、1つの選択肢としてアリだと思います
    • PRに対してコメントする方式で運用する場合はmarocchino/sticky-pull-request-commentが便利です

コンパイルやテストなどの基本的に成功する前提のものはsummaryに出力し、terraformのplan結果など毎回確認したいものはPRにコメントするなど、ニーズに合わせて運用を考えるのが良さそうです。

まとめ

ElixirのCIでGitHub ActionsのJob Summariesを使った場合のworkflow例を紹介しました。ここまで書いたものの、コンパイルやテストが落ちたらjobのログを見にいっちゃう気がします。テストのカバレッジを表示するなど、ログを見るより視認性が良いコンテンツが刺さるのかなと思います。

参考

https://zenn.dev/jrsyo/articles/279fb2c65cd8b2

https://docs.github.com/ja/actions/creating-actions/creating-a-composite-action

Discussion