TORANA TECH BLOG

株式会社トラーナのエンジニアチームの開発ブログ

脱get-diff-actionとecspresso diffをGitHub Actionsで実行する話

SREのクラシマです。
弊社ではGitHub - technote-space/get-diff-action: GitHub Actions to get git diffを多用していたのですが、2023年11月にarchivedになってしまいました。
同actionではnode16を利用していることもあり、git diffコマンドに書き換えることにしました。

ついでなので、terraformと一緒に利用しているecspressoについて、tfcmtのようにecspresso diffがPull Request上で見られると便利だね、ということで同僚が作ったecspresso diff actionについても紹介します。

ecspresso diff action全容

---
name: Diff task definition

on:
  pull_request:
    paths:
      - 'ecspresso/**'
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
env:
  HEAD_REF: ${{ github.head_ref }}
  START_COMMENT: "### ecspresso diff"

jobs:
  get-target-dirs:
    name: get target dirs
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      target-dirs: ${{ steps.target-dirs.outputs.dirs }}
    steps:
      - name: Checkout
        timeout-minutes: 3
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          fetch-depth: 0
          sparse-checkout: |
            ecspresso
      - name: Get target dirs
        timeout-minutes: 2
        id: target-dirs
        run: |
          set -xv
          diff_dirs=$(git diff --diff-filter=AMRCD \
                        --name-only \
                        "origin/$GITHUB_BASE_REF..${GITHUB_REF/refs\//}" \
                        -- ecspresso \
                        | xargs dirname \
                        | sort -u \
                        | jq -cnR '[inputs | select(length > 0)]'
                      )
          echo "#diff : ${diff_dirs}"
          echo "dirs=${diff_dirs}" >> "$GITHUB_OUTPUT"
      - name: Comment remove PR
        timeout-minutes: 1
        env:
          GH_TOKEN: ${{ secrets.PAT }}
        run: |
          set -xv
          remove_comment_ids=$(gh api \
            -H "Accept: application/vnd.github+json" \
            -H "X-GitHub-Api-Version: 2022-11-28" \
            "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
            --jq '.[] | select(.user.login == "torana-dev-user") | select(.body | startswith("${{ env.START_COMMENT }}\n")) | .id')

          if [ -n "$remove_comment_ids" ]; then
            for comment_id in $remove_comment_ids; do
              gh api \
                --method DELETE \
                -H "Accept: application/vnd.github+json" \
                -H "X-GitHub-Api-Version: 2022-11-28" \
                "repos/${{ github.repository }}/issues/comments/$comment_id"
            done
          fi

  diff:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    needs: [get-target-dirs]
    timeout-minutes: 10
    strategy:
      matrix:
        target-dir: ${{ fromJson(needs.get-target-dirs.outputs.target-dirs) }}
    steps:
      - name: Checkout
        if: needs.get-target-dirs.outputs.target-dirs != '[]' && needs.get-target-dirs.outputs.target-dirs != ''
        timeout-minutes: 3
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          fetch-depth: 0
          sparse-checkout: |
            ecspresso
      - uses: aquaproj/aqua-installer@fd2089d1f56724d6456f24d58605e6964deae124 # v2.3.2
        with:
          aqua_version: v2.25.1
      - name: Configure AWS credentials
        timeout-minutes: 1
        uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-oidc-role
          aws-region: ap-northeast-1
      - name: Get service name
        id: get-service-name
        run: |
          service_name=$(echo "${{ matrix.target-dir }}" | awk -F'/' '{print $3"-"$2}')
          echo "service_name=$service_name" >> "$GITHUB_OUTPUT"
      - name: ecspresso diff
        id: ecspresso-diff
        timeout-minutes: 2
        working-directory: ${{ matrix.target-dir }}
        run: |
          set -xv
          # イメージタグは既存の値を使う(差分を無視するため)
          actual_image_tag=$(aws ecs describe-task-definition \
            --task-definition "${{ steps.get-service-name.outputs.service_name }}" \
            | jq -r '.taskDefinition.containerDefinitions[0].image | match(":(.*)$").captures[0].string')
          diff=$(IMAGE_TAG="$actual_image_tag" ecspresso diff --config ecspresso.yml)
          {
            echo "diff<<EOF"
            echo "$diff"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"
      - name: Comment on PR
        timeout-minutes: 1
        env:
          GH_TOKEN: ${{ secrets.PAT }}
        run: |
          set -xv
          comment="${{ env.START_COMMENT }}
          #### Target - ${{ matrix.target-dir }}
          "

          # shellcheck disable=SC2078
          if [ ${{ contains(steps.ecspresso-diff.outputs.diff, '+') }} ] || [ ${{ contains(steps.ecspresso-diff.outputs.diff, '-') }} ]; then
            diff='${{ format(
              '<details><summary>Show Diff</summary>

          ```diff
          {0}
          ```

          </details>',
              steps.ecspresso-diff.outputs.diff
            ) }}'
          else
            diff='```
          No changes
          ```'
          fi

          gh pr comment "${{ env.HEAD_REF }}" --body "$comment""$diff"

get-diff-actionからの切り替え

修正前はこう↓でした。
get-diff-actionを通ると、env.GIT_DIFFに変更差分のファイルが設定されるので、そこから変更差分のディレクトリに変換していました。

    steps:
      - name: Checkout
        timeout-minutes: 3
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          fetch-depth: 0
          sparse-checkout: |
            ecspresso
      - uses: technote-space/get-diff-action@f27caffdd0fb9b13f4fc191c016bb4e0632844af # v6
      - name: Get target dirs
        timeout-minutes: 2
        id: target-dirs
        run: |
          diff_dirs=$(echo ${{ env.GIT_DIFF }} \
                        | tr ' ' '\n' \
                        | grep "ecspresso/" \
                        | xargs -I{} dirname {} \
                        | sort -u \
                        | tr "\n" " " \
                        | jq -cR 'split("\n") | map(select(length > 0))'
                        )
          echo "#diff : ${diff_dirs}"

ecspresso deployは別のworkflowにしているのでこれで良いのですが、terraform用のworkflowはpush時にapplyするようにしているので、更に一工夫いります。
github.eventのbeforeとafterには、それぞれマージ前とマージ後のcommit hashが入っているので、これを使います。

      - name: target branch
        id: target_branch
        run: |
          set -xv
          branch=""
          from=""
          to=""
          if [ "$GITHUB_EVENT_NAME" = "push" ]; then
            branch="$GITHUB_REF_NAME"
            from="${{ toJSON(github.event.before) }}"
            to="${{ toJSON(github.event.after) }}"
          else
            branch="$GITHUB_BASE_REF"
            from="origin/$GITHUB_BASE_REF"
            to="${GITHUB_REF/refs\//}"
          fi
          {
            echo "branch=$branch"
            echo "from=$from"
            echo "to=$to"
          } >> "$GITHUB_OUTPUT"
      - name: get diff
        run: |
          set -xv
          diff=$(git diff --diff-filter="AMRCD" \
                  --name-only \
                  "${{ steps.target_branch.outputs.from }}...${{ steps.target_branch.outputs.to }}" \
                  -- "**.tf" \
                  | xargs)
          echo "GIT_DIFF=$diff" >> "$GITHUB_ENV"

Pull Requestコメントの書き換え

tfcmtでは、複数回terraform planした場合にPull Requestのコメントを上書きしてくれる機能があり、大変便利です。
tfcmt で Terraform の CI/CD を改善する 我々も同じように、と考えたのですがなかなか難しかったので(debugもむずい)、もっと雑に対応しました。
ghコマンドでPRについているecspresso diffで付与したコメントを全部取得して順繰り削除、もう一回コメントし直す、というものです。

ecspresso diff

ecspressoのinstallはGitHub - aquaproj/aqua: Declarative CLI Version manager written in Go. Support Lazy Install, Registry, and continuous update with Renovate. CLI version is switched seamlesslyを使用し、ecspressoのディレクトリ構成は↓こんな感じです。
ECSクラスタ名、ECSサービス名は<service_name>-<env_name>で固定しています。(e.g. hoge-prd, fuga-stgなど)

❯ tree -d ecspresso
ecspresso
├── prd
│   └── service_name
└── stg
    └── service_name

5 directories

実行結果はこんな感じです。

まとめ

無事に脱get-diff-actionできました。
単純な書き換えで済むと思っていたのですが、想像以上にget-diff-actionは利用者に使いやすく作ってあったのだと痛感しました。改めてお礼申し上げます。

本当はclassic PATを使ってる部分もいい感じにしたかったのですが、GitHub App移行の道が険しく断念中です。
permissionsをうまいことやればsecrets.GITHUB_TOKENへの切り替えはできそうなんですが...。