🐥

GitHub Actions: 変更されたファイルに対して処理をおこなう

2024/12/22に公開

GitHub Actions で、変更されたファイルに対して処理をおこなう方法です。

変更されたファイルを取得する

変更されたファイルを TARGETS 環境変数に改行区切りで取得します。

Push トリガーの場合

${{ github.event.before }} で push 前のコミットハッシュが取得できるため、それとの差分をチェックします。

on:
  push:
    branches:
      - main

env:
  BASE_DIR: "./"

jobs:
  foo:
    steps:
      - name: checkout
        uses: actions/checkout@v4

      - name: check files
        run: |
          git fetch origin "${{ github.event.before }}" --depth=1
          TARGETS=$(git diff -z --name-only --diff-filter=ACMR "${{ github.event.before }}.." "${{ env.BASE_DIR }}" | xargs -0 -I {} echo "{}")
          printf "TARGETS<<EOF\n%s\nEOF\n" "${TARGETS}" >> $GITHUB_ENV
  1. git fetch で push 前のソースを取得
  2. git diff --name-only で、変更が加えられたファイル一覧を取得
    • -z オプションをつけて各ファイル名を NUL 文字で区切ります。これにより、スペースを含むファイル名も正しく区切ることができることに加え、日本語を含むファイル名がエスケープされず取得できます。
    • xargs -0 -I {} echo "{}" により、ファイル名を改行で区切り直します。
    • --diff-filter で対象を絞り込むことができます。(Added/Copied/Deleted/Modified/Renamed)
      • D を指定すると、削除されて存在しなくなったファイルも含みます。
    • 特定のファイル名だけ取得する場合は、さらに | grep "\.md$" などを追加して絞り込むこともできます。

Pull Request トリガーの場合

Pull Request イベントの場合、新規 Open 時点では github.event.before は設定されません(Open 後に追加 push した場合は設定される)。代わりにベースブランチ github.base_ref と比較することができます。この場合、push ごとに変更されたファイルでなく毎回 Pull Request 内で変更されたファイルすべてが処理対象となります。

on:
  push:
    pull_requests:
    types:
      - opened
      - synchronize

env:
  BASE_DIR: "./"

jobs:
  foo:
    steps:
      - name: checkout
        uses: actions/checkout@v4

      - name: check files
        run: |
          git fetch origin "${{ github.base_ref }}" --depth=1
          TARGETS=$(git diff -z --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}.." "${{ env.BASE_DIR }}" | xargs -0 -I {} echo "{}")
          printf "TARGETS<<EOF\n%s\nEOF\n" "${TARGETS}" >> $GITHUB_ENV

変更されたファイルに対して処理をおこなう

スクリプトでループする場合

ワークフロー内で TARGETS を改行で区切ってループするには、以下のように記述します。

      - name: process targets
        run: |
          while read -r target
          do
            echo "${target}"
          done <<< "${TARGETS}"

カスタムアクションでループする場合

カスタムアクションに TARGETS を渡します。

      - name: process targets
        if: ${{ env.TARGETS }}
        uses: ./.github/actions/action-name
        with:
          targets: ${{ env.TARGETS }}

カスタムアクション (例: .github/actions/action-name/action.yml) で targets を受け取ります。

inputs:
  targets:
    description: target files
    required: true
runs:
  using: node20
  main: index.js

カスタムアクションのスクリプト (例: .github/actions/action-name/index.js) 内で targets を改行で区切ってループします。

const process = require('node:process');

const targets = process.env.INPUT_TARGETS.split('\n');
for (const target of targets) {
    console.log(target);
}

Discussion