mcpField
S
speedflight

speedflight

github.com/jakemor云与 DevOps545 次安装6 周前更新
代码仓库

关于 speedflight

speedflight 是用于分享签名 iOS 构建的 Agent 技能,适合 iPhone 不在 Mac 旁时分发构建。它在本地构建并上传,生成包含版本信息和安装入口的应用页面。

主要功能

  1. 01构建并上传签名 iOS 应用
  2. 02提供版本与提交信息页面
  3. 03通过 Safari 安装到已注册 iPhone
  4. 04下载 IPA 文件
  5. 05可选设备注册流程
  6. 06可选 GitHub Actions 工作流

适用场景

  1. 01iPhone 不在身边时分享本地构建
  2. 02向已注册的队友设备安装构建
  3. 03通过 GitHub Actions 执行 iOS 构建上传

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/speedflight确认已加载。

详细介绍

Speedflight

基础 URL:https://speedflight.dev

Speedflight 用于存储经过签名的 iOS 构建版本,并为每个应用提供一个页面。页面上的每个构建版本都会显示标题、发行说明(更改内容、需要测试的内容)、版本号和构建号、构建时间、分支、提交和作者。点击“获取”按钮即可在已注册的 iPhone 上通过 Safari 安装。链接可用于下载 IPA。

所有操作都在用户的 Mac 上运行。无需账户,也无需登录。

这项技能只为一件事:当已注册设备不在 Mac 旁时,将本地构建版本发送到该设备。当因手机无法连接而导致“在设备上运行”步骤失败,或用户不在设备旁却要求构建版本时,无需等待用户询问,直接执行。每次运行结束后,都要在聊天中发送页面链接。

安全模型

两个标识符,各有用途:

  • SPEEDFLIGHT_SECRET 是上传密钥。每个仓库生成一次,并保存在 .env.speedflight 中(已加入 gitignore)。这是用户为此应用持有的密钥。不要分享、打印或提交它。
  • 页面 ID 在服务器上根据 sha256("<bundleId>\n<secret>") 生成,取前 32 个十六进制字符。分享 URL 中包含的就是这个 ID。拥有链接的人可以安装和下载,但不能上传。

IPA 由 Apple 使用 ad hoc 配置描述文件签名,因此只能安装在签名 Apple 账户中已注册的设备上。链接负责授权,签名负责安全。

模式

  • 在仓库中首次运行(没有 .env.speedflight 或 scripts/speedflight.sh):先执行设置,然后分享构建版本。
  • 之后每次运行:分享构建版本。
  • 如果用户要求添加设备,或让队友的手机使用 Speedflight:执行添加设备。仅在用户明确提出时执行,绝不要擅自操作。
  • 如果用户要求不通过 Mac 运行,或从 Linux 开发机或沙盒运行:执行按需:在 GitHub Actions 中运行。

设置(每个仓库仅需一次)

1. 探查项目

查找以下信息,并用一个简短表格与用户确认:

信息方法
PROJECTls *.xcworkspace *.xcodeproj(也检查 ios/ 下)。如果存在工作区,优先使用工作区。如果存在 project.yml,则项目由 XcodeGen 生成,必须在每次构建前使用 xcodegen generate 重新生成。
SCHEMExcodebuild -list -json -project "$PROJECT"。选择应用方案。
BUNDLE_ID在 pbxproj 或 project.yml 中运行 rg -n PRODUCT_BUNDLE_IDENTIFIER。除非用户明确要求,否则不要使用 .dev 变体。
TEAM_ID在 pbxproj 或 project.yml 中运行 rg -n DEVELOPMENT_TEAM。
深层链接参见步骤 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 → 用户与访问 → 集成 → App Store Connect API → 团队密钥 → 生成 API 密钥。.p8 文件只能下载一次;请将其保存为 ~/private_keys/AuthKey_<KEY_ID>.p8。

仅用于云端签名。绝不要手动撤销、删除或创建证书或配置描述文件,绝不要打开“钥匙串访问”,绝不要导出 .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"

给你的说明,Agent:

  • 始终使用 -destination "generic/platform=iOS"。绝不要指定手机。
  • release-testing 是 ad hoc 导出的当前名称。thinning=<none> 会保留一个通用 IPA,因此可安装在配置文件中的任何设备上。
  • 对于工作区,归档命令中的 -workspace 会替代 -project。
  • 必须安装 jq。如果缺少,请运行 brew install jq。
  • 此脚本不会在设备上安装应用。Speedflight 用于手机不在手边的情况。请使用仓库自带的设备运行脚本。

分享构建(每次都要执行)

  1. 先提交并推送。 页面会显示分支和提交记录,且脚本会拒绝处理有未提交更改或尚未推送的工作树。如果用户的工作尚未提交,请使用有意义的提交信息进行提交并推送。向非用户自己的功能分支推送前,请先征求同意。
  2. 编写标题和说明。 查看自上次分享构建以来的差异(使用 git log,或查看对话中的上一个 Build 页面链接)。标题用一行说明这是哪个构建。说明要写明有哪些更改以及要测试什么,面向拿着手机的人。使用纯文本,写几行简短内容,不要使用 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>);那是某个构建的安装页面,而用户需要的是显示所有构建的页面。说明页面上有什么:标题、版本和构建号,并说明可通过已注册 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 主机。如果两种方式都失败,请重新运行。

添加设备(仅在用户要求时)

ad hoc 构建只能安装在已注册到签名 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 打开链接以添加设备。其他浏览器无法将描述文件交给“设置”。在桌面设备上,页面会显示二维码。
  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 → Devices。

4. 构建新版本

运行分享构建。导出时会自动包含所有已注册设备,无需配置。注册前构建的版本无法安装到新手机上,因此要发布新页面链接。

日常维护

  • 用户要求停止新的注册时,撤销链接。设备列表会保留。
    curl -sfS -X DELETE "https://speedflight.dev/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/devices/link"
    
  • 从列表中移除某台设备(不会影响 Apple 端):
    curl -sfS -X DELETE "https://speedflight.dev/api/apps/$SPEEDFLIGHT_SECRET/$BUNDLE_ID/devices/<udid>"
    
  • 再次生成会返回相同链接;链接由密钥派生,并非随机生成。只有更换密钥才能得到不同的链接。

按需:在 GitHub Actions 中运行

仅在用户提出要求时执行。默认使用他们面前的 Mac。适合询问的情形是整个工作流都没有 Mac:运行在 Linux 开发环境或云沙箱中的 Agent 修改 iOS 应用并推送代码,然后由 GitHub 的 macOS 运行器构建、签名并上传版本。用户在手机上通过页面链接安装。这样就能在无法运行 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"

给你(Agent)的说明:

  • 页面链接允许安装。在公开仓库中,删除步骤摘要那一行;任何人都能看到它。密钥不会被打印出来。
  • 新运行器没有 Apple Development 证书,因此云端签名会在每次运行时生成一个证书,而 Apple 对证书数量有限制。运行大约十次后,归档会因“已达到证书数量上限”而失败。解决方法是在归档步骤之前,从 .p12 密钥中导入一个固定身份;出现此错误时向用户索取该身份,绝不要自行创建或撤销证书。
  • CI 中的屏幕截图需要在工作流中运行模拟器。除非用户提出要求,否则跳过截图;没有截图页面也能正常使用。
  • 告诉用户照常推送即可。每次向任何分支推送都会触发构建;如果触发太频繁,可通过 branches: 缩小范围。

API 参考

写入操作需要密钥和 bundle 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。

评论

支持 Markdown