主题
Jenkins Pipeline 集成 AI + GitLab 保姆级教程
版本: 1.0 | 日期: 2026-08-25
基于真实生产环境 Build #12 (SUCCESS, 102s) 编写
技术栈: Jenkins LTS (K8s) + GitLab v19.2 + Ollama Qwen3.6:27B
📋 目录
- 一、总览:三个系统如何协作
- 二、前置条件
- 三、第一步:配置 GitLab → Jenkins 触发(Webhook)
- 四、第二步:配置 Jenkins → GitLab 认证(JCasC)
- 五、第三步:编写 Jenkinsfile(核心)
- 六、第四步:编写 AI 代码审查脚本
- 七、第五步:推送代码 & 验证流水线
- 八、完整数据流图:一次构建的生命周期
- 九、配置要素速查表
- 十、常见问题 & 解决方案
- 附录 A:完整 Jenkinsfile(可直接复制)
- 附录 B:完整 ai-code-review.sh(可直接复制)
- 附录 C:JCasC ConfigMap 配置
- 附录 D:Build #12 真实日志摘录
一、总览:三个系统如何协作
开发者 git push
│
▼
┌──────────┐ Webhook POST ┌──────────────┐
│ GitLab │ ─────────────────→ │ Jenkins │
│ :8443 │ ←───────────────── │ :9010 │
│ │ Commit Status │ │
│ demo- │ ✅ jenkins │ Pipeline: │
│ app-v2 │ ✅ ai-review │ Checkout │
│ │ │ Build&Test │
└──────────┘ │ Package │
│ AI Review │
└──────┬───────┘
│
HTTP /api/chat
│
▼
┌──────────────┐
│ Ollama │
│ :11434 │
│ Qwen3.6:27B │
│ 17GB 模型 │
└──────────────┘三条数据通道:
| 通道 | 方向 | 协议 | 用途 |
|---|---|---|---|
| ① GitLab → Jenkins | GitLab Webhook | HTTP POST | Push 事件触发构建 |
| ② Jenkins → Ollama | Agent Pod 内脚本 | HTTP /api/chat | AI 代码审查 |
| ③ Jenkins → GitLab | gitlab-plugin | GitLab API | 构建结果回写 Commit |
二、前置条件
在开始之前,确保以下环境已就绪:
2.1 必需的基础设施
| 组件 | 版本 | 用途 | 验证命令 |
|---|---|---|---|
| K8s 集群 | RKE2 v1.35 | 运行 Jenkins/GitLab | kubectl get nodes |
| Jenkins | LTS JDK21 (Helm) | CI/CD 引擎 | curl https://ai-ear.cn:9010/login |
| GitLab | v19.2 (Helm) | 代码仓库 | curl https://ai-ear.cn:8443/-/health |
| Ollama | 最新版 | AI 推理引擎 | curl http://192.168.122.9:11434/api/tags |
2.2 必需的 Jenkins 插件
在 Jenkins Helm values 的 additionalPlugins 中安装:
| 插件 | 用途 | 在 Pipeline 中的使用 |
|---|---|---|
gitlab-plugin | GitLab 集成 + Commit Status 回写 | updateGitlabCommitStatus |
kubernetes | K8s Agent Pod 动态调度 | agent { label 'maven21' } |
pipeline-utility-steps | 流水线工具步骤 | readJSON, writeJSON |
junit | 测试报告收集 | junit testResults: ... |
2.3 必需的 Agent Pod 模板
在 Jenkins Helm values 的 agent.podTemplates 中配置:
| Label | 镜像 | 用途 |
|---|---|---|
maven21 | maven:3.9-eclipse-temurin-21 | Java 构建 + 测试 |
python | python:3.12-alpine | Python 测试 + AI 审查 |
⚠️ Alpine 镜像限制:Python Agent 基于
python:3.12-alpine,没有bash、curl、git、jq。所有 shell 脚本必须用sh,HTTP 请求必须用 Pythonurllib。
三、第一步:配置 GitLab → Jenkins 触发(Webhook)
3.1 工作原理
开发者 git push origin main
│
▼
GitLab 检测到 push_events
│
│ HTTP POST (JSON payload)
│ 包含: commit SHA, branch, author, files changed
▼
Jenkins Webhook 端点
/job/dev/job/gitlab-cicd-demo/build
│
│ 解析 payload → 匹配 Job
▼
触发 Pipeline 构建3.2 创建 Webhook
方式一:通过 GitLab API(推荐)
bash
# 变量定义
GITLAB_URL="https://ai-ear.cn:8443"
GITLAB_TOKEN="glpat-xxx" # GitLab Personal Access Token
PROJECT_ID=4 # demo-app-v2 的项目 ID
JENKINS_USER="admin"
JENKINS_WEBHOOK_TOKEN="113b41e49846a3238ce2f02d3dd23a9476" # Webhook 认证 Token
JENKINS_HOST="jenkins.jenkins.svc.cluster.local:8080" # K8s 内部地址
JOB_PATH="job/dev/job/gitlab-cicd-demo/build" # Job 路径
# 创建 Webhook
curl -sk -X POST \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
"${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/hooks" \
-d "{
\"url\": \"http://${JENKINS_USER}:${JENKINS_WEBHOOK_TOKEN}@${JENKINS_HOST}/${JOB_PATH}\",
\"push_events\": true,
\"merge_requests_events\": false,
\"enable_ssl_verification\": false
}"URL 格式详解:
http://admin:TOKEN@jenkins.jenkins.svc.cluster.local:8080/job/dev/job/gitlab-cicd-demo/build
│ │ │ │ │ │
│ │ │ │ │ └─ 触发构建的 API
│ │ │ │ └─ Job 名称 (Multibranch)
│ │ │ └─ 文件夹名称
│ │ └─ Jenkins K8s 内部 Service 地址
│ └─ Basic Auth Token (RBAC 中 webhook-trigger 角色的密码)
└─ Basic Auth 用户名为什么用 K8s 内部地址? GitLab 和 Jenkins 都在同一个 K8s 集群中,使用 ClusterIP 地址
jenkins.jenkins.svc.cluster.local:8080可以避免走外网,延迟更低。
方式二:通过 GitLab Web UI
GitLab → 项目 → Settings → Webhooks → Add new webhook
URL: http://admin:TOKEN@jenkins...svc.cluster.local:8080/job/dev/job/gitlab-cicd-demo/build
Trigger: ☑ Push events
SSL verification: ☐ Enable (内网不需要)
→ Add webhook3.3 验证 Webhook
bash
# 查看已配置的 Webhooks
curl -sk -H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
"${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/hooks"
# 输出示例:
# [{"id":2, "url":"http://admin:...@jenkins.../build",
# "push_events":true, "merge_requests_events":false}]
# 发送测试 Push 事件
curl -sk -X POST \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
"${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/hooks/2/test/push_events"
# 然后检查 Jenkins 是否收到触发:
curl -s -u 'admin:xxx' \
"https://ai-ear.cn:9010/job/dev/job/gitlab-cicd-demo/api/json" | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(f'Latest build: #{d[\"lastBuild\"][\"number\"]}')"3.4 Jenkins RBAC 配置(允许 Webhook 触发)
Webhook 以 Basic Auth 方式调用 Jenkins API,需要 RBAC 允许 anonymous 用户触发构建:
yaml
# ConfigMap: jenkins-jenkins-config-rbac
jenkins:
authorizationStrategy:
roleBased:
roles:
global:
- name: "webhook-trigger"
description: "允许匿名 Webhook 触发构建"
permissions:
- "Overall/Read"
- "Job/Build"
- "Job/Read"
entries:
- user: "anonymous" # ← Webhook 使用 Basic Auth,Jenkins 视为 anonymous为什么是 anonymous? GitLab Webhook 的 Basic Auth 凭证 (
admin:TOKEN) 被 Jenkins 解析后,如果该用户不存在于 Jenkins 用户库中,则视为 anonymous。RBAC 中的webhook-trigger角色正是为此设计的。
四、第二步:配置 Jenkins → GitLab 认证(JCasC)
4.1 工作原理
Jenkins 需要两组凭证与 GitLab 交互:
┌─────────────────────────────────────────────────────┐
│ Jenkins Controller (JCasC) │
│ │
│ 凭证 1: gitlab-api-token (String 类型) │
│ 用途: 调用 GitLab API (Commit Status 回写) │
│ 使用者: updateGitlabCommitStatus 步骤 │
│ 值: glpat-xxx │
│ │
│ 凭证 2: gitlab-git-user (UsernamePassword 类型) │
│ 用途: 从 GitLab 克隆代码 (checkout scm) │
│ 使用者: SCM (Git Plugin) │
│ 用户名: xxx │
│ 密码: xxx │
│ │
│ GitLab 连接配置: │
│ 名称: gitlab │
│ URL: http://gitlab-webservice-default.gitlab │
│ .svc.cluster.local:8181 │
│ Token: xxx │
└─────────────────────────────────────────────────────┘4.2 JCasC 配置
在 Jenkins Helm values 的 controller.JCasC.configScripts.gitlab-integration 中配置:
yaml
gitlab-integration: |
credentials:
system:
domainCredentials:
- credentials:
# ===== API Token (用于 Commit Status 回写) =====
- string:
scope: GLOBAL
id: "gitlab-api-token"
# 从 K8s Secret 注入,不硬编码在 ConfigMap 中
secret: "${readFile:/run/secrets/additional/gitlab-token}"
description: "GitLab Root PAT"
# ===== Git 克隆凭证 (用于 checkout scm) =====
- usernamePassword:
scope: GLOBAL
id: "gitlab-git-user"
username: "xxx"
password: "xxx"
description: "GitLab Git clone credentials"
unclassified:
gitLabConnectionConfig:
connections:
- name: "gitlab"
# K8s 内部地址,不走外网
url: "http://gitlab-webservice-default.gitlab.svc.cluster.local:8181"
apiTokenId: "gitlab-api-token"
clientBuilderId: "autodetect"
connectionTimeout: 10
readTimeout: 104.3 Secret 注入方式
${readFile:/run/secrets/additional/gitlab-token} 是 JCasC 的 Secret 注入语法。实际值来自 Helm values:
yaml
# jenkins-values.yaml
controller:
additionalSecrets:
- name: gitlab-token
value: glpat-xxx # ← 实际 Token 值Helm Chart 会自动将其创建为 K8s Secret 并挂载到 Controller Pod 的 /run/secrets/additional/gitlab-token 路径。
4.4 验证凭证
bash
# 通过 Jenkins API 查看凭证列表
curl -s -u 'admin:xxx' \
"https://ai-ear.cn:9010/credentials/store/system/domain/_/api/json?tree=credentials[id,description]"
# 预期输出:
# {"credentials":[
# {"id":"gitlab-api-token","description":"GitLab Root PAT"},
# {"id":"gitlab-git-user","description":"GitLab Git clone credentials"},
# {"id":"sonarqube-token","description":"SonarQube analysis token"}
# ]}五、第三步:编写 Jenkinsfile(核心)
这是整个集成的灵魂文件。它定义了 Pipeline 的完整生命周期。
5.1 Pipeline 骨架
groovy
pipeline {
agent { label 'maven21' } // ① 主 Agent: maven21 Pod (jnlp + maven)
options {
timestamps() // 每行输出带时间戳
timeout(time: 20, unit: 'MINUTES') // 全局超时 20 分钟
buildDiscarder(logRotator(numToKeepStr: '10')) // 保留最近 10 次
}
environment {
OLLAMA_URL = 'http://192.168.122.9:11434' // Ollama 服务地址
OLLAMA_MODEL = 'qwen3.6:27b' // AI 模型名称
PYTHON_DIR = 'src/main/python'
PYTHON_TEST_DIR = 'src/test/python'
}
stages { ... } // ② 构建阶段
post { ... } // ③ 构建后操作 (GitLab 回写)
}agent { label 'maven21' } 的含义:
当 Pipeline 启动时:
1. Jenkins 向 K8s 请求创建一个 Pod
2. Pod 名称: maven21-xxxxx (随机后缀)
3. Pod 包含两个容器:
┌───────────────────────────────┐
│ Pod: maven21-xxxxx │
│ │
│ Container: jnlp │ ← Jenkins Agent 通信
│ image: inbound-agent:jdk21 │
│ memory: 512Mi │
│ │
│ Container: maven │ ← 执行构建命令
│ image: maven:3.9-temurin-21│
│ memory: 2Gi │
│ volumes: │
│ /examples ← ConfigMap │
│ /root/.m2 ← settings.xml │
└───────────────────────────────┘
4. Pipeline 中的所有 sh 命令在 maven 容器内执行5.2 Checkout Stage:从 GitLab 拉取代码
groovy
stage('Checkout') {
steps {
container('maven') { // 在 maven 容器内执行
checkout scm // 自动从 GitLab 克隆代码
sh '''
echo "Branch: ${GIT_BRANCH}"
echo "Commit: ${GIT_COMMIT}"
echo "Build#: ${BUILD_NUMBER}"
find . -type f \\( -name "*.java" -o -name "*.py" \\) | sort
'''
}
}
}checkout scm 做了什么?
1. 从 Multibranch Job 配置中读取 SCM 信息:
- Git URL: http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/demo-app-v2.git
- 凭证 ID: gitlab-git-user (JCasC 中配置)
- 分支: main
2. 执行 git clone:
git clone http://oauth2:TOKEN@gitlab-...svc.cluster.local:8181/root/demo-app-v2.git .
git checkout main
3. 注入环境变量:
GIT_BRANCH = origin/main
GIT_COMMIT = 8605af8d...
GIT_URL = http://gitlab-.../root/demo-app-v2.git5.3 Build & Test Stage:并行构建
groovy
stage('Build & Test') {
parallel {
// ===== Java 构建 & 测试 (在 maven21 Agent 上) =====
stage('Java: Build + Test') {
steps {
container('maven') {
sh 'mvn -B -Dmaven.repo.local=$WORKSPACE/.m2 clean compile'
sh 'mvn -B -Dmaven.repo.local=$WORKSPACE/.m2 test'
}
}
post {
always {
junit allowEmptyResults: true,
testResults: 'target/surefire-reports/*.xml'
}
}
}
// ===== Python 静态检查 & 测试 (切换到 python Agent) =====
stage('Python: Lint + Test') {
agent { label 'python' } // ← 分配新的 python Pod
steps {
container('python') {
sh '''
pip3 install pytest pylint --quiet 2>&1
pylint --disable=C0114,C0115,C0116,R0903 \\
${PYTHON_DIR}/calculator.py \\
${PYTHON_DIR}/utils.py \\
--output-format=text --score=yes 2>&1 || true
python3 -m pytest ${PYTHON_TEST_DIR}/ \\
-v --tb=short \\
--junitxml=python-test-results.xml 2>&1
'''
}
}
post {
always {
junit allowEmptyResults: true,
testResults: 'python-test-results.xml'
}
}
}
}
}并行执行的 Pod 分配:
时间线:
T+0s ┌─ Java Stage ─────────────────────────────┐
│ Agent: maven21-xxxxx (复用主 Agent) │
│ mvn compile → mvn test → 4 tests passed │
T+30s └───────────────────────────────────────────┘
T+0s ┌─ Python Stage ────────────────────────────┐
│ Agent: python-yyyyy (新分配的 Pod) │
│ pip install → pylint → pytest → 36 tests │
T+25s └───────────────────────────────────────────┘
两个 Stage 同时开始,各自在独立的 Pod 中执行。
agent { label 'python' }的效果:当 Python Stage 开始时,Jenkins 会创建一个新的 python Pod(jnlp + python 容器),与 maven21 Pod 并行运行。Stage 结束后,python Pod 空闲 5 分钟后自动销毁。
5.4 Package Stage:打包归档
groovy
stage('Java: Package') {
steps {
container('maven') {
sh 'mvn -B -Dmaven.repo.local=$WORKSPACE/.m2 package -DskipTests'
}
}
post {
success {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}package -DskipTests:跳过测试(已在 Build Stage 运行过)archiveArtifacts:将 JAR 文件归档到 Jenkins,可在 Web UI 下载fingerprint: true:记录文件指纹,可追踪 JAR 的来源构建
5.5 AI Code Review Stage:调用 Ollama
groovy
stage('AI Code Review') {
agent { label 'python' } // 分配 python Agent (有 python3)
steps {
container('python') {
sh '''
echo "=== AI Code Review Environment ==="
python3 --version
which curl 2>/dev/null || echo "curl: not available (using Python urllib)"
echo ""
echo "=== Running AI Code Review ==="
chmod +x ai-code-review.sh
sh ai-code-review.sh
'''
}
}
post {
always {
archiveArtifacts allowEmptyEmpty: true,
artifacts: 'ai-review-report.md, ai-review-result.json'
}
}
}执行流程:
1. 分配 python Agent Pod
┌────────────────────────────────┐
│ Pod: python-zzzzz │
│ │
│ Container: jnlp │
│ Container: python (3.12-alpine)│
│ ✗ bash │
│ ✗ curl │
│ ✗ git │
│ ✓ python3 │
│ ✓ sh │
└────────────────────────────────┘
2. 执行 ai-code-review.sh (sh 脚本)
↓
3. 内嵌 Python 脚本:
a. 收集代码 (git diff 或 glob 扫描)
b. 构造审查 Prompt
c. HTTP POST → http://192.168.122.9:11434/api/chat
d. 解析 AI 响应
e. 保存 ai-review-report.md + ai-review-result.json
4. archiveArtifacts → 报告可在 Jenkins Web UI 查看5.6 Post:回写 GitLab Commit Status
groovy
post {
success {
// 两个 Status Check 回写到 GitLab Commit 页面
updateGitlabCommitStatus name: 'jenkins', state: 'success'
updateGitlabCommitStatus name: 'ai-review', state: 'success'
}
failure {
updateGitlabCommitStatus name: 'jenkins', state: 'failed'
}
}回写效果(在 GitLab 项目 Commits 页面看到):
Commit 8605af8d fix: use sh instead of bash ✅ jenkins ✅ ai-review
Commit a65ebf71 fix: handle missing curl ❌ jenkins
Commit 7f13a6ad fix: correct test assertion ❌ jenkinsupdateGitlabCommitStatus 的工作原理:
1. gitlab-plugin 从环境变量中获取:
- GIT_COMMIT: 当前构建的 Commit SHA
- GitLab Connection: JCasC 中配置的 "gitlab" 连接
2. 调用 GitLab API:
POST http://gitlab-webservice-default.gitlab.svc.cluster.local:8181
/api/v4/projects/4/statuses/{GIT_COMMIT}
Headers: PRIVATE-TOKEN: {gitlab-api-token}
Body: { "name": "jenkins", "state": "success" }
3. GitLab 在 Commit 页面显示状态图标六、第四步:编写 AI 代码审查脚本
6.1 脚本架构
ai-code-review.sh (Shell 包装器, 7行)
│
│ export OLLAMA_URL, OLLAMA_MODEL
│ 调用内嵌 Python 脚本
│
▼
Python 脚本 (内嵌 heredoc, ~150行)
│
├── Step 1: 收集代码变更
│ ├── 优先: git diff origin/main (增量审查)
│ ├── 备选: git diff HEAD~1 (上次提交)
│ └── 兜底: glob 扫描源文件 (全量审查)
│
├── Step 2: 构造 Prompt
│ ├── 角色定义: "senior code reviewer"
│ ├── 输出格式: 评分 + 优点 + 建议 + 问题
│ └── 代码内容 (最多 6000 字符)
│
├── Step 3: 调用 Ollama Chat API
│ ├── POST http://192.168.122.9:11434/api/chat
│ ├── "think": false ← 关键! 防止超时
│ ├── "temperature": 0.3 ← 确定性输出
│ └── "num_predict": 800 ← 限制输出长度
│
├── Step 4: 保存结果
│ ├── ai-review-report.md (AI 审查文本)
│ └── ai-review-result.json (元数据)
│
└── Step 5: 错误处理
└── AI 失败不中断 Pipeline (非阻塞)6.2 完整代码
bash
#!/bin/sh
# =============================================================================
# AI Code Review Script - Ollama Chat API (Qwen3.6:27B, think:false)
# Pure Python implementation - no curl/git dependencies required
# =============================================================================
set -eu
export OLLAMA_URL="${OLLAMA_URL:-http://192.168.122.9:11434}"
export OLLAMA_MODEL="${OLLAMA_MODEL:-qwen3.6:27b}"
echo "========================================"
echo " AI Code Review (${OLLAMA_MODEL})"
echo "========================================"
python3 << 'AIREVIEW'
import json, os, urllib.request, time, subprocess, glob
OLLAMA_URL = os.environ.get('OLLAMA_URL', 'http://192.168.122.9:11434')
OLLAMA_MODEL = os.environ.get('OLLAMA_MODEL', 'qwen3.6:27b')
MAX_DIFF_CHARS = 6000
# ===== Step 1: 收集代码变更 =====
diff = ""
# 尝试 git diff (增量审查)
try:
for base in ['main', 'master']:
try:
result = subprocess.run(
['git', 'diff', f'origin/{base}',
'--', '*.java', '*.py', '*.xml', 'Jenkinsfile'],
capture_output=True, text=True, timeout=10
)
if result.stdout.strip():
diff = result.stdout
print(f" ✓ Git diff vs origin/{base}")
break
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
# 备选: 上次提交的 diff
if not diff:
try:
result = subprocess.run(
['git', 'diff', 'HEAD~1',
'--', '*.java', '*.py', '*.xml', 'Jenkinsfile'],
capture_output=True, text=True, timeout=10
)
if result.stdout.strip():
diff = result.stdout
print(" ✓ Git diff from last commit")
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
except Exception:
pass
# 兜底: 直接读取源文件
if not diff:
print(" ⚠ No git diff, reviewing source files directly...")
for pattern in ['src/main/**/*.java', 'src/main/**/*.py']:
for f in glob.glob(pattern, recursive=True):
if 'test' not in f.lower():
try:
with open(f) as fh:
content = fh.read()[:2000]
diff += f"\n=== {f} ===\n{content}\n"
except Exception:
pass
if not diff:
with open('ai-review-result.json', 'w') as f:
json.dump({"status": "skipped", "reason": "no code found"}, f)
print(" ⚠ No code to review")
exit(0)
# 截断过长的代码
if len(diff) > MAX_DIFF_CHARS:
diff = diff[:MAX_DIFF_CHARS] + "\n... [truncated]"
print(f" Code size: {len(diff)} chars")
# 文件统计
java_files = len(glob.glob('src/main/**/*.java', recursive=True))
python_files = len(glob.glob('src/main/**/*.py', recursive=True))
test_java = len(glob.glob('src/test/**/*.java', recursive=True))
test_python = len(glob.glob('src/test/**/*.py', recursive=True))
print(f" Java: {java_files} src, {test_java} test | "
f"Python: {python_files} src, {test_python} test")
# ===== Step 2: 构造 Prompt =====
prompt = f"""You are a senior code reviewer. Review the following code and provide concise feedback.
Files: {java_files} Java, {python_files} Python source files.
Code:
{diff}
Provide your review in this EXACT format (in Chinese):
## 📊 代码审查总结
- 代码质量评分: X/10
- 总体评价: (1-2句话)
## ✅ 优点
1. ...
2. ...
## ⚠️ 改进建议
1. ...
2. ...
## 🐛 潜在问题
1. ... (如有)
Focus on: correctness, security, performance, readability, best practices."""
# ===== Step 3: 调用 Ollama Chat API =====
payload = json.dumps({
"model": OLLAMA_MODEL,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"think": False, # ← 关键! 关闭思考模式
"options": {
"num_predict": 800, # 限制输出 Token 数
"temperature": 0.3 # 低温度 = 更确定的输出
}
}).encode('utf-8')
print(f"\n Calling Ollama ({OLLAMA_MODEL})...")
start = time.time()
try:
req = urllib.request.Request(
f"{OLLAMA_URL}/api/chat",
data=payload,
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=180) as resp:
data = json.loads(resp.read().decode())
duration = round(time.time() - start, 1)
review_text = data.get('message', {}).get('content', 'No response')
# ===== Step 4: 保存结果 =====
with open('ai-review-report.md', 'w') as f:
f.write(review_text)
with open('ai-review-result.json', 'w') as f:
json.dump({
"status": "success",
"model": OLLAMA_MODEL,
"duration_seconds": duration,
"java_files": java_files,
"python_files": python_files
}, f, indent=2)
print(f" ✓ AI review completed in {duration}s")
print()
print("=" * 50)
print(" AI Code Review Report")
print("=" * 50)
print()
print(review_text)
print()
print("=" * 50)
print(f" Done in {duration}s | ai-review-report.md")
print("=" * 50)
except Exception as e:
# ===== Step 5: 错误处理 (不中断 Pipeline) =====
duration = round(time.time() - start, 1)
print(f" ✗ AI review failed after {duration}s: {e}")
with open('ai-review-result.json', 'w') as f:
json.dump({"status": "error", "reason": str(e)}, f)
with open('ai-review-report.md', 'w') as f:
f.write(f"AI review failed: {e}")
AIREVIEW
echo "========================================"
echo " AI Code Review Complete"
echo "========================================"
cat ai-review-result.json 2>/dev/null || true6.3 关键设计决策
| 决策 | 代码 | 原因 |
|---|---|---|
Shell 用 sh 不用 bash | #!/bin/sh | Alpine 容器没有 bash |
HTTP 用 Python urllib | urllib.request.urlopen() | Alpine 容器没有 curl |
文件扫描用 glob | glob.glob('**/*.py', recursive=True) | Alpine 的 find 行为不同 |
| 关闭思考模式 | "think": False | Qwen3 思考模式会生成大量推理 Token 导致 180s 超时 |
| 低温度 | "temperature": 0.3 | 更一致的审查结果 |
| 限制输出 | "num_predict": 800 | 防止超长响应 |
| 截断代码 | MAX_DIFF_CHARS = 6000 | 防止超出模型上下文窗口 |
| AI 失败不中断 | except Exception: ... (不 exit 1) | AI 审查是非阻塞的辅助功能 |
| 三级 diff 策略 | git diff → HEAD~1 → glob | 优先增量审查,兜底全量审查 |
6.4 Ollama API 参数详解
json
{
"model": "qwen3.6:27b",
"messages": [
{"role": "user", "content": "..."}
],
"stream": false,
"think": false,
"options": {
"num_predict": 800,
"temperature": 0.3
}
}| 参数 | 值 | 作用 | 如果改了会怎样 |
|---|---|---|---|
stream | false | 一次性返回完整结果 | true 需要逐行读取 SSE 流 |
think | false | 跳过思考链,直接输出 | true 会生成 5000+ 思考 Token → 超时 |
num_predict | 800 | 最多生成 800 Token | 改大 → 输出更长但更慢 |
temperature | 0.3 | 输出确定性高 | 改大 → 更有创意但不稳定 |
timeout | 180 | urllib 超时 3 分钟 | 改小 → 可能误杀正常请求 |
七、第五步:推送代码 & 验证流水线
7.1 推送代码到 GitLab
bash
cd /path/to/demo-app-v2
# 确保文件齐全
ls -la
# Jenkinsfile
# ai-code-review.sh
# pom.xml
# src/main/java/cn/ai_ear/demo/App.java
# src/test/java/cn/ai_ear/demo/AppTest.java
# src/main/python/calculator.py
# src/main/python/utils.py
# src/test/python/test_calculator.py
# src/test/python/test_utils.py
# 提交 & 推送
git add .
git commit -m "feat: complete CI/CD with Java + Python + AI review"
git push origin main7.2 观察 Pipeline 执行
bash
# 实时查看构建状态
while true; do
STATUS=$(curl -s -u 'admin:xxx' \
"https://ai-ear.cn:9010/job/dev/job/gitlab-cicd-demo/lastBuild/api/json" | \
python3 -c "
import json,sys
d=json.load(sys.stdin)
r=d.get('result') or 'RUNNING'
print(f'#{d[\"number\"]} {r} ({d[\"duration\"]//1000}s)')
")
echo "$(date '+%H:%M:%S') $STATUS"
echo "$STATUS" | grep -q "RUNNING" || break
sleep 10
done7.3 查看 AI 审查报告
bash
# 下载 AI Review Report
curl -s -u 'admin:xxx' \
"https://ai-ear.cn:9010/job/dev/job/gitlab-cicd-demo/lastBuild/artifact/ai-review-report.md"
# 查看 AI 审查元数据
curl -s -u 'admin:xxx' \
"https://ai-ear.cn:9010/job/dev/job/gitlab-cicd-demo/lastBuild/artifact/ai-review-result.json"7.4 查看 GitLab Commit Status
bash
# 查看最近 Commit 的 Status
LATEST_SHA=$(curl -sk -H "PRIVATE-TOKEN: xxx" \
"https://ai-ear.cn:8443/api/v4/projects/4/repository/commits?per_page=1" | \
python3 -c "import json,sys; print(json.load(sys.stdin)[0]['id'])")
curl -sk -H "PRIVATE-TOKEN: xxx" \
"https://ai-ear.cn:8443/api/v4/projects/4/repository/commits/${LATEST_SHA}/statuses" | \
python3 -c "
import json,sys
for s in json.load(sys.stdin):
print(f' {s[\"name\"]:15s} {s[\"status\"]:10s} {s.get(\"description\",\"\")}')"7.5 验证清单
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| Pipeline 状态 | Jenkins API | SUCCESS |
| Java 测试 | JUnit Report | 4 tests, 0 failures |
| Python 测试 | JUnit Report | 36 tests, 0 failures |
| AI Review | artifact/ai-review-report.md | 有评分和建议 |
| GitLab Status | Commit Status API | jenkins=success, ai-review=success |
| JAR 归档 | artifact/target/*.jar | demo-app-1.0.N.jar |
八、完整数据流图:一次构建的生命周期
基于 Build #12 的真实数据:
T+0s ┌─ GitLab Push Event ──────────────────────────────────┐
│ POST http://admin:TOKEN@jenkins:8080/.../build │
│ Payload: { "ref":"refs/heads/main", ... } │
└──────────────────────────────────────────────────────┘
│
T+1s ┌─ Jenkins 接收 & 调度 ────────────────────────────────┐
│ 创建 Pod: maven21-2zbcs │
│ Container: jnlp (inbound-agent:jdk21) │
│ Container: maven (maven:3.9-temurin-21) │
│ Volumes: /examples, /root/.m2, workspace │
└──────────────────────────────────────────────────────┘
│
T+5s ┌─ Stage: Checkout ────────────────────────────────────┐
│ checkout scm → git clone from GitLab (K8s 内部) │
│ GIT_BRANCH = origin/main │
│ GIT_COMMIT = 8605af8d... │
└──────────────────────────────────────────────────────┘
│
T+5s ┌─ Stage: Build & Test (并行) ─────────────────────────┐
│ │
│ ┌─ Java (maven21 Pod) ──────────────────┐ │
│ │ mvn clean compile → SUCCESS │ │
│ │ mvn test → 4/4 tests passed │ │
│ │ junit → surefire-reports/*.xml │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌─ Python (python Pod, 新分配) ──────────┐ │
│ │ pip3 install pytest pylint │ │
│ │ pylint → 9.45/10 │ │
│ │ pytest → 36/36 tests passed │ │
│ │ junit → python-test-results.xml │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
│
T+35s ┌─ Stage: Java Package ────────────────────────────────┐
│ mvn package -DskipTests │
│ archiveArtifacts → demo-app-1.0.12.jar │
└──────────────────────────────────────────────────────┘
│
T+36s ┌─ Stage: AI Code Review ──────────────────────────────┐
│ Agent: python Pod (新分配) │
│ │
│ T+36s 收集代码: glob 扫描 → 4919 chars │
│ T+37s 构造 Prompt (Java 1 + Python 2 源文件) │
│ T+37s HTTP POST → http://192.168.122.9:11434/api/chat│
│ { │
│ "model": "qwen3.6:27b", │
│ "think": false, │
│ "options": {"num_predict":800,"temp":0.3} │
│ } │
│ │
│ ┌──────────────────────────────┐ │
│ │ Ollama Server (192.168.122.9) │ │
│ │ Qwen3.6:27B (17GB) 推理中... │ │
│ └──────────────────────────────┘ │
│ │
│ T+100s AI 返回审查结果 (62.6s) │
│ 评分: 7/10 │
│ 优点: 4 条 │
│ 建议: 4 条 │
│ 问题: 3 条 │
│ │
│ T+100s 保存 ai-review-report.md │
│ T+100s 保存 ai-review-result.json │
│ T+101s archiveArtifacts │
└──────────────────────────────────────────────────────┘
│
T+102s ┌─ Post Actions ───────────────────────────────────────┐
│ │
│ updateGitlabCommitStatus('jenkins', 'success') │
│ → POST GitLab API /projects/4/statuses/{SHA} │
│ │
│ updateGitlabCommitStatus('ai-review', 'success') │
│ → POST GitLab API /projects/4/statuses/{SHA} │
│ │
│ GitLab Commit 页面: │
│ 8605af8d ✅ jenkins ✅ ai-review │
└──────────────────────────────────────────────────────┘
T+102s Pipeline 完成 ✅
Agent Pods 进入 idle (5分钟后自动销毁)九、配置要素速查表
9.1 四层配置一览
| 层 | 文件/位置 | 方向 | 作用 |
|---|---|---|---|
| ① 触发 | GitLab Webhook (项目设置) | GitLab → Jenkins | Push 事件触发构建 |
| ② 认证 | JCasC ConfigMap (gitlab-integration) | Jenkins → GitLab | 克隆代码 + 回写状态 |
| ③ AI | ai-code-review.sh (仓库内) | Jenkins → Ollama | 调用 AI 审查代码 |
| ④ 回写 | Jenkinsfile post {} | Jenkins → GitLab | 构建结果 → Commit |
9.2 环境变量一览
| 变量 | 值 | 定义位置 | 用途 |
|---|---|---|---|
OLLAMA_URL | http://192.168.122.9:11434 | Jenkinsfile environment {} | Ollama API 地址 |
OLLAMA_MODEL | qwen3.6:27b | Jenkinsfile environment {} | AI 模型名称 |
PYTHON_DIR | src/main/python | Jenkinsfile environment {} | Python 源码目录 |
GIT_BRANCH | origin/main | Jenkins 自动注入 | 当前分支 |
GIT_COMMIT | 8605af8d... | Jenkins 自动注入 | Commit SHA |
BUILD_NUMBER | 12 | Jenkins 自动注入 | 构建编号 |
9.3 网络地址一览
| 连接 | 地址 | 说明 |
|---|---|---|
| GitLab → Jenkins | http://admin:TOKEN@jenkins.jenkins.svc.cluster.local:8080 | K8s 内部 ClusterIP |
| Jenkins → GitLab | http://gitlab-webservice-default.gitlab.svc.cluster.local:8181 | K8s 内部 ClusterIP |
| Jenkins Agent → Ollama | http://192.168.122.9:11434 | 宿主机 IP (K8s 外部) |
十、常见问题 & 解决方案
Q1: Webhook 推送后 Jenkins 没有触发构建
排查步骤:
bash
# 1. 确认 Webhook 存在
curl -sk -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/api/v4/projects/4/hooks"
# 2. 测试 Webhook
curl -sk -X POST -H "PRIVATE-TOKEN: $TOKEN" \
"$GITLAB/api/v4/projects/4/hooks/2/test/push_events"
# 3. 检查 Jenkins 构建队列
curl -s -u 'admin:xxx' "https://ai-ear.cn:9010/queue/api/json?pretty=true"
# 4. 检查 Jenkins 系统日志
# Jenkins Web UI → Manage Jenkins → System Log → All Jenkins Logs常见原因:
- Webhook URL 中的 Token 过期 → 重新生成
- RBAC 没有
webhook-trigger角色 → 添加 JCasC 配置 - Jenkins Controller Pod 未就绪 →
kubectl get pods -n jenkins
Q2: AI Code Review Stage 超时
现象:Pipeline 在 AI Review Stage 卡住 180s 后超时
原因:Qwen3 的"思考模式"生成大量推理 Token
解决:确保 API payload 中有 "think": false
python
payload = json.dumps({
"model": OLLAMA_MODEL,
"messages": [...],
"stream": False,
"think": False, # ← 必须有!
"options": {
"num_predict": 800, # 限制输出
"temperature": 0.3
}
})其他优化:
- 减小
MAX_DIFF_CHARS(6000 → 3000) - 减小
num_predict(800 → 500) - 增大
timeout(180 → 300)
Q3: Alpine 容器中 bash: not found
现象:
/bin/bash: not found
sh: curl: not found解决:
| 需要的功能 | Alpine 替代方案 |
|---|---|
#!/bin/bash | #!/bin/sh |
curl URL | python3 -c "import urllib.request; ..." |
find . -name | python3 glob.glob('**/*.py', recursive=True) |
jq .field | python3 -c "import json; ..." |
Q4: checkout scm 失败: Authentication failed
现象:
ERROR: Error cloning remote repo 'origin'
hudson.plugins.git.GitException: Command "git clone" returned status code 128
stderr: fatal: Authentication failed排查:
bash
# 检查凭证是否存在
curl -s -u 'admin:xxx' \
"https://ai-ear.cn:9010/credentials/store/system/domain/_/api/json"
# 检查 JCasC ConfigMap
kubectl get configmap jenkins-jenkins-config-gitlab-integration -n jenkins -o yaml
# 检查 Secret 是否挂载
kubectl exec -n jenkins jenkins-0 -c jenkins -- \
cat /run/secrets/additional/gitlab-token常见原因:
- GitLab Token 过期 → 在 GitLab 中重新生成
- JCasC ConfigMap 中
id不匹配 → 确保是gitlab-git-user - Secret 未挂载 → 检查
additionalSecrets配置
Q5: updateGitlabCommitStatus 失败
现象:Pipeline 成功但 GitLab 没有显示 Status Check
排查:
bash
# 检查 GitLab 连接配置
kubectl get configmap jenkins-jenkins-config-gitlab-integration -n jenkins -o yaml
# 从 Jenkins Controller 测试 GitLab API
kubectl exec -n jenkins jenkins-0 -c jenkins -- \
curl -s -H "PRIVATE-TOKEN: xxx" \
"http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/api/v4/version"常见原因:
- GitLab Connection
name不匹配 → 确保 JCasC 中是"gitlab" apiTokenId不匹配 → 确保是"gitlab-api-token"- GitLab Token 权限不足 → 需要
apiscope
Q6: 如何修改 AI 审查规则?
修改 ai-code-review.sh 中的 Prompt:
python
prompt = f"""You are a senior code reviewer. Review the following code.
Review rules:
- 命名规范: 变量和方法使用 camelCase
- 安全性: 检查 SQL 注入、XSS、硬编码密码
- 性能: 检查 N+1 查询、内存泄漏、无限循环
- 可维护性: 检查代码重复、过长方法
Code:
{diff}
"""Q7: 如何让 AI 审查失败时阻断 Pipeline?
默认 AI 审查是非阻塞的(失败不影响构建)。如果要阻断:
groovy
stage('AI Code Review') {
steps {
container('python') {
sh '''
sh ai-code-review.sh
# 检查 AI 审查结果
python3 -c "
import json
with open('ai-review-result.json') as f:
data = json.load(f)
if data['status'] != 'success':
exit(1) # 阻断 Pipeline
"
'''
}
}
}附录 A:完整 Jenkinsfile(可直接复制)
groovy
pipeline {
agent { label 'maven21' }
options {
timestamps()
timeout(time: 20, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10'))
}
environment {
OLLAMA_URL = 'http://192.168.122.9:11434'
OLLAMA_MODEL = 'qwen3.6:27b'
PYTHON_DIR = 'src/main/python'
PYTHON_TEST_DIR = 'src/test/python'
}
stages {
stage('Checkout') {
steps {
container('maven') {
checkout scm
sh '''
echo "============================================"
echo " Build Info"
echo "============================================"
echo "Branch: ${GIT_BRANCH}"
echo "Commit: ${GIT_COMMIT}"
echo "Build#: ${BUILD_NUMBER}"
echo "============================================"
'''
}
}
}
stage('Build & Test') {
parallel {
stage('Java: Build + Test') {
steps {
container('maven') {
sh 'mvn -B -Dmaven.repo.local=$WORKSPACE/.m2 clean compile'
sh 'mvn -B -Dmaven.repo.local=$WORKSPACE/.m2 test'
}
}
post {
always {
junit allowEmptyResults: true, testResults: 'target/surefire-reports/*.xml'
}
}
}
stage('Python: Lint + Test') {
agent { label 'python' }
steps {
container('python') {
sh '''
pip3 install pytest pylint --quiet 2>&1
pylint --disable=C0114,C0115,C0116,R0903 \\
${PYTHON_DIR}/calculator.py \\
${PYTHON_DIR}/utils.py \\
--output-format=text --score=yes 2>&1 || true
python3 -m pytest ${PYTHON_TEST_DIR}/ \\
-v --tb=short \\
--junitxml=python-test-results.xml 2>&1
'''
}
}
post {
always {
junit allowEmptyResults: true, testResults: 'python-test-results.xml'
}
}
}
}
}
stage('Java: Package') {
steps {
container('maven') {
sh 'mvn -B -Dmaven.repo.local=$WORKSPACE/.m2 package -DskipTests'
}
}
post {
success {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}
stage('AI Code Review') {
agent { label 'python' }
steps {
container('python') {
sh '''
echo "=== AI Code Review ==="
python3 --version
chmod +x ai-code-review.sh
sh ai-code-review.sh
'''
}
}
post {
always {
archiveArtifacts allowEmptyArchive: true,
artifacts: 'ai-review-report.md, ai-review-result.json'
}
}
}
}
post {
success {
updateGitlabCommitStatus name: 'jenkins', state: 'success'
updateGitlabCommitStatus name: 'ai-review', state: 'success'
}
failure {
updateGitlabCommitStatus name: 'jenkins', state: 'failed'
}
}
}附录 B:完整 ai-code-review.sh(可直接复制)
见第六章 6.2 节,完整脚本可直接复制到项目根目录。
附录 C:JCasC ConfigMap 配置
C.1 GitLab 集成 (ConfigMap: jenkins-jenkins-config-gitlab-integration)
yaml
credentials:
system:
domainCredentials:
- credentials:
- string:
scope: GLOBAL
id: "gitlab-api-token"
secret: "${readFile:/run/secrets/additional/gitlab-token}"
description: "GitLab Root PAT"
- usernamePassword:
scope: GLOBAL
id: "gitlab-git-user"
username: "xxx"
password: "xxx"
description: "GitLab Git clone credentials"
unclassified:
gitLabConnectionConfig:
connections:
- name: "gitlab"
url: "http://gitlab-webservice-default.gitlab.svc.cluster.local:8181"
apiTokenId: "gitlab-api-token"
clientBuilderId: "autodetect"
connectionTimeout: 10
readTimeout: 10C.2 RBAC 权限 (ConfigMap: jenkins-jenkins-config-rbac)
yaml
jenkins:
authorizationStrategy:
roleBased:
roles:
global:
- name: "admin"
permissions: ["Overall/Administer"]
entries:
- user: "admin"
- name: "readonly"
permissions: ["Overall/Read"]
entries:
- group: "authenticated"
- name: "webhook-trigger"
permissions: ["Overall/Read", "Job/Build", "Job/Read"]
entries:
- user: "anonymous"C.3 Helm additionalSecrets
yaml
# jenkins-values.yaml
controller:
additionalSecrets:
- name: gitlab-token
value: glpat-xxx附录 D:Build #12 真实日志摘录
[2026-08-25T14:40:04Z] Started by user Jenkins Admin
[2026-08-25T14:40:04Z] Obtained Jenkinsfile from git
http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/demo-app-v2.git
[2026-08-25T14:40:05Z] Agent maven21-2zbcs is provisioned from template maven21
── Stage: Checkout ──
[2026-08-25T14:40:10Z] Checking out Git revision: 8605af8d...
[2026-08-25T14:40:12Z] Branch: origin/main
[2026-08-25T14:40:12Z] Build#: 12
── Stage: Build & Test (parallel) ──
[2026-08-25T14:40:15Z] [Java] mvn clean compile → BUILD SUCCESS
[2026-08-25T14:40:15Z] [Python] Agent python-abcde is provisioned
[2026-08-25T14:40:20Z] [Python] pip3 install pytest pylint
[2026-08-25T14:40:25Z] [Python] pylint → rated at 9.45/10
[2026-08-25T14:40:30Z] [Java] mvn test → Tests run: 4, Failures: 0
[2026-08-25T14:40:35Z] [Python] pytest → 36 passed in 0.042s
── Stage: Package ──
[2026-08-25T14:40:40Z] mvn package → demo-app-1.0.12.jar
── Stage: AI Code Review ──
[2026-08-25T14:41:34Z] Agent python-xyz99 is provisioned
[2026-08-25T14:41:35Z] python3 --version: Python 3.12.x
[2026-08-25T14:41:35Z] curl: not available (using Python urllib)
[2026-08-25T14:42:42Z] ⚠ No git diff, reviewing source files directly...
[2026-08-25T14:42:42Z] Code size: 4919 chars
[2026-08-25T14:42:42Z] Java: 1 src, 1 test | Python: 2 src, 2 test
[2026-08-25T14:42:42Z] Calling Ollama (qwen3.6:27b)...
[2026-08-25T14:43:45Z] ✓ AI review completed in 62.6s
[2026-08-25T14:43:45Z] === AI Code Review Report ===
[2026-08-25T14:43:45Z] 代码质量评分: 7/10
[2026-08-25T14:43:45Z] 优点: 类型安全, 防御性编程, 文档规范...
[2026-08-25T14:43:45Z] 建议: isPalindrome 双指针优化, flatten 迭代化...
[2026-08-25T14:43:45Z] 问题: 代码截断, chunk_list 类型提示兼容性...
── Post Actions ──
[2026-08-25T14:43:46Z] updateGitlabCommitStatus: jenkins = success
[2026-08-25T14:43:46Z] updateGitlabCommitStatus: ai-review = success
[2026-08-25T14:43:47Z] Finished: SUCCESS (102s)📝 文档说明:本教程基于 Build #12 (2026-08-25, SUCCESS, 102s) 的真实数据编写,所有配置、代码和日志均来自生产环境验证。