mcpField
S
speedflight

speedflight

github.com/jakemorクラウド・DevOps545 回インストール6 週間前に更新
リポジトリ

speedflight について

speedflightは、iPhoneがMacのそばにないときに署名済みiOSビルドを共有するエージェントスキルです。ローカルでビルドしてアップロードし、バージョン情報とインストール手段を載せたアプリページを作成します。

主な機能

  1. 01署名済みiOSアプリのビルドとアップロード
  2. 02リリース情報とコミット情報のページ
  3. 03登録済みiPhoneのSafariからインストール
  4. 04IPAファイルのダウンロード
  5. 05任意のデバイス登録フロー
  6. 06任意のGitHub Actionsワークフロー

活用シーン

  1. 01iPhoneが手元にないときのローカルビルド共有
  2. 02登録済みのチームメンバー端末へのビルド導入
  3. 03GitHub ActionsでのiOSビルドとアップロード

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jakemor/speedflight --skill speedflight

インストール後、Claude Code で/speedflightを入力して読み込まれたか確認します。

詳細

Speedflight

ベース URL:https://speedflight.dev

Speedflight は署名済みの iOS ビルドを保存し、アプリごとに 1 つのページを提供します。ページ上の各ビルドには、タイトル、リリースノート(変更点、テスト項目)、バージョンとビルド番号、ビルド日時、ブランチ、コミット、作成者が表示されます。「入手」ボタンから、登録済みの iPhone で Safari を使ってインストールできます。リンクから IPA をダウンロードできます。

すべての作業はユーザーの Mac 上で実行します。アカウントもログインも不要です。

このスキルの目的は 1 つです。デバイスが Mac のそばにないときに、ローカルビルドを登録済みデバイスに送ることです。スマートフォンに接続できずデバイス上で実行する手順が失敗した場合や、ユーザーが外出中にビルドを求めた場合は、指示を待たずにこれを行います。実行するたびに、ページのリンクをチャットに投稿します。

セキュリティモデル

2 つの識別子には、それぞれ異なる役割があります。

  • SPEEDFLIGHT_SECRET はアップロードキーです。リポジトリごとに一度生成し、.env.speedflight(gitignore 対象)に保存します。これはこのアプリのユーザーのものです。共有、表示、コミットはしないでください。
  • ページ ID はサーバー上で導出されます:sha256("<bundleId>\n<secret>") の先頭 32 文字の 16 進数です。共有 URL に含まれるのはこの ID です。リンクを持つ人は誰でもインストールとダウンロードができますが、アップロードはできません。

IPA は Apple によってアドホックプロファイルで署名されるため、署名に使用した Apple アカウントに登録済みのデバイスにのみインストールできます。リンクが認証手段であり、署名が安全性を担保します。

モード

  • リポジトリでの初回実行(.env.speedflight または scripts/speedflight.sh がない場合):セットアップを行ってから、ビルドを共有します。
  • 2 回目以降の実行:ビルドを共有します。
  • ユーザーからデバイスの追加、またはチームメイトのスマートフォンを Speedflight に登録するよう依頼された場合:デバイスを追加します。明示的に依頼された場合のみ行い、自分からは決して行いません。
  • Mac を使わずに実行するよう依頼された場合、または Linux の開発用マシンやサンドボックスから実行するよう依頼された場合:依頼があった場合:GitHub Actions で実行します。

セットアップ(リポジトリごとに 1 回)

1. プロジェクトを調べる

以下を見つけ、ユーザーに短い表で確認します。

項目方法
PROJECTls *.xcworkspace *.xcodeproj(ios/ の下も確認)。ワークスペースがあれば優先します。project.yml がある場合、プロジェクトは XcodeGen で生成されているため、ビルドの前に毎回 xcodegen generate で再生成する必要があります。
SCHEMExcodebuild -list -json -project "$PROJECT"。アプリのスキームを選びます。
BUNDLE_IDpbxproj または project.yml 内の PRODUCT_BUNDLE_IDENTIFIER を rg -n で検索します。ユーザーから指示がない限り、.dev 版は使用しないでください。
TEAM_IDpbxproj または project.yml 内の DEVELOPMENT_TEAM を rg -n で検索します。
ディープリンク手順 3 を参照してください。

