
如果你是在自己的 Mac mini 或者 MacBook Pro 上第一次尝试把 Gitea Actions 跑起来通常会经历一个很有意思的过程注册 runner、创建 workflow、提交一个echo hello的任务看到绿色后觉得大功告成。但等到你真正把一个需要 Xcode 编译、代码签名、甚至公证的应用构建丢进去它开始报各种奇怪的错误时你才会意识到在 Apple 硬件上跑 Gitea Actions真正的难点从来不是把 runner 装好而是怎么让整个苹果生态的构建流程稳定地运行在一台没有鼠标键盘、常年不关机的硬件上。这篇文章想说的核心判断是把 Gitea Actions 搬到 Apple 硬件本质上不是一次安装任务而是一次把本机手动构建流程“工业化”的过程。它的收益不是省几分钟而是可复用、可追溯、可协作的构建能力它的代价不是命令难学而是证书、环境、权限、睡眠、更新、存储这些细节的长期维护。1. 为什么要在 Apple 硬件上跑 Gitea Actions一个常被低估的需求很多人提到自托管 CI第一反应是 Linux 服务器上跑几个 Docker 容器。但有一类场景绕不开 macOS只要你的交付物是 iOS App、macOS 应用、Homebrew 包或者需要调用 Xcode 工具链就必须有一台能运行完整苹果系统的机器来执行构建。Gitea Actions 在 Apple 硬件上的需求通常不是“想尝鲜”而是刚需。一个典型团队可能已经有 Gitea 仓库代码里包含 iOS 客户端和 macOS 工具过去每次发版都要开发者在自己的 Mac 上手动执行xcodebuild打出一个包再用脚本上传。这个过程看起来很高效实际上一旦换人、换机器、换 Xcode 版本就会开始出现“我这边能编译你那边不行”的经典问题。1.1 需要苹果平台 CI 的真实场景最常见的触发点有几个仓库里有*.xcodeproj或*.xcworkspace需要执行xcodebuild、xcodebuild test。需要产出.app、.dmg、.pkg或者 iOS.ipa。需要使用 XCTest/XCUITest 跑 UI 测试尤其是模拟器上的自动化测试。需要执行swift build、swift test或者集成 Swift Package Manager 相关流程。需要调用 Apple 提供的签名、公证工具例如codesign、notarytool。这些任务在 Linux runner 上无法完成因为苹果官方工具链只运行在 macOS 上而且往往是和系统版本、Xcode 版本强绑定的。你不能用“交叉编译”绕过整套运行时尤其是签名、公证、模拟器测试这些环节。这些需求一旦出现自托管 runner 的必要性就成立你可以把 Xcode 构建和测试从开发者本机挪到一台固定的 Mac 上所有人都通过提交代码触发而不是依赖某个人本地的钥匙串和目录结构。从团队角度看这才是 Gitea Actions 在 Apple 硬件上真正的价值。1.2 它和通用 Linux Runner 的核心差异在 Linux 上跑 Gitea Actions最常见的做法是用 Docker 容器每个 job 可以指定不同的镜像相互隔离跑完即销毁。但在 Apple 硬件上这种体验会明显不同。第一macOS 并不是一个默认可以用 Docker 隔离的操作系统。虽然 Docker 客户端可以跑在 macOS 上但它底层其实是启动了一个 Linux 虚拟机而这个虚拟机不能直接执行 Xcode、模拟器、签名工具除非你再做一层虚拟化方案。所以绝大多数人采用的是“原生 runner”模式act_runner 直接跑在 macOS 系统上Gitea 下发任务后job 直接在这个宿主机上执行 shell 命令。第二资源隔离变弱。Linux 上可以给每个容器限制 CPU、内存、磁盘macOS 上多个 job 如果并发跑都是直接消耗这台机器的物理资源。一旦有两个构建同时执行Xcode 的内存占用很容易把 16GB 的 Mac mini 压满。你不能简单用容器参数去限制一个xcodebuild进程。第三系统状态不稳定。台式 Mac 可能休眠MacBook 可能合盖系统会自动更新用户会不小心退出登录。这些看似不是编程问题的问题会直接决定你的 CI 稳定不稳定。一个 runner 如果因为屏幕锁或睡眠而迟迟不执行任务体验会非常糟糕。所以在 Apple 硬件上跑 Gitea Actions本质上是在维护一台“常驻构建机”而不是“一个容器调度器”。这个认知转变很重要它会决定你后续把精力放在哪里。2. 先搞清楚 Gitea Actions 在苹果平台上的架构与运行方式在动手前先花十分钟理解 Gitea Actions 的运行模型能避免很多“把所有问题都归结为命令报错”的弯路。2.1 Gitea Actions 的基本调度模型Gitea Actions 的运行逻辑可以概括为Gitea 实例是控制中心负责接收 Git 事件、解析 workflow、生成任务runner 是执行节点负责领取任务并执行。workflow 文件通常放在仓库的.gitea/workflows/目录下用 YAML 描述。里面会定义事件触发条件比如 push、pull_request以及要跑什么 job。每个 job 里有一个runs-on字段表示这个 job 需要被什么样标签的 runner 接走。runner 在注册时可以声明自己的标签。比如一个跑在 Apple Silicon Mac mini 上的 runner可以注册为macos-arm64那么在 workflow 里写runs-on: macos-arm64任务就会被这台机器接走。如果写macos-latest但 runner 没有这个标签任务就会一直卡住没有 runner 可执行。这里有个新手容易忽视的点runner 的标签不是自动匹配操作系统的。你需要自己在注册时确定标签并且让 workflow 和 runner 的标签保持一致。第一次配置时宁可手动指定一个明确标签也不要依赖看似默认的标签。act_runner 是目前常见的 runner 客户端。它和 Gitea 服务器之间通过 token 建立信任关系注册后会持续从 Gitea 拉取任务。你可以把它理解成一个“远程执行代理”但没有它自己的文件系统隔离层。2.2 为什么在 Apple 硬件上通常不采用容器方式我第一次在 Mac 上尝试用 Docker 跑 runner 时确实跑通了“注册”这一步但很快就发现 Xcode 构建没法用容器里没有 macOS 系统也没有 Xcode。后来意识到在 Apple 硬件上runner 的第一种常见形态是“原生进程”把 act_runner 直接下载到 Mac 上运行任务里的命令直接在 macOS 环境里执行。第二种形态是做 macOS 虚拟机。如果你手头只有一台 Mac但想同时跑多个相互隔离的 job可以在宿主机上通过虚拟化软件创建多个 macOS 虚拟机每个虚拟机里再装一个 runner。这种方案隔离性好但资源开销大维护成本也高。对于多数小团队和中小项目第一台构建机不需要一上来就上多虚拟机折腾。为什么不能像 Linux 一样用 Docker核心原因是 macOS 的许可和系统架构都决定了它不适合作为公共基础容器镜像被随意分发。你可以在 macOS 上用 Docker Desktop但那是个 Linux VM不是用来跑 Xcode 构建的。所以不用在这个方向上较劲直接接受“runner 跑在宿主系统”这个现实就好。从架构上看还要理解 job 的工作目录、执行用户和环境变量。原生 runner 会以当前用户身份执行命令这意味着它的权限范围、钥匙串访问、文件读写能力都和你启动它时的用户有关。这一点在后续配置签名和证书时尤其重要。3. 从零开始在 Apple Hardware 上部署 Runner 的最小可行流程如果你是第一次做这件事不要一上来就追求高并发、多配置。先搭一个最小的能跑通模型确认链路完整再逐步往里面加复杂度。3.1 前置准备机器、系统、账号、标签硬件上最合适的起点不是一台每天要用来开发、随时可能关机或重启的笔记本而是一台能常驻运行的 Mac mini 或 Mac Studio。虽然 MacBook Pro 也能跑但“插电、不睡眠、不移动”对 CI 来说非常重要。系统层面需要先确认macOS 版本能够安装你要使用的 Xcode 版本。已安装完整 Xcode并且在终端执行xcode-select -p能看到路径。已安装 Git以及可能出现的基础命令行工具。如果只是验证 runner不构建复杂应用用 Command Line Tools 也可以。但只要你接下来要跑 Xcode 工程建议直接装完整 Xcode。它体积很大但这是苹果构建链绕不开的一部分。然后是账号。我不建议直接使用日常办公账号来跑 runner最好在 Mac 上建一个专用用户比如叫ci或builder。用普通权限就好不要给管理员。这样万一 job 里出现破坏性命令影响范围会小一些。当然如果签名和钥匙串需要管理员权限需要单独评估策略但至少先保持最小权限。注册 runner 前需要一个 Gitea 管理员生成的注册 token。如果你是 Gitea 实例的管理员可以在管理后台的“Actions”相关页面看到 Runner 管理入口。注册后runner 会拿到自己的 ID 和 secret。标签建议直接写macos-arm64或macos-x64取决于你的 CPU 架构。不要只写macos因为未来你可能会有多台不同架构的 Mac标签区分越细越容易在 workflow 里精确指定。3.2 注册并启动 Runner然后跑通第一条任务假设你已经下载了对应架构的 act_runner 二进制常见写法是执行一条 register 命令。命令的大致结构是./act_runner register --instance https://gitea.example.com --token 注册token --name mac-mini-1 --labels macos-arm64注意不同版本的 act_runner 参数可能略有差异具体以官方文档为准。这里展示的是常见结构不是某个固定版本的绝对命令。注册完成之后一般还需要初始化配置文件然后启动守护进程./act_runner daemon如果这一步只是在前台运行测试完可以退出。但如果你想让 runner 长期服务下一节会提到怎么做成后台服务。runner 在线后在仓库里创建.gitea/workflows/check.yml写入一个最小任务例如name: Apple check on: push: jobs: check: runs-on: macos-arm64 steps: - name: Checkout uses: actions/checkoutv4 - name: Show Xcode version run: | xcodebuild -version swift --version推送后Gitea 会尝试调度这个 job 到你的 runner。你在 runner 的日志里应该能看到任务被领取、执行、成功完成的流程。这步跑通后先别急着加复杂构建。我建议你在这个阶段确认几个信息runner 进程是不是能持续运行会不会因为终端关闭而退出。执行 job 的用户是什么whoami的输出是否符合预期。Xcode 版本路径是否正确xcode-select -p是否指向你期望的 Xcode。这些听起来很基础但它们是后续所有复杂配置的底座。底座歪了后面所有签名、缓存、并发都会跟着出问题。4. 真正卡人的不是安装而是签名、权限与苹果生态集成当一个 runner 能稳定执行xcodebuild -version之后真正的工程挑战才开始。很多人在这里会遇到一种挫败感明明本机能构建成功换成 runner 就各种失败。原因往往不在模板语法而在苹果生态的集成细节。4.1 代码签名与证书管理CI 中最容易“看似成功实则失败”的环节如果你的构建目标涉及 iOS App、macOS 应用的 Release 包大概率需要代码签名。签名需要开发者证书和对应的私钥而这些信息在 macOS 上是存放在钥匙串里的。本机开发时你可以手动安装证书、信任证书、输入密码但在 CI 环境里没有这些交互。常见做法是先把.p12证书导出再在构建脚本里导入 runner 使用的钥匙串并设置钥匙串不需要每次弹窗解锁。通常涉及两条命令security import Certificate.p12 -k ~/Library/Keychains/login.keychain-db security set-keychain-settings ~/Library/Keychains/login.keychain-db但这里有几个隐藏问题。第一.p12文件本身包含私钥属于高敏感信息不能直接提交到 Git 仓库。应该放进 Gitea 的 Secrets或者只在 runner 机器本地上保存并且严格控制读取权限。第二runner 执行 job 时使用的用户和钥匙串必须和导入证书时一致。你如果在一台机器上用管理员账号导入了证书但 runner 以ci用户运行job 里可能根本看不到这个证书。在实际工作流里我见过很多团队卡在“证书能在钥匙串里看到但 xcodebuild 总是报证书不存在”。这通常不是证书真的不存在而是 Xcode 在找证书时搜索的钥匙串或 team ID 和预期不一致。建议先在 workflow 里临时加一句security find-identity -v -p codesigning看看 runner 环境里到底有哪些可用的签名身份。如果你还要做公证notarization会涉及到更隐私的凭据。现在常用的方式是使用 App Store Connect API Key 或 Apple ID。无论哪种都不要直接写在 YAML 文件里。更稳妥的做法是通过 Gitea Secrets 注入环境变量然后由构建脚本读取。构建过程要避免把日志里的敏感信息打出来。4.2 Xcode 版本、路径和构建缓存一台 Mac 上可能同时存在多个 Xcode 版本。本机开发时你可以随手用open -a选择某一个但在 CI 里必须明确指定用哪个 Xcode。常见方案是在 job 里执行sudo xcode-select -s /Applications/Xcode_15.2.app但要注意如果你在同一台 runner 上并行跑多个 job每个 job 都去改全局xcode-select就会互相干扰。一个 job 把路径切到 Xcode 15.2另一个 job 同一时间又切到 Xcode 16.0最后构建到底用哪个版本就变成随机事件。更安全的思路是尽量为每个 job 固定所需的工具链路径。很多构建工具支持通过环境变量指定开发者目录常见的是export DEVELOPER_DIR/Applications/Xcode_15.2.app/Contents/Developer然后在 workflow 里让每个 job 独立设置DEVELOPER_DIR不互相影响。除了 Xcode 版本构建缓存也是长期使用中绕不开的话题。Xcode 构建会产生大量 DerivedDataSwiftPM 和 CocoaPods 也会生成缓存。如果每次构建都从零开始你会看到一台 16GB 内存的 Mac 在跑了几次构建后磁盘迅速变小。正确的做法是先跑通后再加缓存策略例如把~/Library/Developer/Xcode/DerivedData、~/Library/Caches/org.swift.swiftpm等目录纳入备份或清理体系。但注意不要盲目把整个缓存目录从一个机器复制到另一台机器因为路径和系统版本不同可能带来反效果。这里更推荐的是“先清理不稳定缓存再保留可靠缓存”而不是一味追求缓存命中率。4.3 电源、登录、自动更新与长期驻留这是非编程问题但它们往往比编程问题更致命。一个常驻 CI 的 Mac 需要关闭系统休眠。你可以在“系统设置 - 电池”或使用caffeinate命令来防止休眠。对 Mac mini 这类台式设备来说通常不需要合盖问题但如果用的是 MacBook很需要外接电源并把“合盖休眠”这个行为处理好。另外不要忽略系统自动更新。macOS 自动更新一旦触发可能下载几个 GB 的更新包并在半夜自动重启。等第二天早上你发现所有 job 都在等待 runner 上线时才会意识到这里没处理。建议在系统设置里把自动更新关掉或者至少设置为“下载但不自动安装”然后手动控制更新窗口。runner 本身需要长期运行。最简单的做法是把它注册为 launchd 服务并设置 KeepAlive这样即使机器重启runner 也会自动拉起。常见 plist 结构里会包含运行用户、执行命令、日志输出路径等字段。具体字段各家版本略有差异但思路一致不要依赖“手动打开终端跑 act_runner”。这些维护工作在刚开始会显得麻烦而且不会立刻产生“肉眼可见”的收益。但你要明白自托管 CI 的核心不是“跑通一次”而是“稳定运行几百次”。一台会因为休眠、更新、钥匙串锁定而中断的构建机最终会让团队重新回到手动构建的老路。5. 什么时候值得用什么时候别硬上适用边界与替代方案任何方案都有适用边界。把 Gitea Actions 跑到 Apple 硬件上确实能解决一类问题但并不是所有团队都应该立刻搭一套。先想清楚边界能帮你避免搭完又弃坑。5.1 适合自托管 Apple CI 的场景最适合的场景是团队已经有 Gitea 作为代码托管平台并且有持续产出苹果平台软件的需求。典型画像如下仓库里存在 iOS、macOS、watchOS 等工程构建产物需要 Xcode。团队不希望把代码和构建产物放到外部公网平台或者需要在内网访问数据库、测试服务器。已有专门的 Mac 设备可以被长期占用而不是靠开发者的个人电脑临时顶上。希望构建过程可以复现不要出现“只有张工电脑能打包其他人打不了”的局面。在这种情况下自托管 runner 的价值不只是省一点 CI 费用而是把“本机手动构建”变成“服务化构建”。任何人提交代码都会在统一的 Mac 环境里走同一套流程哪怕人不在电脑前也能得到构建结果和日志。这种可追溯性是手动构建很难提供的。5.2 不适合或需要谨慎的场景如果你的项目只是偶尔构建一次一个月发布一次版本那么自托管 Apple CI 的维护成本可能会高于收益。因为你要兼顾 Xcode 更新、runner 升级、证书过期、磁盘清理、系统更新这些时间加起来并不少。这时候直接在开发者本机执行脚本反而更轻。如果团队没有专人维护基础设施也不建议一上来就搭复杂方案。我见过团队把 runner 装好后就再也没人管证书过期了没人换runner 掉线了没人重启最后大家还是回到手动构建。这不能怪 Gitea Actions而是自托管本身就需要持续投入。还有一个容易被低估的场景你需要的不是一台苹果构建机而是大规模并行苹果构建。比如一个大型 App 有几十个 UI 测试 target想在短时间内全部跑完。单台 Mac mini 即使接走了所有任务也只能顺序执行瓶颈会很明显。这种情况下自购多台 Mac 不一定划算考虑按需开启的云端 macOS 构建服务会更合理。当然这会引入网络、数据、成本等多方面的取舍不一定比自托管好但值得对比。5.3 替代或混合执行方式不是所有任务都必须跑在 runner 里。如果你只是想要一个“自动打包”的能力可以只在发布分支上触发 workflow其他提交只在 Linux runner 上做 lint 和单元测试。这样可以减少对 Mac 资源的占用。也可以保留一台 Mac 作为专用 runner而开发者的笔记本不再承担构建任务。这样团队里的 Mac 配置差异、钥匙串差异、Xcode 版本差异都会被隔离在专用 runner 上反而减少“我这能编译”的扯皮。如果你已经有其他 CI 系统也不用着急迁移。Gitea Actions 的价值在于和 Gitea 深度集成触发简单、仓库内管理。但如果你已经有成熟的 macOS CI 流程并且团队更熟悉那套系统那么混合使用也是常见做法。核心判断是应当先确认你要解决的是“苹果构建可复用”的问题而不是“换个 CI 工具”。6. 把 Apple 硬件上的 CI 变成长期可维护的流程最后这部分是很多人容易忽略但最值得投入的地方。一次成功的 runner 部署只是开始长期稳定运行需要一套流程和纪律。6.1 四步走从最小任务到可复用流程我建议把搭建过程分成四个阶段每个阶段都有明确验收标准。不要跳过前一阶段直接进下一阶段尤其不要一上来就并发跑多个 iOS 构建。阶段目标关键动作验收标准一、连接验证runner 能与 Gitea 正常通信注册 runner跑echo或xcodebuild -version任务能被调度并成功返回结果二、环境固定构建所需的 Xcode、证书、路径可重复设置DEVELOPER_DIR导入签名证书检查钥匙串同一仓库的构建在本机自动结果与手动一致三、真实迁移将真实构建从本机迁到 runner迁移最小 target验证签名、模拟器测试再逐步扩展 target构建产物可下载签名和公证可完成四、工程化保证长期稳定运行设置 launchd、日志轮转、磁盘清理、监控、更新策略无人值守情况下能持续稳定跑数周这个框架不是某个工具自带的而是从大量实践里沉淀出来的一种节奏。核心原则是先确认链路不断再固定环境再增加复杂度最后解决“不掉线”的问题。6.2 一套可复用的排查链路当任务报错时不要直接搜日志最后一行然后盲目改参数。建议按顺序排查任务是否被 runner 接走。如果一直显示“waiting for runner”检查 runner 在线状态、标签是否匹配、token 是否过期。如果任务已执行但失败先看是哪个步骤失败。点击日志定位到具体命令不要看全屏红色就慌。如果是构建类失败确认 Xcode 路径、DEVELOPER_DIR、模拟器 SDK 是否存在。如果是签名失败先执行security find-identity -v -p codesigning查看 runner 当前用户可见的签名身份。如果是证书相关再检查钥匙串、p12 导入、CI 用户是否一致。如果是资源问题查看磁盘空间、内存占用、是否同时跑了多个构建。如果 runner 本身失联检查电源、休眠、launchd 是否还活着、系统是否重启过。这个顺序遵循一个简单逻辑先确认问题出在“任务没有被执行”还是“执行了但环境不对”还是“环境对了但苹果工具链不认”。不要在还没确认 runner 在线时就去调整 Xcode 版本或证书配置那样会浪费很多时间。6.3 长期维护清单维护一台苹果 runner本质上和运维一台 Linux 服务器类似只是工具链不同。定期做这几件事能省掉很多偶尔才出现的“玄学问题”定期更新 act_runner 和 Gitea 实例。版本跨度太大时兼容性容易出问题。在证书过期前设置提醒。可以在 Gitea 里做一个定时任务或用一个单独 workflow 检查证书剩余有效期。清理日志和临时文件。Xcode 构建日志和 DerivedData 增长非常快磁盘满了会导致构建失败。备份 workflow 文件、runner 配置和钥匙串副本。注意钥匙串副本是高敏感信息存放时要加密并严格管控。监控 runner 心跳、任务队列、构建失败率。最简单的做法是可以让 Gitea 定期发一条通知确认 runner 在线。这些动作听起来都很基础但正是这些基础动作决定了你的 CI 能不能真正常年运转。很多团队搭好 runner 后的前两周体验很好一个月后证书过期或磁盘满了就开始抱怨自托管不稳定。实际上不是技术方案不稳定而是维护机制没有跟上。回到最开始的主判断在 Apple 硬件上跑 Gitea Actions不是把 runner 装上就结束而是把苹果构建这件事从个人本机经验变成团队可依赖的基础服务。它真正值得投入的地方不是那几条 workflow 语法而是证书管理与安全策略、Xcode 环境固定、系统驻留与异常恢复、以及长期维护的纪律。如果你正准备在 Mac 上搭第一个 runner我给你的建议只有一条先别急着把完整的 iOS 工程迁移过去。先注册一个带明确标签的 runner跑通一段能打印 Xcode 版本的任务再把证书、电源、launchd 这三个最容易埋坑的地方处理好。等这台机器能连续两周无人值守地稳定运行再开始迁移真实业务构建。这个顺序走得慢但后续会稳很多。