Pipeline

# Pipeline介绍

Pipeline 表示一个构建流程,由一个或者多个 Stage 组成,多个 Stage 依次执行,见 Stage 介绍

一个 Pipeline 的基本配置如下:

name: 流水线名字
docker:
  image: node
  build: dev/Dockerfile
  volumes:
    - /root/.npm:copy-on-write
git:
  enable: true
  submodules: true
services:
  - docker
env:
  TEST_KEY: TEST_VALUE
imports:
  - https://xxx/envs.yml
  - ./env.txt
label:
  type: MASTER
  class: MAIN
stages:
  - name: stage 1
    script: echo "stage 1"
  - name: stage 2
    script: echo "stage 2"
  - name: stage 3
    script: echo "stage 3"
failStages:
  - name: fail stage 1
    script: echo "fail stage 1"
  - name: fail stage 2
    script: echo "fail stage 2"
endStages:
  - name: end stage 1
    script: echo "end stage 1"
  - name: end stage 2
    script: echo "end stage 2"
ifModify:
  - a.txt
  - "src/**/*"
retry: 3
allowFailure: false

# name

  • type: String

指定流水线名,默认为 pipeline。当有多条并行流水线时, 默认流水线名为 pipelinepipeline-1pipeline-2 依此类推, 可定义 name 指定流水线名来区分不同流水线。

# runner

  • type: Object

指定构建机相关参数。

  • tags: 可选,指定使用具备哪些标签的构建机
  • cpus: 可选,指定构建需使用的 cpu 核数

# tags

  • type: StringArray<String>
  • default: cnb:arch:default

指定使用具备哪些标签的构建机。

SaaS 场景下可用的官方构建机标签如下:

  1. cnb:arch:amd64 代表 amd64 架构构建机
  2. cnb:arch:arm64:v8 代表 arm64/v8 架构构建机

ps. cnb:arch:default 代表默认构建机, SaaS场景为 amd64 架构构建机,等同于 cnb:arch:amd64

示例:

main:
  push:
    - runner:
        tags: cnb:arch:arm64:v8
      stages:
        - name: uname
          script: uname -a

# cpus

  • type: Number

指定构建需使用的最大 cpu 核数(memory = cpu 核数 * 2 G), 其中 cpu 和 memory 不超过 runner 机器实际大小。

未配置,则最大可用 cpu 核数由分配到的 runner 机器配置来指定。

示例:

# cpus = 1,memory = 2G
main:
  push:
    - runner:
        cpus: 1
      stages:
        - name: echo
          script: echo "hello world"

# docker

  • type: Object

指定 docker 相关的参数。详情见build-env

  • image: 当前 Pipeline 的环境镜像,在当前 Pipeline 下的所有任务都将在这个镜像环境中执行。
  • build: 指定一个 Dockerfile,构建一个临时镜像,做为 image 的值使用。
  • volumes: 声明数据卷,用于缓存场景。

# image

  • type: Object | String

指定当前 Pipeline 的环境镜像,在当前 Pipeline 下的所有任务都将在这个镜像环境中执行,支持引用环境变量。

  • image.name: String 镜像名,如 node:20
  • image.dockerUser: String 指定 Docker 用户名,用于拉取指定的镜像。
  • image.dockerPassword: String 指定 Docker 用户密码,用于拉取指定的镜像。

如果指定 image 为字符串,则等同于指定了 image.name

示例一,使用公开镜像:

main:
  push:
    - docker:
        # 取 docker 官方镜像仓库中的 node:20 镜像作为构建容器
        image: node:20
      stages:
        - name: show version
          script: node -v

示例二,使用 CNB 制品库私有镜像:

main:
  push:
    - docker:
        # 取非公开镜像作为构建容器,需传入 docker 用户名和密码
        image:
          name: docker.cnb.cool/images/pipeline-env:1.0
          # 使用 CI 构建时默认注入的环境变量
          dockerUser: $CNB_TOKEN_USER_NAME
          dockerPassword: $CNB_TOKEN
      stages:
        - name: echo
          script: echo "hello world"