2. App Store Connect キー

クラウド署名には、TEAM_ID に属する Admin または App Manager ロールの App Store Connect API キーが必要です。ユーザーに尋ねる前に、既存のキーを探します。

ls ~/private_keys/AuthKey_*.p8 2>/dev/null
asc profiles list 2>/dev/null
cat .env.speedflight 2>/dev/null

見つからない場合は、App Store Connect → Users and Access → Integrations → App Store Connect API → Team Keys → Generate API Key で作成するようユーザーに伝えます。.p8 は一度しかダウンロードできません。~/private_keys/AuthKey_<KEY_ID>.p8 として保存します。

クラウド署名のみを使用します。証明書やプロファイルを手動で失効、削除、作成しないでください。Keychain Access を開いたり、.p12 を書き出したり、ターゲットを手動署名に切り替えたりしないでください。キー用のフラグと -allowProvisioningUpdates がすべてを処理します。

3. ディープリンク

インストールページは、URL スキームを使ってインストール後にアプリを開きます。スキームがあるか確認します。

rg -n "CFBundleURLSchemes" -A3 --glob '*.plist' --glob 'project.yml' --glob '*.pbxproj'

アプリにスキームがない場合は追加します。アプリ名から短い小文字のスキーム(例:tressa://)を選び、ユーザーに確認してから追加します。

  • XcodeGen(project.yml)の場合、アプリターゲットの下に追加します:
    info:
      properties:
        CFBundleURLTypes:
          - CFBundleURLName: com.example.app
            CFBundleURLSchemes: [tressa]
    
  • 通常の Xcode の場合:ターゲットの Info.plist に CFBundleURLTypes を追加します。

この変更をコミットします。ディープリンクは .env.speedflight に SPEEDFLIGHT_DEEP_LINK=tressa:// として記載します。

4. 作成者

ビルドページには、誰がビルドしたかが表示されます。GitHub のユーザー名を一度解決します。

gh api user --jq .login 2>/dev/null || git config --get github.user

どちらでも確認できない場合は、ユーザーに尋ねます。SPEEDFLIGHT_AUTHOR として保存します。

5. アイコン(任意)

リポジトリにアセットカタログのアイコンがある場合、ページに表示されます。1024 の PNG を探します。

find . -path '*.appiconset/*.png' -not -path '*/node_modules/*' | head

パスを SPEEDFLIGHT_ICON として保存します。通常の PNG がなければ、この手順は省略します。

6. .env.speedflight を作成する

シークレットを生成し、リポジトリのルート(またはアプリがそこにある場合は ios/)にファイルを書き込みます。

cat > .env.speedflight <<EOF
ASC_KEY_ID=ABC123DEFG
ASC_ISSUER_ID=12345678-abcd-....
SPEEDFLIGHT_SECRET=$(openssl rand -hex 24)
SPEEDFLIGHT_DEEP_LINK=tressa://
SPEEDFLIGHT_AUTHOR=jakemor
SPEEDFLIGHT_ICON=App/Assets.xcassets/AppIcon.appiconset/icon-1024.png
EOF
chmod 600 .env.speedflight
grep -qxF '.env.speedflight' .gitignore || echo '.env.speedflight' >> .gitignore

ASC_PRIVATE_KEY_PATH は省略可能です。デフォルトは ~/private_keys/AuthKey_$ASC_KEY_ID.p8 です。

7. scripts/speedflight.sh を作成する

このスクリプトをリポジトリに書き込み、手順 1 の大文字のプレースホルダーを埋めます。コミットしてください。アーカイブ、エクスポート、アップロード、リンク作成までを行うパイプライン全体です。

#!/bin/bash
# Cuts a signed ad hoc IPA, uploads it to Speedflight, and prints the page
# link as the last line. Signs through the App Store Connect key in
# .env.speedflight; the key must belong to DEVELOPMENT_TEAM.
#
#   scripts/speedflight.sh "<title>" "<notes>" [screenshot.png ...]
#
# The page link is the only auth for installing. The secret in
# .env.speedflight is the only auth for uploading. Do not paste either
# anywhere public.
set -euo pipefail
cd "$(dirname "$0")/.."

TITLE="${1:?usage: speedflight.sh \"<title>\" \"<notes>\" [screenshot.png ...]}"
NOTES="${2:?usage: speedflight.sh \"<title>\" \"<notes>\" [screenshot.png ...]}"
shift 2
SCREENSHOTS=("$@")

if [[ -f .env.speedflight ]]; then
  set -a
  # shellcheck disable=SC1091
  source .env.speedflight
  set +a
fi
: "${ASC_KEY_ID:?set ASC_KEY_ID in .env.speedflight}"
: "${ASC_ISSUER_ID:?set ASC_ISSUER_ID in .env.speedflight}"
: "${SPEEDFLIGHT_SECRET:?set SPEEDFLIGHT_SECRET in .env.speedflight}"
: "${SPEEDFLIGHT_DEEP_LINK:?set SPEEDFLIGHT_DEEP_LINK in .env.speedflight}"
: "${SPEEDFLIGHT_AUTHOR:?set SPEEDFLIGHT_AUTHOR in .env.speedflight}"
ASC_PRIVATE_KEY_PATH="${ASC_PRIVATE_KEY_PATH:-$HOME/private_keys/AuthKey_$ASC_KEY_ID.p8}"
[[ -f "$ASC_PRIVATE_KEY_PATH" ]] || { echo "missing ASC key file: $ASC_PRIVATE_KEY_PATH" >&2; exit 1; }

PROJECT="YOUR_APP.xcodeproj"      # or YOUR_APP.xcworkspace, with -workspace below
SCHEME="YOUR_SCHEME"
BUNDLE_ID="com.example.yourapp"
TEAM_ID="YOUR_TEAM_ID"
BASE="${SPEEDFLIGHT_BASE:-https://speedflight.dev}"
# The same worker on its workers.dev route, kept as an upload fallback. The
# page link stays on the custom domain.
FALLBACK_BASE="https://speedflight.jake-7c3.workers.dev"
OUT="build/share"

# The page shows a branch and commit, so those must be real: everything
# committed, and the commit on the remote. Under CI the checkout is the
# pushed commit by definition, and a detached HEAD has no upstream to test.
BRANCH="${GITHUB_REF_NAME:-$(git rev-parse --abbrev-ref HEAD)}"
COMMIT="$(git rev-parse HEAD)"
# https form of origin, so the page can link the branch and commit.
REPO_URL="$(git remote get-url origin 2>/dev/null | sed -E 's#^git@([^:]+):#https://\1/#; s#\.git$##')"
case "$REPO_URL" in https://*) ;; *) REPO_URL="" ;; esac
if [[ -z "${CI:-}" ]]; then
  if [[ -n "$(git status --porcelain)" ]]; then
    echo "working tree is dirty: commit before sharing a build" >&2
    exit 1
  fi
  if ! git merge-base --is-ancestor "$COMMIT" "@{u}" 2>/dev/null; then
    echo "HEAD is not pushed: git push -u origin $BRANCH" >&2
    exit 1
  fi
fi

# Uncomment for XcodeGen projects: the project file is generated and gitignored.
# xcodegen generate --quiet

rm -rf "$OUT"
mkdir -p "$OUT"

# Archive signed, not with CODE_SIGNING_ALLOWED=NO: an unsigned archive
# carries no entitlements and the export re-sign does not add them back.
# Cloud signing with the ASC key makes a development certificate for the
# archive and the ad hoc one for the export.
xcodebuild -project "$PROJECT" -scheme "$SCHEME" \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -archivePath "$OUT/App.xcarchive" \
  -allowProvisioningUpdates \
  -authenticationKeyID "$ASC_KEY_ID" \
  -authenticationKeyIssuerID "$ASC_ISSUER_ID" \
  -authenticationKeyPath "$ASC_PRIVATE_KEY_PATH" \
  -quiet archive

cat > "$OUT/ExportOptions.plist" <<PLIST
<?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>destination</key><string>export</string>
  <key>method</key><string>release-testing</string>
  <key>signingStyle</key><string>automatic</string>
  <key>teamID</key><string>$TEAM_ID</string>
  <key>thinning</key><string>&lt;none&gt;</string>
</dict>
</plist>
PLIST

xcodebuild -exportArchive \
  -archivePath "$OUT/App.xcarchive" \
  -exportOptionsPlist "$OUT/ExportOptions.plist" \
  -exportPath "$OUT/export" \
  -allowProvisioningUpdates \
  -authenticationKeyID "$ASC_KEY_ID" \
  -authenticationKeyIssuerID "$ASC_ISSUER_ID" \
  -authenticationKeyPath "$ASC_PRIVATE_KEY_PATH" \
  -quiet
mv "$OUT"/export/*.ipa "$OUT/signed.ipa"

# 1. Register the build with its metadata. The server answers with the ids.
META="$(jq -n \
  --arg title "$TITLE" --arg notes "$NOTES" \
  --arg deepLink "$SPEEDFLIGHT_DEEP_LINK" \
  --arg branch "$BRANCH" --arg commit "$COMMIT" \
  --arg author "$SPEEDFLIGHT_AUTHOR" \
  --arg repoUrl "$REPO_URL" \
  '{title:$title, notes:$notes, deepLink:$deepLink, branch:$branch, commit:$commit, author:$author}
   + (if $repoUrl == "" then {} else {repoUrl:$repoUrl} end)')"
create() {
  curl -sfS --retry 3 --retry-all-errors --retry-delay 3 \
    -X POST "$1/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/builds" \
    -H "Content-Type: application/json" --data "$META"
}
CREATED="$(create "$BASE" || create "$FALLBACK_BASE")"
BUILD_ID="$(jq -r .buildId <<<"$CREATED")"
PAGE_URL="$(jq -r .pageUrl <<<"$CREATED")"

# 2. Upload the IPA. The server reads name, version, and build number from
#    its Info.plist and rejects it if the bundle id does not match.
upload() {
  curl -sfS --http1.1 --retry 5 --retry-all-errors --retry-delay 5 \
    -X PUT "$1/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/builds/$BUILD_ID/app.ipa" \
    --data-binary @"$OUT/signed.ipa" >/dev/null
}
upload "$BASE" || upload "$FALLBACK_BASE"

# 3. Screenshots, if given: what changed, as pictures. Named 01-, 02-, ...
#    so the page keeps the order you passed them in.
# The guarded expansion keeps macOS bash 3.2's set -u happy when no
# screenshots were passed; a bare "${SCREENSHOTS[@]}" aborts the script.
n=0
for shot in ${SCREENSHOTS[@]+"${SCREENSHOTS[@]}"}; do
  [[ -f "$shot" ]] || { echo "no such screenshot: $shot" >&2; continue; }
  n=$((n + 1))
  ext="${shot##*.}"
  case "$ext" in png|PNG) type=image/png ;; jpg|jpeg|JPG|JPEG) type=image/jpeg ;; webp) type=image/webp ;; *) echo "skip $shot: not png/jpg/webp" >&2; continue ;; esac
  name="$(printf '%02d-%s' "$n" "$(basename "$shot" | tr -c 'A-Za-z0-9._-\n' '-')")"
  curl -sfS --retry 3 --retry-all-errors --retry-delay 3 \
    -X PUT "$BASE/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/builds/$BUILD_ID/screenshots/$name" \
    -H "Content-Type: $type" --data-binary @"$shot" >/dev/null || echo "screenshot upload failed: $shot" >&2
done

# 4. Icon, if configured. Best effort.
if [[ -n "${SPEEDFLIGHT_ICON:-}" && -f "$SPEEDFLIGHT_ICON" ]]; then
  curl -sS -X PUT "$BASE/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/icon" \
    -H "Content-Type: image/png" --data-binary @"$SPEEDFLIGHT_ICON" >/dev/null || true
fi

echo
echo "Build page: $PAGE_URL"

注意事項(エージェント向け):

  • 常に -destination "generic/platform=iOS" を指定します。実機を指定してはいけません。
  • アドホックエクスポートの現行名は release-testing です。thinning=<none> を指定すると、プロファイル内のどのデバイスにもインストールできるユニバーサル IPA が 1 つ作成されます。
  • ワークスペースの場合、アーカイブコマンドでは -project の代わりに -workspace を使います。
  • jq が必要です。インストールされていない場合は brew install jq を実行してください。
  • このスクリプトはデバイスにインストールしません。Speedflight は、スマートフォンが手元にない場合に使います。デバイスで実行する場合は、リポジトリにある実行用スクリプトを使ってください。

ビルドを共有する(毎回)

  1. 最初にコミットしてプッシュします。 ページにはブランチとコミットが表示されます。また、スクリプトは変更が未コミット、または未プッシュのツリーでは実行できません。ユーザーの作業が未コミットの場合は、適切なメッセージを付けてコミットし、プッシュしてください。ユーザー自身のフィーチャーブランチ以外のブランチにプッシュする場合は、事前に確認してください。
  2. タイトルとメモを書きます。 前回共有したビルド以降の差分を確認します(git log、または会話にある前回の Build ページのリンクを参照)。タイトルはこのビルドの内容を表す 1 行にします。メモには変更点とテスト内容を、スマートフォンを手にする人向けに記載します。プレーンテキストで、短い行を数行にまとめ、Markdown の見出しは使いません。
  3. スクリーンショット(任意、推奨)。 ページには App Store のように表示されますが、マーケティング用ではありません。変更内容を示すため、手を加えた画面を撮影します。役立つ場合は変更前と変更後も用意します。ユーザーから提供されたスクリーンショットや、シミュレーターでのテスト中に撮影したもの(xcrun simctl io booted screenshot 01-paywall.png)など、手元にあるものを使ってください。スクリーンショットを取得するだけのためにシミュレーターのビルドを開始してはいけません。iPhone と iPad、縦向きと横向きは自由に組み合わせられます。ページ側で高さが統一されます。PNG、JPG、WebP に対応し、各ファイルは 10MB 未満、最大 12 枚です。
  4. スクリプトを実行します。
    scripts/speedflight.sh "Onboarding paywall rewrite" "$(cat <<'EOF'
    What changed
    - New onboarding paywall with the annual plan first
    - Fixed the crash when restoring purchases offline
    
    What to test
    - Fresh install, go through onboarding, tap Restore with Wi-Fi off
    - Check the paywall shows 3 plans and the annual one is selected
    EOF
    )" shots/paywall.png shots/restore.png
    
    スクリーンショットのパスはメモの後に、表示したい順序で指定します。所要時間は 2〜5 分です。最後の行に Build page: <url> が表示されます。
  5. チャットにリンクを投稿します。 実行するたびに必ず、タップできるよう URL だけを単独の行に記載します。リンクはアプリのページで、スクリプトが出力する Build page: の行にある https://speedflight.dev/a/<pageId> です。ビルド ID が後ろに付いた URL(/a/<pageId>/<buildId>)を共有してはいけません。これは 1 つのビルドのインストールページですが、ユーザーが必要としているのはすべてのビルドが表示されるページです。ページに表示されているタイトル、バージョン、ビルド番号を伝え、登録済みの iPhone から Safari を使ってインストールできることも説明します。リンクを公開の場所に貼り付けてはいけません。シークレットを出力してはいけません。

スクリプトが失敗した場合:

  • 「No Accounts」または「No profiles」:ASC キーが TEAM_ID に属していないか、App Manager の役割がありません。
  • 「Could not read Info.plist」または「bundle id is X, but this page is for Y」:スキームが BUNDLE_ID と異なるターゲットをビルドしました。スクリプトの設定を修正してください。
  • 「working tree is dirty」または「HEAD is not pushed」:手順 1 に戻ってください。スクリーンショットはコミットする必要はありません。使い捨てのものは git の対象外にしてください。
  • アップロードのタイムアウト:スクリプトは再試行し、フォールバックとして workers.dev ホストを自動で使います。両方とも失敗した場合は、もう一度実行してください。

デバイスを追加する(依頼された場合のみ)

アドホックビルドをインストールできるのは、署名に使った Apple アカウントに登録済みのデバイスのみです。また、IPA 内のデバイスリストはエクスポート時に固定されます。新しいスマートフォンを使うには、UDID を登録してから新しいビルドを作成する必要があります。この Mac に接続されていないスマートフォンから UDID を読み取るために、登録リンクを使います。

ユーザーから「Speedflight にデバイスを追加して」「チームメイトのスマートフォンを追加して」「Sam の iPhone でビルドを使えるようにして」などと頼まれた場合に実行します。自分の判断で実行してはいけません。また、「ビルドを共有する」の一環として実行してはいけません。

1. 登録リンクを発行する

BUNDLE_ID は scripts/speedflight.sh にある値です。

set -a; source .env.speedflight; set +a
curl -sfS -X POST "https://speedflight.dev/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/devices/link"

{enrollId, enrollUrl} が返されます。このリンクはシークレットから生成され、失効するまで有効です。チャットに enrollUrl を単独の行で投稿し、チームメイトがリンクを使って行うことをユーザーに伝えてください。

  1. 追加する iPhone の Safari でリンクを開きます。ほかのブラウザーではプロファイルを「設定」に渡せません。デスクトップでは、ページに QR コードが表示されます。
  2. 名前を入力して「プロファイルをインストール」をタップし、ダウンロードを許可します。
  3. 「設定」を開き、「プロファイルがダウンロードされました」をタップしてから、「インストール」をタップします。確認のため、Safari に戻ります。プロファイルはすぐに削除できます。

このプロファイルは、Apple 独自の UDID 取得機能を使います。読み取るのは UDID、モデル、iOS ビルドのみで、それ以外は読み取りません。また、署名されていないため、インストール画面には「未検証」と表示されます。チームメイトに聞かれたら、そのように伝えてください。

2. デバイスを取得する

ユーザーからチームメイトが完了したと伝えられた場合や、誰が登録したか尋ねられた場合:

curl -sfS "https://speedflight.dev/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/devices" \
  | jq -r '.devices[] | [.udid, (.name // "-"), (.product // "-"), .addedAt] | @tsv'

名前、モデル、UDID、日時の一覧を表示します。次に、チームにすでに登録されているものを確認します。asc(brew install asc)は .env.speedflight と同じ ASC_KEY_ID、ASC_ISSUER_ID、ASC_PRIVATE_KEY_PATH という名前を読み取るので、環境変数を読み込んであれば十分です:

export ASC_KEY_ID ASC_ISSUER_ID ASC_PRIVATE_KEY_PATH
asc devices list --platform IOS --paginate --fields udid,name,status --output table

3. 新しいデバイスを登録する

登録すると元には戻せません。Apple ではメンバーシップ年度ごとに種類ごとで100台まで登録できます。削除したデバイスの枠は、年度が切り替わるまで戻りません。まだチームに登録されていないデバイスをユーザーに表示し、登録前に確認してください。その後、デバイスごとに次を実行します:

asc devices register --name "<name>'s iPhone" --udid "<udid>" --platform IOS

登録名がある場合はそれを使い、ない場合はモデル名を使ってください。asc がインストールされておらず、ユーザーもインストールを希望しない場合は、UDID を渡し、App Store Connect → デバイスから手動で追加してもらってください。

4. 新しいビルドを作成する

ビルドを共有を実行します。エクスポート時に、登録済みのすべてのデバイスが自動的に反映されるため、設定は不要です。登録前に作成したビルドは新しい iPhone にインストールできないため、新しいページリンクを投稿してください。

メンテナンス

  • ユーザーから新たな登録を停止するよう求められたら、リンクを失効させます。デバイス一覧は残ります。
    curl -sfS -X DELETE "https://speedflight.dev/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/devices/link"
    
  • 一覧からデバイスを1台削除します(Apple 側には影響しません):
    curl -sfS -X DELETE "https://speedflight.dev/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/devices/<udid>"
    
  • 再発行しても同じリンクが返されます。リンクはランダムではなく、生成されたものです。別のリンクを取得するには、新しいシークレットが必要です。

要望があった場合:GitHub Actions で実行する

ユーザーから頼まれた場合に限ります。通常は、目の前にある Mac を使います。Mac がまったくないワークフローが必要な場合に検討します。Linux の開発用マシンやクラウドサンドボックス上のエージェントが iOS アプリを編集してプッシュし、GitHub の macOS ランナーがビルドを作成、署名してアップロードします。ユーザーは自分の iPhone でページリンクからインストールします。Xcode を実行できないマシンから、iOS 開発を最初から最後まで行えます。

同じ scripts/speedflight.sh を変更せずに実行できます。CI が設定されている場合は、作業ツリーの変更確認とプッシュ確認が省略されます。

1. シークレットと変数

gh secret set ASC_KEY_ID --body "ABC123DEFG"
gh secret set ASC_ISSUER_ID --body "12345678-abcd-...."
gh secret set ASC_PRIVATE_KEY < ~/private_keys/AuthKey_ABC123DEFG.p8
gh secret set SPEEDFLIGHT_SECRET --body "$(grep SPEEDFLIGHT_SECRET .env.speedflight | cut -d= -f2)"
gh variable set SPEEDFLIGHT_DEEP_LINK --body "tressa://"

ローカルの .env.speedflight と同じ SPEEDFLIGHT_SECRET を使い、ローカルと CI のビルドが同じページに登録されるようにします。

2. ワークフロー

.github/workflows/speedflight.yml を作成します:

name: Speedflight

on:
  workflow_dispatch:
    inputs:
      title:
        description: One line, what this build is
        required: true
      notes:
        description: What changed and what to test
        required: true
  push:
    branches: ["**"]

concurrency:
  group: speedflight-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: macos-latest
    timeout-minutes: 45
    env:
      ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
      ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
      SPEEDFLIGHT_SECRET: ${{ secrets.SPEEDFLIGHT_SECRET }}
      SPEEDFLIGHT_DEEP_LINK: ${{ vars.SPEEDFLIGHT_DEEP_LINK }}
      SPEEDFLIGHT_AUTHOR: ${{ github.actor }}
    steps:
      - uses: actions/checkout@v4
      - name: Install the ASC key
        run: |
          mkdir -p ~/private_keys
          printf '%s' "${{ secrets.ASC_PRIVATE_KEY }}" > ~/private_keys/AuthKey_${ASC_KEY_ID}.p8
      # Only for XcodeGen projects.
      - run: brew install xcodegen
      - name: Cut and share
        run: |
          TITLE="${{ inputs.title }}"
          NOTES="${{ inputs.notes }}"
          # On push, the commit message is the title and its body the notes.
          [ -n "$TITLE" ] || TITLE="$(git log -1 --format=%s)"
          [ -n "$NOTES" ] || NOTES="$(git log -1 --format=%b)"
          [ -n "$NOTES" ] || NOTES="$TITLE"
          scripts/speedflight.sh "$TITLE" "$NOTES" | tee build.log
          grep '^Build page:' build.log >> "$GITHUB_STEP_SUMMARY"

エージェント向けの注意事項:

  • ページリンクを使うと、アプリをインストールできます。公開リポジトリでは、誰でも読めるため、ステップのサマリー行を削除してください。シークレットが出力されることはありません。
  • 新しいランナーには Apple Development 証明書がないため、クラウド署名では実行ごとに証明書が作成されますが、Apple の発行数には上限があります。およそ10回実行すると、「証明書の最大数に達しました」というエラーでアーカイブが失敗します。対処するには、アーカイブ手順の前に .p12 シークレットから固定の署名 ID をインポートします。このエラーが発生したらユーザーに依頼してください。証明書を自分で作成したり、失効させたりしないでください。
  • CI でスクリーンショットを撮るには、ワークフローでシミュレーターを実行する必要があります。ユーザーから依頼がない限り省略してください。ページはスクリーンショットなしでも使えます。
  • 通常どおりプッシュするよう、ユーザーに伝えてください。どのブランチへのプッシュでもビルドが作成されます。頻度が高すぎる場合は branches: を絞り込んでください。

API リファレンス

書き込みにはシークレットとバンドル ID が必要です。読み取りにはページ ID のみが必要です。

POST   /api/apps/:secret/:bundleId/builds
       JSON: {title, notes, deepLink, branch, commit, author}  all required
             {repoUrl}  optional https repo URL; links branch + author on the page
       -> 201 {buildId, pageId, pageUrl}
PUT    /api/apps/:secret/:bundleId/builds/:buildId/app.ipa      raw IPA bytes
       -> {ok, appName, shortVersion, buildVersion, size, pageUrl}
PUT    /api/apps/:secret/:bundleId/builds/:buildId/screenshots/:name   raw image, under 10MB
       name like 01-home.png (png, jpg, webp); at most 12 per build
PUT    /api/apps/:secret/:bundleId/icon                         raw PNG, under 2MB
DELETE /api/apps/:secret/:bundleId/builds/:buildId

POST   /api/apps/:secret/:bundleId/devices/link      activate the enroll link -> {enrollId, enrollUrl}
DELETE /api/apps/:secret/:bundleId/devices/link      revoke it
GET    /api/apps/:secret/:bundleId/devices           -> {enrollUrl | null, devices: [{udid, name, product, version, serial, addedAt}]}
DELETE /api/apps/:secret/:bundleId/devices/:udid     forget one

GET    /api/pages/:pageId                          app + builds JSON
GET    /api/pages/:pageId/builds/:buildId          one build
GET    /api/pages/:pageId/builds/:buildId/app.ipa  download
GET    /api/pages/:pageId/builds/:buildId/manifest.plist   OTA manifest

Page to share:     https://speedflight.dev/a/:pageId   (this one, always)
One build's page:  https://speedflight.dev/a/:pageId/:buildId   (what the QR opens; do not share)
Enroll a device:   https://speedflight.dev/d/:enrollId  (send to the teammate; mint it first)

シークレットの形式は、[A-Za-z0-9_-] の 32~128 文字です。openssl rand -hex 24 で生成します。ページ ID は、サーバーへの往復なしで必要になった場合、printf '%s\n%s' "$BUNDLE_ID" "$SPEEDFLIGHT_SECRET" | shasum -a 256 | cut -c1-32 で取得できます。

コメント