構築目標:検証可能な iOS ビルドパイプラインを通す
Runner をインストールする前に、「構築完了」の受け入れ基準を定義し、Runner 導入後に Archive が署名段階で止まる事態を防ぎます。本実録のゴールは:指定ブランチへの push → GitHub Actions が専用 M4 ノードで起動 → コード checkout → xcodebuild archive 実行 → .xcarchive 正常生成。TestFlight アップロードは本記事の範囲外ですが、Archive が安定すれば fastlane や altool は追加ステップに過ぎません。
テストリポジトリは中規模の SwiftUI アプリ:Swift ソース約60ファイル、CocoaPods でサードパーティ依存管理、Release 設定は Manual 署名。ベース環境は JexMac シンガポールノード上の Mac mini M4(16 GB ユニファイドメモリ、256 GB NVMe)、Xcode 16.2 は xcodes でインストール・固定。
対照群は同一リポジトリを runs-on: macos-14 ホスト Runner で実行した結果:UTC 13:00–18:00(アジア太平洋の午後)の job キュー待ち中央値11分、最長19分;macos-14 プリインストール Xcode バージョンがローカル開発機と不一致で Swift 6 構文互換問題が発生したことも。セルフホストの価値はここで明確——待ち時間を分単位から秒単位へ、Xcode バージョンをノード上で完全固定。
ホスト macOS Runner の3つの隠れコスト
多くのチームが最初に GitHub ホスト Runner を選ぶのは「運用ゼロ」だからですが、iOS シナリオでは隠れコストが請求額より厄介なことが多い。
第1類は時間コスト。GitHub の macOS プール容量は限られ、無料アカウントの月2000分は macOS で10倍ウェイト換算され、実質約200分。1日8回トリガー、1回6分ビルドのプロジェクトなら月約1440加重分——無料枠上限に近く、nightly ビルドを足すと超過しがち。
第2類は環境ドリフト。macos-latest タグは GitHub インフラ更新で基盤 Xcode が切り替わり、「昨日は緑、今日は赤」が何度も起きた。暫定 workaround は workflow に sudo xcode-select ステップを足すことだが、バージョン切替ごとに1〜2分追加され、ローカル開発機と完全一致も保証できない。
第3類はデバッグコスト。ホスト Runner は job 終了後に環境が破棄され、SSH で入って再現できない。キーチェーンダイアログ、プロビジョニングプロファイル期限切れなど「ログインして確認」が必要な障害は、ホスト環境では push を繰り返してログを試すしかない——User interaction is not allowed エラーで push を7回往復し、partition list 欠落にようやく辿り着いた。
本記事のコマンドと所要時間データはすべて JexMac 専有 Mac mini M4 物理ノードで実行。ノードはシンガポールデータセンター、テスト期間は2026年7月下旬。ハードウェア:Apple M4 · 10コア CPU · 16 GB ユニファイドメモリ · 1 Gbps 専有帯域。
ノード納品後:SSH 初回接続と環境ベースライン
JexMac コンソールは支払い確認後、通常1〜5分で SSH 認証情報を配信。パブリック IP 取得後、Ed25519 キーで接続しパスワードログインを無効化してから Runner インストールを開始することを推奨——Runner プロセスは現在の macOS ユーザー権限で動作するため、SSH セキュリティベースラインを先に固める。
-
01
SSH 公開鍵を登録
ssh-copy-id -i ~/.ssh/id_ed25519.pub jexmac@<ノードIP>パスワードレスログイン確認後、
/etc/ssh/sshd_configでPasswordAuthentication noを設定し、sshd を再起動。 -
02
Homebrew と xcodes をインストール
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"brew install xcodesorg/made/xcodesxcodes install 16.2 --experimental-fast-passで Xcode 16.2 をインストール・固定(バージョンはプロジェクト要件に合わせて調整)。 -
03
ビルドチェーンを検証
xcodebuild -versionはXcode 16.2と Build 番号を出力するはず。xcodebuild -showsdks | grep iphoneosで iOS SDK 利用可能を確認。 -
04
CocoaPods をインストール(プロジェクト使用時)
sudo gem install cocoapods -n /usr/local/binノード上で事前に
pod installを1回通し、Specs リポジトリキャッシュを準備。CI 初回ビルドが repo update で止まるのを防ぐ。
ベースライン設定完了後、ノード上に専用ディレクトリ(本記事では ~/ci-runner)を用意し Runner とビルド成果物を日常開発ファイルから分離。DerivedData サブディレクトリを予約し、後続 workflow で -derivedDataPath を明示指定、マルチプロジェクト並行時のパス競合を防止。
GitHub 側:Runner トークンとラベルルーティング設計
対象リポジトリ → Settings → Actions → Runners → New self-hosted runner、プラットフォームは macOS ARM64。ページにワンタイム登録トークン(有効1時間)とダウンロードリンクが生成される。
ラベル設計は workflow が正しいマシンにルーティングできるかを直接左右。当社の命名規則:
mac:すべての macOS セルフホストノード共通ラベルm4:Apple Silicon M4 チップ識別、旧 Intel ノードと区別xcode-16-2:Xcode マイナーバージョン固定、アップグレード時は workflow ロジックではなくラベルを変更sg:データセンター地域(シンガポール)、マルチリージョン展開時に近接ルーティング
登録コマンドの --labels パラメータで上記ラベルを一括設定。workflow では配列形式でマッチ:
jobs:
ios-archive:
runs-on: [self-hosted, mac, m4, xcode-16-2]
concurrency:
group: ios-build-${{ github.ref }}
cancel-in-progress: true
concurrency グループで同一ブランチの Archive 並行実行を防止——16 GB メモリの M4 ノードでは2プロジェクト同時 Clean Build も可能だが、DerivedData 競合で1回あたり所要時間が30%以上変動。複数プロダクトラインがあるチームはラベル分割(product-a、product-b など)と複数ノードを推奨、1台で無理に並行させない。
セルフホスト Runner は公開リポジトリで fork PR からトリガー可能で、悪意ある workflow が Mac 上で任意コードを実行する。必ずプライベートリポジトリまたは Organization レベルで有効化し、Runner プロセスは非管理者アカウントで実行。本番環境では GitHub Environment 保護ルールと組み合わせ、secrets を指定ブランチのみに制限することを推奨。
M4 ノードに Runner をインストールし launchd 常駐を設定
以下の手順は SSH セッションで実行。Runner バージョンは GitHub 登録ページ表示に従う(本例 v2.321.0)。
-
01
ダウンロードして解凍
mkdir -p ~/ci-runner/actions-runner && cd ~/ci-runner/actions-runnercurl -o actions-runner-osx-arm64-2.321.0.tar.gz -L \ https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gztar xzf ./actions-runner-osx-arm64-2.321.0.tar.gz -
02
対話式登録
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \ --token YOUR_ONE_TIME_TOKEN \ --name jexmac-m4-sg-01 \ --labels mac,m4,xcode-16-2,sg \ --unattended--unattendedで対話確認をスキップ、スクリプト化デプロイ向け。Work folder はデフォルト_workのままで可。 -
03
launchd サービスをインストール
./svc.sh install./svc.sh start検証:
./svc.sh statusはactive (running)を表示。GitHub リポジトリ Runners ページに緑の Online 状態が出れば登録成功。
launchd はシステム再起動後に Runner を自動起動するが、注意点あり:Runner はインストール時の macOS ユーザー権限で動作し、そのユーザーは一度ログイン済み(または自動ログイン有効)である必要があり、そうでなければ launchd が Keychain にアクセスできない場合がある。ノード上に専用アカウント ci-bot を作成し、SSH 初回ログインでキーチェーン初期化後に svc.sh をインストール、以降の再起動は人手不要。
Runner ログは ~/ci-runner/actions-runner/_diag/、各 job 終了後に Worker_*.log が生成。「job はノードに割当済みだがステップ出力なし」系の問題は、GitHub UI の Annotations より RunnerListener ログの方が情報が完全。
最小構成 Workflow:checkout から Archive まで
登録完了後、リポジトリに .github/workflows/ios-archive.yml を作成。以下は検証済み最小版、TestFlight アップロードなし、Archive 生成に特化:
name: iOS Archive
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
jobs:
archive:
runs-on: [self-hosted, mac, m4, xcode-16-2]
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: xcodes select 16.2
- name: Install pods
run: pod install --deployment
working-directory: ios
- name: Build archive
run: |
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-sdk iphoneos \
-configuration Release \
-archivePath ./build/MyApp.xcarchive \
-derivedDataPath ~/ci-runner/DerivedData \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution: Your Team (TEAMID)" \
PROVISIONING_PROFILE_SPECIFIER="MyApp AppStore"
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Upload xcarchive artifact
uses: actions/upload-artifact@v4
with:
name: MyApp-xcarchive
path: ./build/MyApp.xcarchive
retention-days: 7
意図的に残した設計選択:
pod install --deployment で Podfile.lock 固定、CI とローカル依存の不一致を防止。-derivedDataPath はノード上の固定ディレクトリを指定、複数ビルドでコンパイルキャッシュ再利用、増分ビルドは4分から約1分40秒へ短縮。upload-artifact で xcarchive を GitHub にアップロード、SSH 権限のない同僚でもダウンロード検証可能——artifact は7日保持、QA スポットチェックに十分。
ヘッドレス環境:キーチェーンと Distribution 証明書インポート
Archive ステップの成否は、CI 環境が GUI なしで Distribution 秘密鍵にアクセスできるかに依存。ホスト Runner はシステムキーチェーンを事前設定済みだが、セルフホストノードは自分で管理——多くのチームが「Runner は入ったが Archive で署名エラー」で止まる原因もここ。
推奨:各 job で一時キーチェーンを作成、p12 インポート後すぐ署名に使用、job 終了で自動破棄、秘密鍵のディスク長期常駐を回避。
# "Build archive" ステップの前に追加
- name: Import signing certificate
run: |
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
echo "${{ secrets.CERTIFICATE_P12_BASE64 }}" | base64 --decode > cert.p12
security import cert.p12 \
-k build.keychain \
-P "${{ secrets.P12_PASSWORD }}" \
-T /usr/bin/codesign \
-T /usr/bin/xcodebuild
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
rm -f cert.p12
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
GitHub Repository Secrets に3つの変数を事前設定:KEYCHAIN_PASSWORD(一時キーチェーンパスワード、ランダム文字列で可)、CERTIFICATE_P12_BASE64(Distribution 証明書の Base64 エンコード)、P12_PASSWORD(p12 エクスポート時に設定したパスワード)。
| エラーキーワード | よくある根本原因 | 対処方法 |
|---|---|---|
User interaction is not allowed |
秘密鍵 partition list 未設定、codesign が UI を表示しようとする | set-key-partition-list コマンドを再実行(上記スクリプト参照) |
errSecItemNotFound |
証明書が誤ったキーチェーンにインポート、または default-keychain 未切替 | import 前に security default-keychain -s build.keychain が実行されていることを確認 |
| could not find signing certificate | CODE_SIGN_IDENTITY 文字列とキーチェーン Identity が完全一致しない |
security find-identity -v -p codesigning build.keychain を実行し完全名称をコピー |
| Provisioning profile doesn't match | プロビジョニングプロファイル期限切れ、または Bundle ID / Capability 不一致 | Apple Developer ポータルで再生成、ダウンロード後 secrets または match で同期 |
インポート成功後、job ログで1コマンド迅速検証可能:
security find-identity -v -p codesigning build.keychain | grep Distribution
1 valid identities found を確認してから Archive ステップへ進めば、「署名失敗だがどこで止まったか不明」の調査時間を大幅削減。
トラブルシューティング実録:追加 push が1回必要だった3つの問題
上記手順どおりでも初回構築で以下が起きうる——すべて実際に踏んだ落とし穴、ログ特徴と解決策付き。
ラベル不一致:job が永遠にキュー待ち
現象:GitHub Actions UI で job 状態 Queued、Runners ページのノードは Online。原因は workflow の runs-on ラベルと登録時が完全一致しない——例:workflow が xcode-16.2(ドット)、登録が xcode-16-2(ハイフン)。GitHub ラベルマッチは完全一致、1文字違いでルーティングされない。
解決:リポジトリ Runners ページでノード名をクリック、実際のラベル一覧を確認し YAML にコピペ、手入力しない。
launchd 再起動後 Runner Offline
現象:ノード reboot または JexMac 側メンテ再起動後、Runner が Offline、手動 SSH で ./svc.sh start が必要。原因は launchd plist の UserName と SSH ログインユーザー不一致、またはそのユーザーが初回グラフィカル/session ログイン未完了。
解決:./svc.sh install と ./svc.sh start を同一ユーザーで実行することを確認;再起動後 ./svc.sh status を確認。失敗時は /Library/Logs/GitHubActionsRunner/ の stderr ログを確認。
DerivedData 権限競合
現象:2回目ビルドで Unable to write to DerivedData または一部 .o ファイル permission denied。原因は1回目 job が root または別ユーザーで DerivedData ディレクトリを生成、後続 job に書込権限なし。
解決:DerivedData パスを統一し workflow 冒頭にクリーンアップ:rm -rf ~/ci-runner/DerivedData && mkdir -p ~/ci-runner/DerivedData。または各 Archive 前に該当プロジェクト hash サブディレクトリのみ削除、再利用可能なコンパイルキャッシュは保持。
常設 Mac がない場合:ビルドリズムに合わせて専用ノードをレンタル
ここまで来れば気づく:GitHub Actions セルフホスト Runner の技術ハードルは高くない、本当のボトルネックは24時間オンラインで Xcode バージョン制御可能な macOS 物理マシンがあるか。ローカル MacBook は CI ホスト向きでない——8 GB メモリ機種は Archive 時にファン全開で頻繁 swap;会社調達 Mac mini は固定資産承認、データセンター托管、証明書ローテ時の現場メンテが必要。
当社のやり方:イテレーション冲刺期(例:リリース前2週間)に JexMac 専有 Mac mini M4 ノードを Runner ホストとしてレンタル、リリース安定後は週単位更新または解放。標準構成 16 GB ユニファイドメモリ、256 GB NVMe、1 Gbps 専有帯域、日額 $21.5〜、支払い後1〜5分で SSH / VNC 接続、契約縛りなし。5拠点——シンガポール、日本(東京)、韓国(ソウル)、中国香港、米国東部——チーム所在地に応じて選択、git fetch と CocoaPods Specs 同期レイテンシ低減。
GitHub ホスト macOS Runner との比較:ピーク時キュー10〜20分 vs セルフホスト25秒以内開始;macOS 分は10倍ウェイト課金 vs 固定日額でコスト予測可能。自社購入ハードウェアとの比較:upfront 投資ゼロ、バージョンアップ時はラベル変更または Xcode 再インストール、調達サイクル待ち不要。
同一ノードをリモート開発デスクトップ兼用——ブラウザ VNC で UI デバッグ、Instruments でパフォーマンス計測、CI アイドル時間を無駄にしない。2〜3人小チームでは「1台 M4 物理マシン = Runner + リモート Mac 開発環境」が、ホスト Runner 分購入とローカルメモリ増設を分けるより経済的なことが多い。
Runner を M4 ノードに接続
記事内の全コマンドは JexMac 専有 Mac mini M4 物理ノードで検証済み。開通 → SSH 接続 → 手順どおり Runner 登録、最短1時間で Archive 通過。日額レンタル、冲刺終了後解放可、年間契約縛りなし。