GitHub Actions's composite-action #doda Developer Group Advent Calendar 2024

この記事は、「#doda Developer Group Advent Calendar 2024」の 25 日目の記事です。
パーソルキャリア株式会社の doda システムアーキテクト部でリードエンジニアを務めている井上1です。
バッチ基盤領域の刷新プロジェクトにおいて、リソースのレビュアとワークフローの整備をしています。
バッチ基盤領域を含む doda サイトでは、GitHub のホスティングとして「GitHub Enterprise Server」2を運用しています。
当プロジェクトでは、GitHub から提供している「GitHub Actions」3を採用しています。
「GitHub Actions」は AWS の EC2 上にインストールしてセルフホステッドランナー4として運用しています。
当記事は、「GitHub Actions」でワークフローを運用した中で、整理した複合アクション5について記載していきます。

前提条件

当記事で取り扱っているリソースのバージョンは以下です。
読んでいる時点のバージョンの差異に留意してご参考ください。

  • GitHub Enterprise Server 3.13
  • SonarQube Developer EditionVersion 8.9.86
  • Gradle 8.87

セルフホステッドランナーで利用している OS8

cat /etc/os-release
NAME="Amazon Linux"
VERSION="2023"
ID="amzn"
ID_LIKE="fedora"
VERSION_ID="2023"
PLATFORM_ID="platform:al2023"
PRETTY_NAME="Amazon Linux 2023.4.20240611"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2023"
HOME_URL="https://aws.amazon.com/linux/amazon-linux-2023/"
DOCUMENTATION_URL="https://docs.aws.amazon.com/linux/"
SUPPORT_URL="https://aws.amazon.com/premiumsupport/"
BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2023"
VENDOR_NAME="AWS"
VENDOR_URL="https://aws.amazon.com/"
SUPPORT_END="2028-03-15"

記載しないこと

  • セルフホステッドランナーとして GitHub Actions を動かす環境構築
  • プロキシサーバーとの通信方法の設定
  • Java のソースコード
  • Gradle のマルチプロジェクトについての解説

課題

当プロジェクトでは、開発中に開発者へ委ねているチェック処理や静的解析が存在しています。 委ねているが故に、未実行や見落としが発生します。 そのため、レビュー時や、疎通試験の実施時に成果物の課題が見つかりました。

アプローチ

上記課題の解として以下のワークフローを構築しました。
当プロジェクトで運用しているワークフローを当記事へ記載するため改変した一部を抜粋します。

.github/workflows/pull-request-check-resources.yaml

name: Pull Request Check Resources
run-name: This is a workflow that is triggered by opening a Pull Request or making a commit.

on:
  pull_request:
    types: [opened, synchronize]

defaults:
  run:
    shell: bash

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read
  id-token: write
  pull-requests: write

jobs:
  check:
    runs-on: self-hosted
    timeout-minutes: 10

    steps:
      - name: Step Summary
        run: |
          echo "# :loudspeaker: We will perform validations from multiple perspectives on the resources we intend to add. :loudspeaker: " >> $GITHUB_STEP_SUMMARY

      - name: Prepare Resource
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Check Branch Name
        if: ${{ github.base_ref != 'main' && github.head_ref != 'release' }}
        # この複合アクションを解説します。
        uses: ./.github/actions/branch-name
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Check New Line Code
        # 省略

      - name: Creation Sub Projects
        # 省略

      - name: Changed files
        # 省略

      - name: Extraction Sub Projects
        id: extraction
        if: ${{ steps.changed-files.outputs.any_changed == 'true' }}
        # この複合アクションを解説します。
        uses: ./.github/actions/extraction-subprojects
        with:
          projects: ${{ steps.sub-project-list.outputs.subprojects }}
          files: ${{ steps.changed-files.outputs.all_changed_files }}

      - name: Setup Java Toolchain
        # 省略

      - name: Build Sub Projects
        # 省略

      - name: Send to SonarQube Server
        if: ${{ steps.changed-files.outputs.any_changed == 'true' }}
        # この複合アクションを解説します。
        uses: ./.github/actions/send-to-sonarqube-server
        with:
          projects: ${{ steps.build.outputs.build_projects }}

      - name: Check Static Analysis
        if: ${{ steps.changed-files.outputs.any_changed == 'true' }}
        # この複合アクションを解説します。
        uses: ./.github/actions/static-analysis
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          host: ${{ secrets.SONAR_HOST_URL }}
          login: ${{ secrets.SONAR_LOGIN }}
          projects: ${{ steps.build.outputs.build_projects }}

      - name: Check Implementation Test Files
        if: ${{ steps.changed-files.outputs.any_changed == 'true' }}
        # この複合アクションを解説します。
        uses: ./.github/actions/implementation-test-files
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          projects: ${{ steps.build.outputs.build_projects }}

      - name: Check Coverage Verification
      # 省略