示例三,使用 docker 官方镜像源私有镜像:

main:
  push:
    - imports: https://xxx/docker.yml
      docker:
        # 取非公开镜像作为构建容器,需传入 docker 用户名和密码
        image:
          name: images/pipeline-env:1.0
          # docker.yml 中导入的环境变量
          dockerUser: $DOCKER_USER
          dockerPassword: $DOCKER_PASSWORD
      stages:
        - name: echo
          script: echo "hello world"

docker.yml

DOCKER_USER: user
DOCKER_PASSWORD: password

# build

  • type: Object | String

指定一个 Dockerfile,构建一个临时镜像,做为 image 的值使用,支持引用环境变量。

  • build.dockerfile:

    • type: String

    Dockerfile路径。

  • build.target:

    • type: String

    对应 docker build 中的 --target 参数,可以选择性地构建 Dockerfile 中的特定阶段,而不是构建整个 Dockerfile。

  • build.by:

    • type: Array<String> | String

    用来声明缓存构建过程中依赖的文件列表。

    注意:未出现在 by 列表中的文件,除了 Dockerfile,其他在构建镜像过程中,都当不存在处理。

    String 类型时,多个文件可用英文逗号分隔。

  • build.versionBy:

    • type: Array<String> | String

    用来进行版本控制,所指向的文件内容发生变化,我们就会认为是一个新的版本, 具体的计算逻辑见这个表达式:sha1(dockerfile + versionBy + buildArgs)。

    String类型时,多个文件可用英文逗号分隔。

  • build.buildArgs:

    • type: Object

    在 build 时插入额外的构建参数 (--build-arg $key=$value), value 值为 null 时只加入 key (--build-arg $key)。

  • build.ignoreBuildArgsInVersion:

    • type: Boolean

    版本计算是否忽略buildArgs。详见 versionBy

  • build.sync:

    • type: String

    是否等待 docker push 成功后才继续。默认为false

如果指定build为字符串,则等同于指定了build.dockerfile

Dockerfile 用法:

main:
  push:
    - docker:
        # 通过 Dockerfile 指定构建环境
        build: ./image/Dockerfile
      stages:
        - stage1
        - stage2
        - stage3
main:
  push:
    - docker:
        # 通过 Dockerfile 指定构建环境
        build:
          dockerfile: ./image/Dockerfile
          dockerImageName: cnb.cool/project/images/pipeline-env
          dockerUser: $DOCKER_USER
          dockerPassword: $DOCKER_PASSWORD
      stages:
        - stage1
        - stage2
        - stage3
main:
  push:
    - docker:
        # 通过 Dockerfile 指定构建环境
        build:
          dockerfile: ./image/Dockerfile
          # 只构建 builder,而不是整个Dockerfile
          target: builder
      stages:
        - stage1
        - stage2
        - stage3

Dockerfile versionBy 用法:

示例:将 pnpm 缓存到环境镜像中,加速后续pnpm i过程

main:
  push:
    # 通过 Dockerfile 指定构建环境
    - docker:
        build:
          dockerfile: ./Dockerfile
          versionBy:
            - package-lock.json
      stages:
        - name: pnpm i
          script: pnpm i
        - stage1
        - stage2
        - stage3
FROM node:20

#替换真实的源
RUN npm config set registry https://xxx.com/npm/ \
  && npm i -g pnpm \
  && pnpm config set store-dir /lib/pnpm

WORKDIR /data/orange-ci/workspace

COPY package.json package-lock.json ./

RUN pnpm i
[pnpm i] Progress: resolved 445, reused 444, downloaded 0, added 100
[pnpm i] Progress: resolved 445, reused 444, downloaded 0, added 141
[pnpm i] Progress: resolved 445, reused 444, downloaded 0, added 272
[pnpm i] Progress: resolved 445, reused 444, downloaded 0, added 444, done
[pnpm i] 
[pnpm i] dependencies:
[pnpm i] + mocha 8.4.0 (10.0.0 is available)
[pnpm i] 
[pnpm i] devDependencies:
[pnpm i] + babel-eslint 9.0.0 (10.1.0 is available)
[pnpm i] + eslint 5.16.0 (8.23.0 is available)
[pnpm i] + jest 27.5.1 (29.0.2 is available)
[pnpm i] 
[pnpm i] 
[pnpm i] Job finished, duration: 6.8s

