GitHub Actions 构建 iOS App:用 Linux 自托管 runner 构建出 ipa
最后更新
GitHub Actions 构建 iOS App 不一定要用按分钟计费的 macOS runner:在自己的 Linux 机器上装好 KXBuild 并登录账号,注册成 GitHub 的自托管 runner,workflow 里写 runs-on: [self-hosted, linux],再用 kxbuild build -c Release --export app-store 直接得到签好名的 ipa。证书和描述文件以 base64 存进 Secrets,构建前解码到临时目录,构建后删除。下文给出完整的 workflow 文件和常见失败的处理办法。
开始前准备好这几样东西。
- 一台 Linux 机器,物理机、云服务器都可以,能访问 GitHub。
- 一个能在本地用 KXBuild 构建 Release 的工程,代码在 GitHub 的私有仓库里。
- Apple Distribution 证书(.p12)和它的密码,App Store 类型的描述文件(.mobileprovision)。获取方法见证书与签名,完整申请步骤见在 Windows 上申请证书和描述文件。
- 工程用到 Swift Package 的,把 Package.resolved 提交进仓库。
GitHub Actions 构建 iOS 一共要几步?
| 步骤 | 在哪里做 | 做一次还是每次 |
|---|---|---|
| 1. 安装 KXBuild 并登录账号 | Linux 构建机 | 一次 |
2. 运行 kxbuild doctor |
Linux 构建机 | 一次,升级后再跑 |
| 3. 注册自托管 runner 并装成服务 | GitHub 仓库设置 + 构建机 | 一次 |
| 4. 证书和描述文件存进 Secrets | GitHub 仓库设置 | 一次,证书更新时再改 |
| 5. 解码证书、构建 ipa、上传 artifact、清理证书 | workflow | 每次运行 |
Linux 构建机要准备什么?
安装 KXBuild 并登录账号
在构建机上,用以后要运行 runner 的那个用户登录,按安装页装好 KXBuild 并登录账号。Linux 用安装页的 install.sh 一条命令安装。
1curl -fsSL https://www.kxbuild.net/install.sh | bash
装完新开一个终端运行体检,全部通过即可。
1kxbuild doctor
安装脚本会把安装目录写进当前用户的环境变量。runner 服务必须用同一个用户运行,否则 workflow 里找不到 kxbuild。
再给这个用户设置环境变量 KXBUILD_NO_UPDATE_CHECK=1,关掉新版本提示和后台检查。升级放在维护时间手动执行 kxbuild doctor,不要写进每次构建,免得两次构建用了不同版本的工具链。目录和环境变量的完整说明见目录与环境变量。
自托管 runner 怎么注册?
在仓库里添加 runner
- 打开 GitHub 仓库主页,点 Settings。
- 左侧栏点 Actions,再点 Runners。
- 点 New self-hosted runner。
- 操作系统选 Linux,架构按构建机的实际情况选 x64 或 ARM64。
- 页面会列出一组命令。在构建机上用装 KXBuild 的那个用户打开终端,按顺序逐条执行。
这组命令先下载并解压 runner 程序,再运行 ./config.sh 把这台机器注册到仓库。注册时会问几个问题,runner 名称可以直接回车用主机名,标签保持默认即可。默认标签里已经有 self-hosted 和 linux,workflow 里就用这两个标签选中它。完整步骤见 GitHub 的添加自托管 runner。
装成系统服务
页面最后一条命令 ./run.sh 会在前台运行 runner,终端一关就断开。确认能连上后按 Ctrl+C 停掉,在 runner 目录里把它装成系统服务,开机自动启动。
1sudo ./svc.sh install
2sudo ./svc.sh start
3sudo ./svc.sh status
服务要用装 KXBuild 的那个用户运行。用这个用户执行上面的命令,服务就以这个用户身份运行。想指定其他用户的,在 install 后面加用户名,例如 sudo ./svc.sh install builder。服务相关的命令见 GitHub 的把 runner 配置为服务。
服务启动后,仓库 Settings → Actions → Runners 里这台 runner 显示为 Idle,就可以接任务了。
让 workflow 找到 kxbuild
服务方式运行的 runner 不一定会读用户的 shell 配置文件,workflow 里可能找不到 kxbuild。稳妥的做法是在 workflow 的第一步把安装目录和它下面的 bin 目录写进 GITHUB_ENV 和 GITHUB_PATH,之后的每一步都能直接调用 kxbuild。默认安装目录是 ~/kxbuild,安装时换过目录的按实际路径改。
1echo "KXBUILD_HOME=$HOME/kxbuild" >> "$GITHUB_ENV"
2echo "$HOME/kxbuild/bin" >> "$GITHUB_PATH"
GITHUB_PATH 从下一个 step 起才生效,所以下一步再运行 kxbuild version,能打印版本号就说明路径没问题。
证书和描述文件放进 Secrets
签名要三样东西,.p12 证书、它的密码、App Store 描述文件。GitHub Secrets 只能存文本,.p12 和 .mobileprovision 是二进制文件,先转成 base64 字符串再存。单个 Secret 最大 48 KB,证书和描述文件的 base64 通常只有几 KB 到十几 KB,放得下。
| Secret 名称 | 内容 |
|---|---|
| IOS_P12_BASE64 | Apple Distribution 证书 .p12 的 base64 |
| IOS_P12_PASSWORD | .p12 的密码 |
| IOS_PROFILE_BASE64 | App Store 描述文件 .mobileprovision 的 base64 |
专用构建机只给一个团队用的话,也可以把证书一次性放进构建机的 ~/kxbuild/keychain,所有工程共用,workflow 里就不用处理证书。代价是任何能在这台 runner 上跑 workflow 的人都能拿到私钥。
生成 base64
在 Windows 上用 PowerShell 生成,结果是不换行的一整行。
1[Convert]::ToBase64String([IO.File]::ReadAllBytes("$PWD\distribution.p12")) | Set-Content -NoNewline p12.b64.txt
2[Convert]::ToBase64String([IO.File]::ReadAllBytes("$PWD\MyApp_AppStore.mobileprovision")) | Set-Content -NoNewline profile.b64.txt
在 Linux 或 WSL 上用 base64 命令,-w0 表示不换行。
1base64 -w0 distribution.p12 > p12.b64.txt
2base64 -w0 MyApp_AppStore.mobileprovision > profile.b64.txt
不要用 Windows 的 certutil -encode,它会加上 BEGIN、END 两行标记,解码时报错。
存进仓库
打开仓库的 Settings → Secrets and variables → Actions,点 New repository secret,按上表逐个添加,值就是 txt 文件的全部内容。装了 GitHub CLI 的,也可以在仓库目录执行下面三条。
1gh secret set IOS_P12_BASE64 < p12.b64.txt
2gh secret set IOS_PROFILE_BASE64 < profile.b64.txt
3gh secret set IOS_P12_PASSWORD
最后一条会提示你输入密码。存完删掉本地的 txt 文件,不要提交进仓库。
在 workflow 里还原
构建前把两个文件解码写进一个临时证书目录,再写一个 password.properties。目录结构和证书与签名里的证书目录一致,password.properties 每行一条「文件名=密码」,文件名必须和 .p12 完全一致,密码不加引号。
1KC="$RUNNER_TEMP/ci-keychain"
2mkdir -p "$KC"
3printf '%s' "$IOS_P12_BASE64" | base64 -d > "$KC/distribution.p12"
4printf '%s' "$IOS_PROFILE_BASE64" | base64 -d > "$KC/MyApp_AppStore.mobileprovision"
5printf 'distribution.p12=%s\n' "$IOS_P12_PASSWORD" > "$KC/password.properties"
构建时用构建设置 CODE_SIGN_KEYCHAIN 指向这个目录,KXBuild 就从这里挑证书和描述文件。自托管 runner 的磁盘会一直保留,构建结束后一定要删掉这个目录,下文 workflow 的最后一步就是做这件事。
用什么命令构建出 ipa?
出包命令和 kxbuild 命令参考里的 CI 用法一致。
1kxbuild build ./MyApp.xcworkspace -s MyApp -c Release --export app-store --clean --derivedDataDir ./out --skip-package-updates --ipa ./out/MyApp.ipa -D CODE_SIGN_KEYCHAIN="$KC" -D CURRENT_PROJECT_VERSION="$GITHUB_RUN_NUMBER"
| 参数 | 作用 |
|---|---|
-s MyApp -c Release |
构建 MyApp 这个 scheme 的 Release 配置 |
--export app-store |
用临时证书目录里的 Apple Distribution 证书和 App Store 描述文件签名,不看工程里的签名设置 |
--clean --derivedDataDir ./out |
构建输出固定在工作区的 out 目录,每次从干净目录开始 |
--skip-package-updates |
Swift Package 严格按 Package.resolved 的版本,不访问远程仓库 |
--ipa ./out/MyApp.ipa |
ipa 的位置,旁边同时生成上传用的 MyApp.AppStoreInfo.plist |
-D CODE_SIGN_KEYCHAIN |
指向临时证书目录 |
-D CURRENT_PROJECT_VERSION |
用 GitHub 的运行号做构建号 |
命令成功时退出码为 0,失败为非 0,Actions 据此把 step 标成失败。只用 .xcodeproj 的工程,把第一个参数换成 ./MyApp.xcodeproj。构建号的完整规则见自动递增构建号。
不同技术栈
- CocoaPods。构建前在工程目录运行
pod install,构建 .xcworkspace,见 CocoaPods。 - Flutter、React Native、Expo。先运行一次带工程目录的体检,例如
kxbuild doctor ./my_flutter_app,它会检查框架环境并准备好 iOS 端工程,再按对应技术栈页的方法安装依赖、构建 ios 目录下的工程,见 Flutter、React Native、Expo。 - 已有 xcodebuild 脚本。KXBuild 自带同名的 xcodebuild 和 xcrun,原来的构建脚本不用改,导出 ipa 的
-exportArchive也照常可用;新写的流水线可以直接用一条kxbuild build -c Release --export app-store,见 xcodebuild / xcrun 兼容。
完整 workflow
把下面的内容存成仓库里的 .github/workflows/ios.yml。它在推送 v 开头的 tag 时自动运行,也可以在 Actions 页手动触发。
1name: iOS build
2
3on:
4 push:
5 tags: ["v*"]
6 workflow_dispatch:
7
8jobs:
9 build-ios:
10 runs-on: [self-hosted, linux]
11 timeout-minutes: 60
12 env:
13 KXBUILD_NO_UPDATE_CHECK: "1"
14 steps:
15 - name: Checkout
16 uses: actions/checkout@v4
17
18 - name: Add KXBuild to PATH
19 run: |
20 echo "KXBUILD_HOME=$HOME/kxbuild" >> "$GITHUB_ENV"
21 echo "$HOME/kxbuild/bin" >> "$GITHUB_PATH"
22
23 - name: Check KXBuild
24 run: kxbuild version
25
26 - name: Prepare signing
27 env:
28 IOS_P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
29 IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
30 IOS_PROFILE_BASE64: ${{ secrets.IOS_PROFILE_BASE64 }}
31 run: |
32 KC="$RUNNER_TEMP/ci-keychain"
33 mkdir -p "$KC"
34 printf '%s' "$IOS_P12_BASE64" | base64 -d > "$KC/distribution.p12"
35 printf '%s' "$IOS_PROFILE_BASE64" | base64 -d > "$KC/MyApp_AppStore.mobileprovision"
36 printf 'distribution.p12=%s\n' "$IOS_P12_PASSWORD" > "$KC/password.properties"
37 echo "KC=$KC" >> "$GITHUB_ENV"
38
39 - name: Build ipa
40 run: |
41 kxbuild build ./MyApp.xcworkspace -s MyApp -c Release --export app-store --clean \
42 --derivedDataDir ./out --skip-package-updates --ipa ./out/MyApp.ipa \
43 -D CODE_SIGN_KEYCHAIN="$KC" \
44 -D CURRENT_PROJECT_VERSION="$GITHUB_RUN_NUMBER"
45
46 - name: Upload ipa
47 uses: actions/upload-artifact@v4
48 with:
49 name: MyApp-${{ github.run_number }}
50 path: |
51 out/*.ipa
52 out/*.AppStoreInfo.plist
53 retention-days: 30
54 if-no-files-found: error
55
56 - name: Clean up signing files
57 if: always()
58 run: rm -rf "$RUNNER_TEMP/ci-keychain"
几点说明。
runs-on: [self-hosted, linux]选中带这两个标签的自托管 runner。有多台 runner、只想让装了 KXBuild 的那台接任务的,注册时给它加一个自定义标签,例如kxbuild,这里写成[self-hosted, linux, kxbuild]。GITHUB_RUN_NUMBER是这个 workflow 在仓库里的运行序号,每次新运行加一,正好做构建号。if-no-files-found: error让没生成 ipa 时这一步直接失败,不会悄悄传一个空的 artifact。- 最后一步
if: always()保证构建失败时也会删除证书文件。 - 版本号 MARKETING_VERSION 想从 tag 取的,在构建命令里加
-D MARKETING_VERSION="${GITHUB_REF_NAME#v}",tagv1.2.0会变成1.2.0。
运行结束后,在仓库的 Actions 页打开这次运行,底部 Artifacts 里下载 MyApp-运行号.zip,解压就是 ipa 和 AppStoreInfo.plist。下载的 ipa 用 Appuploader 上传,见发布与上架。要在 workflow 里接着自动上传,见用 API 密钥自动上传 ipa。
自托管 runner 有哪些安全风险?
- 只在私有仓库里用自托管 runner。 GitHub 的安全使用指南指出,自托管 runner 几乎不应该用于公开仓库,因为任何人都能向公开仓库提交 pull request,在你的构建机上运行代码。私有仓库里能 fork 并提交 pull request 的人,同样能在构建机上运行代码,只把仓库权限给信任的人。
- 证书只交给发布流程。 把三个 Secret 放进一个 Environment,例如
release,job 里加environment: release,再在 Environment 里限制只有受保护的分支和 tag 能用。 - 不要在日志里打印证书。 Actions 会把 Secret 的值替换成
***,但解码后的文件内容不会被遮挡,不要在脚本里cat证书目录里的文件。
自托管 Linux runner 与 GitHub 托管 macOS runner 对比
| 对比项 | 自托管 Linux runner + KXBuild | GitHub 托管 macOS runner + Xcode |
|---|---|---|
| 计费 | GitHub 不按分钟收费,机器和电费自理 | 私有仓库按分钟计费,标准 macOS runner 为 0.062 美元/分钟 |
| 机器 | 自己准备一台 Linux 机器 | GitHub 提供,每次运行都是新机器 |
| 工具链 | 装一次,之后每次直接构建 | 镜像预装 Xcode |
| 出 ipa | kxbuild build -c Release --export app-store 一步得到 |
archive 后再 exportArchive |
| Metal 着色器 | 不支持,工程里有 .metal 文件要换用 Windows 构建机 | 支持 |
| 模拟器测试、UI 测试 | 不支持 | 支持 |
| 文件名大小写 | 区分大小写 | 默认不区分 |
截至 2026 年 10 月,GitHub 的 Actions 计费说明写明自托管 runner 的 Actions 用量免费,托管 macOS runner 的单价见 runner 价格表,以 GitHub 当前公布的为准。
只要出 ipa、不跑模拟器测试的项目,用自托管 Linux runner 最省。要跑模拟器测试的,可以拆成两个 job,测试 job 用托管 macOS runner,打包 job 用自托管 Linux runner。
GitHub Actions 构建 iOS 失败怎么办?
| 现象 | 原因 | 处理 |
|---|---|---|
| 任务一直排队,显示等待 runner | runner 离线,或标签和 runs-on 对不上 |
在 Settings → Actions → Runners 确认显示 Idle,用 sudo ./svc.sh status 看服务状态 |
kxbuild: command not found |
runner 服务的用户不是装 KXBuild 的用户,或 bin 目录不在 PATH 里 | 确认服务用户,按上文写 GITHUB_PATH,安装目录换过的写实际路径 |
base64: invalid input |
Secret 里有换行或 BEGIN、END 标记 | 按上文用 -w0 或 ToBase64String 重新生成 |
No signing certificate matching '…' found |
证书目录里没有工程要求的证书类型或团队 | 确认 .p12 是 Apple Distribution,且属于工程的 DEVELOPMENT_TEAM |
| .p12 密码错误 | password.properties 里的文件名和 .p12 不一致,或密码有误 | 文件名写 distribution.p12,重新设置 IOS_P12_PASSWORD |
| 找不到描述文件 | 描述文件的 Bundle ID 和工程不一致,或工程指定了别的描述文件名 | 用 App Store 描述文件,工程指定了 PROVISIONING_PROFILE_SPECIFIER 的要和描述文件名一致 |
| Swift Package 解析失败 | Package.resolved 没提交,--skip-package-updates 无版本可用 |
本地解析后提交 Package.resolved |
| 本地能编译,CI 报找不到头文件 | Linux 区分文件名大小写,#import "myview.h" 找不到 MyView.h |
把引用改成和文件名大小写完全一致 |
| .metal 文件编译失败 | Linux 上没有 Metal 编译器 | 改用 Windows 自托管 runner 构建,见构建与配置 |
| 构建或体检报环境问题 | 工具链不完整或升级后没有体检 | 在构建机上用 runner 的用户运行 kxbuild doctor,按提示修复 |
用 GitHub Actions 构建 iOS 失败时,先看 Build ipa 这一步的完整日志,错误详情在普通输出里。排查方法见常见问题排查。
常见问题
GitHub Actions 为什么要用自托管 runner 构建 iOS?
KXBuild 需要在构建机上登录一次账号,GitHub 托管的 runner 每次运行都是新机器,不保留任何状态。自托管 runner 登录一次就能一直用,每次运行不用重新安装工具链,GitHub 也不按分钟收费。
GitHub Actions 不用 macOS runner 能打 iOS 包吗?
能。在自己的 Linux 机器上装好 KXBuild 并注册成自托管 runner,证书从 Secrets 解码到临时目录,用 kxbuild build -c Release --export app-store 就能得到签好名的 ipa,结构和 Xcode 导出的一样,可以直接上传 App Store Connect。
p12 证书怎么放进 GitHub Secrets?
先把 .p12 转成不换行的 base64 字符串存成一个 Secret,密码单独存一个 Secret。workflow 里用 base64 -d 解码写回文件,再在同目录的 password.properties 里写一行「文件名=密码」。
公开仓库能用自托管 runner 吗?
不建议。GitHub 官方建议自托管 runner 只用于私有仓库,因为任何人都能向公开仓库提交 pull request,在你的构建机上运行代码。开源项目要出包的,把打包放在一个私有仓库或只由维护者触发的流程里。
工程里有 Metal 着色器,Linux runner 能构建吗?
不能。Linux 上没有 Metal 编译器,这类工程要在装了 Metal Developer Tools for Windows 的 Windows 构建机上构建,可以在自己的 Windows 机器上注册自托管 runner。