提示した課題を一括で解決する魔法のような手段はありません。

最初に着手したことは、成果物の課題がどうして発生したのかを文面に書き出すことでした。
思考のみで考えたことは、発散してしまい何が課題なのかを多角的に考察できません。
書き出した課題と解決するためのアプローチを整理して、優先度をつけてワークフローで解決できるように実装をしました。

課題の真因を整理すると、プロジェクトの開発中に解決できることが分かりました。
そのため Pull Request のコミット単位で、以下のことを遵守してもらうアプローチをとることにしました。
こちらで提示した困りごとを回避するための手段をチェックポイントとして設けました。
チェックポイントをクリアしなければ、 Pull Request をマージができないようにしました。

人の気遣いを当てにするのではなく、機械的にリソースのチェックを行うことを決めました。

上記のアプローチを含めたワークフローをコミット単位で起動することで、リソースの健全化を図ることにしました。
チェックポイントを満たさない場合は、 Pull Request のコメントへ追記して改善するための行動を起こすようにフィードバックをしました。
以下の行動を取ってもらい学習を促すことを狙いとしています。

  1. チェックポイントを満たさない。
  2. チェックポイントの要件を確認する。
  3. チェックポイントの要件を満たす修正をする。
  4. チェックポイントへの理解を深めて次回以降気を付けるようになる。

相手に課題を示して、課題解決のためのアクションを提示して取り組むことでプロダクトへの理解を深めていくようになります。

現在、確認している観点は以下の項目です。

■ プロジェクト指定のチケット番号が含まれていること。

name: Check Branch Name

プロダクト管理チーム側で未確認のリソースがマージされないようにするためです。
プロダクト側で払い出した番号が含まれていない場合は、ワークフローが失敗します。
マージしたプロジェクトについてどこへ問い合わせをするべきか追跡できるようにするためでもあります。
チケット管理システムと番号の照会をしているわけではなく、相手の信頼度を図るバロメーターとして運用しています。
プロダクト管理チームと足並みを揃える気概があるチームか、適当な数字を入れてこのチェックポイントを回避しようとする悪癖を抱えているチームかという判断の秤にかけています。

■ 改行コードが統一されていること。

name: Check New Line Code

ワークフローを動かしている環境は、 Amazon Linux です。
そのため、Gradle のビルドプロセスのタスク中に、フォーマッターチェックを行う際、改行コードによって想定外の挙動を行うことがあります。
開発端末が Windows です。
改行コードが CRLF や LF が混在している場合は、差分がでてしまい、ブランチを切り替える際に不便と感じることがあります。
そのため、リポジトリ内のファイルの改行コードを都度チェックしています。
リポジトリ内のファイルを対象としているのは、Web 上の GitHub からリソースをアップロードできるので早期検知のためでもあります。

■ テストコードが実装されていること。

name: Check Implementation Test Files

Gradle のビルドプロセスのタスクでは、テストコードの実装の有無のチェックを行うことができませんでした。
開発作業中にテストコードが未作成の場合があるので、Gradle のビルドプロセスのタスクに含めるのは早計と判断しました。
Pull Request を提出するということは開発が終了または、レビューに値するところまで進んでいると判断してワークフローに含めました。

■ 網羅率が一定以上を満たしていること。

name: Check Coverage Verification

Gradle のビルドプロセスのタスクで判断して満たしていない場合は、ワークフローを落とすようにしています。
網羅率を満たすためのテスト価値性の低いテストコードの実装が発生しますが、割り切るようにしました。
プロダクトの成長度合いとしては、テストコードの実装の試行錯誤の段階であり、よりテスト価値が高い実装は、視野に入れていません。

■ SonarQube へ解析結果を提供する。

name: Send to SonarQube Server 

チェックポイントではないのですが、静的解析の結果提供を実施しています。

SonarQube の Web API を利用してプロジェクト単位での測定結果を表形式で提供しています。
Web 上で確認できるようにリンクを提供して開発者の動線を整えるようにしました。

