跳转至

第三章:Jobs 与 Stages

Stages 详解

默认 Stages

# GitLab 默认提供 5 个阶段
stages:
  - .pre    # 始终第一个执行
  - build
  - test
  - deploy
  - .post   # 始终最后执行

自定义 Stages

stages:
  - lint
  - build
  - test
  - security
  - staging
  - production

Stage 执行顺序

┌─────────────────────────────────────────────────────────────────┐
│                         Pipeline                                 │
├─────────────────┬─────────────────┬─────────────────────────────┤
│    Stage: lint  │    Stage: build │    Stage: test              │
│                 │                 │                             │
│ Job: eslint     │ Job: build:app  │ Job: test:unit              │
│ Job: prettier   │ Job: build:lib  │ Job: test:integration       │
│    (并行)        │    (并行)        │ Job: test:e2e               │
│                 │                 │    (并行)                    │
└─────────────────┴─────────────────┴─────────────────────────────┘
        ↓                   ↓                   ↓
    lint 完成          build 完成          test 完成

阶段依赖

stages:
  - build
  - test
  - deploy

build_app:
  stage: build
  script: npm run build

test_unit:
  stage: test
  script: npm test
  needs: [build_app]  # 不等待整个 build 阶段完成

deploy:
  stage: deploy
  script: echo "Deploy"
  needs: [test_unit]

Jobs 详解

Job 名称规则

# 有效名称
build_app:
  script: echo "Build"

test-unit:
  script: echo "Test"

deploy_to_production:
  script: echo "Deploy"

# 无效名称(保留字)
image:
  script: echo "Invalid"  # 错误:image 是关键字

services:
  script: echo "Invalid"  # 错误:services 是关键字

隐藏 Job

# 以 . 开头的 Job 不会执行
.hidden_job:
  script:
    - echo "This job is hidden"

# 用于继承模板
.base_job:
  image: node:18
  before_script:
    - npm install

build:
  extends: .base_job
  script:
    - npm run build

Job 模板继承

# 定义模板
.build_template:
  image: node:18
  before_script:
    - npm ci
  script:
    - npm run build

# 继承模板
build:app:
  extends: .build_template
  script:
    - npm run build:app

build:lib:
  extends: .build_template
  script:
    - npm run build:lib

# 多重继承
build:all:
  extends:
    - .build_template
    - .deploy_template

Job 执行控制

# 条件执行
deploy:production:
  stage: deploy
  script:
    - echo "Deploy to production"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
    - when: never

# 失败后执行
cleanup:
  stage: .post
  script:
    - echo "Cleanup after failure"
  when: on_failure

# 总是执行
notify:
  stage: .post
  script:
    - echo "Send notification"
  when: always

并行执行

并行 Job 实例

test:
  parallel: 5
  script:
    - echo "Instance $CI_NODE_INDEX of $CI_NODE_TOTAL"
    - npm run test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL

矩阵构建

test:
  parallel:
    matrix:
      - NODE_VERSION: [16, 18, 20]
        OS: [linux, windows]
  image: node:${NODE_VERSION}
  script:
    - echo "Testing Node $NODE_VERSION on $OS"
  tags:
    - ${OS}

多平台构建

build:
  parallel:
    matrix:
      - ARCH: [amd64, arm64]
        PLATFORM: [linux, darwin]
  script:
    - GOOS=$PLATFORM GOARCH=$ARCH go build -o app-$PLATFORM-$ARCH
  artifacts:
    paths:
      - app-$PLATFORM-$ARCH

Job 依赖管理

needs 关键字

# 无依赖,立即开始
lint:
  stage: lint
  script: npm run lint
  needs: []

# 依赖特定 Job
test:unit:
  stage: test
  script: npm test
  needs: [build]

# 依赖多个 Job
deploy:
  stage: deploy
  script: echo "Deploy"
  needs:
    - build
    - test:unit
    - test:integration

# 可选依赖
deploy:
  stage: deploy
  script: echo "Deploy"
  needs:
    - job: test:unit
      optional: true

DAG 执行

# 传统阶段执行
# lint → build → test → deploy(串行)

