本地测试通过、CI 却失败时,先给代理一份环境对照
本文目录
本地检查成功、CI 失败时,先把两次运行的对照交给编程代理,再要求修复。从相同的检出提交和测试选择开始,比较工作目录、运行时、依赖状态、输入与工作流事件,再根据差异选择一个能被聚焦检查推翻的假设。
GitHub 的工作流排查指南将运行日志、触发条件、执行条件和运行器列为调查入口。下面给出一种原创整理方法,把这些信息组织成代理请求;它不代表已经确定某次真实 CI 失败的原因。
把记录绑定到同一份代码
下面的项目、配置、路径、运行记录和观察均为虚构。记录是编写的示例,不是本文捕获的日志或实际运行的命令。EXAMPLE-C17 是示意提交标签,不是真实 Git 对象 ID。
假设仓库中有两份模式样例文件:
shop/
fixtures/
export-schema.json
packages/
exporter/
fixtures/
export-schema.json
tests/
test_schema.py
ci/
task-description.txt
根目录的样例是较早的一份:
{"id_field": "orderId"}
packages/exporter/ 内的样例是:
{"id_field": "order_id"}
packages/exporter/tests/test_schema.py 的相关部分是:
import json
from pathlib import Path
import unittest
class SchemaTests(unittest.TestCase):
def test_export_id_field(self):
schema = json.loads(
Path("fixtures/export-schema.json").read_text(encoding="utf-8")
)
self.assertEqual(schema["id_field"], "order_id")
if __name__ == "__main__":
unittest.main()
断言表达了应用要求的字段名,但不能确定某次运行实际打开了哪份样例文件。代码使用相对路径,因此运行时工作目录与这个问题有关。测试语法及 assertEqual 的含义可参阅 Python 3.13 unittest 文档。
与具体 CI 提供方无关的任务描述如下,保留示例配置的原文:
checkout: requested revision
restore cache: workspace-fixtures
working directory: checkout root
runtime: Python 3.13.2
command: python "$CHECKOUT/packages/exporter/tests/test_schema.py"
这是一份虚构任务描述,不是可直接部署的 GitHub Actions 工作流。缓存步骤有意没有写完整:它没有说明恢复了哪些文件。
写清楚实际记录了什么
虚构的本地记录写道:
Revision label: EXAMPLE-C17
CHECKOUT: /work/shop
Working directory: /work/shop/packages/exporter
Runtime reported: Python 3.13.2
Command: python "$CHECKOUT/packages/exporter/tests/test_schema.py"
Selected test: SchemaTests.test_export_id_field
Reported outcome: success
Fixture path and content captured during run: not recorded
Working-tree changes captured during run: not recorded
虚构的 CI 记录写道:
Checked-out revision label: EXAMPLE-C17
CHECKOUT: /work/shop
Working directory: /work/shop
Runtime reported: Python 3.13.2
Command: python "$CHECKOUT/packages/exporter/tests/test_schema.py"
Selected test: SchemaTests.test_export_id_field
Reported outcome: assertion mismatch, actual orderId, expected order_id
Event: pull request
Fixture path and content captured during run: not recorded
Post-cache working-tree changes captured during run: not recorded
这些记录提供了一条具体线索:工作目录不同,两份所给样例的内容也不同。但它们不能证明任何一次运行中实际发生了什么;缓存或其它准备步骤可能在检出后替换了样例文件。
填写环境差异表
| 维度 | 本地记录 | CI 记录 | 下一步要确定什么 |
|---|---|---|---|
| 检出修订 | EXAMPLE-C17 |
EXAMPLE-C17 |
真实场景中记录完整检出提交 ID,包括 CI 是否测试了合并修订 |
| 工作目录 | packages/exporter |
仓库根目录 | 在失败步骤当下记录目录,不用更早的准备步骤代替 |
| 命令与选择 | 同一脚本和测试 | 同一脚本和测试 | 确认封装脚本没有改变参数或测试发现方式 |
| 运行时 | 报告为 Python 3.13.2 | 报告为 Python 3.13.2 | 仍怀疑解释器选择时,记录可执行文件路径 |
| 依赖 | 脚本使用上面所示标准库导入 | 声明为同一脚本 | 提议升级依赖前,先查实际导入与配置 |
| 选中的样例 | 未记录 | 未记录 | 记录解析后的样例路径及不含敏感信息的内容 |
| 缓存与准备 | 本地准备未记录 | 恢复了样例缓存 | 查看恢复路径,在恢复后比较样例内容 |
| 事件与修订处理 | 手工调用 | pull request 事件 | 联合查看事件、检出和条件化准备过程 |
| 相关环境 | 已记录 CHECKOUT |
已记录 CHECKOUT |
只补充某个假设需要的特定变量 |
| 执行时工作区 | 未记录 | 缓存恢复后未记录 | 确定未跟踪文件或已改文件是否影响运行 |
相同修订只是起点。生成文件、恢复的产物、环境变量和命令封装,仍可能让真正使用的输入不同。检查这些差异前,不应把两次运行描述为等价。
至少区分两种原因
假设 A:工作目录选中了错误的样例文件。 按所示代码,从根目录运行时会寻找根目录样例;从 exporter 目录运行时会寻找 exporter 内的样例。所给例子的字段名与记录中的不匹配现象相符。
验证 A,可以在失败环境读取样例时,立即记录解析后的路径和字段值,或采用等价的小范围诊断。如果失败运行实际打开的是 exporter 样例,而且该文件仍包含 order_id,那么 A 就不能解释这份记录。不要只是重复最初的对照表,就说已经“确认”了 A。
假设 B:恢复或生成的内容与受版本控制的样例不同。 两次运行可以检出同一提交,而 CI 的样例缓存随后提供了不同字节。
验证 B,可以在可丢弃的诊断运行中,查看缓存覆盖的路径范围,并比较准备步骤前后的实际样例内容。如果相关内容没有改变、也与受版本控制的样例一致,这个具体的缓存内容假设就缺少支持。命中缓存本身不等于缓存损坏。
先从 A 开始,因为现有记录明确给出目录差异,而聚焦的路径观察能直接区分两份已知样例。如果观察与 A 矛盾,再调查 B。同一次尝试中大幅修改运行时、缓存和测试,会让结果更难解释。
复制这份调查请求
调查同一检出修订上的 SchemaTests.test_export_id_field,
为什么本地记录成功、CI 记录失败。
所给证据:
上面的目录树、两份模式样例、测试片段、与提供方无关的任务描述,
以及成对记录。这些就是当前证据;根因仍为 UNKNOWN。
先确定实际检出提交、测试选择、测试步骤当下的工作目录、
运行时可执行文件,以及相关准备/缓存条件。
不要把本地分支顶端与尚未核实的 CI 合并修订作比较。
第一个假设:
相对样例路径在 CI 中选中了根目录样例。
提出一项诊断,在失败步骤记录解析后的样例路径和相关字段。
说明什么观察能够推翻这个假设。
第二个假设:
缓存或生成过程在检出后改变了样例内容。
如果第一个假设不成立,查看相关路径和实际内容。
先读源码和已有日志。确定要做哪项诊断前,不重跑工作流、
不修改配置,也不编辑测试。
不要输出全部环境变量值或任何秘密信息。
证据只保留指定路径、字段、运行时、修订和相关准备条件。
不要删除断言、把 order_id 改成无法解释的实际值、禁用检查、
扩大权限,或拿升级依赖当试探性修复。
返回对照表、支持和反对各假设的证据、最小下一项检查,
以及仅在有支持时提出的小范围改动方案。
区分已有运行记录、手工预测和新的执行。
让观察结果决定怎样修复
后续证据如果确认仓库原本要使用 exporter 样例,而路径从错误目录解析,就可以考虑显式指定预期路径,或调整配置的工作目录。具体选择应遵循仓库测试约定和任务范围,之后仍需确认检查继续约束 order_id。
如果是缓存恢复提供了错误内容,就调查恢复范围及产物身份。修改预期断言,不能解释输入为何出错。
本练习交付的是调查请求和两个可验证假设。用于区分原因的检查没有在本文执行,因此实际根因仍然未知。处理自己的场景时,应先把检查结果补进对照,再请代理实施修复。
在 CueSpec 会话中,带着新的日志片段回复前,可先阅读最新回答,让下一条请求对应当前假设。