解説

複合アクションの解説が当記事のメイントピックですが、ワークフローの定義についても、一部解説をします。

ワークフローの名前について

name: Pull Request Check Resources

長い名前になるとワークフローの一覧から全量を見ることができないので選択時のストレスになります。短くすることをオススメします。
ワークフロー一覧のサイドパネルの横幅を調整する機能がないため、ワークフローの詳細を確認しなければ全量を見ることができません。
バージョンアップを重ねることでいつか解決できることを期待しています。

pull_requestとpull_request_targetの違いについて

on:
  pull_request:
    types: [opened, synchronize]

pull_request9と pull_request_target10 との違いはリポジトリのリソースへの影響範囲の差なので、無難に pull_request を選択で大丈夫です。

シェルスクリプトを定義について

defaults:
  run:
    shell: bash

デフォルトのシェルスクリプトを定義11することで、ワークフロー内のシェルスクリプトの定義をスキップできます。
これにより Step 毎での定義が不要となります。

ワークフローの優先度について

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

過去、別のプロダクトの開発で、下記のケースが発生して障害が発生したのを受けて設定しました。
多重クリックを行いワークフローが壊れたケースがありました。
多重クリックミスで複数の同一のワークフローが立ち上がった際、前のワークフローを停止して後続のワークフローを優先して進めます。

ワークフローのリソースへの権限について

permissions:
  contents: read
  id-token: write
  pull-requests: write

Step 単位で設定もできますが、ワークフロー全体でのリソースへの権限12を付与しています。
社内のワークフローなのでワークフロー全体で設定していますが、本来は慎重に設定する必要があります。

ワークフローの起動時間の制限について

jobs:
  check:
    runs-on: self-hosted
    timeout-minutes: 10

10 分経過した場合に強制的にワークフローを停止させます。
10 分を超える場合は、ループが終わらないなど考えられ、サーバーが止まるといった被害を広げる可能性があります。
このように定義することで、セルフホステッドランナーを載せているサーバーへの負担を軽減させることができます。

ワークフローのサマリーについて

steps:
  - name: Step Summary
    run: |
      echo "# :loudspeaker: We will perform validations from multiple perspectives on the resources we intend to add. :loudspeaker: " >> $GITHUB_STEP_SUMMARY

ワークフロー実行後のサマリーに文面を記載しています。
これにより、実行したワークフローで何を実行したのかの過程を確認できます。
ワークフローの開発中などのトラブルシューティングで役に立ちました。
絵文字などを含めることができるので、強調したい部分などを修飾できて便利です。

最初の一手

- name: Prepare Resource
  uses: actions/checkout@v4
  with:
    fetch-depth: 0

複合アクションを利用するために、複合アクションを含むリポジトリをダウンロードする必要があるので、最初に定義をしています。

変数展開について

Check Branch Name を例にとって解説します。

- name: Check Branch Name
  if: ${{ github.base_ref != 'main' && github.head_ref != 'release' }}
  uses: ./.github/actions/branch-name
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
if: ${{ github.base_ref != 'main' && github.head_ref != 'release' }}

上記は、変数展開で定義しなくてもいいのですが、あえてこのやり方で統一をしています。
書き方を統一することで、読み手の負荷を下げる試みを行っています。

複合アクションの解説

実際に運用している複合アクションの一部を抜粋して解説します。

.github/actions/branch-name/action.yaml

以下コードです。
このコードを抜き出しながら解説します。

name: Branch Name
description: This is an action to verify that the branch name follows the rules specified by the Batch Management Team. The purpose is an initial measure to ensure that resources not recognized by the Batch Management Team are not included.

inputs:
  token:
    description: This is a token for using the GitHub API

