CI/CD 実践ガイド(三):GitHub Actions を使った iOS アプリの CI と CD ワークフロー実装
iOSアプリの自動ビルド、テスト、デプロイを実現するGitHub Actionsの完全実装手順

Photo by Robs
はじめに
前回の「CI/CD 実践ガイド(二):GitHub Actions と Self-hosted Runner の使い方と構築大全」では、GitHub Actions の基本知識と動作フロー、さらに自分のマシンを Runner として使う方法を紹介し、簡単な自動化 Actions を3つ実装しました。
本稿では、実際に GitHub Actions を使って iOS アプリの CI/CD ワークフローを構築する方法に重点を置きます。今回も手取り足取り、一歩ずつ進めながら GitHub Actions に関する知識を補足していきます。
App CI/CD フロー関係図

本稿では GitHub Actions を使った CI/CD の構築部分に焦点を当てます。次回の「CI/CD 実践ガイド(4):Google Apps Script Web App を使って GitHub Actions と連携し、無料で使いやすいパッケージングツールプラットフォームを構築する」では、後半部分の Google Apps Script Web App を使ったチーム横断のパッケージングプラットフォーム構築について紹介します。
動作フロー:
-
GitHub Actions プルリクエストトリガーまたはフォームトリガーまたは定期トリガー
-
対応するワークフロージョブ/ステップを実行する
-
ステップに対応する Fastlane(iOS)または(Android Gradle)スクリプトを実行する
-
Fastlane が実行する xcodebuild(iOS)コマンド
-
実行結果の取得
-
後続のワークフロージョブ/ステップの処理結果
-
完了
GitHub Actions 成果図
まずは最終成果をお見せして、皆さんに実装のモチベーションを!



