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

Photo by Robs
はじめに
前回の「CI/CD 実践ガイド(二):GitHub Actions と Self-hosted Runner の使用と構築大全」では、GitHub Actions の基本知識と運用フロー、そして自分のマシンを Runner として使う方法について紹介し、3つの簡単な自動化 Actions を実装しました。本稿では、現実的に GitHub Actions を使って App(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 で Pull Request トリガー、手動トリガー、またはスケジュールトリガー
-
対応する Workflow のジョブ/ステップを実行する
-
ステップに対応する Fastlane(iOS)または(Android Gradle)スクリプトを実行する
-
Fastlane が実行する xcodebuild (iOS) コマンド
-
実行結果を取得する
-
後続の Workflow ジョブ/ステップの処理結果
-
完了
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 は古い iOS プロジェクトで依然としてよく使われています。ここでは簡単に Snapkit をデモとして追加します。
XCodeGen
多人開発での .xcodeproj / .xcworkspace の変更によるコンフリクトを避けるため、Project.yaml で XCode プロジェクトの内容を統一して定義し、ローカルで自分自身で Project ファイルを生成する(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: すべてのタイプのテストを実行する(Snapshot+Unit)
-
run_unit_tests: 単体テストのみ実行(Unit)
-
beta: Firebase App Distributionへのビルドとデプロイ
Fastlane — Match
Demoプロジェクトの制限により、ここではMatchを使ってチームの開発・配布証明書を管理していませんが、チームのすべての開発・配布証明書を管理するためにMatchを使うことを推奨します。管理や一括更新が容易になります。
Matchを使えば、プロジェクトのセットアップ時に
match allのようなコマンドで、開発に必要な証明書を一括インストールできます。
- Fastlane Match は別のプライベートリポジトリで証明書と鍵を管理するため、GitHub Actions では SSH Agent を設定して別のプライベートリポジトリをクローンできるようにする必要があります。
(詳細は記事末の補足を参照してください)
[2026/02 Update] 参考資料 — 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
help: ## ヘルプ表示
@echo "Usage:"
@echo " make setup # 環境セットアップ"
@echo " make lint # コードの静的解析"
@echo " make test # テスト実行"
@echo " make build # アプリのビルド"
@echo " make deploy # デプロイ実行"
Makefile を使った統一プロジェクトセットアップ手順:
-
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 — プルリクエストで単体テストを実行する
-
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 フィールド
inputs:
# 実行する Test Fastlane Lane
TEST_LANE:
description: 'Test Lane'
default: 'run_unit_tests'
type: choice
options:
- run_unit_tests
- run_all_tests
# 他の Workflow から呼び出されるトリガー
# Nightly Build で使用
workflow_call:
# フォーム Inputs フィールド
inputs:
# 実行する Test Fastlane Lane
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
# Runner ラベル - GitHub Hosted Runner 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 \\|\\| '' }}
# ========== 環境セットアップステップ ==========
# プロジェクトで指定された 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 の場合は 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日間ヒットしなければ自動削除、単一キャッシュ最大10GB、アクション成功時のみキャッシュ
# Public Repo: 無料無制限
# Private Repo: 5GB から
# Self-hosted は独自に shell script でキャッシュ & リストア戦略を作るか他ツール利用可能
# 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)
# デフォルトはプロジェクトの ./Product/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
# ...
# その他 setup コマンド
make setup
# Makefile にまとめた install コマンドを実行、具体的には:
# mint run yonaskolb/XcodeGen --quiet
# bundle exec pod install
# ...
# その他 install コマンド
make install
# Fastlane Unit テスト Lane を実行
- name: Run Tests
id: testing
# 作業ディレクトリ指定、以降のコマンドで cd ./Product/ を省略可能
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_x.x.x バージョンを指定
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 に書き込む
# 後で内容を解析し、ビルド失敗かテスト失敗かを判断して PR にコメントを投稿
set +e
# EXIT_CODE は実行結果の終了コードを格納
# 0 = OK
# 1 = exit
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]}(bundle exec 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 Parser Actions をそのまま利用:https://github.com/mikepenz/action-junit-report
uses: mikepenz/action-junit-report@v5
# if:
# 前ステップ(Testing) 成功 または
# 前ステップ(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 2台が同時に 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 にマージしたいすべてのブランチがこのルールの対象になります。
-
バイパスリスト:特定のユーザーやチームを指定して、この制限を免除できます
-
ブランチルール:

-
Restrict deletions: ブランチの削除を禁止する
-
Require a pull request before mergin: マージは必ずプルリクエスト経由で行う
Required approvals: 承認が必要な人数を制限する -
Require status checks to pass: どのチェックがパスしないとマージできないかを制限する
- Add checks をクリックして
Testingを入力し、GitHub Actions のマークが付いたものを選択します。
ここで小さな問題があります。もし Suggestions にTestingが見つからない場合は、一度 Actions に戻って(PRを作成して試すなど)成功を1回トリガーする必要があります。そうするとここに表示されます。
- Add checks をクリックして

- Block force pushes: 強制プッシュをブロックする
保存し、Enforcement status が Active であることを確認すると、ルールが有効になります。
すべて設定が完了したら、PRを開いてテストしてみましょう:

- Checks に CI-Testing(必須)が表示され、マージがブロックされ、「少なくとも X 件の承認レビューが書き込み権限を持つレビュアーから必要です」と表示されている場合、設定は成功しています。
もしプロジェクトのビルドに失敗した場合 (Build Failed) はコメントします:

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

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

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

これで PR をマージできます。
- Pushで新しいコミットがあると、自動的にChecksテストが再実行されます。
完全なコード: 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 → ワークフローを自分で設定する。
# ワークフロー(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 を使用してジョブを実行
# 注意:このプロジェクトはパブリックリポジトリなので無制限に無料で使用可能
# 注意:このプロジェクトはパブリックリポジトリなので無制限に無料で使用可能
# 注意:このプロジェクトはパブリックリポジトリなので無制限に無料で使用可能
# プライベートリポジトリの場合は従量課金で、macOSマシンは最も高価(10倍)、10回実行で2,000分の無料上限に達する可能性あり
# self-hostedランナーの利用を推奨
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 は別のプライベートリポジトリで証明書を管理するが、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に保存
# GitHub Actionsステップ内で動的に読み込み、一時ファイルに書き込み、正しい場所に移動してシステムに認識させる
# 詳細は記事参照
#
- 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 Key
# ビルド環境ではほぼ必須のApp Store Connect API Fastlane JSON Key (.json)
# フォーマット: .json 内容フォーマット:https://docs.fastlane.tools/app-store-connect-api/
# App Store Connect APIの.p8キーを含む
# 後続でFastlaneに渡し、TestflightやApp Store APIのアップロードに使用
#
# GitHub Actions Secret はファイル保存不可のため、すべてBase64エンコードされた文字列としてSecretに保存
# ステップ内で動的に読み込み、一時ファイルに書き込み他のステップで参照
# 詳細は記事参照
- 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からAPIキーをインポート
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権限があることを確認
# 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
# ランナーの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: \\|
# # 各ランナーごとに独自の.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、アクション成功時のみキャッシュ
# パブリックリポジトリ:無料無制限
# プライベートリポジトリ: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)
# デフォルトはプロジェクト内の ./Product/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
# ...
# その他setupコマンド
make setup
# Makefileでラップされたinstallコマンドを実行。内容はおおよそ:
# mint run yonaskolb/XcodeGen --quiet
# bundle exec pod install
# ...
# その他installコマンド
make install
- name: Deploy Beta
id: deploy
# 作業ディレクトリ指定。以降のコマンドでcd ./Product/不要
working-directory: ./Product/
env:
# ビルド入力パラメータ
VERSION_NUMBER: ${{ inputs.VERSION_NUMBER \\|\\| '' }}
BUILD_NUMBER: ${{ inputs.BUILD_NUMBER \\|\\| '' }}
RELEASE_NOTE: ${{ inputs.RELEASE_NOTE \\|\\| '' }}
AUTHOR: ${{ github.actor }}
# リポジトリ -> 設定 -> 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_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
# 「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
- リポジトリの 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 → 新しい Secret を追加:FIREBASE_CLI_TOKEN に Firebase CLI トークンを貼り付けます。

このトークン = あなたのログインID です。大切に保管してください。アカウントを退職する際には必ず更新が必要です。
技術的詳細 — 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 を使用しています)