runs:
  using: "composite"
  steps:
    - name: Check branch Name
      id: check
      shell: bash
      run: |
        echo "::group::Check branch Name"
          pattern='^(feature|hotfix|support)/("チケット管理システムによって異なります")-[0-9]+'
          branch_name=${{ github.head_ref }}
          if [[ ! $branch_name =~ $pattern ]]; then
            # Detailed message construction for workflow behavior.
            echo "- :x: Invalid branch name: $branch_name" :x: >> $GITHUB_STEP_SUMMARY
            echo "::notice::Branch name convention is $pattern"
            echo "branch=${branch_name}" >> $GITHUB_OUTPUT
            exit 1
          fi
          echo "- :tada: Branch name is valid: $branch_name :tada: " >> $GITHUB_STEP_SUMMARY
        echo "::endgroup::"

    - name: Set up next action announce message
      id: announce
      shell: bash
      env:
        BRANCH: ${{ steps.check.outputs.branch }}
      if: ${{ failure() }}
      run: |
        echo "::group::Set up next action announce message"
          lf='\n'
          message="# :x: Invalid branch name :wood: ${BRANCH}. :x:"
          message+="${lf}@${{github.actor}}"
          message+="${lf}${lf}The branch name does not follow the naming convention defined for the project. :memo:"
          message+="${lf}${lf}> [!NOTE]"
          message+="${lf}> **Examples:**"
          message+="${lf}> **feature/{"チケット管理システムによって異なります"}-123**"
          message+="${lf}> **hotfix/{"チケット管理システムによって異なります"}-456**"
          message+="${lf}> **support/{"チケット管理システムによって異なります"}-789**"
          message+="${lf}${lf}## Next Action :loudspeaker:"
          message+="${lf}**Please change the branch name.**"
          echo "message=${message}" >> $GITHUB_OUTPUT
        echo "::endgroup::"

    - name: Add recover comment to Pull Request
      shell: bash
      env:
        URI: https://www.example.com/api/v3/repos
        TOKEN: ${{ inputs.token }}
        REPOSITORY: ${{ github.repository }}
        NUMBER: ${{ github.event.pull_request.number }}
        MESSAGE: ${{ steps.announce.outputs.message }}
      if: ${{ failure() }}
      # We are executing using the GitHub API to avoid being affected by updates to actions/github-script.
      run: |
        echo "::group::Add recover comment to Pull Request"
          curl -L -X POST \
            -H "Accept: application/vnd.github+json" \
            -H "Authorization: Bearer ${TOKEN}" \
            -H "X-GitHub-Api-Version: 2022-11-28" \
            ${URI}/${REPOSITORY}/issues/${NUMBER}/comments \
            -d "{\"body\": \"${MESSAGE}\" }"
        echo "::endgroup::"

複合アクションの入力
この複合アクションは、 GitHub API を利用するので、要求事項として定義しています。

inputs:
  token:
    description: This is a token for using the GitHub API

複合アクション側から secrets 経由で直接取得できないので呼び出しているワークフローから受け取るようにしています。
セキュリティ観点13からは妥当な仕様だと思っています。

シェルスクリプトの定義について

runs:
  using: "composite"
  steps:
    - name: Check branch Name
        id: check
        shell: bash

複合アクションは他のワークフローへ流用するのを想定しているので、定義をしています。

ログのグルーピング化

ログのグルーピング14を行うことのメリットは、 Step 単位で開閉ができるので、視覚的に把握できることが挙げられます。

 echo "::group::Check branch Name"
 echo "::endgroup::"

サマリーへの追記

リソースのチェックを行ったとき、どこのステップで成功や失敗したのかを示すために Step の見出しを追記しています。
絵文字をサマリーに表示できます。

以下、別パターンの実行結果です。

echo "- :x: Invalid branch name: $branch_name" :x: >> $GITHUB_STEP_SUMMARY

処理した結果をStep間への受け渡しについて

echo "branch=${branch_name}" >> $GITHUB_OUTPUT

処理した結果を Step 間で利用したい場合は、$GITHUB_OUTPUTへ送ることで利用できます。
$GITHUB_ENVでも同じことができますが、Job 間だと参照できないので$GITHUB_OUTPUT15 を利用します。
$GITHUB_OUTPUTは 2022 年 10 月リリースした機能です。 set-outputは、利用ができなくなる見込みです。16

 curl -L -X POST \
   -H "Accept: application/vnd.github+json" \
   -H "Authorization: Bearer ${TOKEN}" \
   -H "X-GitHub-Api-Version: 2022-11-28" \
   ${URI}/${REPOSITORY}/issues/${NUMBER}/comments \
   -d "{\"body\": \"${MESSAGE}\" }"

マーケットプレイスで提供しているアクションを紐解くと上記のように GitHub API を駆使していることが多いです。17

Pull Request にコメントを追記する処理を定義しています。
追記した結果は下記のように表示されます。

処理した結果の定義方法

name: Changed Files
description: This is a workflow dag files.

