首页
/ Ansible CI 失败日志排障实战:/azp-logs 技能与 hacking/azp/download.py 深度解析

Ansible CI 失败日志排障实战:/azp-logs 技能与 hacking/azp/download.py 深度解析

2026-09-04 13:20:25作者:霍妲思

本文以仓库中的 azp-logs 技能定义 为核心,系统讲解 Ansible 项目中如何把 Azure Pipelines(AZP)CI 构建日志下载到本地进行故障分析:从 /azp-logs 的三种入参形式、完整执行流程,到底层脚本 download.py 的 timeline API 调用、job 过滤与日志命名规则。读完本文,你既能熟练用该技能拉取 CI 日志定位测试失败,也能从源码层面理解"一条构建 ID 如何变成一组 .log 文件"的完整链路。

技能定位:ansibot 与 Web UI 之外的最后一道排障手段

Ansible 的 CI 跑在 Azure Pipelines 上,PR 提交后由 CI 机器人(ansibot)在 PR 评论区回报失败摘要。AGENTS.md 给出的标准排障路径是三级递进:

  1. 先读 ansibot 评论(gh pr view <number> --comments),多数 sanity 失败会直接给出文件与行号;
  2. gh pr checks <number> 拿到 Azure Pipelines 构建 URL 与逐 job 状态;
  3. 当上述信息不足以定位问题时,用 /azp-logs 技能把完整控制台日志拉到本地做细粒度分析。

azp-logs 技能 就定义在这条链路的最末端。它本质是一个 Claude Code 技能,YAML frontmatter 声明了它的身份与约束:

name: azp-logs
description: Download Azure Pipelines CI logs for analysis
argument-hint: <pr_number|build_id|build_url>
allowed-tools: [Bash(gh pr view:*), Bash(gh pr checks:*), Bash(ls:*), Read, Grep]
user-invocable: true

几个关键字段的含义:argument-hint 提示该技能接受三种入参之一;allowed-tools 限定技能执行时只允许使用 gh pr viewgh pr checksls 以及文件读取/检索类工具——即它自身不直接执行下载命令,下载动作由 hacking/azp/download.py 这个长期存在于仓库中的脚本承担,技能只是把它包装成一条带交互确认的"下载—定位失败"工作流;user-invocable: true 表示用户可以直接以 /azp-logs 前缀调用。

调用方式与三种入参

技能文档给出的调用格式为:

/azp-logs <pr_number|build_id|build_url>

三种入参的语义与处理路径如下(均出自 SKILL.md):

入参 说明 技能内部的处理方式
pr_number GitHub PR 编号 先执行 gh pr checks <number>,从最近一次 CI 运行的检查项中提取 Azure Pipelines URL,进而得到 build ID
build_id Azure Pipelines 构建 ID(纯数字) 直接传给 download.py
"build_url" 完整构建结果 URL,形如 https://dev.azure.com/ansible/ansible/_build/results?buildId=12345 直接传给 download.py,脚本内部自动解析出 buildId

关于 URL 形式的入参,可以在脚本源码中得到精确印证。download.py 的 run_id_arg 函数 用一个正则同时接受"纯数字 ID"和"dev.azure.com 构建结果 URL"两种写法:

def run_id_arg(arg):
    m = re.fullmatch(r"(?:https:\/\/dev\.azure\.com\/ansible\/ansible\/_build\/results\?buildId=)?(\d+)", arg)
    if not m:
        raise ValueError("run does not seems to be a URI or an ID")
    return m.group(1)

也就是说 URL 前缀是可选的,最终归一化输出的一定是纯数字 ID——后续所有 API 请求、输出目录命名都基于这个数字 ID。

下载前的预期管理

技能文档把"下载前的告知义务"单独列为强约束:执行前必须先征得用户确认,并说明三点预期(引自 SKILL.md):

  • 一次完整 CI 运行的日志下载通常耗时 5–10 分钟,非常大的 CI 运行可能更久;
  • 日志会保存到以构建 ID 命名的目录 <build_id>/ 下;
  • 下载体积视 job 数量而定,常见约 10–50MB。

这些提示解释了为什么 gh pr checks 给出的 URL 虽然"随手可见",却仍值得一条专用技能来封装耗时操作。

技能执行流程四步走

SKILL.md 定义的标准执行序列是:

  1. 向用户确认:说明将要下载什么、预计耗时;
  2. 确定 build ID:按上表三种入参路径之一解析;
  3. 下载日志:执行
    ./hacking/azp/download.py <build_id_or_url> --console-logs -v
    
    这是技能推荐的默认命令——只取控制台日志并开启 -v 显示正在下载哪些文件,适合绝大多数 CI 失败分析场景;
  4. 分析日志:进入 <build_id>/ 目录,grep 常见失败特征(见后文"日志分析实操"一节),聚焦失败 job 的日志,并与 ansibot 评论相互印证。

第 3 步命令中"控制台日志"为何是首选,第 4 步"job 名怎么对应到文件",都要从脚本的下载实现讲起。