GitHub Actions x Self-hosted Runner 基本知識
GitHub Actions と Self-hosted Runner にまだ慣れていない場合は、ぜひ前回の記事「CI/CD 実践ガイド(二):GitHub Actions と Self-hosted Runner の使い方と構築大全」を先にご覧いただくか、前回の知識と合わせて実装することを強くおすすめします。
実装開始!
iOS Demo プロジェクトのインフラ構成
本文で使用している iOS プロジェクトの内容およびテスト項目は AI によって生成されています。iOS のプログラムの詳細は気にせず、Infra と CI/CD の部分のみを議論します。
以下のツールは過去の経験に基づいていますが、新しいプロジェクトでは最新の mise と tuist の使用を検討してください。
Mint
Mint ツールは依存ツールのバージョンを統一管理できます(Gemfile は Ruby Gems のみ管理可能です)、例えば XCodeGen、SwiftFormat、SwiftLint、Periphery などです。
…など
Mintfile:
yonaskolb/[email protected]
yonaskolb/[email protected]
nicklockwood/[email protected]
ここでは3つだけ使用します。
複雑すぎると感じた場合は、無理に使わずに Action Workflow Step 内で直接 brew install を使って必要なツールをインストールしてください。
Bundle
Gemfile:
source 'https://rubygems.org'
gem 'cocoapods', '~>1.16.0'
gem 'fastlane', '~>2.228.0'
plugins_path = File.join(File.dirname(__FILE__), 'Product', 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
Ruby(Gems)関連の依存関係を管理する際、一般的なiOSプロジェクトで最もよく使われるのはこの2つ、cocoapods と fastlane です。
Cocoapods
Product/podfile:
platform :ios, '13.0'
use_frameworks!
target 'app-ci-cd-github-actions-demo' do
pod 'SnapKit'
end
CocoaPodsのSpecsリポジトリはまもなくメンテナンス終了と宣言されていますが、古いiOSプロジェクトではまだCocoaPodsがよく使われています。ここでは簡単にSnapkitをデモとして追加します。
XCodeGen
多人開発での .xcodeproj / .xcworkspace の変更によるコンフリクトを避けるため、Project.yaml で XCode プロジェクトの内容を統一して定義し、ローカルで自分自身でプロジェクトファイルを生成する(Gitには上げない)。
Product/project.yaml:
name: app-ci-cd-github-actions-demo
options:
bundleIdPrefix: com.example
deploymentTarget:
iOS: '13.0'
usesTabs: false
indentWidth: 2
tabWidth: 2
configs:
Debug: debug
Release: release
targets:
app-ci-cd-github-actions-demo:
type: application
platform: iOS
sources:
- app-ci-cd-github-actions-demo
resources:
- app-ci-cd-github-actions-demo/Assets.xcassets
- app-ci-cd-github-actions-demo/Base.lproj
info:
path: app-ci-cd-github-actions-demo/Info.plist
properties:
CFBundleIdentifier: $(PRODUCT_BUNDLE_IDENTIFIER)
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.test.appcicdgithubactionsdemo
cocoapods: true
app-ci-cd-github-actions-demoTests:
type: bundle.unit-test
platform: iOS
sources:
- app-ci-cd-github-actions-demoTests
dependencies:
- target: app-ci-cd-github-actions-demo
info:
path: app-ci-cd-github-actions-demoTests/Info.plist
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.test.appcicdgithubactionsdemo.tests
app-ci-cd-github-actions-demoUITests:
type: bundle.ui-testing
platform: iOS
sources:
- app-ci-cd-github-actions-demoUITests
dependencies:
- target: app-ci-cd-github-actions-demo
info:
path: app-ci-cd-github-actions-demoUITests/Info.plist
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.test.appcicdgithubactionsdemo.uitests
app-ci-cd-github-actions-demoSnapshotTests:
type: bundle.unit-test
platform: iOS
sources:
- path: app-ci-cd-github-actions-demoSnapshotTests
excludes:
- "**/__Snapshots__/**"
dependencies:
- target: app-ci-cd-github-actions-demo
- product: SnapshotTesting
package: SnapshotTesting
info:
path: app-ci-cd-github-actions-demoSnapshotTests/Info.plist
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.test.appcicdgithubactionsdemo.snapshottests
packages:
SnapshotTesting:
url: https://github.com/pointfreeco/swift-snapshot-testing
from: 1.18.4
SnapshotTesting: Swift Package Manager を使用して管理。
Fastlane
xcodebuild コマンドのラップ、App Store Connect API や Firebase API などのサービス連携の複雑な手順のラップ。
Product/fastlane/Fastfile:
default_platform(:ios)
platform :ios do
desc "すべてのテストを実行する(ユニットテスト+UIテスト)"
lane :run_all_tests do \\|options\\|
device = options[:device]
scan(
scheme: "app-ci-cd-github-actions-demo",
device: device,
clean: true,
output_directory: "fastlane/test_output",
output_types: "junit"
)
end
desc "ユニットテストのみを実行する"
lane :run_unit_tests do \\|options\\|
device = options[:device]
scan(
scheme: "app-ci-cd-github-actions-demo",
device: device,
clean: true,
only_testing: [
"app-ci-cd-github-actions-demoTests"
],
output_directory: "fastlane/test_output",
output_types: "junit"
)
end
desc "ビルドしてFirebase App Distributionにアップロードする"
lane :beta do \\|options\\|
if options[:version_number] && options[:version_number].to_s.strip != ""
increment_version_number(version_number: options[:version_number])
end
if options[:build_number] && options[:build_number].to_s.strip != ""
increment_build_number(build_number: options[:build_number])
end
update_code_signing_settings(
use_automatic_signing: false,
path: "app-ci-cd-github-actions-demo.xcodeproj",
team_id: ENV['TEAM_ID'],
code_sign_identity: "iPhone Developer",
sdk: "iphoneos*",
profile_name: "cicd"
)
gym(
scheme: "app-ci-cd-github-actions-demo",
clean: true,
export_method: "development",
output_directory: "fastlane/build",
output_name: "app-ci-cd-github-actions-demo.ipa",
export_options: {
provisioningProfiles: {
"com.test.appcicdgithubactionsdemo" => "cicd",
},
}
)
firebase_app_distribution(
app: "1:127683058219:ios:98896929fa131c7a80686e",
firebase_cli_token: ENV["FIREBASE_CLI_TOKEN"],
release_notes: options[:release_notes] \\|\\| "New beta build"
)
end
end
註: provisioningProfiles、profile_name は App Developer の Profiles 証明書名 に対応しています。(もし match を使用している場合は、これらの指定は不要です。)

Fastlane は iOS CI/CD において欠かせない部分 であり、用意されたメソッドを使うだけで CI/CD の実際の実行ステップを素早く開発できます。私たちは全体のスクリプト設計に集中でき、複雑な API 連携やコマンド作成に悩む必要はありません。
例えば、Fastlane は「scan(xxx)」と書くだけでテストを実行できますが、xcodebuild で書く場合は「xcodebuild -workspace ./xxx.xcworkspace -scheme xxx -derivedDataPath xxx ‘platform=iOS Simulator,id=xxx’ clean build test」のように記述する必要があります。パッケージングやデプロイも自分で行うとなるとさらに面倒で、App Store Connect や Firebase API と連携しなければなりません。鍵の認証だけでも10行以上のコードを書く必要があります。
Demo プロジェクトには Lane が3つだけあります:
-
run_all_tests: すべての種類のテストを実行する(スナップショット+ユニット)
-
run_unit_tests: ユニットテストのみ実行
-
beta: Firebase App Distributionへのパッケージとデプロイ
Fastlane — Match
Demoプロジェクトの制限により、ここではMatchを使ってチームの開発・配布証明書を管理していませんが、チームのすべての開発・配布証明書を管理するにはMatchの使用を推奨します。管理や更新が容易になります。
Matchを使えば、プロジェクトのセットアップ手順で
match allのようなコマンドを使い、一括で開発に必要なすべての証明書をインストールできます。
- Fastlane Match は別のプライベートリポジトリで証明書と鍵を管理するため、GitHub Actions では SSH Agent を設定して別のプライベートリポジトリをクローンできるようにする必要があります。
(詳細は記事末の補足を参照してください)
[2026/02 更新] 関連記事 — iOS証明書、Fastlane Match、CI/CDの詳細補足:
— — —
Makefile

Makefile
開発者側と CI/CD の両方で Makefile を使ってコマンドを実行することで、同じ環境、パス、操作を統一して管理しやすくします。
典型的なケースとして、ローカルにインストールした
pod installを使う人もいれば、Bundler 管理のbundle exec pod installを使う人もいます。バージョンが異なると差異が生じる可能性があります。
複雑すぎると感じたら使わなくても問題ありません。その場合は、Action Workflow Step 内に直接実行したいコマンドを書くだけで大丈夫です。
Makefile:
#!make
PRODUCT_FOLDER = ./Product/
SHELL := /bin/zsh
.DEFAULT_GOAL := install
MINT_DIRECTORY := ./mint/
export MINT_PATH=$(MINT_DIRECTORY)
## 👇 Help function
.PHONY: help
help:
@echo ""
@echo "📖 利用可能なコマンド:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \\| \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
@echo ""
## Setup
.PHONY: setup
setup: check-mint ## Ruby と Mint の依存関係をインストール
@echo "🔨 Ruby依存関係をインストール中..."
bundle config set path 'vendor/bundle'
bundle install
@echo "🔨 Mint依存関係をインストール中..."
mint bootstrap
## Install
.PHONY: install
install: XcodeGen PodInstall ## XcodeGen と CocoaPods を実行してインストール
.PHONY: XcodeGen
XcodeGen: check-mint ## XcodeGenで .xcodeproj を生成
@echo "🔨 XcodeGenを実行中"
cd $(PRODUCT_FOLDER) && \
mint run yonaskolb/XcodeGen --quiet
.PHONY: PodInstall
PodInstall: ## CocoaPods依存関係をインストール
@echo "📦 CocoaPods依存関係をインストール中..."
cd $(PRODUCT_FOLDER) && \
bundle exec pod install
### Mint
check-mint: check-brew ## Mintがインストールされているか確認、なければ自動インストール
@if ! command -v mint &> /dev/null; then \
echo "🔨 mintをインストール中..."; \
brew install mint; \
fi
### Brew
check-brew: ## Homebrewがインストールされているか確認、なければ自動インストール
@if ! command -v brew &> /dev/null; then \
echo "🔨 Homebrewをインストール中..."; \
/bin/bash -c "$$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; \
fi
## Format only git swift files
.PHONY: format
format: check-mint ## Product/ 以下の全ての Swift ファイルをフォーマット
mint run swiftformat $(PRODUCT_FOLDER)
システム全体や他のプロジェクトへの影響を避けるために、Dependency パッケージ(例:mint、bundle など)のパスはできるだけプロジェクトディレクトリ内に指定し、.gitignore で除外しています。
├── mint (Mint 依存関係)
│ └── packages
├── Product
│ ├── app-ci-cd-github-actions-demo
│ ├── app-ci-cd-github-actions-demo.xcodeproj
│ ├── app-ci-cd-github-actions-demo.xcworkspace
│ ├── app-ci-cd-github-actions-demoSnapshotTests
│ ├── app-ci-cd-github-actions-demoTests
│ ├── app-ci-cd-github-actions-demoUITests
│ ├── fastlane
│ └── Pods (Cocoapods 依存関係)
└── vendor (Bundle 依存関係)
└── bundle

make help コマンドは、Makefile に定義された利用可能なターゲットの一覧と説明を表示します。
これは、プロジェクトで設定されているビルドやタスクの概要を知るのに便利です。
一般的な使い方は以下の通りです:
make help
このコマンドを実行すると、Makefile 内の help ターゲットに記述された説明が表示され、どのターゲットが利用可能かを確認できます。
Makefine を使った統一プロジェクトセットアップ手順:
-
git clone repo -
cd ./repo -
make setup
必要なツールの依存関係をインストールする(brew、mint、bundle、xcodegen、swiftformat、…) -
make install
プロジェクトを生成する(pod install と xcodegen を実行) -
完了
-
プロジェクトを開いて実行する
CI/CDでも新人のオンボーディングでも、上記の手順に従ってプロジェクトを構築します。
本記事の GitHub Actions CI/CD 事例
本記事では、3つのGitHub Actions CI/CDワークフロー構築事例を紹介します。皆さんもこれらの手順を参考にして、自分のチームに合ったCI/CDを構築できます。
-
CI — Pull Requestで単体テストを実行
-
CD — Firebase App Distributionへのパッケージングとデプロイ
-
CI + CD — Nightly Buildでスナップショット+単体テスト+ビルド+Firebase App Distributionへのデプロイを実行
デモの制限により、本記事では Firebase App Distribution へのパッケージとデプロイのみを扱います。Testflight や App Store へのパッケージも同様の手順ですが、Fastlane 内のスクリプトが異なるだけですので、ご自身で応用してください。
CI — Pull Requestで単体テストを実行する
フロー
Develop ブランチは直接プッシュできません。更新するには Pull Request を作成する必要があります。すべての Pull Request はレビュー承認と単体テストの成功が必須で、マージ可能です。新しいコミットがプッシュされると再テストが行われます。
CI-Testing.yml
Repo → Actions → New workflow → 自分でワークフローを設定する。
# ワークフロー(Action)名
name: CI-Testing
# Actions ログのタイトル名
run-name: "[CI-Testing] ${{ github.event.pull_request.title \\|\\| github.ref }}"
# 同じ Concurrency Group に新しいジョブがある場合、実行中のものをキャンセルする
# 例:Push Commit トリガーのジョブがまだ実行されているときに再度 Push Commit された場合、前のジョブをキャンセル
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number \\|\\| github.ref }}
cancel-in-progress: true
# トリガーイベント
on:
# PR イベント
pull_request:
# PR - オープン、再オープン、新しい Push Commit 時
types: [opened, synchronize, reopened]
# 手動トリガー
workflow_dispatch:
# 入力フォームのフィールド
inputs:
# 実行するテスト Fastlane レーン
TEST_LANE:
description: 'Test Lane'
default: 'run_unit_tests'
type: choice
options:
- run_unit_tests
- run_all_tests
# 他のワークフローからの呼び出しによるトリガー
# Nightly Build で使用
workflow_call:
# 入力フォームのフィールド
inputs:
# 実行するテスト Fastlane レーン
TEST_LANE:
description: 'Test Lane'
default: 'run_unit_tests'
# workflow_call inputs は choice をサポートしない
type: string
BRANCH:
description: 'Branch'
type: string
# ジョブ
# ジョブは並行実行される
jobs:
# ジョブID
testing:
# ジョブ名(省略可能だがログ表示用に設定推奨)
name: Testing
# ランナーラベル - GitHub Hosted Runner macos-15 を使用
# 注意:このプロジェクトは Public Repo のため無料で無制限に利用可能
# 注意:このプロジェクトは Public Repo のため無料で無制限に利用可能
# 注意:このプロジェクトは Public Repo のため無料で無制限に利用可能
# Private Repo の場合は従量課金、macOS マシンは最も高価(10倍)で、10回実行で2000分無料上限に達する可能性あり
# self-hosted Runner の使用を推奨
runs-on: macos-15
# 最大タイムアウト時間を設定し、異常時の無限待機を防止
timeout-minutes: 30
# zsh を使用
# 省略可能。私は zsh を好むため設定。デフォルトは bash
defaults:
run:
shell: zsh {0}
# ステップ
# ステップは順番に実行される
steps:
# git clone とチェックアウト
- name: Checkout repository
uses: actions/checkout@v3
with:
# Git Large File Storage はテスト環境で不要
# default: false
lfs: false
# 指定があればそのブランチをチェックアウト、なければデフォルト(現在のブランチ)
# on: schedule は main ブランチのみ実行のため、Nightly Build 等はブランチ指定が必要
# 例:on: schedule -> main ブランチ、Nightly Build は master ブランチ
ref: ${{ github.event.inputs.BRANCH \\|\\| '' }}
# ========== 環境セットアップステップ ==========
# プロジェクト指定の Xcode バージョンを読み込み
# 後続で手動で Xcode_x.x.x.app を指定し使用
# xcversion は非推奨で不安定のため使用しない
- name: Read .xcode-version
id: read_xcode_version
run: \\|
XCODE_VERSION=$(cat .xcode-version)
echo "XCODE_VERSION: ${XCODE_VERSION}"
echo "xcode_version=${XCODE_VERSION}" >> $GITHUB_OUTPUT
# ここでグローバルに Xcode バージョンを指定することも可能(後続で DEVELOPER_DIR 指定不要)
# ただし sudo 権限が必要。self-hosted runner の場合は実行環境に sudo 権限があることを確認
# sudo xcode-select -s "/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
# プロジェクト指定の Ruby バージョンを読み込み
- name: Read .ruby-version
id: read_ruby_version
run: \\|
RUBY_VERSION=$(cat .ruby-version)
echo "RUBY_VERSION: ${RUBY_VERSION}"
echo "ruby_version=${RUBY_VERSION}" >> $GITHUB_OUTPUT
# Runner の Ruby バージョンをプロジェクト指定バージョンに設定
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "${{ steps.read_ruby_version.outputs.ruby_version }}"
# 設定してもしなくても良い(self-hosted で複数 Runner を同時起動すると cocoapods repos が共有ディレクトリで競合する可能性があるため)
# 問題:同時に pod install 実行時に cocoapods repos の競合が起きる場合がある(デフォルトは $HOME/.cocoapods/)
# GitHub Hosted Runner では不要
# - name: Change Cocoapods Repos Folder
# if: contains(runner.labels, 'self-hosted')
# run: \\|
# # 各 Runner ごとに専用の .cocoapods フォルダを使いリソース競合を防止
# mkdir -p "$HOME/.cocoapods-${{ env.RUNNER_NAME }}/"
# export CP_HOME_DIR="$HOME/.cocoapods-${{ env.RUNNER_NAME }}"
# rm -f "$HOME/.cocoapods-${{ env.RUNNER_NAME }}/repos/cocoapods/.git/index.lock"
# ========== キャッシュ設定ステップ ==========
# self-hosted でもキャッシュはクラウドキャッシュで容量計算される
# ルール:7日間未使用で自動削除、1キャッシュ最大10GB、Action 成功時のみキャッシュ
# Public Repo: 無制限無料
# Private Repo: 5GBから
# Self-hosted は独自のシェルスクリプトや他ツールでキャッシュ管理可能
# Bundle キャッシュ (Gemfile)
# Makefile で Bundle インストール先を ./vendor に指定している対応
- name: Cache Bundle
uses: actions/cache@v3
with:
path: \\|
./vendor
key: ${{ runner.os }}-bundle-${{ hashFiles('Gemfile.lock') }}
restore-keys: \\|
${{ runner.os }}-bundle-
# CocoaPods キャッシュ (Podfile)
# デフォルトは プロジェクト/Pods 下
- name: Cache CocoaPods
uses: actions/cache@v3
with:
path: \\|
./Product/Pods
key: ${{ runner.os }}-cocoapods-${{ hashFiles('Product/Podfile.lock') }}
restore-keys: \\|
${{ runner.os }}-cocoapods-
# Mint キャッシュ
# Makefile で Mint インストール先を ./mint に指定している対応
- name: Cache Mint
uses: actions/cache@v3
with:
path: ./mint
key: ${{ runner.os }}-mint-${{ hashFiles('Mintfile') }}
restore-keys: \\|
${{ runner.os }}-mint-
# ====================
# プロジェクトセットアップ&依存関係インストール
- name: Setup & Install Dependency
run: \\|
# Makefile にまとめた Setup コマンドを実行(例)
# brew install mint
# bundle config set path 'vendor/bundle'
# bundle install
# mint bootstrap
# ...
make setup
# Makefile にまとめた Install コマンドを実行(例)
# mint run yonaskolb/XcodeGen --quiet
# bundle exec pod install
# ...
make install
# Fastlane のユニットテストレーンを実行
- name: Run Tests
id: testing
# 作業ディレクトリを指定し、以降のコマンドで cd 省略可能に
working-directory: ./Product/
env:
# テストプラン:全テスト実行か単体テストのみか
# PR トリガーなら run_unit_tests、そうでなければ inputs.TEST_LANE の値(デフォルト run_all_tests)
TEST_LANE: ${{ github.event_name == 'pull_request' && 'run_unit_tests' \\|\\| github.event.inputs.TEST_LANE \\|\\| 'run_all_tests' }}
# このジョブで使う Xcode バージョンを指定
DEVELOPER_DIR: "/Applications/Xcode_${{ steps.read_xcode_version.outputs.xcode_version }}.app/Contents/Developer"
# Repo -> Settings -> Actions secrets and variables -> variables
# 使用するシミュレーター名
SIMULATOR_NAME: ${{ vars.SIMULATOR_NAME }}
# シミュレーターの iOS バージョン
SIMULATOR_IOS_VERSION: ${{ vars.SIMULATOR_IOS_VERSION }}
# 現在の Runner 名称
RUNNER_NAME: ${{ runner.name }}
# Xcodebuild コマンドのタイムアウト時間とリトライ回数を増やす
# マシンの負荷が高いと3回で失敗することがあるため
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 60
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 10
run: \\|
# self-hosted で同一マシンに複数 Runner を起動するとシミュレーター競合が発生(後述)
# 競合回避のため、シミュレーター名を Runner 名にして各 Runner に専用シミュレーターを割り当てるのが推奨
# 例)bundle exec fastlane run_unit_tests device:"${RUNNER_NAME} (${SIMULATOR_IOS_VERSION})"
# ここは GitHub Hosted Runner なので問題なく直接 device:"${SIMULATOR_NAME} (${SIMULATOR_IOS_VERSION})" を使用
# エラー発生時も即終了せず、全出力を temp/testing_output.txt に記録
# 後で内容を解析し Build Failed か Test Failed か判別し、PR に異なるコメントを投稿
set +e
# EXIT_CODE に実行結果の exit code を格納
# 0 = 正常終了
# 1 = 異常終了
EXIT_CODE=0
# 全出力をファイルに書き込み
bundle exec fastlane ${TEST_LANE} device:"${SIMULATOR_NAME} (${SIMULATOR_IOS_VERSION})" \\| tee "$RUNNER_TEMP/testing_output.txt"
# 現在 EXIT_CODE が0なら、${PIPESTATUS[0]}(fastlane 実行結果)を設定
[[ $EXIT_CODE -eq 0 ]] && EXIT_CODE=${PIPESTATUS[0]}
# エラー時は終了モードに戻す
set -e
# テスト出力の解析
# "Error building" があれば is_build_error=true を Actions 環境変数に設定しビルド失敗扱い
# "Tests have failed" があれば is_test_error=true を設定しテスト失敗扱い
if grep -q "Error building" "$RUNNER_TEMP/testing_output.txt"; then
echo "is_build_error=true" >> $GITHUB_OUTPUT
echo "❌ Detected Build Error"
elif grep -q "Tests have failed" "$RUNNER_TEMP/testing_output.txt"; then
echo "is_test_error=true" >> $GITHUB_OUTPUT
echo "❌ Detected Test Error"
fi
# exit code を復元して終了
exit $EXIT_CODE
# ========== 結果処理ステップ ==========
# *.junit テストレポートを解析し結果をマーク、PR ならコメントも投稿
- name: Publish Test Report
# 他者作成の .junit パーサー Actions を利用:https://github.com/mikepenz/action-junit-report
uses: mikepenz/action-junit-report@v5
# if:
# 前ステップ(Testing)が成功または
# 前ステップが失敗かつ is_test_error(ビルド失敗はここをスキップ)
if: ${{ (failure() && steps.testing.outputs.is_test_error == 'true') \\|\\| success() }}
with:
check_name: "Testing Report"
comment: true
updateComment: false
require_tests: true
detailed_summary: true
report_paths: "./Product/fastlane/test_output/*.junit"
# ビルド失敗時のコメント投稿
- name: Build Failure Comment
# if:
# 前ステップ(Testing)が失敗かつ is_build_error かつ PR 番号がある場合
if: ${{ failure() && steps.testing.outputs.is_build_error == 'true' && github.event.pull_request.number }}
uses: actions/github-script@v6
env:
action_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}"
with:
script: \\|
const action_url = process.env.action_url
const pullRequest = context.payload.pull_request \\|\\| {}
const commitSha = pullRequest.head?.sha \\|\\| context.sha
const creator = pullRequest.user?.login \\|\\| context.actor
const commentBody = [
`# プロジェクトまたはテストのビルド失敗 ❌`,
`Pull Request が正しくビルド・テスト実行できるかご確認ください。`,
``,
`🔗 **Action**: [View Workflow Run](${action_url})`,
`📝 **Commit**: ${commitSha}`,
`👤 **Author**: @${creator}`
].join('\n')
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: commentBody
})
技術的なポイント説明:
-
runs-on: self-hosted Runner の使用を推奨します。 GitHub Hosted Runner macOS は非常に高価です 。
-
手動で
.xcode-versionファイルを読み取り、指定された Xcode バージョンを取得し、Xcode を指定する必要があるステップでDEVELOPER_DIR環境変数を設定することで、Sudo を使わずに簡単に Xcode を切り替えられます。 -
Cache: 依存関係のインストール速度を高速化できますが、self-hosted Runner でも GitHub Cloud Cache を使用するため、料金制限の影響を受ける点に注意してください。
-
set +eを使用してコマンドの失敗時にすぐに終了しないようにし、出力をすべてファイルに書き込み、そのファイルを読み取って Build Failed か Test Failed かを判定します。これを行わないと、メッセージが一律 Test Failed になってしまいます。
また、Underlying Error: Unable to boot the Simulator.のような他のエラー判定にも拡張可能です。これはシミュレーター起動失敗を意味し、再試行してください。 -
Checkout Code は指定したブランチを受け入れ可能:
on: scheduleイベントは main(デフォルトブランチ)でしかトリガーされないため、スケジュールを他のブランチで実行したい場合は、ブランチを指定する必要があります。 -
指定する .cocoapods Repo のパスは設定してもしなくてもよいです。以前、同じマシンの self-hosted で二つの Runner が同時に pod install で止まってしまったのは、たまたま両方が .cocoapods Repo を操作していて git lock が発生したためです。
(ただし、発生確率は非常に低いです) -
もし Private Pods Repo があり、Clone の権限を得るために SSH Agent の設定が必要な場合。
(詳細は文末の補足を参照してください) -
リポジトリの Settings → Actions secrets and variables → variables に以下を追加してください:
SIMULATOR_IOS_VERSIONシミュレーターの iOS バージョン
SIMULATOR_NAMEシミュレーターの名前