outputs:
  changed:
    description: if changed files?
    value: ${{ steps.changed-files.outputs.any_changed }}
  files:
    description: files
    value: ${{ steps.changed-files.outputs.all_changed_files }}

runs:
  using: "composite"
  steps:
    - name: Checkout Repository
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Creation Changed Files
      id: changed-files
      uses: tj-actions/changed-files@v44.5.5
      with:
        files: |
          dags/*.py

Step 内で処理した結果は、 outputs で定義した変数に格納できます。

ソースコードの解析するための送信処理
.github/actions/send-to-sonarqube-server/action.yaml

name: Send to SonarQube Server
description: Send to sonarqube-service

inputs:
  projects:
    description: Sub Projects

runs:
  using: "composite"
  steps:
    - name: Send to SonarQube Server
      shell: bash
      env:
        PROJECTS: ${{ inputs.projects }}
      run: |
        echo "::group::Send to SonarQube Server"
          chmod +x ./gradlew
          for project in ${PROJECTS}; do
            ./gradlew ${project}:sonar
            echo "- :tada: Send to SonarQube Server. Project: ${project}. :tada: " >> $GITHUB_STEP_SUMMARY
          done
        echo "::endgroup::"

当プロダクトは、Gradle のマルチプロジェクトを採用しています。
複数のプロジェクトがコミットに含まれるケースがあります。
そのため上記のような反復処理を定義しています。
コミットに含まれているプロジェクトを、./gradlew sonar18 で SonarQube のサーバーへ送信しています。

ソースコードの解析結果の受信処理
解析結果を Sonar Qube のサーバーへ送った後は、解析結果を取得します。

.github/actions/static-analysis/action.yaml

name: Static Analysis
description: We will add the static analysis results of the projects registered in SonarQube to the pull request comments.

inputs:
  token:
    description: This is a token for using the GitHub API
  host:
    description: SonarQube Host Url
  login:
    description: SonarQube Host Login
  projects:
    description: Sub Projects

runs:
  using: "composite"
  steps:
    - name: Check Static Analysis
      id: analysis
      shell: bash
      env:
        TOKEN: ${{ inputs.token }}
        HOST: ${{ inputs.host }}
        LOGIN: ${{ inputs.login }}
        PROJECTS: ${{ inputs.projects }}
      run: |
        echo "::group::Check Static Analysis"
          METRIC_KEYS="ncloc,duplicated_lines,code_smells,complexity,cognitive_complexity,reliability_rating,security_rating,security_review_rating,sqale_rating,open_issues,tests,test_errors,test_failures,skipped_tests,test_success_density,test_execution_time"

          for project in ${PROJECTS}; do
            response=$(curl -u ${LOGIN}: "${HOST}/api/measures/component?component={リポジトリ名}:${project}&branch=develop&metricKeys=${METRIC_KEYS}")
            if ! echo "$response" | grep -q "not found"; then

              # Initialize metric values
              declare -A metric_key_values
              metric_key_values=(
                [ncloc]="-"
                [duplicated_lines]="-"
                [code_smells]="-"
                [complexity]="-"
                [cognitive_complexity]="-"
                [reliability_rating]="-"
                [sqale_rating]="-"
                [security_rating]="-"
                [security_review_rating]="-"
                [open_issues]="-"
                [tests]="-"
                [test_errors]="-"
                [test_failures]="-"
                [skipped_tests]="-"
                [test_success_density]="-"
                [test_execution_time]="-"
              )

              # Parse JSON response and extract measures
              metrics=$(echo "$response" | grep -oP '"metric":"\K[^"]+')
              metric_values=$(echo "$response" | grep -oP '"value":"\K[^"]+')

              # Convert values to an array
              mapfile -t keys <<< "$metrics"
              mapfile -t values <<< "$metric_values"

              for (( i=0; i<${#keys[@]}; i++ )); do
                key=${keys[$i]}
                value=${values[$i]}
                # Convert the values of the specified key items to ABCDE ratings.
                case $key in
                  reliability_rating|sqale_rating|security_rating|security_review_rating)
                    case $value in
                      1.0) value="A" ;;
                      2.0) value="B" ;;
                      3.0) value="C" ;;
                      4.0) value="D" ;;
                      5.0) value="E" ;;
                      *) value="-" ;;
                    esac
                    ;;
                esac
                metric_key_values[$key]=$value
              done

              # Construct the message
              lf='\n'
              sonarQube_page="https://www.example.com/component_measures?branch={ブランチ名}&id={リポジトリ名}"

              message="# :rotating_light: Project's metric :alembic: ${project}. :rotating_light:"
              message+="${lf}@${{github.actor}}"
              message+="${lf}Here are the current project's metrics."
              message+="${lf}Please review the table and use it as a reference for design improvements. :memo:"
              message+="${lf}## :alembic: SonarQube's metric :alembic:"
              message+="${lf}${lf}| metric | value |"
              message+="${lf}| --- | --- |"
              # Ordered keys for maintaining the specified order
              ordered_keys=("ncloc" "duplicated_lines" "code_smells" "complexity" "cognitive_complexity" "reliability_rating" "sqale_rating" "security_rating" "security_review_rating" "open_issues" "tests" "test_errors" "test_failures" "skipped_tests" "test_success_density" "test_execution_time")
              for key in "${ordered_keys[@]}"; do
                message+="${lf}| ${key} | ${metric_key_values[$key]} |"
              done
              message+="${lf}${lf}> [!IMPORTANT]"
              message+="${lf}> **Please refer to the following documentation for details on the measurement items.**"
              message+="${lf}> [metric](/.github/actions/static-analysis/README.md)"
              message+="${lf}> **Please refer to the following URL for details on the SonarQube static analysis results.**"
              message+="${lf}> ${sonarQube_page}:${project}"

              curl -L -X POST \
                -H "Accept: application/vnd.github+json" \
                -H "Authorization: Bearer ${TOKEN}" \
                -H "X-GitHub-Api-Version: 2022-11-28" \
                https://www.example.com/api/v3/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \
                -d "{\"body\": \"${message}\" }"

              echo "- :alembic: ${project}'s [metric](${sonarQube_page}:${project}) :alembic: " >> $GITHUB_STEP_SUMMARY
            fi
          done
        echo "::endgroup::"

Web API を利用してレスポンスを表形式へ変換してコメントへ追記しています。
この Step の実行結果は以下のコメントのように追記されます。

テストコードの実装チェック

Spring Batch を利用して当プロダクトを作成しています。
JUnit でテストコードを実行した際に、実行結果のレポートが生成されます。
テストコードがない場合は、生成されません。
その部分を判定してテストの有無の判断をしています。

.github/actions/implementation-test-files/action.yaml

name: Implementation Test Files
description: implementation Test Files.

inputs:
  token:
    description: This is a token for using the GitHub API
  projects:
    description: Sub Projects

runs:
  using: "composite"
  steps:
    - name: Check Implementation Test Files
      id: implementation
      shell: bash
      env:
        PROJECTS: ${{ inputs.projects }}
      run: |
        echo "::group::Check Implementation Test Files"
          not_found_projects=""
          for project in ${PROJECTS}; do
            cd ${project}
            if [ ! -f build/reports/tests/test/index.html ]; then
              not_found_projects="${not_found_projects} ${project}"
            fi
            cd -
          done

          # Remove leading space
          not_found_projects=$(echo $not_found_projects | sed 's/^ *//g')
          echo "not_found_projects=$not_found_projects" >> "$GITHUB_OUTPUT"
        echo "::endgroup::"

    - name: Set up next action announce message
      id: announce
      shell: bash
      env:
        PROJECTS: ${{ steps.implementation.outputs.not_found_projects }}
      if: ${{ steps.implementation.outputs.not_found_projects != '' }}
      run: |
        echo "::group::Set up next action announce message"
          lf='\n'
          message="# :bangbang: Not Implementation Test Files :memo:. :bangbang:"
          message+="${lf}${lf}## Not Implementation Test Files Project List."
          for project in ${PROJECTS}; do
            message+="${lf} - ${project}".
            echo "- :x: Not Implementation Test Files Project: ${project}. :x: " >> $GITHUB_STEP_SUMMARY
          done
          message+="${lf}${lf}@${{github.actor}}"
          message+="${lf}${lf}When adding resources to this repository, it is mandatory to implement test code. :receipt:"
          message+="${lf}${lf}## Next Action :loudspeaker:"
          message+="${lf} **Please ensure that test code is implemented.**"
          message+="${lf}${lf}> [!NOTE]"
          message+="${lf}> [unit-test-guideline](https://www.example.com/documents/unit-test-guideline.md)"
          echo "message=${message}" >> $GITHUB_OUTPUT
          exit 1
        echo "::endgroup::"

    - name: Add recover comment to Pull Request
      shell: bash
      env:
        URI: https://www.example.com/api/v3/repos
        TOKEN: ${{ inputs.token }}
        REPOSITORY: ${{ github.repository }}
        NUMBER: ${{ github.event.pull_request.number }}
        MESSAGE: ${{ steps.announce.outputs.message }}
      if: ${{ steps.implementation.outputs.not_found_projects != '' && failure() }}
      run: |
        echo "::group::Add recover comment to Pull Request"
          curl -L -X POST \
            -H "Accept: application/vnd.github+json" \
            -H "Authorization: Bearer ${TOKEN}" \
            -H "X-GitHub-Api-Version: 2022-11-28" \
            ${URI}/${REPOSITORY}/issues/${NUMBER}/comments \
            -d "{\"body\": \"${MESSAGE}\" }"
        echo "::endgroup::"

