
在移动端和客户端开发这个圈子里一直存在一个非常现实的矛盾只有 Apple 硬件才能编译和签名 iOS 应用但 Apple 硬件并不便宜更不好远程管理。很多团队的做法是买一台 Mac mini 放在机柜里有人需要打包的时候远程登录上去手动点 Xcode。遇到版本更新、证书过期、需要批量出包的时候整个流程就变得非常痛苦。于是大家自然会想到引入 CI但一搜方案又撞上另一堵墙GitHub Actions 的 macOS runner 是按分钟计费的而且代码要推到 GitHub 上Jenkins 倒是能自托管但配置越来越重插件版本、权限体系和 Pipeline 脚本维护成本都不低。如果你正在用 Gitea或者正打算在内网搭建一套轻量代码托管和 CI 服务那么 Gitea Actions 值得认真看一下。这套方案可以让你把自己手里的 Mac 变成 CI 执行节点直接在 Apple 硬件上跑 iOS 构建、macOS 打包、Xcode 测试这类任务语法风格接近 GitHub Actions团队迁移成本低而且代码仓库、CI 状态、Runner 管理都在 Gitea 一个平台里完成。这篇文章会从运行原理讲起然后完整演示如何把一台 Mac 接入 Gitea Actions最终跑通一个 iOS 构建工作流。读完你可以照着落地也能避开常见的那几个坑。1. 为什么要在 Apple 硬件上跑 Gitea Actions先回答一个更根本的问题为什么偏偏要把 CI 跑在 Apple 硬件上原因是移动端构建有硬性依赖。iOS 应用必须使用 Xcode 编译而 Xcode 只能运行在 macOS 上提交到 App Store 的分发包还需要签名签名过程涉及证书和私钥这些敏感信息放在可控的内网环境里更安全。也就是说无论你选什么 CI 系统最终编译那一步大概率还是得落到一台 Mac 上。对比一下几种常见方案的处境GitHub Actions 托管 macOS runner方便但按分钟计费且默认策略需要你把代码放到 GitHub。对于企业内部项目、未公开代码或合规要求高的环境这条路通常走不通。自建 Jenkins macOS 节点老牌方案功能强大但 Jenkins 的安装、插件管理、权限配置、节点标签、Pipeline 脚本都有一定使用门槛。团队如果只想“快速跑一个打包脚本”Jenkins 显得偏重。GitLab CI macOS runner也是一个成熟方案注册 Runner 后用 tag 控制任务分发很多团队在用。不过如果仓库本身已经迁到 Gitea再单独维护一套 GitLab 就不太合理了。Gitea Actions 的价值在于它把“代码托管、CI 触发、Runner 管理、日志查看”整合在了同一个轻量系统里。Gitea 本身非常轻量一台 2 核 4G 的小机器就能长期稳定运行Actions 功能开启后开发者只需要在仓库里放一个.gitea/workflows/*.yml文件推送代码就能自动触发构建。这套交互方式对用过 GitHub Actions 的开发者来说几乎是零成本。这篇文章的核心判断是如果你的团队已经有 Mac 设备、又在用 Gitea那么 Gitea Actions 是当前把 Apple 硬件变成 CI 节点最平滑的方案之一。它的成本主要是前期一次性的 Runner 接入收益是后续每次提交都能自动完成构建和检查。2. Gitea Actions 的运行模型与核心概念在动手操作之前有必要把 Gitea Actions 的几个核心概念讲清楚否则后面排查问题时容易一头雾水。2.1 四个关键组件Gitea Actions 的架构可以拆成四个部分Gitea 服务端负责存储仓库、提供 Web 界面、接收 Git 事件并把事件转换成 CI 任务放进队列。act_runner一个独立进程部署在某台机器上负责不断向 Gitea 服务端询问“有没有任务需要执行”。工作流文件存放在仓库.gitea/workflows/目录下的 YAML 文件描述“在什么事件触发下执行哪些步骤”。执行环境act_runner 拿到任务后在指定环境中执行工作流步骤。这个环境可以是容器也可以是 Runner 所在的宿主机。用通俗的比喻来说Gitea 是“调度中心”act_runner 是“快递员”工作流文件是“配送单”执行环境就是快递员脚下的“交通工具”。2.2 label 是什么为什么它决定任务能否跑起来act_runner 启动时会向 Gitea 服务端报告自己支持哪些 label。工作流里的runs-on字段会指定这次任务需要哪个 labelGitea 会找到匹配的 Runner 并分配任务。举个例子某台 Linux 机器上的 Runner 声明支持ubuntu-latest、linux/amd64某台 Mac mini 上的 Runner 声明支持macos-arm64、macos-latest工作流里写runs-on: macos-arm64任务只会被分配给 Mac mini 上的 Runner。很多初次使用 Gitea Actions 的用户遇到“任务一直排队但 Runner 明明在线”的问题十有八九就是 label 不匹配。这一点在 Apple 硬件场景尤其重要因为 macOS Runner 默认的 label 往往需要手动指定成平台相关的名称。2.3 与 GitHub Actions 的兼容性边界Gitea Actions 的底层执行引擎是 actnektos/act 的一个 fork/兼容实现它的设计目标是尽量兼容 GitHub Actions 的语法。也就是说name、on、jobs、steps这些核心结构可以直接沿用actions/checkout、actions/upload-artifact这类常见 action 大部分可用但部分 GitHub 云服务专属能力比如一些深度依赖 GitHub API 的 action、需要访问 GitHub Cache Service 的缓存逻辑在 Gitea 上行为可能不同。所以更稳妥的工作流写法是把重心放在run脚本上减少对第三方 action 的依赖尤其是那些和 GitHub 账号体系绑定的 action。这对在 macOS 上跑 Xcode 构建来说问题不大因为核心步骤通常就是几条xcodebuild命令。下表总结了常见平台和 Gitea Actions 的定位差异对比维度GitHub ActionsGitLab CIJenkinsGitea Actions仓库托管GitHubGitLab无独立 CIGitea托管 macOS Runner有按分钟计费有配套收费无无需自建 Runner内网部署不支持支持支持支持工作流语法YAMLYAMLPipeline/脚本GitHub Actions 风格 YAML资源占用无需自维护相对较重较重很轻3. 在 Apple 硬件上运行 act_runner模式与前置条件这一节先解决两个问题act_runner 在 macOS 上到底怎么执行任务以及开始之前需要准备哪些东西。3.1 Docker 模式还是宿主机模式act_runner 默认支持容器化执行任务也就是把每个 Job 放到一个临时容器里运行。这种方式隔离性好适合 Linux 环境比如你用一台服务器跑后端测试任务。但到了 Apple 硬件场景情况不同iOS 构建必须用到 Xcode、模拟器、钥匙串、签名证书这些在 macOS 的 Docker 容器里很难完整映射。更常见的做法是让 act_runner 直接以宿主机模式执行任务——也就是 Job 里的run命令直接在 macOS 系统上执行。这样xcodebuild能正常访问 Xcode 工具链签名时也能访问钥匙串。宿主机模式的代价是隔离性变差只要被授予了运行权限的仓库其工作流代码就能直接读写这台 Mac 的文件系统和部分系统配置。所以一个基本原则是不要把不信任的仓库指向这台 Runner更不能把公网可提交的仓库挂在宿主机模式的 Runner 上。3.2 前置条件清单部署之前建议先确认以下条件一台可用的 Mac芯片类型确认清楚Apple Silicon 还是 Intel这直接影响后面的 label 命名macOS 版本尽量满足 Xcode 的要求已安装 Xcode并且在终端里执行xcode-select -p能正常输出路径已安装 GitmacOS 自带的git一般够用一个可访问的 Gitea 服务端实例版本需要支持 Actions 功能能创建一个 Gitea 访问令牌或通过后台注册 Runner。补充一点如果只想在 Linux 环境下体验 Gitea Actions并不需要 Apple 硬件但如果你要跑 iOS 构建前面这些条件缺一不可。也有人在 Windows 上跑 Gitea配合 Windows Runner 做 .NET 构建那属于另一套配置不在本文展开。3.3 在 Gitea 服务端开启 Actions在较新版本的 Gitea 中Actions 功能通常需要在配置文件app.ini中显式开启。具体配置项会随版本变化建议以你部署的 Gitea 版本官方文档为准。基本思路是[actions] ENABLED true修改配置后需要重启 Gitea 服务。如果你是从旧版本升级还需要检查实例是否已经初始化相关数据表。这一部分如果配置不对Web 界面里不会出现 Actions 相关入口。3.4 用户 SSH 密钥管理由于私有仓库的克隆通常走 SSHRunner 在宿主机上 checkout 代码时需要用 SSH 密钥。建议在 Gitea 后台为专门的 CI 账号例如ci-bot配置 SSH 公钥并把私钥放到 Mac Runner 的~/.ssh/目录下或者在 checkout 阶段使用 HTTP 方式并在 URL 中嵌入令牌。Gitea 的用户 SSH 密钥管理很简单登录账号后进入“设置 - SSH 密钥”把公钥粘进去保存即可。不要用个人账号的高权限密钥直接放在 CI 机器上更不要把私钥提交到仓库。4. 部署 act_runner 到 Mac 的完整步骤前置条件确认无误后开始把 act_runner 装到 Mac 上。4.1 下载或安装 act_runneract_runner 是 Gitea 官方提供的 Runner 程序。可以到 Gitea 官方 Releases 页面下载对应 macOS 架构的二进制包也可以使用 Homebrew 等包管理工具安装。考虑到网络环境差异这里直接使用二进制方式演示# 假设已下载 act_runner 并放到 /usr/local/bin 下 chmod x act_runner sudo mv act_runner /usr/local/bin/act_runner # 验证版本 act_runner --version如果你的 Gitea 服务端是通过 Docker 部署的act_runner 也可以作为容器启动。但如前面所说在 macOS 上跑 iOS 构建建议用宿主机模式因此这里直接以二进制方式运行。4.2 在 Gitea 后台创建 Runner 注册令牌登录 Gitea 实例进入“管理后台 - Runner”或“站点管理 - Actions”找到创建 Runner 的入口生成一个新的注册令牌。这一步生成的 token 只用于初始注册并不会长期保存在 Runner 配置里。如果找不到管理入口可以确认一下当前账号是否有管理员权限以及 Gitea 版本是否已开启 Actions。4.3 注册 Runner 并指定 label在 Mac 终端执行注册命令act_runner register \ --instance http://gitea.example.com \ --token 这里填你的注册令牌 \ --name mac-mini-1 \ --labels macos-arm64 \ --no-interactive注意几点--instance是 Gitea 服务端地址内网环境请使用内网地址避免流量绕行--labels这里非常关键务必根据 Mac 芯片类型填写。Apple Silicon 写macos-arm64Intel 写macos-amd64--no-interactive表示非交互式注册适合脚本化执行。注册成功后当前目录会生成一个config.yaml文件这个文件保存了 Runner 的详细配置。4.4 检查并调整 config.yaml打开config.yaml重点看 Runner 的 label 和执行模式。由于不同版本配置项有差异这里只说明通用思路。需要确认 labels 和注册时一致同时如果希望任务直接在宿主机执行需要确保执行模式不是强制容器模式。一个常见问题是注册时把 label 写成了linux/amd64导致工作流里写macos-arm64时找不到 Runner。如果发现 label 写错可以直接修改config.yaml后重启 Runner或者用--reset参数重新注册。4.5 启动 Runner 并保持后台运行前台启动方便调试act_runner daemon --config config.yaml看到类似“runner started successfully”的日志后说明 Runner 已连接到 Gitea。要长期运行可以把它注册为 launchd 服务或者用nohup放到后台。考虑到 Mac 重启后 CI 服务最好能自动恢复简单起见可以写一个全局 launchd plist也可以先用 nohupnohup act_runner daemon --config config.yaml runner.log 21 更推荐的做法是配置 launchd让 Runner 在开机后自动拉起。如果你习惯用 Linux也可以把 Runner 放到一台 Linux 机器上用 systemd 管理但执行任务的 Mac 和运行 act_runner 的机器需要在同一个可访问网络内。4.6 验证 Runner 在线状态回到 Gitea 管理后台的 Runner 列表页面应该能看到名为mac-mini-1的 Runner状态为在线。如果显示离线优先检查act_runner 进程是否还活着config.yaml里的 Gitea 地址是否可达注册 token 是否过期。到这里Apple 硬件接入 Gitea Actions 的基础工作已经完成。下一步开始写工作流。5. 编写第一个跑在 macOS 上的 Actions 工作流在仓库根目录创建.gitea/workflows/ios-build.yml写入一个最小但完整的 iOS 构建工作流。5.1 最小可运行示例name: iOS Build on: push: branches: - main jobs: build: runs-on: macos-arm64 steps: - name: Checkout uses: actions/checkoutv4 - name: Show Xcode version run: | xcodebuild -version xcode-select -p - name: Build without signing run: | xcodebuild build \ -project MyApp.xcodeproj \ -scheme MyApp \ -configuration Release \ -destination generic/platformiOS \ CODE_SIGNING_ALLOWEDNO这段工作流的关键点runs-on: macos-arm64必须与 Runner 注册时的 label 完全一致actions/checkoutv4用于拉取代码如果你的内网环境访问 GitHub 不稳定建议使用 Gitea 兼容的 checkout 方式或者手动执行git cloneCODE_SIGNING_ALLOWEDNO适用于先验证编译是否通过不代表最终打包可以省略签名。5.2 增加归档和导出 IPA 的完整流程实际发布场景下通常需要导出可分发的 IPA 文件。下面示例增加了 archive 和 exportArchive 两步name: iOS Archive and Export on: push: tags: - v* workflow_dispatch: jobs: archive: runs-on: macos-arm64 steps: - name: Checkout uses: actions/checkoutv4 - name: Archive run: | mkdir -p build xcodebuild archive \ -project MyApp.xcodeproj \ -scheme MyApp \ -configuration Release \ -archivePath build/MyApp.xcarchive \ CODE_SIGNING_ALLOWEDNO - name: Export IPA run: | xcodebuild -exportArchive \ -archivePath build/MyApp.xcarchive \ -exportOptionsPlist ExportOptions.plist \ -exportPath build/ipa - name: Upload IPA uses: actions/upload-artifactv4 with: name: MyApp-ipa path: build/ipa/*.ipa如果项目还没有导出选项文件需要先在 Xcode 中生成一份ExportOptions.plist或者手动创建并指明导出方式App Store / Ad Hoc / Development。5.3 处理签名证书签名是 iOS 自动构建里绕不开的一步。常见做法是把.p12证书文件和描述文件放到 Runner 机器的一个安全目录或者作为 Base64 环境变量传入在构建前把证书导入钥匙串并设置钥匙串为默认搜索路径在xcodebuild命令中指定DEVELOPMENT_TEAM和PROVISIONING_PROFILE_SPECIFIER。示例片段- name: Import signing certificate env: CERT_BASE64: ${{ secrets.CERT_BASE64 }} CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }} run: | echo $CERT_BASE64 | base64 --decode /tmp/signing.p12 security create-keychain -p ci temp.keychain security import /tmp/signing.p12 -k temp.keychain -P $CERT_PASSWORD -A security set-key-partition-list -S apple-tool:,apple: -k ci temp.keychain security list-keychains -d user -s temp.keychain这里涉及很多团队特定的证书管理方式需要根据实际情况调整。注意secrets需在 Gitea 仓库或组织设置中提前配置并且不要把证书密码写死在 YAML 里。5.4 把工作流放到 Gitea 后如何触发将 YAML 文件提交并推送后on.push会立即触发一次任务。也可以先不写触发事件使用 Gitea Web 界面上的 “Run workflow” 按钮手动触发前提是配置了workflow_dispatch。6. 运行结果与效果验证工作流提交后在 Gitea 仓库页面的 “Actions” 标签页里应该能看到新出现的运行记录。点击进入可以看到 job 列表和每个 step 的实时日志。正常情况下你会看到Checkout 步骤成功结束xcodebuild 输出 Xcode 版本信息Build without signing 步骤返回BUILD SUCCEEDED如果配置了 artifact 上传运行结束后可以下载到 IPA 或 xcarchive。如果 Runner 一直处于等待状态优先检查runs-on标签是否与 Runner 注册的 label 一致。在线 Runner 但任务无人接收大多是这种情况下发生的。在 Mac 终端也可以观察 act_runner 的实时日志。使用tail -f runner.log能看到 Runner 是否收到任务、是否开始执行工作流、每个 step 的开始和结束时间。这一步对排查问题非常有帮助比反复刷新 Web 页面更直接。7. 常见问题与排查思路以下是在 Apple 硬件上跑 Gitea Actions 时最容易遇到的问题按经验整理成表格。问题现象可能原因排查方式解决方案Runner 在线但任务一直 pending工作流runs-on与 Runner label 不一致查看 Runner 详情页的 label检查 YAML 中的runs-on修改 YAML 或重新注册 Runner保证两者完全匹配Runner 显示离线进程退出、网络不通、token 失效查看 act_runner 日志ping Gitea 地址重新启动 daemon必要时重新注册Checkout 失败提示无权限SSH 密钥未配置或密钥权限过高在 Runner 机器执行 git 克隆测试为 CI 账号配置 SSH 公钥或使用内置令牌 HTTP 克隆提示 xcodebuild 找不到Command Line Tools 指向错误执行xcode-select -p执行sudo xcode-select -s /Applications/Xcode.app/Contents/DeveloperBuild 报签名相关错误未配置签名参数、证书未导入查看签名 Error 中的具体描述先用CODE_SIGNING_ALLOWEDNO验证编译再补签名配置Job 在容器中执行找不到 Xcode使用了容器模式跑 macOS 任务检查 act_runner 执行模式配置改为宿主机模式并确保工作流使用宿主机 RunnerAction 内部的脚本使用 GitHub API 失败第三方 action 依赖 GitHub 云服务查看具体 action 的日志换成 Gitea 兼容的 action或直接用run写命令导出的 IPA 缺少描述文件ExportOptions.plist 配置不正确对比 Xcode 本地导出配置从 Xcode 重新生成 ExportOptions.plist排查时有一条通用路径先看 Gitea Web 日志再看 act_runner 日志最后到 Mac 本机手动执行 Xcode 命令。把手工能跑通的命令一步一步还原到工作流里问题通常出在环境变量、PATH 或密钥访问权限上。8. 最佳实践与工程建议8.1 label 命名规范建议从一开始就建立一套清晰的 label 规则避免 Runner 一多就混乱。例如macos-arm64Apple Silicon 通用任务macos-amd64Intel Mac 通用任务macos-arm64-ios专门承担 iOS 构建的 Apple Silicon 节点。工作流中使用更具体的 label可以有效避免任务被调度到不合适的机器上。8.2 权限与安全边界宿主机模式的 Runner 拥有较高系统权限必须严格控制能触发任务的仓库范围。建议将 Runner 注册在组织级或仓库级而不是所有仓库共享不使用个人高权限账号的 token 存放 Runner定期轮换 Runner 注册令牌和 CI 机器人账号的访问令牌存放证书和私钥的目录只允许 CI 机器人账号读取。8.3 不要在一台 Mac 上跑过多并发Apple 芯片性能虽强但 iOS 构建是 CPU、磁盘高度密集的任务。如果一台 Mac 同时被多个 job 占满会出现构建时间飙升、模拟器资源冲突等问题。建议在 Gitea Runner 配置中设置并发数限制给每台机器留出余量。8.4 与现有 CI/CD 链路的衔接很多团队已经有 Jenkins 或 Drone也有的在用 Gitea 配合 Harbor、Docker 和 Nginx 搭建完整发布链路。Gitea Actions 适合作为这整条链路里的“触发与构建”环节Gitea 负责代码托管和事件触发Actions 负责在 Mac 上完成 iOS/macOS 构建或者在后端仓库里完成 Java 包构建、Docker 镜像构建产物可以推送到 Harbor 镜像仓库或内部文件服务部署环节可以继续由 Drone、Jenkins 或专门的发布系统负责。如果你的团队目前是 “Jenkins Gitea 实现 Spring Boot 打包部署”不要急于推翻 Jenkins可以先从 iOS 构建这种 Jenkins 配置成本高的任务切入逐步把 Gitea Actions 引入流程。8.5 日常维护清单定期检查 macOS 系统更新和 Xcode 版本兼容性为 Runner 机器保留健康监控磁盘不足时构建会莫名失败备份config.yaml和 CI 机器人账号的配置Gitea 服务端升级前先确认新版本对 Actions 的兼容性。9. 总结与后续实践方向这篇文章从“为什么 CI 需要 Apple 硬件”这个痛点出发讲清楚了 Gitea Actions 的架构、act_runner 的执行模式、label 匹配机制以及如何把一台 Mac 完整接入 Gitea 并跑通 iOS 构建工作流。核心结论是Gitea Actions 真正降低的是自托管 CI 的接入门槛尤其是对已有 Mac 设备和 Gitea 仓库的团队可以用很少的成本获得一套接近 GitHub Actions 使用体验的构建系统。下一步建议你从最小示例开始先提交一个只打印 Xcode 版本的工作流确认 Runner 调度正常再逐步加入签名、归档、导出和产物上传。跑通之后可以继续研究这套体系下的缓存策略、私有 action 管理、多 Runner 调度和与 Harbor/Drone 的联动。自托管 CI 的安全边界始终不能放松Runner 一旦暴露给不可信任务风险是真实存在的。