4. 阶段、作业与依赖编排

在 GitLab CI/CD 中,.gitlab-ci.yml 默认按 stage 顺序串行推进:同一 stage 内的作业并行执行,只有当前 stage 全部成功,下一 stage 才会启动。这种“阶段闸门”模型简单可靠,但在大型研发体系中会带来明显等待——例如单元测试、静态扫描、镜像构建本可并行,却因被放进不同 stage 而被迫排队。要突破这一限制,就需要掌握 needs、dependencies、rules 与 only/except 这四组控制手段。

一、stage 是骨架,needs 是“抄近道”

stages 定义流水线的阶段顺序,每个作业通过 stage 关键字归属到某个阶段。默认依赖是“前一阶段全部完成”。而 needs 允许一个作业跳过阶段顺序,直接依赖指定的上游作业,只要这些作业完成即可启动,无需等待整个 stage 结束。这就是 DAG(有向无环图)流水线。

stages: [build, test, package, deploy]

build:app:
  stage: build
  script: ["./mvnw -q package -DskipTests"]

test:unit:
  stage: test
  needs: ["build:app"]        # 不等 build 阶段其他作业
  script: ["./mvnw -q test"]

scan:sast:
  stage: test
  needs: []                   # 无依赖,流水线创建后立即运行
  script: ["./run-sast.sh"]

package:image:
  stage: package
  needs: ["test:unit"]        # 只等单测,不等 scan:sast
  script: ["./build-image.sh"]

上例中,scan:sast 用 needs: [] 声明“无前置依赖”,可在流水线创建后立刻执行;package:image 只等 test:unit,不必等 scan:sast 结束。这样关键路径被显著缩短。需要注意:needs 引用的作业必须存在于同一流水线中,且不能形成环,否则配置校验会直接报错。

二、dependencies 与 needs 的区别

两者常被混淆。needs 控制的是作业何时可以开始(调度依赖);dependencies 控制的是从哪些上游作业下载 artifacts(产物依赖)。默认情况下,作业会下载所有前序 stage 的 artifacts,这既慢又可能引入无关文件。用 dependencies 精确指定,可以只取需要的产物。

test:unit:
  stage: test
  needs: ["build:app"]
  dependencies: ["build:app"]   # 只下载 build:app 的产物
  script: ["./mvnw -q test"]

deploy:prod:
  stage: deploy
  needs: ["package:image"]
  dependencies: []              # 不需要任何 artifacts
  script: ["./deploy.sh"]

实践建议:凡是显式写了 needs 的作业,最好同时写 dependencies,把产物来源收敛到最小集合,避免“依赖了 A,却下载了 B、C、D 的产物”这类隐性开销。若某作业确实不需要任何产物,写 dependencies: [] 是最干净的做法。

三、rules:条件触发的主力

only/except 是早期语法,基于分支名、标签、事件做简单匹配,逻辑表达能力弱,官方已推荐用 rules 替代。rules 按顺序逐条求值,命中第一条即决定作业是否加入流水线,并可同时覆盖 when、变量等属性。

deploy:staging:
  stage: deploy
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
      when: on_success
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: manual
      allow_failure: true
    - when: never            # 其余情况不创建该作业

deploy:prod:
  stage: deploy
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
      when: manual
    - when: never

这里 deploy:staging 在 develop 分支自动执行,在 MR 场景变为手动且允许失败,其他情况完全不创建;deploy:prod 仅在符合语义化版本标签(如 v1.4.0)时出现,且必须人工点击。这种“条件 + 手动门禁”的组合,是生产发布最常见的控制方式。

四、only/except 的定位与迁移

老项目中仍会见到 only: [main, tags]、except: [ schedules ] 这类写法。它适合极简场景,但无法表达“if + 变量 + when”的组合,也不支持 rules 那样的顺序短路。迁移时把握一条原则:把每个 only/except 条件翻译成一条 rules 的 if,最后补一条 when: never。例如 only: [main] 等价于 rules: [{ if: '$CI_COMMIT_BRANCH == "main"' }, { when: never }]。

同一作业中不要混用 rules 与 only/except,GitLab 会报配置冲突。迁移应整作业替换,而非逐条叠加。

五、编排关系总览

下面这张图把本节四组关键字的作用位置放在同一条流水线上:stage 决定纵向骨架,needs 画出横向捷径,dependencies 决定产物流向,rules 决定节点是否出现以及以何种 when 出现。

stage: build stage: test stage: package stage: deploy build:app 产出 jar test:unit needs: build:app scan:sast needs: [] package:image needs: test:unit deploy:prod rules: tag + manual needs 捷径 无依赖立即跑 只等单测 dependencies: 只下载 build:app 的 jar rules 判定 tag 匹配? → manual / never 默认阶段依赖 needs 调度捷径 dependencies 产物流 rules 条件门禁

六、编排时的常见判断

  • 关键路径优先:先找出从提交到部署的最长依赖链,用 needs 把非关键作业从链上摘出去,缩短总时长。
  • 产物最小化:每个作业只声明真正需要的 dependencies,减少下载与解压开销。
  • 条件收敛:用 rules 的 when: never 兜底,避免作业在非预期分支上意外创建。
  • 手动门禁:生产部署用 when: manual,配合标签或保护分支,形成人工确认点。
  • 避免环:needs 关系必须无环,否则流水线无法创建。
使用 needs 后,作业不再受 stage 顺序保护。若上游作业尚未产出所需 artifacts,下游可能拿到空目录。务必让 needs 与 dependencies 指向同一批作业,保证调度与产物一致。

把 stage 当作骨架、needs 当作捷径、dependencies 当作产物管道、rules 当作开关,四者配合就能在保持可读性的同时,把大型流水线的等待时间压到最低。实际落地时,建议先在测试分支验证 DAG 结构,再逐步推广到主干与发布流程。