download.py 选项全解:以源码为准的完整参数表

SKILL.md 列出的选项与 download.py 的参数解析代码 完全对应,此处以源码为准补全每个选项的精确语义:

选项 源码位置 作用
RUN(位置参数) L60 构建 ID 或 dev.azure.com 构建 URL,经 run_id_arg 归一化为数字
-v, --verbose L62-L65 打印正在下载的每个文件路径
-t, --test L67-L70 干跑模式:只列出"将会下载"的内容,不实际下载、不创建目录
-p, --pipeline-id L72 指定 pipeline 编号,默认 20,仅影响 run 元数据请求
--artifacts L74-L76 下载测试制品(zip 包)并解压
--console-logs L78-L80 下载控制台日志(CI 失败分析推荐)
--run-metadata L82-L84 下载运行元数据 JSON
--all L86-L88 等价于同时开启上三项
--match-artifact-name L90-L93 正则过滤制品/日志名称,默认 .*(不过滤)
--match-job-name L95-L98 正则过滤 job,默认 .*(不过滤)

源码中有两处逻辑值得特别注意:

其一,--all 并非独立功能,而是在解析后展开为三个开关(L105-L108):

if args.all:
    args.artifacts = True
    args.run_metadata = True
    args.console_logs = True

其二,脚本强制要求至少选择一种下载类型,否则直接报错退出(L110-L117):

selections = (
    args.artifacts,
    args.run_metadata,
    args.console_logs
)

if not any(selections):
    parser.error('At least one download option is required.')

这意味着 ./hacking/azp/download.py 12345 -v 这样的"裸"命令会被拒绝,必须显式声明下载意图。此外脚本头部带有 # PYTHON_ARGCOMPLETE_OK 标记并可选加载 argcompleteL2-L2L100-L101),在安装了 argcomplete 的环境中可获得 shell 补全。

源码深潜:一条构建 ID 如何变成一组 .log 文件

download_run 函数(L122-L221)是整个脚本的核心,它对 Azure DevOps REST API 发起了最多四类请求,全部通过 urllib.request 匿名访问——这也印证了技能文档中"Ansible 项目是公开的,下载无需认证"的说法。

1. 输出目录以构建 ID 命名

output_dir = '%s' % args.run

非干跑模式下先创建 <build_id>/ 目录,后续所有元数据、制品、日志都落在其中。这就是技能文档"日志保存到以构建 ID 命名的目录"的实现来源。

2. 运行元数据:写入 run.json

开启 --run-metadata 时,请求 pipeline runs 接口(L131-L143):

run_url = 'https://dev.azure.com/ansible/ansible/_apis/pipelines/%s/runs/%s?api-version=6.0-preview.1' % (args.pipeline_id, args.run)
...
path = os.path.join(output_dir, 'run.json')
contents = json.dumps(run, sort_keys=True, indent=4)

响应 JSON 以键排序、4 空格缩进后写入 run.json。注意 URL 中的 pipeline ID 来自 -p 参数(默认 20)。

3. timeline:构建父子关系树,这是日志命名与 job 过滤的基础

无论选择哪种下载类型,脚本都会请求构建 timeline(L145-L161):

with urllib.request.urlopen('https://dev.azure.com/ansible/ansible/_apis/build/builds/%s/timeline?api-version=6.0' % args.run) as timeline_response:
    timeline = json.load(timeline_response)

然后遍历 timeline['records'],以每条记录的 id/parentId 构建出 roots(无父节点者)、by_idparent_ofchildren_of 四张索引,即整棵 stage → job → task 的层级树。

job 过滤发生在树的"第一层"(L163-L177):对每个根节点(stage),检查其每个直接子节点(job),当且仅当 --match-job-name 正则能匹配上 "stage 名 job 名" 的拼接串时才放行:

for ci in children_of.get(r['id'], []):
    c = by_id[ci]
    if not args.match_job_name.match("%s %s" % (r['name'], c['name'])):
        continue
    allow_recursive(c['id'])

allow_recursive 会把被放行 job 的整个子树(含全部子任务)加入允许集 allowed。这里可以推断两点:正则匹配的对象是"stage 名 + 空格 + job 名",所以像 AGENTS.md 中高级用法示例的 --match-job-name "Sanity.*",实际是去匹配包含 sanity 测试 job 的那一层;而一旦某个 job 被放行,其内部所有 task 日志都会随之下载。

4. 控制台日志:按层级命名落盘

开启 --console-logs 时(L197-L221),脚本遍历所有 timeline 记录,对"带有 log 下载链接、id 在允许集内、且名称通过 --match-artifact-name 过滤"的记录,沿着 parent_of 链一路回溯到根,把各级名称收集起来:

names = []
parent_id = r['id']
while parent_id is not None:
    p = by_id[parent_id]
    name = p['name']
    if name not in names:
        names = [name] + names
    parent_id = parent_of.get(p['id'], None)

