关于 speedflight
speedflight 是用于分享签名 iOS 构建的 Agent 技能,适合 iPhone 不在 Mac 旁时分发构建。它在本地构建并上传,生成包含版本信息和安装入口的应用页面。
主要功能
- 01构建并上传签名 iOS 应用
- 02提供版本与提交信息页面
- 03通过 Safari 安装到已注册 iPhone
- 04下载 IPA 文件
- 05可选设备注册流程
- 06可选 GitHub Actions 工作流
适用场景
- 01iPhone 不在身边时分享本地构建
- 02向已注册的队友设备安装构建
- 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. 探查项目
查找以下信息,并用一个简短表格与用户确认:
| 信息 | 方法 |
|---|---|
PROJECT | ls *.xcworkspace *.xcodeproj(也检查 ios/ 下)。如果存在工作区,优先使用工作区。如果存在 project.yml,则项目由 XcodeGen 生成,必须在每次构建前使用 xcodegen generate 重新生成。 |
SCHEME | xcodebuild -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><none></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 用于手机不在手边的情况。请使用仓库自带的设备运行脚本。
分享构建(每次都要执行)
- 先提交并推送。 页面会显示分支和提交记录,且脚本会拒绝处理有未提交更改或尚未推送的工作树。如果用户的工作尚未提交,请使用有意义的提交信息进行提交并推送。向非用户自己的功能分支推送前,请先征求同意。
- 编写标题和说明。 查看自上次分享构建以来的差异(使用
git log,或查看对话中的上一个 Build 页面链接)。标题用一行说明这是哪个构建。说明要写明有哪些更改以及要测试什么,面向拿着手机的人。使用纯文本,写几行简短内容,不要使用 Markdown 标题。 - 截图(可选,建议提供)。 页面会像 App Store 一样显示截图,但它们不是营销素材。截图应展示更改内容:你修改过的屏幕,必要时提供修改前后的对比。使用现有截图:用户提供的截图,或你在模拟器上测试时拍摄的截图(
xcrun simctl io booted screenshot 01-paywall.png)。不要仅为获取截图而启动模拟器构建。iPhone 和 iPad、纵向和横向截图可任意组合,页面会统一设置高度。支持 PNG、JPG 或 WebP 格式;每张小于 10MB,最多 12 张。 - 运行脚本。
截图路径应放在说明之后,并按显示顺序排列。运行需要 2 到 5 分钟。最后一行是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.pngBuild page: <url>。 - 在聊天中发送链接。 每次运行后都要发送,并且始终将纯 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 单独放在一行发到聊天中,并告诉用户队友要如何使用:
- 在 iPhone 上用 Safari 打开链接以添加设备。其他浏览器无法将描述文件交给“设置”。在桌面设备上,页面会显示二维码。
- 输入名称并轻点“安装描述文件”,然后允许下载。
- 打开“设置”,轻点“已下载描述文件”,再轻点“安装”。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。