3、CI 流水线结构:阶段(Stage)、任务(Job)、步骤(Step)的概念与日志对应关系

做日志分析的第一步,不是盯着报错信息死磕,而是先搞清楚报错到底出在流水线的哪一层。说白了,你得知道这错误是「哪个阶段」爆的、「哪个任务」跑的、「哪一步」执行的。

CI 流水线不是一团乱麻,而是分了三层结构:Stage(阶段)、Job(任务)、Step(步骤)。我见过太多人,一拿到日志就翻到底,结果连是哪个 Job 报的错都没分清。嗯,这里咱们得彻底讲清楚。

我个人习惯:拿到 CI 失败日志,第一件事不是看错误内容,而是扫一遍日志里的标记行——Stage、Job、Step 都有固定的标识符。

Stage:流水线的骨架

Stage 是最高级别的逻辑分组。比如一个典型的 CI 流水线,会分成:

  • Build(构建):编译代码、打包制品
  • Test(测试):跑单元测试、集成测试
  • Deploy(部署):推送到测试环境或生产环境

每个 Stage 只关心一件事。我在项目中遇到过一个问题:某次构建被拆成了三个 Stage,但日志里 Stage 名称用了默认的「Stage 1」「Stage 2」「Stage 3」——等你排查的时候根本分不清哪个阶段干了什么。从那以后,我规定团队必须给 Stage 起有业务含义的名字。

Job:Stage 里的执行单元

Stage 里面会有多个 Job,它们可以并行执行。举个例子,「测试」这个 Stage 里可能有:

  • Job: unit-test(单元测试)
  • Job: integration-test(集成测试)
  • Job: lint-check(代码规范检查)

每个 Job 是独立的最小执行单元,失败只影响自身。日志里会清晰标出哪个 Job 失败了。我曾经遇到一个诡异场景:并行 Job 全部失败,但错误内容却不一样。最终发现是其中一个 Job 的环境变量写死了,影响了共用的缓存目录。如果没有按 Job 隔离分析,根本找不到根因。

注意:有些流水线工具(比如旧的 Jenkins 声明式语法)会把多个 Step 塞进同一个 Job,导致日志混杂。遇到这种情况,必须把输出重定向到独立文件,否则你分不清哪行输出对应哪一步。

Step:Job 里的原子操作

Step 是最小执行单元。常见的 Step 包括:

  • 运行 shell 命令
  • 下载依赖包
  • 执行测试用例
  • 上传制品

每个 Step 都有开始和结束标记。日志里典型的 Step 输出像这样:

[Step 1/3] 开始执行:npm install
...
[Step 2/3] 开始执行:npm run build
...
[ERROR] 构建失败,退出码 1
[Step 2/3] 执行结束:失败

你想想看,如果日志里只写了「构建失败」,你根本不知道是 npm install 出的问题还是 build 出的问题。但是一旦有了 Step 标记,一眼就能锁定是第 2 步出错了。

三层结构与日志的对应关系

我整理了一个对照表,方便你在日志里快速定位:

层级 日志常见标识 失败时典型表现 排查要点
Stage [Stage: Build] 或 ##[section]Starting: Build 整个 Stage 变红,内部所有 Job 可能被跳过 先确认是哪个 Stage 失败,再进入下一层
Job [Job: compile] 或 Job 'test' failed 只有当前 Job 标红,其他 Job 正常执行 查看 Job 级别的日志摘要,找退出码
Step Step X/Y 或 + command 报错信息紧跟在 Step 输出之后 查看 Step 开始/结束标记,定位具体命令
关键在于:失败信息往往藏在 Step 级别。Stage 和 Job 只是给你一个入口,真正的错误原因在 Step 的详细输出里。

实战:从日志中读取出三层结构

下面是一个真实的 GitLab CI 日志片段(我稍微改动了内容):

Running with gitlab-runner 14.0
Preparing the "docker" executor
...
[Stage: Build]
[Job: compile-app]
  Step 1: docker build -t myapp .
    Sending build context to Docker daemon  2.048kB
    Step 1/8 : FROM node:16
    ...
    Step 8/8 : RUN npm install
    npm ERR! code ECONNRESET
    npm ERR! network request to https://registry.npmjs.org/...
  Step 1 failed with exit code 1
Job compile-app failed
Stage Build failed

你看,从下往上读,先看到 Stage,再找到 Job,最后定位到 Step 1 里的 npm install 网络超时。如果没有这种层级意识,你可能被 ECONNRESET 吓到,然后跑去调 Docker 配置——其实换个思路,把 npm registry 换成国内镜像就行了。

为什么会这样?因为日志分析不是瞎猜,而是按层级做「漏斗式」排查。Stage 给你方向,Job 缩小范围,Step 给出真相。

SVG:CI 流水线三层结构图

下面这张图把 Stage、Job、Step 的关系画清楚了。你可以把它当成排查时的「地图」:

CI 流水线三层结构 Stage: Build(构建阶段) Job: compile (编译代码) Job: package (打包制品) Job: lint (代码检查) Stage: Test(测试阶段) Job: unit-test (单元测试) Job: integration-test (集成测试) Stage: Deploy(部署阶段) Job: deploy-staging (部署到预发环境) ↳ 每个 Job 内部包含多个 Step(如 npm install、npm run build),Step 是原子操作单元

不同 CI 工具下的日志标识差异

不同工具用的标记格式不一样,但核心结构是相同的。我列个对比:

CI 工具 Stage 标记 Job 标记 Step 标记
Jenkins Pipeline Stage 'Build' running Running job 'compile' [Pipeline] // step
GitLab CI Stage: build Job: compile Step 1/3 : docker build
Azure Pipelines ##[section]Starting: Build Job compile ##[command]docker build
GitHub Actions Run: build (workflow 内) run: npm install 无显式 Step 编号,按顺序输出
遇到没有显式 Step 编号的工具(比如 GitHub Actions),你可以在 Job 里手动加 echo "Step X: ..." 来标记。我自己的团队就是这么干的——工具不给力,咱们自己补。

踩过的坑:Stage 失败但 Job 成功?

你可能觉得奇怪:Stage 失败了,里面所有 Job 不都应该失败吗?不一定。我曾经碰到过一个情况:Pipeline 设置了 allow_failure: true(允许任务失败),结果某个 Job 报错了,Stage 却还是绿色的。后来我排查日志,发现 Stage 的状态是由「所有必须成功(required)的 Job」决定的,而不是看全部 Job。

所以读日志的时候,一定要看每个 Job 独立的退出码,别被 Stage 的颜色骗了。

一句话总结本章

Stage 定方向,Job 定范围,Step 定细节。拿到日志,先按这个三层结构做「剥洋葱」,把问题一层层剥出来,别一上来就扎进报错文本里。