🐈

Github Actionsでswift-snapshot-testingを使ったVRTを構築してみた(2/2)〜Workflow対応編

に公開

前編~Github Actionsでswift-snapshot-testingを使ったVRTを構築してみた(1/2)〜ローカル対応編

はじめに

この記事は、
「書いてある通りにやればVRTができる!」ではなく、
どのように検証を進めていったかの備忘録になります。

導入環境

  • XcodeGen
  • FeatureModule
  • SPM
  • Github Actions
    • self-hosted-runner
    • namespace

設計

Workflow概要

  1. 今日の日付を取得
  2. mainブランチの最新5件のコミットを取得
  3. featureブランチならCache復元 (key:今日の日付, restore-keys 最新5件のコミット)
  4. cacheがある or main,schedule実行ならスナップショットテスト
  5. mainブランチ or scheduleならcacheに保存
  6. ImageMagickのセット
  7. 差分画像の生成
  8. Artifactにアップロード
  9. PRコメント作成,投稿
  10. 比較用ブランチ作成 & 画像をコミット

※実際に導入したworkflowは公開できないので、
個人の検証用に書いたworkflowを置いておきます。
本番向けのパフォーマンス改善の処理がいくつか入っていません。

name: iOS starter workflow

on:
  pull_request:
    branches: [ "**" ]

