Gitea Actions 可以构建 iOS 应用,但只能在你自己注册的 Mac 上构建,因为 Gitea 不提供托管 runner,且 Xcode 只能在 macOS 上运行。两种实用方案是:一台运行在 act_runner host 模式下的 Mac,或者一台将 iOS 构建发送到云构建服务的普通 Linux runner。本指南会针对 Capacitor 应用逐一介绍这两种方案,并提供可直接放入以下位置的 workflow 文件: .gitea/workflows/.
团队选择 Gitea,是因为希望将 code 部署在自己可控的基础设施上。仅仅因为应用需要 Xcode,这一点就不应改变。以下两种配置都能确保源代码保留在你的服务器上。
Gitea Actions 如何运行任务
Gitea Actions 使用与 GitHub Actions 兼容的语法,任务运行在 act_runner 你向 Gitea 服务器注册的实例上。每个 runner 都会声明标签,例如 ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest。冒号后的部分决定了任务的运行方式:
docker://image在容器中运行任务。这是 Linux runner 的默认行为。host直接在机器上运行任务,使用机器上已安装的环境。
macOS 无法在 Docker 容器中运行,因此 Mac runner 必须使用 host 标签。这会带来一些影响,我们稍后会讲到。
在从 GitHub 复制工作流之前,请注意以下两点兼容性说明:
- Gitea 会读取
.gitea/workflows/,如果第一个文件夹不存在,则回退到.github/workflows/。 uses: actions/checkout@v4之所以有效,是因为 Gitea 默认从 GitHub 解析 actions(即DEFAULT_ACTIONS_URL设置)。隔离实例需要镜像其使用的 actions,并通过完整 URL 引用它们。
方案 1:使用 act_runner 的 Mac runner
注册 Mac
从 Firebase Console 下载 act_runner 从 Gitea 发布页面下载 macOS 的二进制文件(Apple Silicon 为 arm64),然后从 站点管理 > Actions > Runners (实例级)或组织/仓库设置中获取注册 token。
./act_runner register --no-interactive \
--instance https://gitea.example.com \
--token <registration_token> \
--name mac-mini-01 \
--labels macos-arm64:host
./act_runner daemon
以专用用户身份将守护进程作为 launch agent 运行,以便在重启后自动恢复,并拥有独立的登录钥匙串。
在宿主机上安装工具链
Host 模式表示任务使用 Mac 自身的工具。请安装:
- Xcode 26(自 2026 年 4 月 28 日起,App Store Connect 上传必须使用)并运行
sudo xcodebuild -license accept - Node.js 22 或更高版本,以及 Bun(Capacitor 8 需要 Node 22)
- Ruby 和 fastlane,或者使用
Gemfile配合bundle exec - CocoaPods,仅当你的 iOS 项目仍在使用时。新的 Capacitor 8 项目使用 Swift Package Manager。
该工作流
# .gitea/workflows/ios-mac.yaml
name: iOS release (Mac runner)
on:
push:
tags:
- 'v*'
jobs:
ios:
runs-on: macos-arm64
env:
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
APP_STORE_CONNECT_TEAM_ID: ${{ vars.APP_STORE_CONNECT_TEAM_ID }}
PROFILE_NAME: ${{ vars.IOS_PROFILE_NAME }}
steps:
- uses: actions/checkout@v4
- run: bun install --frozen-lockfile
- run: bun run build
- run: bunx cap sync ios
- name: Decode signing files
run: |
echo "${{ secrets.IOS_CERTIFICATE_BASE64 }}" | base64 --decode > dist.p12
echo "${{ secrets.IOS_PROFILE_BASE64 }}" | base64 --decode > profile.mobileprovision
- run: bundle exec fastlane ios beta
- name: Clean up
if: always()
run: rm -f dist.p12 profile.mobileprovision
本指南中介绍的 beta lane 会将证书导入临时 keychain,把项目切换为手动签名,从 TestFlight 递增 build number,归档并上传。Capacitor 项目的完整 Fastfile 见 使用 Bitbucket Pipelines 构建并部署 iOS 应用;它在 Gitea 上同样适用。关于 fastlane 的背景知识,请参阅 GitHub Actions iOS 指南.
Host 模式的成本
本指南中介绍的 act_runner 官方文档对此直言不讳:host jobs 并非隔离的。实际上:
- 所有 job 共享相同的用户、磁盘和 keychain。中途失败的 job 可能会留下临时 keychain 或过期的 provisioning profile,导致下一个 job 出错。
- 任何针对该 label 的 workflow 都会以该用户的权限在那台 Mac 上运行 code。仅为受信任的仓库注册 host runners,并避免在来自 fork 的 pull request 上运行它们。
- DerivedData、archives 和 simulator runtimes 会不断增长,直到磁盘写满。
- Xcode 升级由你负责,而 Apple 每年春天都会提高最低 SDK 要求。
- 一台 Mac 一次只能构建一个 job。发布日就会排队。
如果你的团队已经拥有 Mac 且有人负责维护,这行得通。否则,下一个选项可以将 Mac 从你的基础设施中移除。
方案 2:通过 Linux runner 使用 Capgo Build 构建 iOS
Capgo 构建 在 Capgo 托管的 Mac 上编译并签名 Capacitor 应用。您的 Gitea 任务仍运行在现有的 Linux runner 上。它会检出 code,构建 web 层,并运行 cap sync ios,然后调用 build request。Capgo CLI 会上传准备好的原生项目,将 Xcode 日志流式传输回 Gitea 任务日志,并在失败时返回非零退出码。
这种模式非常适合自托管 Gitea:
- 无需入站访问。 Capgo 不会克隆您的仓库。您的 runner 通过 HTTPS 向外推送准备好的项目,因此位于防火墙或 VPN 后的 Gitea 服务器无需任何更改即可工作。
- 无需仓库 token。 Capgo 端无需配置任何 Git 连接。
- 您的 web 源码保留在本地。 仅上传原生
ios/文件夹、Capacitor 配置以及插件的原生部分。src/,.git/,以及.env文件不会上传。
一次性设置
在开发机上:
bunx @capgo/cli@latest login
bunx @capgo/cli@latest app add
bunx @capgo/cli@latest build init --platform ios
build init 创建或复用您的分发证书和 App Store provisioning profile,并保存 App Store Connect API 密钥。在本地运行一次 build,确认签名正常:
bun run build && bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
然后导出凭据:
bunx @capgo/cli@latest build credentials manage --appId com.example.app --platform ios
# select "Export to .env"
在 Gitea 中添加 Secrets
打开 仓库设置 > Actions > Secrets 并添加导出文件中的每个密钥,以及您的 Capgo API 密钥:
| 密钥 | 用途 |
|---|---|
CAPGO_TOKEN |
具有 upload 权限的 Capgo API 密钥 |
BUILD_CERTIFICATE_BASE64 |
分发证书(.p12) |
P12_PASSWORD |
证书密码 |
CAPGO_IOS_PROVISIONING_MAP_BASE64 |
描述文件映射 |
APPLE_KEY_ID |
App Store Connect API 密钥 ID |
APPLE_ISSUER_ID |
App Store Connect Issuer ID |
APPLE_KEY_CONTENT |
Base64 .p8 密钥 |
APP_STORE_CONNECT_TEAM_ID |
Apple 团队 ID |
Gitea 拒绝以 GITEA_ 或 GITHUB_开头的密钥和变量名,因此请保持这些名称不变。将应用 ID 等非机密值放在 变量 并读取它们 ${{ vars.NAME }}。在组织级别设置的 Secrets 会被该组织下的所有仓库共享,这对于从同一个 Apple 团队发布多个应用非常方便。完成后请删除导出的文件。
推送 tag 时发布
# .gitea/workflows/ios-release.yaml
name: iOS release
on:
push:
tags:
- 'v*'
jobs:
release-ios:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run build
- run: bunx cap sync ios
- name: Build, sign, and upload to TestFlight
env:
CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
CAPGO_IOS_PROVISIONING_MAP_BASE64: ${{ secrets.CAPGO_IOS_PROVISIONING_MAP_BASE64 }}
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
run: |
bunx @capgo/cli@latest build request ${{ vars.APP_ID }} \
--platform ios \
--build-mode release \
--ios-distribution app_store \
--store-release-name "${{ gitea.ref_name }}"
注意:
${{ gitea.ref_name }}是 tag 名称。这个github.*context 也可以作为别名,这在迁移 workflow 时很有用。- Gitea 会设置
CI=true在每个 job 中,因此 Capgo CLI 永远不会等待交互式输入。 - Capgo 会基于最新的 TestFlight build 递增 build number,这样连续两个 tag 也不会冲突。
- 如果不使用
--submit-to-store-review,build 会进入 TestFlight 并停止。如果你希望 CI 也提交 App Review,请加上该 flag。
现在发布流程是 git tag v2.3.0 && git push origin v2.3.0:任何能推送 v* tag 的人都能发布,因此请添加受保护的 tag 规则,针对 v* 在……下 设置 > 标签 并限制为发布经理。
测试人员的 Ad hoc 构建
对于直接安装在已注册设备上的 QA 构建,请使用 ad hoc 模式构建并获取下载链接:
# .gitea/workflows/ios-adhoc.yaml
name: iOS ad hoc build
on:
workflow_dispatch:
jobs:
adhoc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile && bun run build
- run: bunx cap sync ios
- name: Ad hoc build
env:
CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
run: |
echo "${{ secrets.ADHOC_PROFILE_BASE64 }}" | base64 --decode > adhoc.mobileprovision
bunx @capgo/cli@latest build request ${{ vars.APP_ID }} \
--platform ios \
--ios-distribution ad_hoc \
--ios-provisioning-profile ./adhoc.mobileprovision \
--output-upload --output-retention 3d \
--output-record /tmp/build.json
bunx @capgo/cli@latest build last-output --path /tmp/build.json --field outputUrl
Ad hoc 构建需要一个列出测试设备的 ad hoc 描述文件。将该描述文件以 base64 编码存储为 secret(ADHOC_PROFILE_BASE64 如上所示),在任务中解码它,并通过 --ios-provisioning-profile。CLI 标志优先于环境变量,因此这仅针对此构建覆盖 App Store 描述文件。使用 iOS UDID 查询工具。对于大多数测试人员来说,TestFlight 内部测试更简单,因为它不需要 UDID。
手动发布审批
GitHub 允许你为环境设置审核者要求。Gitea Actions 未实现环境保护规则,因此指定环境的任务不会暂停等待审批。可以改用以下两种模式:
- 受保护的标签。 只有发布经理可以推送
v*标签,然后由标签工作流发布。 - 构建,然后晋级。 每次推送到以下分支时构建
main配合--ios-distribution ad_hoc或仅上传至 TestFlight 进行内部测试,然后使用workflow_dispatchworkflow(当前 Gitea 版本已支持),以重新构建已批准的 commit,并使用--submit-to-store-review.
无需新二进制文件即可发布 Web 变更
大多数对 Capacitor 应用的提交只更改 JavaScript、CSS 和 HTML。为这些变更重新构建 iOS 会浪费几分钟和 App Review 时间。通过 Capgo 实时更新,一个任务即可选择正确的路径:
- name: Live update or native build
env:
CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
# plus the iOS signing secrets from above
run: |
if bunx @capgo/cli@latest build needed ${{ vars.APP_ID }} --channel production; then
bunx @capgo/cli@latest bundle upload ${{ vars.APP_ID }} --channel production
else
bunx cap sync ios
bunx @capgo/cli@latest build request ${{ vars.APP_ID }} --platform ios --build-mode release
fi
build needed 当原生依赖与 channel 上当前生效的内容一致时以 0 退出,需要新的 binary 时以 1 退出。此外,当以下路径下的文件发生变化时,也会强制走原生路径: ios/ 或 capacitor.config.* 因为这些文件不参与依赖对比。参见 自动选择 live update 或 Native Build.
Mac runner 与云端 build 对比
| Mac 上的 act_runner | Capgo 从 Linux 构建 | |
|---|---|---|
| 硬件 | 自行购买并托管 | 无 |
| Xcode 升级 | 手动,逐台机器 | 由 Capgo 处理 |
| 任务间隔离 | host 模式下无隔离 | 每个任务使用全新的构建环境 |
| 并行构建 | 每台 Mac 一个 | 多个,可来自任意 runner |
| 签名文件 | 存放在 Gitea secrets 中,在 Mac 上解码 | 存放在 Gitea secrets 中,随每次构建发送 |
| 可在防火墙后运行 | 是 | 是的,仅限出站 HTTPS |
| 原生 Swift 应用(不含 Capacitor) | 是 | 否,仅限 Capacitor 应用 |
| 构建时间限制 | 由你设置 | 每次构建 10 分钟 |
故障排除
| 现象 | 修复 |
|---|---|
| 任务会一直排队 | 没有带该标签的在线 Runner runs-on。检查 runner 列表和 label 拼写。 |
uses: actions/checkout@v4 下载失败 |
Runner 无法访问 GitHub。请在 Gitea 上镜像该 action,并通过完整 URL 引用。 |
| Job 中的 Secret 值为空 | 名称开头为 GITEA_/GITHUB_,或者该 secret 定义在另一个仓库中。 |
security: SecKeychainItemImport: MAC verification failed |
错误 .p12 密码,或者一个 .p12 使用 OpenSSL 3 创建且没有 -legacy。请重新导出。 |
No profiles for 'com.example.app' were found |
Secrets 中的 profile 与 bundle ID 或证书不匹配。请重新运行 build init 并重新导出。 |
| SDK 版本上传被拒绝 | Mac runner 上的 Xcode 版本仍低于 26。 |
cap sync ios 在 Linux 上对 CocoaPods 发出警告 |
符合预期。Capgo 构建机会安装 pods。 |
更多故障模式,请参阅 Capacitor 的 CI/CD:常见陷阱 以及 Capgo 构建故障排除指南.
总结
一旦确定 Xcode 在哪里运行,Gitea Actions 处理 iOS 就没问题。host 模式的 Mac runner 给你完全的控制权,也意味着完全的责任。用 Linux 任务调用 Capgo Build,Gitea 配置不用改,无需入站访问,还能把 TestFlight 发布变成一次 tag push。无论选哪种,签名材料都放在 Gitea secrets 里,保护好 release tag,纯 Web 的改动走 live updates 发布,这样原生 pipeline 只在必要时才跑。