実行結果はこちらのように追記されます。

設計思想

複合アクションを検討し始めるタイミングについて
最初は、複合アクションを利用せず、実直に記載をしたほうが良いです。
アプリケーション構築では類似の処理が 2 回以上出てきた際に、初めて検討をしたほうが良いです。

複合アクションで不可分な範囲の塊として分割することにより、流用ができました。
特に、ワークフロー内でアプリケーションを利用する際に、使用している箇所を複合アクションにすることで、バージョンアップの箇所をまとめることができます。

ワークフローでメイン処理を展開していきながら、枝葉を複合アクションに委譲していくことが、私自身のアプリケーションの構築思想と合致しています。 そのため、この複合アクションの登場は私にとってとても良いものでした。

当記事で紹介はしませんが、他にワークフローとして行っていることは、以下があります。

  • Pull Request 作成時にプロダクトを開発する上で必要なドキュメントを案内する。
  • リポジトリに含まれている全プロジェクトのテストコードを実施する。
  • マージ時に、 AWS の S3 と ECR へアップロードするといったデプロイ作業の実施。

最後に

問題を解決するのは、泥臭い試行錯誤の連続でした。
しかし、問題を解決する醍醐味を味わうことは、開発者として、得難い経験です。
整備をした道でより良い開発体験ができるように追及していく所存です。
そんなことに思いを馳せながら、2024 年を締めくくっていきます。
ここまでお読みいただきありがとうございました。

