この記事は「外出先のスマホからでも自宅 Mac の Xcode を叩いて iOS アプリを TestFlight に上げたい」エンジニア向け。スマホ → 母艦ノートPC(WSL2)→ 別マシンの Mac に多段 ssh で繋ぎ、archive → IPA → TestFlight アップロードまで 1 コマンドで完結させる手順。codesign が SSH で詰まる理由と回避策(専用 build keychain)、配信スクリプト 3 本の中身も全部書く。
この記事でできること
- 外出先のスマホから(または WSL2 のターミナルから)SSH 1 行で TestFlight に新ビルドが上がる
- Mac は Xcode と
xcodebuildの実行環境としてだけ使い、普段は触らない - App Store Connect API Key(
.p8)認証で 2 段階認証ダイアログを出さない - VPS や GitHub Actions を使わずに自宅 LAN 内で完結(Mac mini や旧 MacBook の余生活用にも向く)
4つのアプローチ比較
| 構成 | SSH + 自前スクリプト (本記事) |
Mac で Xcode 操作 | GitHub Actions (macos) | fastlane 主導 |
|---|---|---|---|---|
| 月額コスト | 0 円(自宅 Mac の電気代のみ) | 0 円 | macOS runner は分単価高め | 0 円(自宅 Mac) |
| エディタ | WSL2 の好みのもの(VS Code / vim 等) | Xcode | 任意 | 任意 |
| 配信 1 回の所要時間 | 3〜6 分(archive + IPA + altool) | 5〜10 分(GUI 経由) | push 後 10〜15 分(CI 起動含む) | 3〜6 分 |
| 秘匿情報の置き場 | Mac ローカル(~/.appstoreconnect) |
Mac ローカル | GitHub Secrets | Mac ローカル / Match |
構成図
エディタ + git push + ssh の起点
▼ ssh + bash scripts/upload.sh
build.keychain-db / xcodebuild / altool
使ったもの
- WSL2 マシン: Ubuntu 上の bash と Git、ssh クライアント
- Mac: Xcode 26.x がインストール済みのもの。Mac mini / 旧 MacBook で十分。普段は SSH 接続のみで GUI を触らない
- Apple Developer Program: 年額 99 USD。Apple Distribution 証明書と TestFlight 配信用の Provisioning Profile を発行しておく
- App Store Connect の API Key (
.p8): App Store Connect → ユーザーとアクセス → 統合から発行。App Manager 以上の権限
なぜ setup_build_keychain.sh が必要か
普通に Mac の Terminal で xcodebuild archive すれば通るのに、SSH 経由だと codesign が「キーチェーンを開けません」で落ちるのが本記事のスタートライン。
原因: login keychain は SSH ログインで unlock しても codesign から触れない。GUI ログインセッションに紐づいた security agent が居ないため、ACL 上の「アプリ確認なしに使ってよい」リストが働かず、対話プロンプトを出そうとして失敗する。
解決策は 専用の build keychain を作って Apple Distribution 証明書を複製、ACL を緩めて codesign から GUI 確認なしに使える状態にする。これを scripts/setup_build_keychain.sh として 1 度だけ Mac の Terminal(GUI セッション)で実行する。
以下のコード中の placeholder は自分の環境の値に置き換えてください({kcpw} は build keychain をローカルでアンロックするための任意文字列で、外部秘匿情報ではありません)。
{user} 自分のユーザ名(Mac の login user)
{mac_addr} Mac の IP/ホスト名(例: 192.168.x.y)
{app_name} アプリ名(Xcode scheme / archive 名と一致)
{bundle_id} Bundle ID(例: com.example.MyApp)
{team_id} Apple Developer Team ID(10 文字)
{kcpw} build keychain ローカル鍵(任意の文字列)
{provisioning_profile_name} Provisioning Profile 名
#!/bin/bash
set -euo pipefail
KCNAME="build.keychain"
KCPATH="$HOME/Library/Keychains/${KCNAME}-db"
KCPW="{kcpw}" # build keychain アンロック用(ローカル container 鍵。任意の文字列)
EXPORTPW="$(uuidgen)" # .p12 一時暗号用(本スクリプト中だけ使用)
if [[ -f "$KCPATH" ]]; then
echo "build keychain は既に存在: $KCPATH"; exit 1
fi
# 1. build keychain 作成
security create-keychain -p "$KCPW" "$KCNAME"
security set-keychain-settings -lut 21600 "$KCNAME"
security unlock-keychain -p "$KCPW" "$KCNAME"
# 2. login keychain から identity を export(GUI で「常に許可」を選ぶ)
TMPP12="$(mktemp -t build-cert).p12"
trap 'rm -f "$TMPP12"' EXIT
security export -k "$HOME/Library/Keychains/login.keychain-db" \
-t identities -f pkcs12 -P "$EXPORTPW" -o "$TMPP12"
# 3. build keychain へ import + codesign / productbuild が触れるよう -T 指定
security import "$TMPP12" -k "$KCNAME" -P "$EXPORTPW" \
-T /usr/bin/codesign -T /usr/bin/security \
-T /usr/bin/productbuild -T /usr/bin/productsign
# 4. ACL で GUI プロンプトを抑制
security set-key-partition-list \
-S 'apple-tool:,apple:,codesign:' -s -k "$KCPW" "$KCNAME"
# 5. search list の先頭に追加
ORIGINAL_LIST="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g' | tr '\n' ' ')"
security list-keychains -d user -s "$KCNAME" $ORIGINAL_LIST
security find-identity -v -p codesigning "$KCNAME"
ポイントは 4 番目の set-key-partition-list。ここで apple-tool: / apple: / codesign: をアクセス許可リストに入れて、codesign 系ツールが GUI 確認なしに鍵を使える状態にする。これが SSH 経由でも証明書アクセスできる正体。
archive.sh — Release archive を非対話で生成
2 本目のスクリプト。SSH 経由で xcodebuild archive を叩く。実行前に build keychain を unlock して、6 時間自動ロックを設定する。
#!/bin/bash
set -euo pipefail
APP_NAME="{app_name}" # ← 自分のアプリ名に置き換え
KCPW="{kcpw}" # ← setup_build_keychain.sh で使ったのと同じローカル鍵
ARCHIVE_PATH="${ARCHIVE_PATH:-/tmp/${APP_NAME}.xcarchive}"
BUILD_KCPW="${BUILD_KCPW:-${KCPW}}"
WORKSPACE="${WORKSPACE:-$(pwd)/${APP_NAME}.xcworkspace}"
if [[ ! -f "$HOME/Library/Keychains/build.keychain-db" ]]; then
echo "✗ build.keychain-db が無い。setup_build_keychain.sh を先に。" >&2
exit 1
fi
# build keychain unlock(6h 自動ロック)
security unlock-keychain -p "$BUILD_KCPW" build.keychain
security set-keychain-settings -lut 21600 build.keychain
# archive
rm -rf "$ARCHIVE_PATH"
LOG=/tmp/archive.log
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$APP_NAME" \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE_PATH" \
archive \
> "$LOG" 2>&1 || true
if grep -q "ARCHIVE SUCCEEDED" "$LOG"; then
echo "✓ ARCHIVE SUCCEEDED -> $ARCHIVE_PATH"
else
echo "✗ ARCHIVE FAILED (log: $LOG)"
grep -B1 -A4 -E 'error:|errSec|FAILED' "$LOG" | tail -30
exit 1
fi
ログを /tmp/archive.log に逃がして、成否を ARCHIVE SUCCEEDED 文字列で判定する形。xcodebuild の標準出力は冗長なので SSH 越しに見るのは消耗する。
ExportOptions.plist — Manual signing を明示
archive から IPA をエクスポートするときの設定ファイル。Auto signing で何度かハマったので、Manual signing で profile 名と Team ID を明示する形に固定した。git にコミットしても秘匿情報は含まれない。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>teamID</key>
<string>{team_id}</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>{bundle_id}</key>
<string>{provisioning_profile_name}</string>
</dict>
<key>uploadSymbols</key><true/>
<key>compileBitcode</key><false/>
<key>stripSwiftSymbols</key><true/>
</dict>
</plist>
Bundle ID とプロファイル名は自分の値に置き換える。プロファイル名は Apple Developer ポータルで作成したもの。
upload.sh — IPA エクスポート + altool アップロード
3 本目。archive を再利用して IPA を吐き、xcrun altool で TestFlight に投げる。altool は App Store Connect API Key 認証だと 2 段階認証ダイアログが出ない。
#!/bin/bash
set -euo pipefail
APP_NAME="{app_name}" # ← 自分のアプリ名に置き換え
KCPW="{kcpw}" # ← setup_build_keychain.sh で使ったのと同じローカル鍵
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ARCHIVE_PATH="${ARCHIVE_PATH:-/tmp/${APP_NAME}.xcarchive}"
EXPORT_DIR="${EXPORT_DIR:-/tmp/${APP_NAME}-export}"
EXPORT_OPTIONS="${EXPORT_OPTIONS:-$SCRIPT_DIR/ExportOptions.plist}"
ALTOOL_ENV="${ALTOOL_ENV:-$PROJECT_ROOT/Config/altool.env}"
BUILD_KCPW="${BUILD_KCPW:-${KCPW}}"
# Config/altool.env から ASC_KEY_ID / ASC_ISSUER_ID をロード
source "$ALTOOL_ENV"
# archive が無ければ自動で archive.sh を呼ぶ(再利用が効く)
if [[ ! -d "$ARCHIVE_PATH" ]]; then
bash "$SCRIPT_DIR/archive.sh"
fi
# IPA 出力(exportArchive も再署名するので keychain unlock)
security unlock-keychain -p "$BUILD_KCPW" build.keychain
rm -rf "$EXPORT_DIR"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$EXPORT_OPTIONS" \
> /tmp/export.log 2>&1 || true
IPA_PATH="$(find "$EXPORT_DIR" -maxdepth 1 -name '*.ipa' | head -n1)"
[[ -f "$IPA_PATH" ]] || { echo "✗ IPA 生成失敗"; exit 1; }
# TestFlight アップロード
xcrun altool --upload-app -f "$IPA_PATH" -t ios \
--apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
Config/altool.env は .gitignore 済みで、中身は 2 行だけ。
ASC_KEY_ID=ABC123XYZ4
ASC_ISSUER_ID=12345678-90ab-cdef-1234-567890abcdef
.p8 ファイル本体は ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 に置く(altool がこのパスを自動で見に行く)。
WSL2 から 1 行で TestFlight に上げる
ここまで揃えると、WSL2 側の運用は 1 行で済む。
# CFBundleVersion を bump して push したあとに
ssh {user}@{mac_addr} 'cd ~/path/to/{app_name} && bash scripts/upload.sh'
3〜6 分後に TestFlight に新ビルドが現れる。App Store Connect の Processing が完了したらすぐ自分の iPhone で Install 可能。
詰まりポイント
codesignが GUI プロンプトを出して固まる: build keychain のset-key-partition-listを忘れている。apple-tool:,apple:,codesign:を必ず指定- Auto signing が別チームの Development cert にフォールバック: Release は Manual signing 固定 + Team ID 明示が安全。
ExportOptions.plistのsigningStyle = manual - build keychain が 6 時間で自動ロックされて翌日のビルドが落ちる:
archive.sh/upload.shの冒頭で毎回security unlock-keychainを呼ぶ。一度のset-keychain-settings -lut 21600だけだと足りない - SPM の dynamic framework が Embed されない: RealmSwift のような source-distributed dynamic framework は
project.pbxprojの Embed Frameworks に手動登録が必要。Firebase / GoogleMobileAds の XCFramework binary target は auto-embed されるので忘れがち - altool の 2 段階認証ダイアログ: パスワード認証だと 2FA でブロックされる。必ず App Store Connect API Key(
.p8)認証を使う - Mac の sshd が起動していない: システム設定 → 一般 → 共有 → リモートログイン を有効化
よくある質問
Q. Mac は普段スリープでも大丈夫?
A. 配信するときだけ叩くなら Wake-on-LAN + システム設定の「ネットワークアクセスによりスリープ解除」を ON にすれば SSH パケットで起きる。常時稼働させるなら蓋を閉じても寝ない設定(caffeinate + 電源接続中はスリープしない)にしておく。
Q. Apple Distribution の証明書はどう発行する?
A. Mac の Xcode で Settings → Accounts → Apple Developer の Manage Certificates から「Apple Distribution」を発行 → login keychain に登録される。setup_build_keychain.sh を走らせるとそれが build keychain に複製される。
Q. Provisioning Profile を更新したらどうする?
A. Apple Developer ポータルでダウンロード → Mac で開く → Xcode の Signing & Capabilities で読み込まれているか確認。ExportOptions.plist の profile 名は変わらないので、スクリプト側の修正は不要。
Q. Mac mini を専用 archive サーバにしたい
A. M2 / M4 の Mac mini なら Xcode 26 + SPM の archive で 3〜5 分。常時電源 ON でも消費電力 10W 前後。Mac mini + LINE Notify でビルド完了通知を飛ばせば、外出中でも完走を把握できる。
Q. fastlane を使ったほうが早い?
A. fastlane は match による証明書同期や lane の構造化が便利。本記事のスクリプトは 「Mac 1 台で完結する個人配信」に絞っているので fastlane の機能のごく一部だけを薄く再実装した形。チーム開発になったら fastlane に乗り換える価値はある。
※本記事の手順は執筆時点(2026 年 5 月)の Xcode 26 / macOS 15 系で動作確認しています。Apple Distribution 証明書の発行手続きや App Store Connect の UI は変わる場合があります。動かない場合は コメント欄でお知らせください。
まとめ
Xcode を WSL2 から完全リモートで動かすのは build keychain と App Store Connect API Keyの 2 点を押さえれば素直に組める。シェルスクリプト 3 本(setup_build_keychain.sh / archive.sh / upload.sh)と ExportOptions.plist 1 枚で完結する。Mac は Xcode 専用の archive マシンとして奥に置いて、普段の開発は Linux の好みの環境で行える。
この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。
関連記事
- tmux + Tailscale + Termius でスマホから Claude Code を動かす最短手順【母艦ハブ型】(スマホから母艦への ssh 経路はこちら)
- ブログ用 AI 画像をローカル GPU で生成する【SDXL + IP-Adapter + img2img】
参考
- Apple — Distributing your app for beta testing and releases
- App Store Connect API Key の発行手順
- TN2415: Xcode Help — exportArchive と ExportOptions.plist
- man security (1) — keychain 操作の正本
※この記事は Claude Code を使った自動更新を試しています。
0 件のコメント:
コメントを投稿