🐈
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
設計
- スナップショット
- 差分画像の生成
- ImageMagick (「さようなら ImageMagick」の考察)
- 内部に閉じた環境かつ、policy.xmlを定義しておく
- スナップショット保存
- スナップショットダウンロード
- スナップショットのホスト
- compareブランチ
- マージしたらブランチ削除
- compareブランチ
- テスタブルなScreen設計
- Perceptionなどを使って、ScreenにObservableなUIStateを参照させることで、初期状態のセットで自由にUIを組み替えることができます。
Workflow概要
- 今日の日付を取得
- mainブランチの最新5件のコミットを取得
- featureブランチならCache復元 (key:今日の日付, restore-keys 最新5件のコミット)
- cacheがある or main,schedule実行ならスナップショットテスト
- mainブランチ or scheduleならcacheに保存
- ImageMagickのセット
- 差分画像の生成
- Artifactにアップロード
- PRコメント作成,投稿
- 比較用ブランチ作成 & 画像をコミット
※実際に導入した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のみをテストすることができます
セキュリティ対策
-
Pin actions to a full length commit SHA
タグへの参照はコミットの付け替えが可能(攻撃が可能)なので、usesの指定はコミットハッシュを使う
# 例
# ×
uses: actions/upload-artifact@v4
# ◯
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
-
「さようなら ImageMagick」の考察
webで公開されたフォームなど、外部からの入力がある場合は要注意 - Issueへの画像ホスト
- 現在は対策済みだが、Issueへ画像をホストすると特定の操作で外部から参照可能になる場合があったらしい
採用について
タップルでは、新たな仲間を募集しております!
経験を最大化しながら、少子化などの社会問題の解決をめざし、
あなたの技術で世の中を幸せにしませんか?
ぜひ下記URLを覗いてみてください!
Discussion