現在二つのファイル: 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 → 新しい Secret を追加: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
「app_store_connect_api.json」をBase64エンコードしてクリップボードにコピーするコマンド
文字列の内容を 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など)にそれぞれデプロイする必要があります。そのため、ビルドは1つのAction、デプロイは別のActionに分けるのが望ましいです。そうしないとビルドが2回実行されてしまいますし、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:
# 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-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
キャッシュと同様に、現在 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:
# Job 作業項目
# Job は並行実行される
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 or master...etc
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:
# Job はデフォルトで並行実行、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 or master...etc
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 Deploy 失敗\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 Deploy 成功\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台のシミュレーターを設定するだけです。
複数のRunnerを同じマシンで使う際のシミュレーター設定
同じコンピュータ上に 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は引き続きリポジトリの変数を参照します。
組み合わせ結果は(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]"
SSH キーを生成するコマンド
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 Repo に 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 }}
# 🛡️ host verificationエラーを防ぐために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 を活用した無料で使いやすいチーム横断のパッケージングプラットフォームツールの構築について解説します。
シリーズ記事:
🍺 Buy me a beer on PayPal
本シリーズの記事は多くの時間と労力をかけて執筆しています。内容があなたやチームの作業効率や製品品質向上に役立つ場合は、ぜひコーヒーをご馳走してください。ご支援ありがとうございます!

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



コメント