Sparkle CodesSparkle
项目 / CI-CD

我们是怎么把 GitLab CI/CD 跑通的:从并行误判到 artifacts 传递

x
xpx
Jul 14, 2025
Editorial Insight
#CI/CD#DevOps#GitLab

最近我们在给一个小项目补 CI 的时候,原本以为这事不会花太多时间。需求很简单:先在流水线里生成一个文件,再到下一个步骤里验证它存在。按直觉看,这几乎就是两条 shell 命令的事。

结果真正动手以后,问题并不出在命令本身,而是团队里不少人对 GitLab CI/CD 的运行方式理解得比较“想当然”。大家知道 .gitlab-ci.yml 要写在仓库根目录,也知道 job 里可以写 script,但一到“为什么前一个 job 生成的文件,后一个 job 看不见”这种问题,讨论就开始跑偏了。

我后来索性把这个过程重新拆了一遍。因为这类坑很典型:不是语法不会写,而是对 pipeline、stage、job、runner 和 artifacts 之间的关系没有真正建立起工程上的直觉。只要这层直觉没搭起来,后面无论是接 Node 项目、Python 项目,还是做多阶段构建,都会反复踩同一类坑。

先别急着抄模板,先把几个基本角色分清

GitLab CI/CD 的入口文件就是项目根目录下的 .gitlab-ci.yml。这个文件不是“可选配置”,而是 GitLab 识别 pipeline 的标准入口。pipeline、stage、job 的关系,也都从这里展开。

我们在内部复盘时,先把三个概念重新说清楚了:

  • pipeline 是一次完整执行。
  • stage 是执行顺序上的分组。
  • job 才是真正跑命令的最小单元。

这个区分看起来很基础,但实际写配置时最容易混。因为很多人会把 job 当成“脚本块”,把 stage 当成“标签”。一旦这么理解,后面就会自然认为“只要我先写 create,再写 test,它就会按顺序执行”。但 GitLab 不是按你在 YAML 里的书写顺序理解依赖关系,而是按 stage 来调度;同一个 stage 里的多个 job 默认可以并行执行。没有显式声明 stage 的 job,还会默认落到 test。这也是我们第一次把流水线跑挂的直接原因。

补充:YAML 里要记的是层级而非“缩进口诀”

我们一开始也有人纠结缩进到底该怎么写。后来统一口径:别记这种口口相传的“规则口诀”,只记一条——YAML 层级必须正确,而且尽量只用空格,不要混 tab。真正排查时,用内置的 CI Lint 工具比背经验靠谱得多。

第一个误判:我们以为“写在前面”的 job 会先执行

一开始我们的配置大概是这个思路:

YAML
create_file:
  image: alpine:latest
  script:
    - mkdir build
    - touch build/some-file.txt
test_file:
  image: alpine:latest
  script:
    - test -f build/some-file.txt

表面上看,这样写没什么问题。先创建文件,再检查文件。人脑读起来很顺。

但 pipeline 一跑,test_file 直接失败。这个阶段最容易把锅甩给 Linux 命令,或者怀疑 mkdir、touch 没执行成功。我们当时也先往这个方向查了几分钟,去翻 job log,看是不是 shell 环境不对,或者 Alpine 镜像里缺东西。

后来回头看配置才发现,问题根本不在命令,而在调度模型。两个 job 都没写 stage,于是它们都落进默认的 test stage 里了。既然同一 stage 默认并行,那就没有“先 create 后 test”这回事。

事实查验:默认 Stage 行为

截至 GitLab 18.10,未声明 stage 的 job 默认属于 test 阶段。如果整个流水线都没有定义 stages 列表,GitLab 会使用默认的顺序:.pre -> build -> test -> deploy -> .post。

所以第一步不是改命令,而是把顺序显式写出来:

YAML
stages:
  - build
  - test
create_file:
  stage: build
  image: alpine:latest
  script:
    - mkdir build
    - touch build/some-file.txt
test_file:
  stage: test
  image: alpine:latest
  script:
    - test -f build/some-file.txt

到这里,执行顺序终于对了:build 先跑,test 后跑。

第二个误判:我们以为前一个 job 生成的文件会自然留给下一个 job

把 stage 分开之后,我们本来以为这次应该稳了。结果第二轮 pipeline 还是失败,test_file 继续报找不到 build/some-file.txt。

这个阶段最容易让人产生一种错觉:明明第一个 job 已经跑成功了,为什么第二个 job 还是看不到文件?是不是 GitLab 根本没把文件写出来?是不是 Web IDE 提交有问题?是不是 runner 把工作目录清掉了?