参考資料

alt

井上 剣一 Kenichi Inoue

カスタマープロダクト本部 プロダクト開発統括部 dodaシステムアーキテクト部 dodaシステムアーキテクト第2グループ リードエンジニア

2022年3月にパーソルキャリアへ中途入社。小吉ってなんでしょうね。運がいいかどうかは自分で決めた方がいいですよね。あ、ギザギザの10円をお釣りでもらいました。幸せです。

※2024年9月現在の情報です。


  1. https://techtekt.persol-career.co.jp/entry/member/220317_01
  2. https://docs.github.com/en/enterprise-server@3.13/admin/overview/about-github-enterprise-server
  3. https://docs.github.com/en/enterprise-server@3.13/actions
  4. https://docs.github.com/en/enterprise-server@3.13/actions/hosting-your-own-runners
  5. https://docs.github.com/en/enterprise-server@3.13/actions/sharing-automations/creating-actions/creating-a-composite-action
  6. https://github.com/SonarSource/sonarqube/releases/tag/8.9.8.54436
  7. https://docs.gradle.org/8.8/release-notes.html
  8. https://aws.amazon.com/amazon-linux-2/
  9. https://docs.github.com/en/enterprise-server@3.13/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request
  10. https://docs.github.com/en/enterprise-server@3.13/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target
  11. https://docs.github.com/en/enterprise-server@3.13/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_iddefaultsrunshell
  12. https://docs.github.com/en/enterprise-server@3.13/actions/writing-workflows/workflow-syntax-for-github-actions#permissions
  13. https://docs.github.com/en/enterprise-server@3.13/actions/security-for-github-actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
  14. https://docs.github.com/en/enterprise-server@3.13/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#grouping-log-lines
  15. https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-output-parameter
  16. https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
  17. https://github.com/actions/toolkit/blob/main/packages/github/src/internal/utils.ts#L43
  18. https://docs.github.com/ja/enterprise-server@3.13/rest/issues/comments?apiVersion=2022-11-28#create-an-issue-comment