path = " ".join(names)
# Some job names have the separator in them.
path = path.replace(os.sep, '_')
log_path = os.path.join(output_dir, '%s.log' % path)

由此得到技能文档那条命名规则的确切含义:控制台日志文件以"Stage 名 Job 名(.log)"命名,即从该记录沿层级树回溯得到的名称序列按空格拼接;若名称中恰好含路径分隔符则替换为 _ 避免误入子目录。日志内容则是直接流式复制 record['log']['url'] 指向的远端文件。

5. 制品:zip 流式下载并就地解压

开启 --artifacts 时,脚本先拉取制品列表接口,再逐个下载(L179-L195)。每个制品须同时满足"其来源 job 在允许集内"与"名称匹配 --match-artifact-name"两个条件,下载后按 zip 包解压到 <build_id>/

with urllib.request.urlopen(artifact['resource']['downloadUrl']) as response:
    with io.BytesIO() as buffer:
        shutil.copyfileobj(response, buffer)
        buffer.seek(0)
        with zipfile.ZipFile(buffer) as archive:
            archive.extractall(path=output_dir)

整条链路自始至终没有认证参数,依赖项目本身的公开访问权限。

日志分析实操:grep 模式与失败类型对照

日志落地后,SKILL.md 给出的分析模式如下,可直接复用:

# 找出所有错误与失败
grep -r "FAILED\|ERROR\|Traceback" <build_id>/

# 定位具体失败的测试
grep -r "FAILED test" <build_id>/

# 定位 sanity 测试失败
grep -r "The test" <build_id>/ | grep -i "failed"

# 列出所有已下载的日志文件
ls -lh <build_id>/

配合 AGENTS.md 中"Advanced usage"一节的两个实战变体,可以进一步收窄下载范围、把 5–10 分钟的全量下载压缩到只拉需要的 job:

# 只下载 sanity 相关 job 的日志
./hacking/azp/download.py <build_id> --console-logs --match-job-name "Sanity.*"

# 下载全部(日志 + 制品 + 元数据)
./hacking/azp/download.py <build_id> --all

grep 结果如何解读,可以对照仓库中 CI 常见失败模式文档 的三分法:

  • Sanity 失败:通常有明确修法(尾随空白、import 错误等),日志中一般会带 file:line 引用,是最好处理的一类;
  • 集成测试失败:可能需要特定平台的容器或测试调整,需要完整阅读失败 job 的输出与 traceback;
  • 单元测试失败:往往指向真实代码问题,需要在本地用 ansible-test 复现调试。

分析时的完整闭环是:先在 gh pr checks 输出里圈出标记为失败的 job 名 → 在 <build_id>/ 中按 "Stage 名 Job 名.log" 规则找到对应日志 → grep 定位错误行 → 与 ansibot 评论中的摘要比对,确认上下文一致。

同一脚本的另一面:coverage 制品下载

download.py 虽然由 azp-logs 技能在"排障"语境下主推,但它本身是通用 CI 结果下载器。hacking/azp/README.md 记录了它在"附带性代码覆盖率(incidental coverage)"分析流程中的用法:

# 下载指定构建的制品与元数据到 ansible/ansible 目录
hacking/azp/download.py 14075 --artifacts --run-metadata -v
# 随后结合 incidental.py 分析覆盖率
hacking/azp/incidental.py 14075/

可见 --artifacts --run-metadata 组合(不取控制台日志)对应的是"分析制品数据"场景,而 --console-logs 对应"排障"场景——这正是技能文档推荐"多数 CI 失败分析用 --console-logs"的原因。

使用注意事项小结

  • 无需认证:脚本对 dev.azure.com 的接口均为匿名访问,前提是 Ansible 项目保持公开;
  • 目录约定:一切下载产物集中在以构建 ID 命名的目录中,ls -lh <build_id>/ 是最快的全局盘点方式;
  • 干跑先行:面对超大 CI 运行,可先加 -t 观察将要下载的规模,再决定是否全量执行;
  • 过滤优先--match-job-name / --match-artifact-name 均为完整正则(默认 .* 全取),在只需个别 job 日志时应显式收窄,既省时间也减小目录规模;
  • 入参归一化:无论传 PR 派生的 URL、完整构建 URL 还是纯数字 ID,最终都以数字构建 ID 作为 API 路径参数与输出目录名;
  • 参数互斥性:至少必须指定 --console-logs / --artifacts / --run-metadata / --all 之一,否则脚本直接报错。

.claude/skills/azp-logs/SKILL.md 的技能定义,到 hacking/azp/download.py 的 timeline 树构建与日志落盘逻辑,再到 AGENTS.md 的三级排障工作流,Ansible 项目把"CI 失败深挖"沉淀成了一条工具链完整、可被 Agent 直接调用的标准化路径:先靠 ansibot 评论快速定性,必要时一条命令把远端日志搬回本地,用 grep 完成从"红叉"到 file:line 的最后一步定位。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384