一个事实,三种报错:macOS Self-hosted Runner 的 SessionCreate 陷阱
1 | errSecInternalComponent |
如果你在自建的 macOS GitHub Actions Runner 上遇到过其中任意一条报错,这篇文章也许能帮你省下几个小时。
它们看起来毫不相干,但在我这台机器上,最后都指向同一个隐藏原因——而三条报错里,没有任何一条提到它。
先说结论:
1 | plutil -p ~/Library/LaunchAgents/actions.runner.*.plist \ |
SessionCreate 来自 GitHub Actions Runner 的默认 macOS plist 模板。通过官方 svc.sh install 安装服务时,它会被写入 Runner 的 LaunchAgent 配置。
注意:这三条报错并不总是等价,也不一定都由
SessionCreate引起。本文记录的是它们在同一台机器、同一组实验中汇聚到同一个根因的过程。
症状:只有纯 SwiftPM 测试能通过
这是一个 macOS 菜单栏 App。CI 需要运行三组测试:
- 纯 SwiftPM 核心库单元测试:不打开 Xcode 工程,也不涉及签名
- App Target 单元测试:包含
TEST_HOST,需要构建并签名整个 App - XCUITest
第一组一直是绿色。
第二组却卡在 CodeSign:
1 | /…/IPPeek.app/Contents/Frameworks/XCUIAutomation.framework: |
签名身份明明存在:
1 | Apple Development: ... |
更奇怪的是,CodeSign 并非全部失败,而是每次随机坏掉一部分。
一轮构建有 9 次 CodeSign:
- 有时失败 5 个
- 有时失败 6 个
- 有时只失败 3 个
每次失败的 Framework 还不一样。
第一条路:钥匙串锁了
errSecInternalComponent 经常与 Keychain 有关。
于是,我先在 CI 中打印钥匙串状态:
1 | - run: security show-keychain-info \ |
Runner 中的输出是:
1 | User interaction is not allowed |
但在机器的 Terminal 中执行同一条命令,看到的却是:
1 | no-timeout |
我先按最常见的方式处理:
1 | security set-keychain-settings \ |
重跑,依然失败。
第二条路:私钥 ACL
另一个经典原因是 Keychain 的 partition list。
security 的 man page 明确提到:如果要让 /usr/bin/codesign 使用私钥,partition list 中必须包含 apple:。
于是执行:
1 | security set-key-partition-list \ |
第一次执行后,失败反而更多了。
后来才发现,我中途重新同步过一次证书。
set-key-partition-list 修改的是执行当时已经存在的私钥 ACL。后来同步进来的私钥,自然没有获得相同配置。
重新执行一次后,失败数量从:
1 | 5/9 |
ACL 的确有影响,但它仍然不是根因。
第三条路:并发竞争
“每次失败的对象都不同”,实在太像竞争条件了。
于是,我写了一个最小探针:使用同一把私钥,分别串行和并行签名 9 个文件。
结果是:
1 | Sequential failures: 9/9 |
串行同样全部失败。
这个实验至少证明:当前故障并不依赖并发,CodeSign 并发竞争不是主因。
这里有一个很重要的经验:
部分失败,不一定代表竞争。
看到随机失败时,我们很容易下意识怀疑锁、线程或并发。但更节省时间的办法,是先写一个最小实验。
十分钟的串行/并行对照,可能省掉后面几个小时。
真正的原因:Runner 不在登录会话里
重新整理所有现象后,有一个矛盾始终无法解释:
同一个用户、同一个钥匙串、同一时刻,看到的状态却不同。
| 执行上下文 | security show-keychain-info |
|---|---|
| Terminal | no-timeout |
| Runner 作业内 | User interaction is not allowed |
直到我检查 GitHub Actions Runner 的 LaunchAgent:
1 | <key>SessionCreate</key> |
问题终于串起来了。
SessionCreate 会让 launchd 为该任务创建新的 security audit session。Apple 对 Security Session API 的说明也提到:创建新 session 会丢弃调用进程此前建立的安全信息,包括与 Keychain 相关的信息。
换句话说:
Runner 与登录用户打开的 Terminal,并不共享同一个 security session。
因此:
- 在 Terminal 中解锁钥匙串
- 在 Terminal 中确认授权弹窗
- 在 Terminal 中看到某个 Keychain 状态
都不代表 Runner 所在的 session 会得到相同结果。
回头再看三个报错:
| 报错 | 在本案例中的表现 |
|---|---|
errSecInternalComponent | CodeSign 无法完成签名 |
Timed out while enabling automation mode | UI 自动化授权无法在 Runner 上下文中完成,最终超时 |
The test runner hung before establishing connection | XCTest 无法继续建立测试连接 |
它们最终都落在同一条因果链上:
1 | SessionCreate |
真正让我定位问题的,并不是报错本身,而是 Runner 与登录会话处在不同的 security session。
这也带来了第二个教训:
当“我明明改了,却没有生效”时,先确认修改发生在哪个上下文。
我花了三轮修钥匙串,最后才发现:我一直修的是 Terminal 所在的 session,Runner 根本不在里面。
为什么没有直接删除 SessionCreate
删除 SessionCreate 后,这三个问题的确一起消失了。
但新的问题也随之出现。
这台 Runner 同时服务多个仓库。如果让 Runner 直接进入登录用户的安全上下文,工作流就可能接触该上下文中已经解锁、且 ACL 允许当前工具访问的开发私钥,以及其他符合访问条件的 Keychain 条目。
对于一台服务多个仓库的共享 Runner,这不是我愿意接受的安全边界。
所以,我最终没有采用这个方案。
SessionCreate不是一个单纯的“兼容性开关”。关闭它之前,应该先把它当作安全边界的变化来评估。
最后的取舍
最终,我把测试拆成三段:
- SwiftPM 单元测试
- App 单元测试
- UI 测试
CI 始终运行第一段;后两段保留为本地门禁(Gate)。
这并不是因为后两段在技术上一定无法放进 CI,而是权衡之后,我不愿意为了几十条测试,让共享 Runner 与登录用户的 Keychain 安全上下文靠得太近。
工程里,很多时候没有绝对正确的答案。
只有清楚知道:自己得到了什么,又放弃了什么。
排查顺序
如果你也在配置 macOS Self-hosted Runner,可以按下面的顺序排查。
1. 检查 Runner 是否启用了 SessionCreate
1 | plutil -p ~/Library/LaunchAgents/actions.runner.*.plist |
2. 确认修改发生在哪个 security session
很多在 Terminal 中执行的解锁和授权,并不会自动作用到 Runner。
至少分别记录两个上下文中的:
1 | security show-keychain-info \ |
3. 不要急着归因于竞争
如果失败对象看起来随机,先做最小实验,对比串行与并行结果。
随机只是现象,不是并发的证据。
4. 关闭 SessionCreate 前,先评估安全边界
它可能解决问题,但也会改变 Runner 所在的安全上下文。尤其是 Runner 服务多个仓库时,不要把它当作无成本修复。
总结
这一轮真正帮助我解决问题的,不是某条神奇命令,而是两个探针:
- 在 Runner 内打印钥匙串状态
- 用串行/并行实验验证竞争假设
很多时候,我们最大的敌人不是系统,而是自己的推断。
在一个无法直接观察的上下文里:
先测量,再假设。
参考资料
- GitHub Actions Runner:macOS LaunchAgent 默认模板
- GitHub Docs:将 Self-hosted Runner 配置为服务
- Apple Developer Documentation:SessionCreate
- Apple Developer Documentation:Root and Login Sessions
本文初稿由 Claude Code 生成,经 ChatGPT 修改润色。

