一个事实,三种报错:macOS Self-hosted Runner 的 SessionCreate 陷阱

1
2
3
errSecInternalComponent
Timed out while enabling automation mode
The test runner hung before establishing connection

如果你在自建的 macOS GitHub Actions Runner 上遇到过其中任意一条报错,这篇文章也许能帮你省下几个小时。

它们看起来毫不相干,但在我这台机器上,最后都指向同一个隐藏原因——而三条报错里,没有任何一条提到它。

先说结论:

1
2
3
4
plutil -p ~/Library/LaunchAgents/actions.runner.*.plist \
| grep SessionCreate

# "SessionCreate" => true

SessionCreate 来自 GitHub Actions Runner 的默认 macOS plist 模板。通过官方 svc.sh install 安装服务时,它会被写入 Runner 的 LaunchAgent 配置。

注意:这三条报错并不总是等价,也不一定都由 SessionCreate 引起。本文记录的是它们在同一台机器、同一组实验中汇聚到同一个根因的过程。


症状:只有纯 SwiftPM 测试能通过

这是一个 macOS 菜单栏 App。CI 需要运行三组测试:

  1. 纯 SwiftPM 核心库单元测试:不打开 Xcode 工程,也不涉及签名
  2. App Target 单元测试:包含 TEST_HOST,需要构建并签名整个 App
  3. XCUITest

第一组一直是绿色。

第二组却卡在 CodeSign:

1
2
3
4
/…/IPPeek.app/Contents/Frameworks/XCUIAutomation.framework:
replacing existing signature
errSecInternalComponent
Command CodeSign failed with a nonzero exit code

签名身份明明存在:

1
Apple Development: ...

更奇怪的是,CodeSign 并非全部失败,而是每次随机坏掉一部分。

一轮构建有 9 次 CodeSign:

  • 有时失败 5 个
  • 有时失败 6 个
  • 有时只失败 3 个

每次失败的 Framework 还不一样。


第一条路:钥匙串锁了

errSecInternalComponent 经常与 Keychain 有关。

于是,我先在 CI 中打印钥匙串状态:

1
2
- run: security show-keychain-info \
~/Library/Keychains/login.keychain-db

Runner 中的输出是:

1
User interaction is not allowed

但在机器的 Terminal 中执行同一条命令,看到的却是:

1
no-timeout

我先按最常见的方式处理:

1
2
3
4
5
security set-keychain-settings \
~/Library/Keychains/login.keychain-db

security unlock-keychain \
~/Library/Keychains/login.keychain-db

重跑,依然失败。


第二条路:私钥 ACL

另一个经典原因是 Keychain 的 partition list。

security 的 man page 明确提到:如果要让 /usr/bin/codesign 使用私钥,partition list 中必须包含 apple:

于是执行:

1
2
3
4
5
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s \
-k "$PASSWORD" \
~/Library/Keychains/login.keychain-db

第一次执行后,失败反而更多了。

后来才发现,我中途重新同步过一次证书。

set-key-partition-list 修改的是执行当时已经存在的私钥 ACL。后来同步进来的私钥,自然没有获得相同配置。

重新执行一次后,失败数量从:

1
2
3
5/9

3/9

ACL 的确有影响,但它仍然不是根因。


第三条路:并发竞争

“每次失败的对象都不同”,实在太像竞争条件了。

于是,我写了一个最小探针:使用同一把私钥,分别串行和并行签名 9 个文件。

结果是:

1
2
Sequential failures: 9/9
Parallel failures: 9/9

串行同样全部失败。

这个实验至少证明:当前故障并不依赖并发,CodeSign 并发竞争不是主因。

这里有一个很重要的经验:

部分失败,不一定代表竞争。

看到随机失败时,我们很容易下意识怀疑锁、线程或并发。但更节省时间的办法,是先写一个最小实验。

十分钟的串行/并行对照,可能省掉后面几个小时。


真正的原因:Runner 不在登录会话里

重新整理所有现象后,有一个矛盾始终无法解释:

同一个用户、同一个钥匙串、同一时刻,看到的状态却不同。

执行上下文security show-keychain-info
Terminalno-timeout
Runner 作业内User interaction is not allowed

直到我检查 GitHub Actions Runner 的 LaunchAgent:

1
2
<key>SessionCreate</key>
<true/>

问题终于串起来了。

SessionCreate 会让 launchd 为该任务创建新的 security audit session。Apple 对 Security Session API 的说明也提到:创建新 session 会丢弃调用进程此前建立的安全信息,包括与 Keychain 相关的信息。

换句话说:

Runner 与登录用户打开的 Terminal,并不共享同一个 security session。

因此:

  • 在 Terminal 中解锁钥匙串
  • 在 Terminal 中确认授权弹窗
  • 在 Terminal 中看到某个 Keychain 状态

都不代表 Runner 所在的 session 会得到相同结果。

回头再看三个报错:

报错在本案例中的表现
errSecInternalComponentCodeSign 无法完成签名
Timed out while enabling automation modeUI 自动化授权无法在 Runner 上下文中完成,最终超时
The test runner hung before establishing connectionXCTest 无法继续建立测试连接

它们最终都落在同一条因果链上:

1
2
3
4
5
6
7
SessionCreate

Runner 进入独立的 security session

Terminal 中的 Keychain 解锁与授权无法直接作用于 Runner

CodeSign/Automation/XCTest 分别以不同方式失败

真正让我定位问题的,并不是报错本身,而是 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
2
security show-keychain-info \
~/Library/Keychains/login.keychain-db

3. 不要急着归因于竞争

如果失败对象看起来随机,先做最小实验,对比串行与并行结果。

随机只是现象,不是并发的证据。

4. 关闭 SessionCreate 前,先评估安全边界

它可能解决问题,但也会改变 Runner 所在的安全上下文。尤其是 Runner 服务多个仓库时,不要把它当作无成本修复。


总结

这一轮真正帮助我解决问题的,不是某条神奇命令,而是两个探针:

  • 在 Runner 内打印钥匙串状态
  • 用串行/并行实验验证竞争假设

很多时候,我们最大的敌人不是系统,而是自己的推断。

在一个无法直接观察的上下文里:

先测量,再假设。


参考资料

本文初稿由 Claude Code 生成,经 ChatGPT 修改润色。