一、实战场景模板
1.1 Node.js 前端项目
stages:
- install
- lint
- test
- build
- deploy
default:
image: node:20-alpine
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
install:
stage: install
cache:
policy: pull-push
script:
- npm ci
artifacts:
paths:
- node_modules/
expire_in: 1 hour
lint:
stage: lint
cache:
policy: pull
script:
- npm run lint
allow_failure: true
unit-test:
stage: test
cache:
policy: pull
script:
- npm test -- --coverage
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
stage: build
cache:
policy: pull
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 day
deploy-vercel:
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- npx vercel deploy --prod --token=$VERCEL_TOKEN
environment:
name: production
url: https://your-app.vercel.app1.2 Java Spring Boot 项目 + Docker 镜像
stages:
- test
- build
- docker
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
DOCKER_REGISTRY: registry.example.com
IMAGE_NAME: $DOCKER_REGISTRY/$CI_PROJECT_NAME
cache:
key:
files:
- pom.xml
paths:
- .m2/repository/
default:
image: maven:3.9-eclipse-temurin-21
unit-test:
stage: test
script:
- mvn test
artifacts:
reports:
junit:
- target/surefire-reports/TEST-*.xml
build:
stage: build
script:
- mvn package -DskipTests
artifacts:
paths:
- target/*.jar
expire_in: 1 hour
docker-build:
stage: docker
image: docker:latest
services:
- docker:dind
variables:
DOCKER_TLS_CERTDIR: ""
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY
script:
- docker build -t $IMAGE_NAME:$CI_COMMIT_SHORT_SHA .
- docker tag $IMAGE_NAME:$CI_COMMIT_SHORT_SHA $IMAGE_NAME:latest
- docker push $IMAGE_NAME:$CI_COMMIT_SHORT_SHA
- docker push $IMAGE_NAME:latest
deploy-k8s:
stage: deploy
image: bitnami/kubectl:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- kubectl set image deployment/$CI_PROJECT_NAME \
app=$IMAGE_NAME:$CI_COMMIT_SHORT_SHA \
--namespace=production
environment:
name: production1.3 Python 项目
stages:
- test
- build
- deploy
default:
image: python:3.12-slim
cache:
key:
files:
- requirements.txt
- pyproject.toml
paths:
- .venv/
- ~/.cache/pip/
before_script:
- pip install --upgrade pip
- pip install -r requirements.txt
lint:
stage: test
script:
- pip install ruff
- ruff check .
- ruff format --check .
allow_failure: true
test:
stage: test
script:
- pip install pytest pytest-cov
- pytest --cov=. --cov-report=xml --junitxml=junit.xml
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
docker-build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t myapp:$CI_COMMIT_SHORT_SHA .
- docker push myapp:$CI_COMMIT_SHORT_SHA
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG1.4 多环境部署(dev / staging / production)
stages:
- build
- deploy-dev
- deploy-staging
- deploy-prod
.deploy-template: &deploy-template
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
script:
- |
ssh $DEPLOY_HOST "
cd /app &&
docker pull $IMAGE:$CI_COMMIT_SHORT_SHA &&
docker stop app-${ENV} || true &&
docker run -d --name app-${ENV} --restart always \
-p ${PORT}:8080 $IMAGE:$CI_COMMIT_SHORT_SHA
"
deploy-dev:
<<: *deploy-template
stage: deploy-dev
variables:
ENV: dev
PORT: "8081"
DEPLOY_HOST: $DEV_HOST
environment:
name: development
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy-staging:
<<: *deploy-template
stage: deploy-staging
variables:
ENV: staging
PORT: "8080"
DEPLOY_HOST: $STAGING_HOST
environment:
name: staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-prod:
<<: *deploy-template
stage: deploy-prod
variables:
ENV: prod
PORT: "8080"
DEPLOY_HOST: $PROD_HOST
environment:
name: production
when: manual
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/1.5 Monorepo 差异化构建
stages:
- detect
- build
- deploy
# 第一步:检测变更,设定变量
detect-changes:
stage: detect
image: alpine:latest
script:
- |
if git diff --name-only HEAD~1 | grep -q "^services/api/"; then
echo "BUILD_API=true" >> build.env
fi
if git diff --name-only HEAD~1 | grep -q "^services/web/"; then
echo "BUILD_WEB=true" >> build.env
fi
if git diff --name-only HEAD~1 | grep -q "^services/worker/"; then
echo "BUILD_WORKER=true" >> build.env
fi
artifacts:
reports:
dotenv: build.env # ← 将变量传递给后续 Job
build-api:
stage: build
needs: [detect-changes]
rules:
- if: $BUILD_API == "true"
script:
- cd services/api && dotnet publish
build-web:
stage: build
needs: [detect-changes]
rules:
- if: $BUILD_WEB == "true"
script:
- cd services/web && npm run build
build-worker:
stage: build
needs: [detect-changes]
rules:
- if: $BUILD_WORKER == "true"
script:
- cd services/worker && go build1.6 代码质量 + 安全扫描一体
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Code-Quality.gitlab-ci.yml
stages:
- test
- build
# 自定义代码质量门槛
code-quality-check:
stage: test
image:
name: registry.gitlab.com/gitlab-org/ci-cd/codequality:latest
entrypoint: [""]
script:
- /run.sh
artifacts:
reports:
codequality: gl-code-quality-report.json
allow_failure: true二、常见错误排查指南
2.1 "yaml invalid" —— YAML 语法错误
症状:Pipeline 页面显示 "YAML invalid",配置未生效。
常见原因:
缩进混用了 Tab 和空格
冒号后没有空格(image:node:20 应为 image: node:20)
字符串中包含冒号但未加引号
解决步骤:
用 CI Lint 验证:项目 → Build → Pipeline Editor → Validate
如果用 VS Code,安装 "GitLab CI/CD" 插件获得实时语法高亮
检查是否有不可见字符:cat -A .gitlab-ci.yml
2.2 "This job is stuck" —— Job 卡住不动
症状:Job 一直处于 pending 状态,显示为灰色。
排查清单:
2.3 "no runner with tags matching xxx"
症状:Job 提示 "This job is stuck because you don't have any active runners with these tags assigned to them."
解决:
检查 Runner 标签和 Job 标签是否一致(注意大小写)
确认 Runner 没有被标记为 paused
如果是 Shared Runner,确认项目没有禁用 Shared Runner:Settings → CI/CD → Runners → "Enable shared runners"
2.4 "permission denied" —— 权限不足
常见场景与解决:
# 场景 1:Docker 命令权限不足
script:
- docker build -t myapp . # ❌ permission denied解决:Runner 注册时挂载 Docker Socket,或在 config.toml 中设置 privileged = true
# 场景 2:SSH 部署密钥权限
script:
- ssh user@server "deploy.sh" # ❌ permission denied解决:在 GitLab Variables 中创建 File 类型的 SSH 私钥变量
deploy:
before_script:
- chmod 600 $SSH_PRIVATE_KEY
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
script:
- ssh -i $SSH_PRIVATE_KEY user@server "deploy.sh"2.5 缓存不生效
调试方法:在 Job 脚本中打印目录内容确认:
script:
- ls -la node_modules/ # 存在吗?
- du -sh node_modules/ # 大小合理吗?常见原因:
cache:key 不匹配 → 不同分支的 $CI_COMMIT_REF_SLUG 不同,缓存在不同分支间不共享
paths 使用了绝对路径 → 必须用相对路径
缓存文件太大超过限制(默认 100MB)→ 排除不必要文件
Shell Executor 默认不支持缓存 → 需要配置缓存目录
2.6 artifacts 找不到
常见原因:
生成文件的命令执行失败,但被 allow_failure: true 掩盖了
paths 中的文件确实不存在(构建失败)
依赖的 Job 没有运行(被 rules 跳过了)
dependencies 限制了产物传递
调试:在获取 artifacts 的 Job 中加 ls 确认:
deploy:
dependencies:
- build
script:
- ls -la dist/ # 确认文件存在
- ls -la target/ # 确认文件存在三、最佳实践清单
3.1 安全实践
3.2 性能实践
3.3 可维护性实践
3.4 架构设计实践
3.5 推荐目录结构
项目根目录/
├── .gitlab-ci.yml # 入口文件(stages 定义 + includes)
├── ci/
│ ├── common.yml # 全局变量、默认值、隐藏 Job 基类
│ ├── build.yml # 构建相关 Job
│ ├── test.yml # 测试相关 Job
│ ├── deploy.yml # 部署相关 Job
│ └── security.yml # 安全扫描 Job四、关键词速查表
五、系列总结:从入门到精通的成长路径
回顾本系列的六篇文章:
┌────────────────────────────────────────────┐
│ 📖 第一篇:概念入门 │
│ Pipeline / Stage / Job / Runner 是什么? │
│ 写出第一份 .gitlab-ci.yml │
├────────────────────────────────────────────┤
│ 🔧 第二篇:Runner 部署 │
│ Shell / Docker / K8s 三种执行器 │
├────────────────────────────────────────────┤
│ 🧱 第三篇:核心关键字 │
│ stages / jobs / variables / cache / artifacts│
├────────────────────────────────────────────┤
│ 🧠 第四篇:流程控制 │
│ rules / workflow / when / needs / parallel │
├────────────────────────────────────────────┤
│ 🏗️ 第五篇:进阶与复用 │
│ include / extends / trigger / environment │
├────────────────────────────────────────────┤
│ 🎯 第六篇:实战 + 排错 + 最佳实践 │
│ 拿来就用的模板 + 常见问题排查 │
└────────────────────────────────────────────┘如果你跟着这个路径走下来,现在应该能:
✅ 独立为任何项目编写 CI/CD 配置
✅ 理解并使用 30+ 个 YAML 关键字
✅ 搭建模块化、可复用的配置体系
✅ 快速排查 Pipeline 故障
✅ 应用经过验证的最佳实践
GitLab CI/CD 的世界远不止于此——还有 Auto DevOps、CI/CD Catalog、GitLab Duo AI 辅助等更高级的功能等你探索。但掌握好本系列的基础,你已经站在了一个坚实的起点上。Happy CI/CD! 🚀
参考资料: