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

運用フロー:

  1. GitHub Actions で Pull Request トリガー、手動トリガー、またはスケジュールトリガー

  2. 対応する Workflow のジョブ/ステップを実行する

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

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

  5. 実行結果を取得する

  6. 後続の Workflow ジョブ/ステップの処理結果

  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 は古い 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

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

help: ## ヘルプ表示
	@echo "Usage:"
	@echo "  make setup     # 環境セットアップ"
	@echo "  make lint      # コードの静的解析"
	@echo "  make test      # テスト実行"
	@echo "  make build     # アプリのビルド"
	@echo "  make deploy    # デプロイ実行"

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

  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 — プルリクエストで単体テストを実行する

  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 フィールド
    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回トリガーする必要があります。そうするとここに表示されます。

  • Block force pushes: 強制プッシュをブロックする

保存し、Enforcement status が Active であることを確認すると、ルールが有効になります。

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

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

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

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

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

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

Demo PR

Demo PR

これで 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 ファイルをマージする必要があります。

タスクの完了を待つ:

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 → 新しい Secret を追加:FIREBASE_CLI_TOKEN に Firebase CLI トークンを貼り付けます。

このトークン = あなたのログインID です。大切に保管してください。アカウントを退職する際には必ず更新が必要です。

技術的詳細 — 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 を使用しています)

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

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

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台のシミュレーターを設定するだけです。

複数の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 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 リポジトリ

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 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

コーヒーをおごる

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

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

ZhgChgLi

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

コメント