Commit ファイルをリポジトリのメインブランチにプッシュし、手動で一度検証をトリガーする:


正しく続けて設定してください。
GitHub ワークフロー設定
Repo → Settins → Rules → Rulesets。

-
Ruleset Name: 規則名
-
Enforcement status: 有効/無効 このルールの制限
-
Target branches: 対象となるベースブランチ。Default Branch に設定すると、main や develop にマージするすべてのブランチがこのルールの対象になります。
-
Bypass リスト: 特殊な権限やチームを指定して、この制限を回避できます
-
Branchルール:

-
Restrict deletions: ブランチの削除を禁止する
-
Require a pull request before mergin: プルリクエストを経てマージのみ許可
Required approvals: 必要な承認者数を設定する -
Require status checks to pass: マージに必要なチェックを指定する
- Add checks をクリックして
Testingと入力し、GitHub Actions のアイコン付きのものを選択します。
ここで小さな問題があります。もし Suggestions にTestingが表示されない場合は、先に Actions に戻りトリガー(PRを作成して試す)を成功させる必要があります。そうするとここに表示されます。
- Add checks をクリックして

- Block force pushes: フォースプッシュをブロックする
保存し、Enforcement status が Active になったら、ルールが有効になります。
すべて設定したら、PRを開いてテストしてみましょう:

- Checks に CI-Testing ( Required ) が表示され、Merging is blocked、書き込み権限を持つレビュアーによる最低 X 件の承認レビューが必要と表示されていれば、設定は成功しています。
もしプロジェクトのビルドが失敗した場合(Build Failed)はコメントします:

プロジェクトのビルドは成功したがテストケースが失敗した場合(Test Failed)にコメントします:

プロジェクトのビルドが成功し、テストも成功した場合(Test Success)にコメント:

Review Approve + チェックテスト通過後:

これでPRをマージできます。
- 新しいコミットがプッシュされると、自動的にテストが再実行されます。
完全なコード: CI-Testing.yml
自動マージ:
また、Repo Settings → General → Pull Request の中の以下を有効にすることもできます:

-
Automatically delete head branches: PRマージ後に自動でブランチを削除する
-
Allow Auto-merge: チェックが通過し、条件を満たした承認が得られると自動的にPRがマージされます
条件が設定されていて、かつ現在の条件でまだマージできない場合にのみ「Enable auto-merge」ボタンが表示されます。

CD — Firebase App Distributionへのパッケージングとデプロイ
フロー
GitHub Actions のフォームトリガーでビルドを実行し、バージョン番号やリリースノートを指定できます。ビルド完了後、自動的に Firebase App Distribution にアップロードされ、チームがダウンロードしてテストできます。
CD-Deploy.yml
Repo → Actions → New workflow → set up a workflow yourself。
# ワークフロー(Action)名
name: CD-Deploy
# Actions ログのタイトル名
run-name: "[CD-Deploy] ${{ github.ref }}"
# 同じ Concurrency Group に新しいジョブがある場合、実行中のものをキャンセルする
# 例:同じブランチのビルドジョブが重複トリガーされた場合、前のジョブをキャンセルする
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
# トリガーイベント
on:
# 手動フォームトリガー
workflow_dispatch:
# フォームの入力項目
inputs:
# アプリのバージョン番号
VERSION_NUMBER:
description: 'アプリのバージョン番号(例:1.0.0)。空欄の場合はXcodeプロジェクトから自動検出します。'
required: false
type: string
# アプリのビルド番号
BUILD_NUMBER:
description: 'アプリのビルド番号(例:1)。空欄の場合はタイムスタンプを使用します。'
required: false
type: string
# アプリのリリースノート
RELEASE_NOTE:
description: 'デプロイのリリースノート。'
required: false
type: string
# 他のワークフローからの呼び出しトリガー
# Nightly Buildで使用
workflow_call:
inputs:
# アプリのバージョン番号
VERSION_NUMBER:
description: 'アプリのバージョン番号(例:1.0.0)。空欄の場合はXcodeプロジェクトから自動検出します。'
required: false
type: string
# アプリのビルド番号
BUILD_NUMBER:
description: 'アプリのビルド番号(例:1)。空欄の場合はタイムスタンプを使用します。'
required: false
type: string
# アプリのリリースノート
RELEASE_NOTE:
description: 'デプロイのリリースノート。'
required: false
type: string
BRANCH:
description: 'ブランチ'
type: string
# グローバル静的変数の定義
env:
APP_STORE_CONNECT_API_KEY_FILE_NAME: "app_store_connect_api_key.json"
# ジョブ定義
# ジョブは並行実行される
jobs:
# ジョブID
deploy:
# ジョブ名(省略可能。ログ表示用に設定推奨)
name: Deploy - Firebase App Distribution
# ランナーラベル - GitHubホストランナー macos-15 を使用
# 注意:このプロジェクトはPublic Repoなので無制限に無料で使用可能
# 注意:このプロジェクトはPublic Repoなので無制限に無料で使用可能
# 注意:このプロジェクトはPublic Repoなので無制限に無料で使用可能
# Private Repoの場合は従量課金、macOSマシンは最も高価(10倍)、10回実行で2,000分無料上限に達する可能性あり
# self-hosted Runnerの使用を推奨
runs-on: macos-15
# 最大タイムアウト時間設定。異常時の無限待機を防止
timeout-minutes: 30
# zshを使用
# 省略可能。私はzshを好むだけでデフォルトはbash
defaults:
run:
shell: zsh {0}
# ワークステップ
# ステップは順番に実行される
steps:
# git cloneして現在のリポジトリをチェックアウト、実行中のブランチへ
- name: Checkout repository
uses: actions/checkout@v3
with:
# Git Large File Storageはテスト環境では不要
# default: false
lfs: false
# 指定があればそのブランチをチェックアウト、なければデフォルト(現在のブランチ)
# on: schedule イベントはmainブランチのみなので、Nightly Buildなどは別ブランチ指定が必要
# 例:on: schedule -> mainブランチ、Nightly Build -> masterブランチ
ref: ${{ github.event.inputs.BRANCH \\|\\| '' }}
# ========== 証明書関連ステップ ==========
# Fastlane - Matchで開発証明書を管理し、Lane内でmatchを実行して設定することを推奨
# Matchは別のPrivate Repoで証明書管理するが、SSH Agent設定が必要でprivate repoのgit clone権限が必要
# ref: https://stackoverflow.com/questions/57612428/cloning-private-github-repository-within-organisation-in-actions
#
#
# --- Fastlane - Matchを使わない場合に直接証明書をダウンロード&インポートする方法 ---
# ref: https://docs.github.com/en/actions/how-tos/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development
#
# GitHub Actions Secretはファイルを保存できないため、証明書ファイルは全てBase64エンコードした文字列でSecretに保存
# Step内で動的に読み込み、一時ファイルへ書き出しシステムで利用できる場所へ移動する
# 詳細設定は記事を参照
#
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.BUILD_CERTIFICATE_P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
# GitHub Hosted Runnerはカスタム文字列
# Self-hosted Runnerはマシンログインパスワード
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: \\|
# 変数作成
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Secretから証明書とプロビジョニングプロファイルをデコードして書き出し
echo -n "$BUILD_CERTIFICATE_BASE64" \\| base64 --decode -o $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" \\| base64 --decode -o $PP_PATH
# 一時キーチェーン作成
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# キーチェーンへ証明書インポート
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# プロビジョニングプロファイル適用
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# App Store Connect API Fastlane JSON キー
# ビルド環境でほぼ必須のApp Store Connect API Fastlane JSON キー (.json)
# フォーマット: .json 内容:https://docs.fastlane.tools/app-store-connect-api/
# App Store Connect APIの.p8キーを含む
# FastlaneでTestflightやApp Store APIにアップロードする際に使用
#
# GitHub Actions Secretはファイル保存不可なのでBase64エンコード文字列として保存
# Step内で動的に読み込み、一時ファイルへ書き出し他ステップで利用
# 詳細は記事参照
- name: Read and Write Apple Store Connect API Key to Temp
env:
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
APP_STORE_CONNECT_API_KEY_PATH: "${{ runner.temp }}/${{ env.APP_STORE_CONNECT_API_KEY_FILE_NAME }}"
run: \\|
# Secretからデコードして一時ファイルに書き出し
echo -n "$APP_STORE_CONNECT_API_KEY_BASE64" \\| base64 --decode -o $APP_STORE_CONNECT_API_KEY_PATH
# ========== 環境セットアップステップ ==========
# プロジェクト指定のXcodeバージョンを読み込み
# 後続で手動指定したXcode_x.x.x.appを使用
# xcversionは非推奨で不安定のため使用しない
- name: Read .xcode-version
id: read_xcode_version
run: \\|
XCODE_VERSION=$(cat .xcode-version)
echo "XCODE_VERSION: ${XCODE_VERSION}"
echo "xcode_version=${XCODE_VERSION}" >> $GITHUB_OUTPUT
# ここでグローバルにXcodeバージョン指定も可能
# そうすれば後続でDEVELOPER_DIR指定不要
# ただしsudo権限が必要。self-hosted runnerの場合は権限確認要
# sudo xcode-select -s "/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
# プロジェクト指定のRubyバージョンを読み込み
- name: Read .ruby-version
id: read_ruby_version
run: \\|
RUBY_VERSION=$(cat .ruby-version)
echo "RUBY_VERSION: ${RUBY_VERSION}"
echo "ruby_version=${RUBY_VERSION}" >> $GITHUB_OUTPUT
# RunnerのRubyバージョンをプロジェクト指定にセットアップ
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "${{ steps.read_ruby_version.outputs.ruby_version }}"
# 設定は任意。self-hostedで複数runnerが同時にCI/CD実行時にcocoapodsのrepos共有ディレクトリ問題を回避
# 問題点:同時pod install時にreposの競合が発生する可能性がある(デフォルトは$HOME/.cocoapods/)
# GitHub Hosted Runnerでは不要
# - name: Change Cocoapods Repos Folder
# if: contains(runner.labels, 'self-hosted')
# run: \\|
# # 各Runnerごとに独自の.cocoapodsフォルダを使いリソース競合を防止
# mkdir -p "$HOME/.cocoapods-${{ env.RUNNER_NAME }}/"
# export CP_HOME_DIR="$HOME/.cocoapods-${{ env.RUNNER_NAME }}"
# rm -f "$HOME/.cocoapods-${{ env.RUNNER_NAME }}/repos/cocoapods/.git/index.lock"
# ========== キャッシュ設定ステップ ==========
# self-hostedでもキャッシュはクラウドキャッシュで容量計算される
# ルール:7日間未ヒットで自動削除、単一キャッシュ最大10GB、Action成功時のみキャッシュ
# Public Repo: 無料無制限
# Private Repo: 5GBから
# Self-hostedは独自にshellスクリプト等でキャッシュ戦略を作成可能
# Bundle Cache (Gemfile)
# MakefileでBundleインストール先を./vendorに指定している対応
- name: Cache Bundle
uses: actions/cache@v3
with:
path: \\|
./vendor
key: ${{ runner.os }}-bundle-${{ hashFiles('Gemfile.lock') }}
restore-keys: \\|
${{ runner.os }}-bundle-
# CocoaPods Cache (Podfile)
# デフォルトはプロジェクトのPodsディレクトリ
- name: Cache CocoaPods
uses: actions/cache@v3
with:
path: \\|
./Product/Pods
key: ${{ runner.os }}-cocoapods-${{ hashFiles('Product/Podfile.lock') }}
restore-keys: \\|
${{ runner.os }}-cocoapods-
# Mint cache
# MakefileでMintインストール先を./mintに指定している対応
- name: Cache Mint
uses: actions/cache@v3
with:
path: ./mint
key: ${{ runner.os }}-mint-${{ hashFiles('Mintfile') }}
restore-keys: \\|
${{ runner.os }}-mint-
# ====================
# プロジェクトセットアップ&依存関係インストール
- name: Setup & Install Dependency
run: \\|
# Makefileのsetupコマンド実行。具体的には:
# brew install mint
# bundle config set path 'vendor/bundle'
# bundle install
# mint bootstrap
# ...
# 等のセットアップ処理
make setup
# Makefileのinstallコマンド実行。具体的には:
# mint run yonaskolb/XcodeGen --quiet
# bundle exec pod install
# ...
# 等のインストール処理
make install
- name: Deploy Beta
id: deploy
# 作業ディレクトリ指定。以降のコマンドでcd不要
working-directory: ./Product/
env:
# ビルド用入力パラメータ
VERSION_NUMBER: ${{ inputs.VERSION_NUMBER \\|\\| '' }}
BUILD_NUMBER: ${{ inputs.BUILD_NUMBER \\|\\| '' }}
RELEASE_NOTE: ${{ inputs.RELEASE_NOTE \\|\\| '' }}
AUTHOR: ${{ github.actor }}
# Repo -> Settings -> Actions secrets and variables -> secrets
# Firebase CLI トークン(取得方法は記事参照)
FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }}
# Apple Developer Program Team ID
TEAM_ID: ${{ secrets.TEAM_ID }}
# このジョブで使用するXCode_x.x.x指定バージョン
DEVELOPER_DIR: "/Applications/Xcode_${{ steps.read_xcode_version.outputs.xcode_version }}.app/Contents/Developer"
run: \\|
# 現在のタイムスタンプ取得
BUILD_TIMESTAMP=$(date +'%Y%m%d%H%M%S')
# BUILD_NUMBERが空の場合、タイムスタンプをAppビルド番号に使用
BUILD_NUMBER="${BUILD_NUMBER:-$BUILD_TIMESTAMP}"
ID="${{ github.run_id }}"
COMMIT_SHA="${{ github.sha }}"
BRANCH_NAME="${{ github.ref_name }}"
AUTHOR="${{ env.AUTHOR }}"
# リリースノートを組み立て
RELEASE_NOTE="${{ env.RELEASE_NOTE }}
ID: ${ID}
Commit SHA: ${COMMIT_SHA}
Branch: ${BRANCH_NAME}
Author: ${AUTHOR}
"
# Fastlaneでビルド&デプロイLane実行
bundle exec fastlane beta release_notes:"${RELEASE_NOTE}" version_number:"${VERSION_NUMBER}" build_number:"${BUILD_NUMBER}"
# GitHub Actions推奨のself-hostedセキュリティ設定:
# ref: https://docs.github.com/en/actions/how-tos/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development#required-clean-up-on-self-hosted-runners
# Step: Install the Apple certificate and provisioning profile に対応
# マシン上のダウンロードした証明書・鍵を削除する目的
# Matchを使う場合はMatchのクリーン処理に書き換えが必要
- name: Clean up keychain and provisioning profile
if: ${{ always() && contains(runner.labels, 'self-hosted') }}
run: \\|
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
- Repo -> Settings -> Actions secrets and variables -> secrets に
TEAM_IDという変数を追加し、内容は Apple Developer Team ID の文字列にしてください。