jobs:
  build:
    name: Build and Test default scheme using any available iPhone simulator
    runs-on: macos-15

    steps:
      - name: Checkout
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4

      - name: Set Xcode version
        run: sudo xcode-select -s /Applications/Xcode_16.0.app

      - name: Set Default Scheme
        run: |
          scheme_list=$(xcodebuild -list -json | tr -d "\n")
          default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]")
          echo $default | cat >default
          echo Using default scheme: $default

      - name: Build
        env:
          scheme: ${{ 'default' }}
          platform: ${{ 'iOS Simulator' }}
        run: |
          device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
          if [ $scheme = default ]; then scheme=$(cat default); fi
          if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
          file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
          xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=iPhone 16"

      - name: Restore VRT snapshots
        uses: actions/cache@https://github.com/actions/cache/commit/5a3ec84eff668545956fd18022155c47e93e2684 # v3
        id: restore-vrt
        with:
          path: ./citest02/Features/**/__Snapshots__/**
          key: "snapshot-test"

      - name: Check if images exist in SnapshotFilePath folder
        run: |
          count=$(find ./citest02/Features/**/__Snapshots__/** -type f \( -iname "*.png" -o -iname "*.jpg" \) | wc -l)
          if [ $count -gt 0 ]; then
            echo "Found $count image(s) in SnapshotFilePath folder."
          else
            echo "No images found in SnapshotFilePath folder."
          fi

      - name: Test
        env:
          scheme: ${{ 'default' }}
          platform: ${{ 'iOS Simulator' }}
        run: |
          device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
          if [ $scheme = default ]; then scheme=$(cat default); fi
          if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
          file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
          xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=iPhone 16"

      - name: Install ImageMagick on macOS
        if: failure()
        run: brew install imagemagick

      - name: Generate diff images for all failed snapshots using compare
        if: failure()
        run: |
          set -e
          FAILED_DIR="./SnapshotsFailure"
          SUCCESS_DIR="./citest02/Features"
          DIFF_DIR="$(pwd)/artifacts/diffs"
          SUCCESS_OUT_DIR="$(pwd)/artifacts/success"
          FAILED_OUT_DIR="$(pwd)/artifacts/failed"

          mkdir -p "$DIFF_DIR" "$SUCCESS_OUT_DIR" "$FAILED_OUT_DIR"

          # ログ出力: FAILED_DIR 内の画像一覧(再帰的に検索)
          echo "----- Failed snapshots in $FAILED_DIR -----"
          find "$FAILED_DIR" -type f

          # ログ出力: SUCCESS_DIR 内の __Snapshots__ 以下の画像一覧
          echo "----- Success snapshots (under __Snapshots__ directories) in $SUCCESS_DIR -----"
          find "$SUCCESS_DIR" -type f -path "*/__Snapshots__/*"

          # FAILED_DIR 内の全画像ファイルを再帰的に検索してループ
          find "$FAILED_DIR" -type f | while read failed; do
            filename=$(basename "$failed")
            # ファイル名のみで検索(ディレクトリ構造は無視)
            found_success=$(find "$SUCCESS_DIR" -type f -name "$filename" | head -n 1)

            if [ -n "$found_success" ]; then
              echo "Match found for: $filename"
              diff_file="$DIFF_DIR/$filename"
              compare "$found_success" "$failed" "$diff_file" || true

              cp "$found_success" "$SUCCESS_OUT_DIR/$filename"
              cp "$failed" "$FAILED_OUT_DIR/$filename"
            else
              echo "No matching success image found for: $failed"
            fi
          done

      - name: Upload diff images as Artifact
        if: failure()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
        with:
          name: snapshot-diffs
          path: artifacts/diffs/*.png

      - name: Upload success images as Artifacts
        if: failure()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
        with:
          name: snapshot-success
          path: artifacts/success/*.png

      - name: Upload failed images as Artifacts
        if: failure()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
        with:
          name: snapshot-failed
          path: artifacts/failed/*.png

      - name: 比較用ブランチ作成 & 画像をコミット
        if: ${{ failure() }}
        run: |
          git config --global user.name "GitHub Actions"
          git config --global user.email "actions@github.com"
          git checkout --orphan comparison-screenshots
          git reset --hard
          git add artifacts
          git commit -m "Add snapshot test screenshots for comparison"
          git push origin comparison-screenshots --force

      - name: コメント本文を生成
        if: ${{ failure() }}
        shell: bash
        run: |
          shopt -s nullglob
          # 各ディレクトリからユニークなファイル名を集める(連想配列の代替)
          files=()
          for file in artifacts/success/*.png artifacts/diffs/*.png artifacts/failed/*.png; do
            if [ -e "$file" ]; then
              fname=$(basename "$file")
              found=0
              for existing in "${files[@]}"; do
                if [ "$existing" = "$fname" ]; then
                  found=1
                  break
                fi
              done
              if [ $found -eq 0 ]; then
                files+=("$fname")
              fi
            fi
          done
          
          REPO=${GITHUB_REPOSITORY}
          BRANCH=comparison-screenshots
          BASE_URL="https://raw.githubusercontent.com/${REPO}/${BRANCH}/artifacts"
          
          rows=""
          for fname in "${files[@]}"; do
            rows="${rows}"$'    <tr>\n'
            rows="${rows}"$'      <td>'${fname}'<br><img src="'${BASE_URL}'/success/'${fname}'" width="300"></td>'
            rows="${rows}"$'      <td>'${fname}'<br><img src="'${BASE_URL}'/diffs/'${fname}'" width="300"></td>'
            rows="${rows}"$'      <td>'${fname}'<br><img src="'${BASE_URL}'/failed/'${fname}'" width="300"></td>'
            rows="${rows}"$'    </tr>\n'
          done
          if [ -z "$rows" ]; then
            rows=$'    <tr><td colspan="3">No images found</td></tr>\n'
          fi
          
          comment=$(cat <<EOF | sed '/./,$!d'
          <table>
            <thead>
              <tr>
                <th>既存</th>
                <th>差分</th>
                <th>今回</th>
              </tr>
            </thead>
            <tbody>
          ${rows}  </tbody>
          </table>
          EOF
          )
          # 生成したコメントをファイルに書き出す
          echo "$comment" > comment.md
          # ログ出力して内容を確認
          cat comment.md

      - name: PR にコメント投稿
        if: ${{ failure() }}
        uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 #v3
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          issue-number: ${{ github.event.pull_request.number }}
          body-path: comment.md

      - name: Cache VRT snapshots
        if: always()
        uses: actions/cache/save@36f1e144e1c8edb0a652766b484448563d8baf46 #v3
        id: vrt-cache
        with:
          path: ./citest02/Features/**/__Snapshots__/**
          key: "snapshot-test"

検証サイクルの効率化

VRTをチームリポジトリに導入する際、テストの実行時間は検証時間を伸ばす要因となります。

  • ビルドキャッシュ
  • 固定キー
    • swift-snapshot-testingの性質上、比較対象の画像がない場合はテストが失敗します
    • 日付やmainの最新コミットの更新でキーが変わらないように、固定キーでテストをします
  • ダミー画像
    • 1~7をスキップし、8~11のみをテストすることができます

セキュリティ対策

# 例
# ×
uses: actions/upload-artifact@v4
# ◯
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
  • 「さようなら ImageMagick」の考察
    webで公開されたフォームなど、外部からの入力がある場合は要注意
  • Issueへの画像ホスト
    • 現在は対策済みだが、Issueへ画像をホストすると特定の操作で外部から参照可能になる場合があったらしい

採用について

タップルでは、新たな仲間を募集しております!
経験を最大化しながら、少子化などの社会問題の解決をめざし、
あなたの技術で世の中を幸せにしませんか?
ぜひ下記URLを覗いてみてください!

Discussion