# DAG 执行(needs)
# lint(立即开始)
# build(立即开始)
# test:unit(等待 build)
# test:integration(等待 build)
# deploy(等待所有 test)

stages:
  - lint
  - build
  - test
  - deploy

lint:
  stage: lint
  script: npm run lint
  needs: []

build:
  stage: build
  script: npm run build
  needs: []

test:unit:
  stage: test
  script: npm run test:unit
  needs: [build]

test:integration:
  stage: test
  script: npm run test:integration
  needs: [build]

deploy:
  stage: deploy
  script: echo "Deploy"
  needs:
    - lint
    - test:unit
    - test:integration

制品传递

生成制品

build:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
      - build/
    exclude:
      - dist/*.log
    expire_in: 1 week
    when: always

使用制品

test:
  stage: test
  script:
    - ls dist/
  dependencies:
    - build  # 只下载 build 的制品

deploy:
  stage: deploy
  script:
    - rsync -avz dist/ server:/app/
  needs:
    - job: build
      artifacts: true

缓存策略

# 全局缓存
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/

# Job 级别缓存
build:
  cache:
    key: ${CI_COMMIT_REF_SLUG}-build
    paths:
      - node_modules/
      - .npm/
    policy: pull-push  # 默认

# 只拉取缓存
test:
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
    policy: pull

# 只推送缓存
setup:
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
    policy: push

实战示例

前端项目

stages:
  - install
  - lint
  - build
  - test
  - deploy

variables:
  NODE_VERSION: "18"

.node_template:
  image: node:${NODE_VERSION}
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

install:
  extends: .node_template
  stage: install
  script:
    - npm ci
  cache:
    policy: push
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

lint:
  extends: .node_template
  stage: lint
  script:
    - npm run lint
  needs: [install]

build:
  extends: .node_template
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
  needs: [install]

test:unit:
  extends: .node_template
  stage: test
  script:
    - npm run test:unit -- --coverage
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  needs: [install]

test:e2e:
  extends: .node_template
  stage: test
  image: cypress/included:latest
  script:
    - npm run test:e2e
  artifacts:
    paths:
      - cypress/screenshots/
      - cypress/videos/
    when: on_failure
  needs:
    - job: install
    - job: build

deploy:staging:
  stage: deploy
  script:
    - echo "Deploy to staging"
  environment:
    name: staging
  only:
    - develop
  when: manual
  needs: [build]

deploy:production:
  stage: deploy
  script:
    - echo "Deploy to production"
  environment:
    name: production
  only:
    - main
  when: manual
  needs:
    - build
    - test:unit
    - test:e2e

后端项目

stages:
  - lint
  - build
  - test
  - security
  - deploy

variables:
  PYTHON_VERSION: "3.11"

.lint_template:
  image: python:${PYTHON_VERSION}
  before_script:
    - pip install -r requirements-dev.txt

flake8:
  extends: .lint_template
  stage: lint
  script:
    - flake8 app/
  allow_failure: true

mypy:
  extends: .lint_template
  stage: lint
  script:
    - mypy app/

build:
  image: python:${PYTHON_VERSION}
  stage: build
  script:
    - pip install build
    - python -m build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

test:
  image: python:${PYTHON_VERSION}
  stage: test
  services:
    - postgres:15
    - redis:7
  variables:
    POSTGRES_DB: test_db
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
    DATABASE_URL: postgres://test:test@postgres:5432/test_db
  script:
    - pip install -r requirements-dev.txt
    - pytest --cov=app tests/ --junitxml=report.xml
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      junit: report.xml

security:
  image: python:${PYTHON_VERSION}
  stage: security
  script:
    - pip install safety bandit
    - safety check
    - bandit -r app/
  allow_failure: true

deploy:
  stage: deploy
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:
    - main
  when: manual

小结

本章学习了:

  • ✅ Stages 详解
  • ✅ Jobs 详解
  • ✅ 并行执行
  • ✅ Job 依赖管理
  • ✅ 制品传递
  • ✅ 缓存策略
  • ✅ 实战示例

下一章

第四章:变量与缓存 - 学习变量和缓存的高级用法。