# volumes

  • type: Array<String> | String

声明数据卷,多个数据卷可用通过数组或者用,号做分割符传入,可引用环境变量,支持的格式有:

  1. <group>:<path>:<type>
  2. <path>:<type>
  3. <path>

各项含义:

  • group: 可选,数据卷分组,不同组间相互隔离
  • path: 必填,数据卷挂载绝对路径,支持绝对路径(/ 开头) 或 相对路径(./ 开头),相对于工作区
  • type: 可选,数据卷类型,缺省值为 copy-on-write,支持以下类型:
    • read-writerw : 读写,并发写冲突需自行处理,适用于串行构建场景
    • read-onlyro : 只读,写操作抛出异常
    • copy-on-writecow : 读写,变更(新增、修改、删除)在构建成功后被合并,适用于并发构建场景
    • copy-on-write-read-only : 只读,变更(新增、删除、修改)在构建结束后丢弃
    • data : 创建一个临时数据卷,该数据卷在流水线结束时会自动清理

# copy-on-write

用于缓存场景,支持并发

copy-on-write 技术允许系统在需要修改数据之前共享相同的数据副本,从而实现高效的缓存复制。 在并发环境中,这种方法避免了缓存的读写冲突,因为只有在实际需要修改数据时,才会创建数据的私有副本。 这样,只有写操作会导致数据复制,而读操作可以安全地并行进行,无需担心数据一致性问题。 这种机制显著提高了性能,尤其是在读多写少的缓存场景中。

# data

用于共享数据,将容器中的指定目录,共享给其他容器中使用。

通过创建数据卷,然后 mount 到各容器中。 与直接将构建机上目录 mount 到容器中方式不同的是:当指定的目录在容器中已经存在, 会先把容器中内容自动复制到数据卷,而不是将数据卷内容直接覆盖容器中目录。

# volumes 示例

示例1 : 挂载构建节点上目录到容器中,实现本地缓存效果

main:
  push:
    - docker:
        image: node:20
        # 声明数据卷
        volumes:
          - /data/config:read-only
          - /data/mydata:read-write

          # 使用缓存,同时更新
          - /root/.npm

          # 使用 main 缓存,同时更新
          - main:/root/.gradle:copy-on-write

      stages:
        - stage1
        - stage2
        - stage3
  pull_request:
    - docker:
        image: node:20

        # 声明数据卷
        volumes:
          - /data/config:read-only
          - /data/mydata:read-write

          # 使用 copy-on-write 缓存
          - /root/.npm
          - node_modules

          # pr 使用 main 缓存,但不更新
          - main:/root/.gradle:copy-on-write-read-only

      stages:
        - stage1
        - stage2
        - stage3

示例2:将打包在容器中的文件,共享到其他容器中使用

# .cnb.yml
main:
  push:
    - docker:
        image: go-app-cli # 假设有个go应用在镜像的/go-app/cli路径下
        # 声明数据卷
        volumes:
          # 此路径在go-app-cli镜像存在,所以执行环境镜像时,会将此路径内容复制到临时数据卷中,可共享给其他任务容器里使用
          - /go-app
      stages:
        - name: show /go-app-cli in job container
          image: alpine
          script: ls /go-app

# git

  • type: Object

提供 Git 仓库相关配置。

# git.enable

  • type: Boolean
  • default: true

指定是否拉取代码。

branch.delete 事件,默认值为 false。其他事件,默认值为 true

# git.submodules

  • type: Object | Boolean
  • default: true

指定是否要拉取子项目(submodules)。

支持 Object 形式指定具体参数,字段缺省时,默认值为:

{
  "enable": true,
  "remote": false
}

基本用法:

main:
  push:
    - git:
        enable: true
        submodules: true
      stages:
        - name: echo
          script: echo "hello world"
    - git:
        enable: true
        submodules:
          enable: true
          remote: true
      stages:
        - name: echo
          script: echo "hello world"

# git.submodules.enable

是否指定是否要拉取子项目 submodules

# git.submodules.remote

执行 git submodule update 时是否添加 --remote 参数,用于每次拉取 submodule 最新的代码

# services

  • type: Array<String>

用于声明构建时需要的服务,格式:name:[version], version 是可选的。

目前支持的服务有:

  • docker
  • vscode

# service:docker

用于开启 dind 服务,

当构建过程中需要使用 docker builddocker login 等操作时声明, 会自动在环境注入 docker daemondocker cli

示例:

main:
  push:
    - services:
        - docker
      docker:
        image: alpine
      stages:
        - name: docker info
          script:
            - docker info
            - docker ps

# service:vscode

需要远程开发时声明。

示例:

$:
  vscode:
    - services:
        - vscode
        - docker
      docker:
        image: alpine
      stages:
        - name: uname
          script: uname -a

# env

  • type: Object

指定环境变量。可以定义一组环境变量,在任务执行中使用。对当前 Pipeline 内的非插件任务均有效。

# imports

  • type: Array<String> | String

指定某个本仓库或另外 CNB Git 仓库文件路径,可以读取此文件作为环境变量来源。本地路径如 ./env.yml 会被拼接成远端文件地址进行加载。

一般使用一个私有仓库来存放诸如 npmdocker 等账号密码。

注意:对插件任务无效

云原生构建 现支持密钥仓库,安全性更高

支持的文件格式列表:

  • yaml:所有根路径下的属性名都会导出为环境变量,解析文件后缀为 .yml.yaml
  • json: 所有根路径下的属性名都会导出为环境变量,解析文件后缀为 .json
  • plain: 每行格式为 key=value,除了以上涉及的后缀都以此方式解析。(不推荐)

同名 key 优先级:

  • 当配置 imports 为数组时,如遇到参数重复的情况,后面的配置会覆盖前面的。
  • 如果和 env 参数中重复,那么 env 中的参数会覆盖掉 imports 文件中的。

变量赋值:

imports 文件路径可读取环境变量。若是数组,下面的文件路径可读取上面文件中的变量

main:
  push:
    - imports:
        - ./env1.json
        - $FILE
        - https://xxx/xxs.yml
      stages:
        - name: echo
          script: echo $TEST_ENV

引用配置文件权限控制参考 配置文件引用鉴权

示例:

team_name/project_name/*,匹配一个项目下面的所有仓库:

key: value

allow_slugs:
  - team_name/project_name/*

允许被所有仓库引用

key: value

allow_slugs:
  - "**"

# label

  • type: Object

为流水线指定标签。每个标签的值可以是一个字符串,也可以是一个字符串数组。该标签可用于后续流水线记录筛选等功能。

这里举一种工作流的例子:main 分支合并即发布预发布环境,打 tag 后发布正式环境

main:
  push:
    - label:
        # Master 分支的常规流水线
        type:
          - MASTER
          - PREVIEW
      stages:
        - name: install
          script: npm install
        - name: CCK-lint
          script: npm run lint
        - name: BVT-build
          script: npm run build
        - name: UT-test
          script: npm run test
        - name: pre release
          script: ./pre-release.sh

$:
  tag_push:
    - label:
        # 产品发布分支的常规流水线
        type: RELEASE
      stages:
        - name: install
          script: npm install
        - name: build
          script: npm run build
        - name: DELIVERY-release
          script: ./release.sh

# stages

  • type: Array<Job>

定义一组阶段任务,每个阶段串行运行。

# failStages

  • type: Array<Job>

定义一组失败阶段任务。当正常流程失败,会依次执行此阶段任务。

# endStages

  • type: Array<Job>

定义流水线结束阶段执行的一组任务。当流水线 stages/failStages 执行完,流水线结束前,会依次执行此阶段任务。

当流水线 prepare 阶段成功,无论 stages 是否成功,endStages 都将执行。 且 endStages 是否成功不影响流水线状态(即 endStages 失败,流水线状态也可能是成功)。

# ifNewBranch

  • type: Boolean
  • default: false

true 表示当前分支属于新分支(即 CNB_IS_NEW_BRANCHtrue)时,才执行此 Pipeline

当同时存在 ifNewBranch / ifModify 时,其中有一个条件满足,此 Pipeline 就会执行。

# ifModify

  • type: Array<String> | String

指定只有相应文件变动时,才执行此 Pipeline。 是一个 glob 表达式字符串或字符串数组。

示例1:

当修改文件列表中包含 a.js 或者 b.js,会执行此 Pipeline

ifModify:
  - a.js
  - b.js

示例2:

当修改文件列表中包含有 js 后缀的文件时,会执行此 Pipeline。 其中 **/*.js 表示匹配所有子目录中的 js 后缀文件,*.js 表示所有根目录中的 js 后缀文件。

