ZhgChg.Li

GitHub Actions|iOS App CI/CD 自動化構築でビルドからデプロイまで最速実現

iOS開発者向けにGitHub Actionsでの自動ビルド、テスト、デプロイ手順を具体的に解説。手作業の負担を削減し、品質向上とリリース速度アップを実現します。

GitHub Actions|iOS App CI/CD 自動化構築でビルドからデプロイまで最速実現
本記事は AI による翻訳です。お気づきの点があればお知らせください。

CI/CD 実践ガイド(三):GitHub Actions を使った iOS アプリの CI と CD ワークフロー実装

iOSアプリの自動ビルド、テスト、デプロイを実現するGitHub Actionsの完全実装手順

Photo by Robs

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 を使ったチーム横断のパッケージングプラットフォーム構築について紹介します。

動作フロー:

  1. GitHub Actions プルリクエストトリガーまたはフォームトリガーまたは定期トリガー

  2. 対応するワークフロージョブ/ステップを実行する

  3. ステップに対応する Fastlane(iOS)または(Android Gradle)スクリプトを実行する

  4. Fastlane が実行する xcodebuild(iOS)コマンド

  5. 実行結果の取得

  6. 後続のワークフロージョブ/ステップの処理結果

  7. 完了

GitHub Actions 成果図

まずは最終成果をお見せして、皆さんに実装のモチベーションを!

CI Testing

CI Testing

CI Nightly Build, CD Deploy

CI Nightly Build, CD Deploy

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 の部分のみを議論します。

以下のツールは過去の経験に基づいていますが、新しいプロジェクトでは最新の misetuist の使用を検討してください。

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つ、cocoapodsfastlane です。

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

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

make help コマンドは、Makefile に定義された利用可能なターゲットの一覧と説明を表示します。
これは、プロジェクトで設定されているビルドやタスクの概要を知るのに便利です。

一般的な使い方は以下の通りです:

make help

このコマンドを実行すると、Makefile 内の help ターゲットに記述された説明が表示され、どのターゲットが利用可能かを確認できます。

Makefine を使った統一プロジェクトセットアップ手順:

  1. git clone repo

  2. cd ./repo

  3. make setup
    必要なツールの依存関係をインストールする(brewmintbundle、xcodegen、swiftformat、…)

  4. make install
    プロジェクトを生成する(pod install と xcodegen を実行)

  5. 完了

  6. プロジェクトを開いて実行する

CI/CDでも新人のオンボーディングでも、上記の手順に従ってプロジェクトを構築します。

本記事の GitHub Actions CI/CD 事例

本記事では、3つのGitHub Actions CI/CDワークフロー構築事例を紹介します。皆さんもこれらの手順を参考にして、自分のチームに合ったCI/CDを構築できます。

  1. CI — Pull Requestで単体テストを実行

  2. CD — Firebase App Distributionへのパッケージングとデプロイ

  3. 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を作成して試す)を成功させる必要があります。そうするとここに表示されます。

  • Block force pushes: フォースプッシュをブロックする

保存し、Enforcement status が Active になったら、ルールが有効になります。

すべて設定したら、PRを開いてテストしてみましょう:

  • Checks に CI-Testing ( Required ) が表示され、Merging is blocked、書き込み権限を持つレビュアーによる最低 X 件の承認レビューが必要と表示されていれば、設定は成功しています。

もしプロジェクトのビルドが失敗した場合(Build Failed)はコメントします:

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

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

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

Demo PR

Demo PR

これで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 ファイルをマージしてください。

タスクの完了を待つ:

Demo

Demo

パッケージ+デプロイ成功 ✅

完全なコード: 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

cicd.mobileprovision

development.cer

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 ファイルをリポジトリのメインブランチにプッシュし、手動でテストとビルドをトリガーして結果を確認します:

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

Demo

Demo

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

私たちは直接スマホに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 Repo

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 Repo

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 matchpod install のプライベートポッドも正しく実行できるはずです。

まとめ

この文章は GitHub Actions を使った完全な iOS CI/CD フローの開発を詳細に記録しています。次回はユーザー側(エンジニア/PM/デザイナー)の体験を最適化し、Slack 通知の充実と Google Apps Script Web App を使った GitHub Actions 連携による無料で使いやすいクロスチームパッケージプラットフォームツールの構築を行います。

シリーズ記事:

🍺 Buy me a beer on PayPal

本シリーズの記事は多くの時間と労力をかけて執筆しました。内容があなたのお役に立ち、チームの作業効率や製品品質の向上に貢献できたなら、ぜひコーヒーをご馳走してください。ご支援ありがとうございます!

Buy me a coffee

🍺 Buy me a beer on PayPal

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

GitHub で編集
この記事を改善
本記事は Medium で初公開
オリジナルを読む
この記事をシェア
リンクをコピー · SNS でシェア
ZhgChgLi
著者

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

コメント