Commit ファイルをリポジトリのメインブランチにプッシュし、ビルド機能をテストします:

他のブランチでこの Action を使う場合は、まずメインブランチの CD-Deploy.yml ファイルをマージしてください。
タスクの完了を待つ:


パッケージ+デプロイ成功 ✅
完全なコード: CD-Deploy.yml
技術的な詳細 — Firebase CLI トークンの取得と設定
Firbase公式ドキュメントの手順 に従って:
まず Firebase CLI ツールをインストールしてください:
curl -sL https://firebase.tools \\| bash
実行:
firebase login:ci
ログインと認証を完了する:


Terminal に戻り、Firebase CLI トークンをコピーします:

Repo → Settings → Secrets and variables → Actions → 新しいシークレット FIREBASE_CLI_TOKEN を追加し、Firebase CLI トークンを貼り付けます。

この Token はあなたのログイン認証情報です。 大切に保管してください。アカウントを離れる場合は必ず交換してください。
技術的詳細 — Apple証明書とプロビジョニングプロファイルのインストール
Runner に開発証明書をインポートする手順の詳細補足。
GitHub Actions の Secret はファイルを保存できないため、すべての証明書ファイルは事前に Base64 エンコードされた文字列形式で Secrets に保存します。GitHub Actions のステップ内で動的に読み込み、一時ファイルに書き込み、システムが読み取れる正しい場所に移動します。
Development のビルドには2つの証明書が必要です:

cicd.mobileprovision

development.cer
從 Apple Developer からダウンロードした証明書は .cer 形式ですが、必要なのは .p12 形式です。まずダウンロードした .cer をダブルクリックしてキーチェーンにインストールし、キーチェーンで該当の証明書を右クリックしてエクスポートしてください。

ファイル名:cicd.p12、フォーマット .p12
P12 キーのパスワード:安全なカスタム文字列を入力してください(例は良くない例で、123456 を使用しています)


現在2つのファイル: cicd.p12、cicd.mobileprovision は準備できました
BASE64形式の文字列に変換してRepo Secretsに保存:
base64 -i cicd.mobileprovision \\| pbcopy
(コメントなし、コードはそのまま)
Repo → Settings → Secrets and variables → Actions → 新しい Secret を追加:BUILD_PROVISION_PROFILE_BASE64 に上記の内容を貼り付けます。
-
base64 -i cicd.p12 \\| pbcopy
Repo → Settings → Secrets and variables → Actions → 新しい Secret を追加:BUILD_CERTIFICATE_BASE64 に上記の内容を貼り付けます。
-
Repo → Settings → Secrets and variables → Actions → 新しいシークレットを追加:P12_PASSWORD 内容は先ほどエクスポートした P12 キーのパスワードです。
-
Repo → Settings → Secrets and variables → Actions → 新しい Secret: KEYCHAIN_PASSWORD を追加します:
GitHub Hosted Runner の場合は任意の文字列を入力してください。Self-hosted Runner の場合は macOS Runner ユーザーのログインパスワードを入力します。

技術的詳細 — App Store Connect API キー
Fastlane を使って App Store や Testflight にパッケージをデプロイする際に必要な .json キー は、GitHub Actions の Secrets に文字列としてしか保存できない制限があるため、キーの内容を Base64 文字列に変換します。GitHub Actions のステップ内で動的に読み込み、一時ファイルに書き出して、そのファイルパスを Fastlane に渡して使用します。
まずは App Store Connect で App Store Connect API Key (.p8) を作成&ダウンロード してください:
-----BEGIN PRIVATE KEY-----
sss
axzzvcxz
zxzvzcxv
vzxcvzxvczxcvz
-----END PRIVATE KEY-----
app_store_connect_api.json ファイルを新規作成します(内容参考):
{
"key_id": "App Store Connect に記載されている Key ID",
"issuer_id": "App Store Connect に記載されている Issuer ID",
"key": "-----BEGIN PRIVATE KEY-----改行は\nに置き換えてください-----END PRIVATE KEY-----",
"duration": 1200, # 任意(最大1200)
"in_house": false # 任意ですが、match/sigh を使用する場合は必要になることがあります
}
ファイルを保存した後に実行:
base64 -i app_store_connect_api.json \\| pbcopy
(コメントなし、コマンドはそのまま)
文字列の内容を Repo → Settings → Secrets and variables → Actions → 新しい Secret: APP_STORE_CONNECT_API_KEY_BASE64 に貼り付けてください。