ifModify:
  - "**/*.js"
  - "*.js"

示例3:

反向匹配,排除目录 legacy 和排除所有 Markdown 文件,有其他文件变更时触发

ifModify:
  - "**"
  - "!(legacy/**)"
  - "!(**/*.md)"
  - "!*.md"

示例4:

反向匹配,src 目录并且除目录 src/legacy 以外有变更时触发

ifModify:
  - "src/**"
  - "!(src/legacy/**)"

# 支持事件

  • 非新建分支的 push 事件,会对比 beforeafter 统计变更文件。
  • 非新建分支的 push 事件流水线中通过 cnb:apply 触发的事件,变更文件统计规则同上。
  • PR 触发的事件,统计 PR 中的变更文件。
  • PR 触发的事件通过 cnb:apply 触发的事件,统计 PR 中的变更文件。

因为文件变更可能非常多,变更文件的统计限制为最多300个。

# breakIfModify

  • type: Boolean
  • default: false

Job 执行前,如果源分支已更新,则终止构建。

# retry

  • type: Number
  • default: 0

失败重试次数, 0 表示不重试。

# allowFailure

  • type: Boolean
  • default: false

是否允许当前流水线 失败。

当此参数设置为 true 时,流水线的失败的状态不会上报到 CNB 上。

# lock

  • type: Object | Boolean

pipeline 设置锁,pipeline 执行完后自动释放锁,锁不能跨仓库使用。

表现: 流水线 A 获取到锁后,流水线 B 再申请锁,可以终止A或等待A执行完释放锁后,获取到锁再继续执行任务。

  • key:

    • type: String

    自定义锁名,默认为 分支名-流水线名,既锁范围默认为当前 pipeline

  • expires:

    • type: Number
    • default: 3600(一小时)

    锁过期时间,过期后自动释放锁,单位“秒”。

  • timeout:

    • type: Number
    • default: 3600(一小时)

    超时时间,用于等待锁的场景下,单位“秒”。

  • cancel-in-progress:

    • type: Boolean
    • default: false

    是否终止占用锁或等待锁的流水线,让当前流水线获取锁并执行

  • wait:

    • type: Boolean
    • default: false

    锁被占用是否等待(不占用流水线资源和耗时),为 false 则直接报错,不能与 cancel-in-progress 同时使用

  • cancel-in-wait:

    • type: Boolean
    • default: false

    是否终止正在等待锁的流水线,让当前流水线加入等待锁队列。需配合wait属性使用。

例1: lock 是 Boolean 格式

main:
  push:
    - lock: true
      stages:
        - name: stage1
          script: echo "stage1"

例2: lock 是 Object 格式

main:
  push:
    - lock:
        key: key
        expires: 600 # 10分钟
        wait: true
        timeout: 60 # 最多等待 1分钟
      stages:
        - name: stage1
          script: echo "stage1"

例3: 停止 pull_request 下上一条正在进行的流水线

main:
  pull_request:
    - lock:
        key: pr
        cancel-in-progress: true
      stages:
        - name: echo hello
          script: echo "stage1"