我们当时就真的沿着这些方向排过一轮。后来才发现,核心问题其实特别基础:job 彼此独立。

在工程上,这意味着:前一个 job 的运行环境,不会自动成为后一个 job 的起点。GitLab 的 job 是交给 runner 执行的,而 runner 的执行方式(executor)决定了环境是否“干净”。如果 runner 用的是 Docker executor,每个 job 都是在独立的容器里启动的;job 一结束,容器就被销毁,临时文件也随之消失。

踩坑预警:产物并不等于仓库文件

CI 里 touch build/some-file.txt 只是让文件出现在当前 job 的临时工作目录里,它既不会进 Git 仓库,也不会自动穿透到后续 job。如果你想传产物,必须显式声明(Exits: 使用 artifacts 字段)。

真正把流水线接起来的,不是 stage,而是 artifacts

顺序问题解决后,接下来要解决的是“怎么把前一个 job 的产物带给后一个 job”。

GitLab 给的标准机制就是 artifacts。这不是一个“顺手可加可不加”的附加配置,而是跨 stage 传递构建产物的正式通道。你在前一个 job 里声明路径,后续 stage 的 job 默认会下载这些 artifacts。

我们最后跑通的最小示例,就是下面这个版本:

YAML
stages:
  - build
  - test
create_file:
  stage: build
  image: alpine:latest
  script:
    - echo "Building"
    - mkdir build
    - touch build/some-file.txt
  artifacts:
    paths:
      - build/
test_file:
  stage: test
  image: alpine:latest
  script:
    - test -f build/some-file.txt

这里真正关键的是 artifacts: paths: - build/。它把 build/ 目录从“当前 job 里的临时产物”,变成了“可以被后续 stage 消费的流水线产物”。这一步加上之后,test_file 才真的能拿到 build/some-file.txt。

image 这个字段,我们也差点理解歪了

另一个在入门阶段很容易被讲偏的点,是 image。

看到日志里出现 “Using Docker executor with image alpine” 这一类输出,很多人会以为 GitLab CI 就是默认跑在 Docker 里的。

这个理解不准确。更准确的说法应该是:job 运行在 runner 上,而 runner 又根据 executor 决定具体怎么执行 job。只有当 runner 使用 Docker executor 时,image 才表示这个 job 的容器镜像。

我们后来在团队里统一了一种更不容易出错的表述:

推荐表述

不是 “GitLab 用 Docker 跑 job”,而是 “当前这个 runner 用 Docker executor 跑 job,所以需要用 image 指定运行镜像”。

这个表述在真实项目里很重要。因为一旦后面切到 Shell executor(直接在宿主机跑),或者迁到 Kubernetes executor,你对 image 的理解就要跟着变。

这个示例很小,但它已经把真实项目最核心的链路踩了一遍

我们后来把这个例子拿去给新同学讲,不是因为它“简单”,而是因为它刚好够小,能把几个最重要的工程事实压缩在一起:

  1. 定义 stage:说明执行顺序。
  2. 定义 job:说明每一步做什么。
  3. 选运行环境:也就是 runner/executor 对应的上下文。
  4. 显式声明产物:解决跨 job 传递问题。

这条链路一旦理解了,后面往真实项目替换内容(比如换成 npm run build 或 pytest)就很自然了,差别只是产物从一个测试文件,变成了编译结果、测试报告或者打包镜像。

实战建议:还有两个点别省

第一个是 检查 Runner 是否可用。 很多入门讲解会忽略 runner。但 job 能跑的前提是项目有可用 runner(Shared 或 Specific)。如果没有,流水线会卡在 stuck 状态。

第二个是 写完先过 CI Lint。 YAML 配置的细碎错误(字段层级、少个短横线)靠肉眼很慢。GitLab 顶部导航栏的 CI/CD -> Editor 里的 Lint 工具能帮你筛掉 90% 的纯语法问题。

最后:四个认知校准

  1. .gitlab-ci.yml 只是入口,核心在于理解其背后的调度模型。
  2. Stage 决定顺序,Job 执行命令,同阶段默认并行。
  3. Job 默认独立,运行期文件不会自动保留。
  4. 传递产物必须明确使用 artifacts。

这套做法能帮你把最容易在第一天理解错的那部分钉牢。早点把它们讲透,后面接真实项目时,配置就不是“死记硬背”,而是真的知道自己在声明什么。

BACK TO BLOG
The End of Interaction