Read and Write Apple Store Connect API Key to Temp ステップが完了した後、以降のステップでは env APP_STORE_CONNECT_API_KEY_PATH を渡すだけで良いです:
- name: Deploy
env:
APP_STORE_CONNECT_API_KEY_PATH: "${{ runner.temp }}/${{ env.APP_STORE_CONNECT_API_KEY_FILE_NAME }}"
run: \\|
....
Fastlane は自動で取得できます。
技術的拡張 — Reuse Action Workflowでビルドとデプロイの処理を分割する
このケースでは、Fastlane の beta レーンを使って、ビルドとデプロイの2つの処理を直接実行します。
実際のケースでは、同じビルド成果物を異なるプラットフォーム(Firebase、Testflightなど)にそれぞれデプロイする必要があります。そのため、ビルドは一つのAction、デプロイは別のActionに分けるのが望ましいです。そうしないとビルドを二回繰り返すことになり、CI/CDの責任分担にも適しています。
以下はサンプル紹介です:
CI-Build.yml:
name: Build
on:
push:
branches:
- main
workflow_call:
inputs:
RELEASE_NOTE:
description: 'デプロイのリリースノート。'
required: false
type: string
jobs:
build:
runs-on: macos-latest
steps:
- name: コードのチェックアウト
uses: actions/checkout@v4
- name: 依存関係のインストール
run: \\|
make steup
make instal
- name: プロジェクトのビルド
run: bundle exec fastlane build
- name: ビルド成果物のアップロード
uses: actions/upload-artifact@v4
with:
name: build-artifact
path: ./fastlane/build/
CD-Deploy-Firebase.yml:
name: Deploy Firebase
on:
# ビルドアクション完了時に自動でトリガーされる
workflow_run:
workflows: ["Build"]
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
# 完了かつ成功した場合のみデプロイを実行
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install Dependencies
run: \\|
make steup
- name: Download Build Artifact
uses: actions/download-artifact@v4
with:
name: build-artifact
path: ./fastlane/build/
- name: Deploy to Production
run: \\|
bundle exec fastlane deploy-firebase
CD-Deploy-Testflight.yml:
name: Deploy Testflight
on:
# Build Action が完了したときに自動で実行
workflow_run:
workflows: ["Build"]
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
# 完了かつ成功した場合にのみデプロイを実行
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install Dependencies
run: \\|
make steup
- name: Download Build Artifact
uses: actions/download-artifact@v4
with:
name: build-artifact
path: ./fastlane/build/
- name: Deploy to Production
run: \\|
bundle exec fastlane deploy-testflight
また、Reusing Workflow を使うこともできます:
CD-Deploy-Firebase.yml:
name: Deploy Firebase
on:
# 任意のトリガー条件、ここでは手動フォームトリガーの例
workflow_dispatch:
inputs:
RELEASE_NOTE:
description: 'デプロイのリリースノート。'
required: false
type: string
jobs:
build:
needs: Build
uses: ./.github/workflows/CD-Build.yml
secrets: inherit
with:
RELEASE_NOTE: ${{ inputs.RELEASE_NOTE }}
deploy:
runs-on: ubuntu-latest
# ジョブはデフォルトで並列実行、needsでbuild完了待ちに制限
needs: [build]
# 成功時のみデプロイ実行
if: ${{ always() && needs.deploy.result == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install Dependencies
run: \\|
make steup
- name: Download Build Artifact
uses: actions/download-artifact@v4
with:
name: build-artifact
path: ./fastlane/build/
- name: Deploy to Production
run: \\|
bundle exec fastlane deploy-firebase
GitHub Actions — Artifact
Cache と同様に、現在 Self-hosted Runner の Artifact 機能も GitHub Cloud を経由するため 使用量制限を受けます ( 無料アカウントは 500MB から )。
Self-hosted Runnerで同様の効果を得るには、共有ホストディレクトリを自分で作成するか、他のツールを代替として使用してください。
したがって、現在 Artifact は実際にはスナップショットテストのエラー結果やテストレポートなど、小さなデータの保存にのみ使用しています。
CI— Nightly Build 実行スナップショット+単体テスト+ビルド+CD Firebase App Distribution へのデプロイ
フロー
毎日午前3時に main(develop または master)ブランチに対して全てのテスト(ユニットテスト+スナップショットテスト)を自動実行し、失敗した場合は Slack ワークグループに失敗通知を送信します。成功した場合はビルドと Firebase App Distribution へのデプロイを行い、ビルドの成功・失敗のいずれも Slack に通知します。
CI-Nightly-Build-And-Deploy.yml
Repo → Actions → New workflow → 自分でワークフローを設定する。
# ワークフロー(Action)名
name: CI-Nightly Build And Deploy
# Actions ログのタイトル名
run-name: "[CI-Nightly Build And Deploy] ${{ github.ref }}"
# トリガーイベント
on:
# スケジュール定期自動実行
# https://crontab.guru/
# UTC時間
schedule:
# UTCの19:00 = 毎日 UTC+8 の 03:00
- cron: '0 19 * * *'
# 手動トリガー
workflow_dispatch:
# ジョブの作業項目
# ジョブは並行実行される
jobs:
# テストジョブ
testing:
# Reuse Workflow (workflow_call)
uses: ./.github/workflows/CI-Testing.yml
# すべての Secrets を CD-Testing.yml に渡す
secrets: inherit
with:
# 全テスト実行
TEST_LANE: "run_all_tests"
# 対象ブランチ:main, develop, master など
BRANCH: "main"
deploy-env:
runs-on: ubuntu-latest
outputs:
DATE_STRING: ${{ steps.get_date.outputs.DATE_STRING }}
steps:
- name: 日付文字列を取得
id: get_date
run: \\|
VERSION_DATE=$(date -u '+%Y%m%d')
echo "${VERSION_DATE}"
echo "DATE_STRING=${VERSION_DATE}" >> $GITHUB_ENV
echo "DATE_STRING=${VERSION_DATE}" >> $GITHUB_OUTPUT
deploy:
# ジョブはデフォルトで並行実行、needsでtestingとdeploy-env完了まで待機
needs: [testing, deploy-env]
# テスト成功時のみ実行
if: ${{ needs.testing.result == 'success' }}
# Reuse Workflow (workflow_call)
uses: ./.github/workflows/CD-Deploy.yml
# すべての Secrets を CD-Deploy.yml に渡す
secrets: inherit
with:
VERSION_NUMBER: NightlyBuild-${{ needs.deploy-env.outputs.DATE_STRING }}
RELEASE_NOTE: NightlyBuild-${{ needs.deploy-env.outputs.DATE_STRING }}
# 対象ブランチ:main, develop, master など
BRANCH: "main"
# ----- Slack 通知 -----
testing-failed-slack-notify:
needs: [testing]
runs-on: ubuntu-latest
if: ${{ needs.testing.result == 'failure' }}
steps:
- name: Slack チャンネルにテキストを投稿
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: \\|
channel: ${{ vars.SLACK_TEAM_CHANNEL_ID }}
text: ":x: Nightly Build - テスト失敗\nWorkflow: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\\|実行を見る>"
deploy-failed-slack-notify:
needs: [deploy]
runs-on: ubuntu-latest
if: ${{ needs.deploy.result == 'failure' }}
steps:
- name: Slack チャンネルにテキストを投稿
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: \\|
channel: ${{ vars.SLACK_TEAM_CHANNEL_ID }}
text: ":x: Nightly Build デプロイ失敗\nWorkflow: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\\|実行を見る>"
deploy-success-slack-notify:
needs: [deploy]
runs-on: ubuntu-latest
if: ${{ needs.deploy.result == 'success' }}
steps:
- name: Slack チャンネルにテキストを投稿
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: \\|
channel: ${{ vars.SLACK_TEAM_CHANNEL_ID }}
text: ":white_check_mark: Nightly Build デプロイ成功\nWorkflow: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\\|実行を見る>"
Commit ファイルをリポジトリのメインブランチにプッシュし、手動でテストとビルドをトリガーして結果を確認します:

今後は毎日自動でトリガーされます。


テストタスク、パッケージングとデプロイメントタスク、通知タスクがすべて完了した後、結果を確認します。


私たちは直接スマホにNightly Build版をインストールして、先行体験テストを行うことができます。
技術的詳細
この Action は前に設計した CI-Testing と CD-Deploy をそのまま再利用し、Nightly Build に組み合わせています。非常に柔軟で使いやすいです!
完全なコード: CI-Nightly-Build-And-Deploy.yml
Self-hosted Runner 注意事項
本文は Public Repo なので直接 GitHub Hosted の macOS Runner を使用していますが、実際の業務では Repo は必ず Private です。GitHub Hosted Runner を直接使うと非常に高額になるため(約1ヶ月分で会社に Mac Mini を購入して使い放題の方が得)、各マシンの性能に応じて複数の Runner を同時に起動して並行処理を行っています。
詳細は前回の記事の「Self-hosted Runner の構築と切り替え」部分をご参照ください 。ローカルPCにXcodeと基本環境をインストール後、Runnerを登録・有効化し、Action Workflow YAMLの
runs-onを[self-hosted]に変更するだけです。
同じコンピュータ上で複数の Runner を使う問題は、上記の Actions でほとんど解決しています。例えば、すべての依存する共有ディレクトリをローカルディレクトリに変更することです。また、テストで直面する問題の一つに、シミュレーターの競合があります:「同じマシン上で2つの Runner が同時に2つのテストジョブを実行し、同じシミュレーターを指定すると、互いに干渉してテストが失敗する。」
解決方法も簡単で、各 Runner にそれぞれ1台のシミュレーターを設定することです。
複数のランナーで同じマシンのシミュレーター設定
同じコンピュータ上に 2つの Runner が並行してジョブを受け取る場合:
-
ZhgChgLideMacBook-Pro-Runner-A -
ZhgChgLideMacBook-Pro-Runner-B

XCode シミュレーターの設定では、2つのシミュレーターを追加する必要があります:


- モデル、iOS バージョンおよびテスト環境
CI-Testing.yml のテストステップを以下のように変更:
# FastlaneのUnitテストLaneを実行
- name: Run Tests
id: testing
# 作業ディレクトリを指定、以降のコマンドで特にcd ./Product/しなくてよい
working-directory: ./Product/
env:
# ...
# Repo -> Settings -> Actions secrets and variables -> variables
# シミュレーターのiOSバージョン
SIMULATOR_IOS_VERSION: ${{ vars.SIMULATOR_IOS_VERSION }}
# 現在のRunner名
RUNNER_NAME: ${{ runner.name }}
# ...
run: \\|
# ...
bundle exec fastlane ${TEST_LANE} device:"${RUNNER_NAME} (${SIMULATOR_IOS_VERSION})" \\| tee "$RUNNER_TEMP/testing_output.txt"
# ...
-
deviceを${RUNNER_NAME} (${SIMULATOR_IOS_VERSION})に変更 -
SIMULATOR_IOS_VERSIONは引き続き Repo variables の変数を参照します。
組み合わせの結果は(18.4の場合):
-
Runner:
ZhgChgLideMacBook-Pro-Runner-Aシミュレーター: ZhgChgLideMacBook-Pro-Runner-A(18.4) -
Runner:
ZhgChgLideMacBook-Pro-Runner-B
シミュレーター: ZhgChgLideMacBook-Pro-Runner-B(18.4)
このように、2つのランナーが同時にテストを実行すると、それぞれが別々のシミュレーターを起動して動作します。
完全なプロジェクトリポジトリ
SSH Agent の設定補足 — Fastlane Match またはプライベート CocoaPods リポジトリ用
Fastlane Match や Private CocoaPods Repo を使用する場合、別の Private Repo 内にあるため、現在の Repo/Action 環境では直接 git clone できません。ssh agent を設定して環境を整え、Action 実行時に操作権限を持つようにする必要があります。
Step 1. SSHキーの生成

ssh-keygen -t ed25519 -C "[email protected]"
Enter file in which to save the key (/Users/zhgchgli/.ssh/id_ed25519): /Users/zhgchgli/Downloads/zhgchgli
- ダウンロードパスに入力すると、内容をコピーしやすくなります
“/Users/zhgchgli/Downloads/zhgchgli”のパスフレーズを入力してください(パスフレーズなしの場合は空のまま):
-
空欄にしてください: CI/CDで使用するため、CLIの対話式入力でパスフレーズを入力できません。そのため空欄にしてください
-
生成完了(.pub/private_key)

Step 2. PrivateリポジトリにDeploy Keyを設定する

github-actions-ci-cd-demo-certificates リポジトリ
Settings → Security → Deploy keys → Add deploy key。
-
Title: キー名を入力してください
-
Key:
.pub キーの内容を貼り付け
完了。

Step 3. Action のリポジトリに SSH プライベートキーを Secrets として設定する

github-actions-ci-cd-demo リポジトリ
Settings → Secrets and variables → Actions → New repository secret。
-
Name: キー変数名
SSH_PRIVATE_KEYを入力してください -
Secret:
private_key の内容を貼り付け
完了。
Step 4. SSH Agent 設定完了ら、Git Clone Private Repo の権限を確認しましょう
Demo-Git-Clone-Private-Repo.yml :
name: Demo Git Clone Private Repo
on:
workflow_dispatch:
jobs:
clone-private-repo:
name: Git Clone Private Repo
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# 🔐 SSHエージェントを起動し、秘密鍵を追加する
- name: Setup SSH Agent
uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
# 🛡️ github.comをknown_hostsに追加し、ホスト認証エラーを防ぐ
- name: Add GitHub to known_hosts
run: \\|
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
# 📦 SSHを使ってプライベートリポジトリをクローンし、検証する
- name: Clone and Verify Private Repo
run: \\|
git clone [email protected]:ZhgChgLi/github-actions-ci-cd-demo-certificates.git ./fakeMatch/
if [ -d "./fakeMatch/.git" ]; then
echo "✅ ./fakeMatch/に正常にリポジトリをクローンしました"
cd ./fakeMatch
echo "📌 現在のコミット: $(git rev-parse --short HEAD)"
else
echo "❌ クローンに失敗しました。SSHエージェントの設定が正しくない可能性があります。"
exit 1
fi
上記の Action を使って設定が成功したかどうかを確認できます。

成功しました。以降、fastlane match や pod install のプライベートポッドも正しく実行できるはずです。
まとめ
この文章は GitHub Actions を使った完全な iOS CI/CD フローの開発を詳細に記録しています。次回はユーザー側(エンジニア/PM/デザイナー)の体験を最適化し、Slack 通知の充実と Google Apps Script Web App を使った GitHub Actions 連携による無料で使いやすいクロスチームパッケージプラットフォームツールの構築を行います。
シリーズ記事:
-
CI/CD 実践ガイド(1):CI/CD とは?CI/CD を使って安定かつ効率的な開発チームを作るには?ツールの選び方は?
-
CI/CD 実践ガイド(3):GitHub Actions を使ったアプリプロジェクトの CI と CD ワークフローの実装
-
CI/CD 実践ガイド(4):Google Apps Script Web App を使って GitHub Actions と連携し、無料で使いやすいパッケージングツールプラットフォームを構築する
🍺 Buy me a beer on PayPal
本シリーズの記事は多くの時間と労力をかけて執筆しました。内容があなたのお役に立ち、チームの作業効率や製品品質の向上に貢献できたなら、ぜひコーヒーをご馳走してください。ご支援ありがとうございます!

Post MediumからZMediumToMarkdownによって変換されました。



コメント