主题
GitLab + Jenkins + AI 自动化 CI/CD 保姆级教程
版本: 1.0 | 日期: 2026-08-25 | 环境: RKE2 + Helm + JCasC + Ollama
📋 目录
- 第一章:环境总览
- 第二章:基础设施搭建
- 第三章:GitLab 搭建与配置
- 第四章:Jenkins 搭建与配置
- 第五章:Ollama AI 部署
- 第六章:CI/CD 流水线实战
- 第七章:运行与验证
- 第八章:问题排查与踩坑记录
- 附录 A:命令速查表
- 附录 B:Helm Values 完整配置
- 附录 C:网络拓扑图
第一章:环境总览
1.1 技术栈总览
┌─────────────────────────────────────────────────────────────────────┐
│ 基础设施层 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ RKE2 v1.35.6 集群 (6 节点, Rocky Linux 9.8) │ │
│ │ ├─ Control Plane × 3 (192.168.122.23/24/31) │ │
│ │ └─ Worker × 3 (192.168.122.28/29/30) │ │
│ │ 网络插件: Calico | 容器运行时: containerd │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ NFS 存储 │ │ Harbor │ │ Ollama │ │ DNS/Nginx │ │
│ │ NVME+HDD │ │ Registry │ │ LLM 推理 │ │ 反向代理 │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 应用服务层 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
│ │ GitLab v19.2 │ │ Jenkins LTS │ │ SonarQube │ │
│ │ Helm Chart │ │ Helm + JCasC │ │ Helm Chart │ │
│ │ 代码托管/MR/CI │ │ CI/CD 引擎 │ │ 静态代码分析 │ │
│ └─────────────────┘ └─────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ CI/CD 流水线层 │
│ GitLab Push → Webhook → Jenkins │
│ ├─ Java Agent (maven21) → Maven Build/Test/Package │
│ ├─ Python Agent (python) → PyLint/Pytest │
│ └─ AI Review Agent (python) → Ollama Qwen3.6:27B 代码审查 │
│ → GitLab Commit Status 回写 │
└─────────────────────────────────────────────────────────────────────┘1.2 网络地址表
| 服务 | 内网地址 | 外网入口 | 说明 |
|---|---|---|---|
| Jenkins | http://jenkins.jenkins.svc.cluster.local:8080 | https://ai-ear.cn:9010 (NodePort 30880) | CI/CD 引擎 |
| GitLab | http://gitlab-webservice-default.gitlab.svc.cluster.local:8181 | https://ai-ear.cn:8443 (Ingress) | 代码仓库 |
| SonarQube | http://sonarqube.sonarqube.svc.cluster.local:9000 | https://ai-ear.cn:9011 (NodePort 30900) | 代码质量 |
| Ollama | http://192.168.122.9:11434 | 仅内网 | AI 推理引擎 |
| Harbor | http://192.168.122.156:30000 | 仅内网 | 镜像仓库 |
| NFS | 192.168.122.156 | - | 共享存储 |
1.3 凭证清单
| 凭证 | 用途 | 值/位置 |
|---|---|---|
| Jenkins Admin | Web 登录 | \1xxx\2 |
| GitLab PAT | API 访问 | glpat-xxx |
| Jenkins→GitLab Token | Webhook | glpat-xxx |
| GitLab OAuth Client ID | SSO 登录 | a6b688a0... |
| SonarQube Token | 代码扫描 | K8s Secret sonar-credentials |
第二章:基础设施搭建
2.1 RKE2 集群
RKE2(Rancher Kubernetes Engine 2)是 Rancher 出品的企业级 Kubernetes 发行版,内置安全加固,适合生产环境。
2.1.1 服务器规划
本环境共 6 台服务器,3 台 Control Plane + 3 台 Worker:
角色 IP 配置 用途
─────────────────────────────────────────────────────────────
Control+Worker 192.168.122.23 16C 64G 调度 + 工作负载
Control+Worker 192.168.122.24 16C 64G 调度 + 工作负载
Control+Worker 192.168.122.31 16C 64G 调度 + 工作负载 (Ollama)
Worker 192.168.122.28 16C 64G 工作负载 (Jenkins Pod 所在)
Worker 192.168.122.29 16C 64G 工作负载
Worker 192.168.122.30 16C 64G 工作负载2.1.2 安装 RKE2(首个 Control Plane 节点)
bash
# 1. 创建安装配置
mkdir -p /etc/rancher/rke2
cat > /etc/rancher/rke2/config.yaml << 'EOF'
write-kubeconfig-mode: "0644"
node-name: sza122023.local
token: xxx
tls-san:
- k8s.ai-ear.cn
- 192.168.122.23
disable:
- rke2-ingress-nginx # 使用自建 Ingress
EOF
# 2. 执行安装
curl -sfL https://get.rke2.io | INSTALL_RKE2_CHANNEL=latest sh -
# 3. 启动服务
systemctl enable --now rke2-server.service
# 4. 配置 kubectl
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
kubectl get nodes2.1.3 加入其他 Control Plane 节点
bash
# 在其他 Control Plane 节点上
cat > /etc/rancher/rke2/config.yaml << 'EOF'
write-kubeconfig-mode: "0644"
token: xxx
server: https://192.168.122.23:9345
node-name: sza122024.local
EOF
curl -sfL https://get.rke2.io | INSTALL_RKE2_TYPE="server" sh -
systemctl enable --now rke2-server.service2.1.4 加入 Worker 节点
bash
# 在 Worker 节点上
cat > /etc/rancher/rke2/config.yaml << 'EOF'
token: xxx
server: https://192.168.122.23:9345
node-name: sza122028.local
EOF
curl -sfL https://get.rke2.io | INSTALL_RKE2_TYPE="agent" sh -
systemctl enable --now rke2-agent.service2.1.5 验证集群
bash
kubectl get nodes -o wide
# 预期输出:
# NAME STATUS ROLES VERSION
# sza122023.local Ready control-plane,etcd,worker v1.35.6+rke2r1
# sza122024.local Ready control-plane,etcd,worker v1.35.6+rke2r1
# sza122028.local Ready worker v1.35.6+rke2r1
# sza122029.local Ready worker v1.35.6+rke2r1
# sza122030.local Ready worker v1.35.6+rke2r1
# sza122031.local Ready control-plane,etcd,worker v1.35.6+rke2r12.2 NFS 存储
Jenkins、GitLab 等有状态服务需要持久化存储。本环境使用 NFS 作为共享存储后端。
2.2.1 NFS Server 配置(192.168.122.156)
bash
# 安装 NFS Server
dnf install -y nfs-utils
# 创建共享目录
mkdir -p /data/nfs/{jenkins,gitlab,sonarqube,mysql,redis,elasticsearch}
chmod -R 777 /data/nfs
# 配置 exports
cat >> /etc/exports << 'EOF'
/data/nfs/jenkins 192.168.122.0/24(rw,sync,no_subtree_check,no_root_squash)
/data/nfs/gitlab 192.168.122.0/24(rw,sync,no_subtree_check,no_root_squash)
/data/nfs/sonarqube 192.168.122.0/24(rw,sync,no_subtree_check,no_root_squash)
/data/nfs/mysql 192.168.122.0/24(rw,sync,no_subtree_check,no_root_squash)
/data/nfs/redis 192.168.122.0/24(rw,sync,no_subtree_check,no_root_squash)
EOF
exportfs -ra
systemctl enable --now nfs-server2.2.2 安装 NFS Provisioner(K8s 侧)
为每个服务创建独立的 NFS Provisioner,隔离存储:
bash
# Jenkins 专用 NFS Provisioner (NVME 高速盘)
helm repo add nfs-subdir-external-provisioner \
https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm upgrade --install nfs-nvme-provisioner \
nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--namespace nfs-provisioner --create-namespace \
--set nfs.server=192.168.122.156 \
--set nfs.path=/data/nfs-nvme/jenkins \
--set storageClass.name=nfs-nvme \
--set storageClass.defaultClass=false \
--set storageClass.reclaimPolicy=Delete \
--set storageClass.volumeBindingMode=Immediate \
--set storageClass.allowVolumeExpansion=true
# GitLab 专用
helm upgrade --install gitlab-nfs-provisioner \
nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--namespace gitlab --create-namespace \
--set nfs.server=192.168.122.156 \
--set nfs.path=/data/nfs/gitlab \
--set storageClass.name=nfs-gitlab \
--set storageClass.defaultClass=false2.2.3 验证 StorageClass
bash
kubectl get sc
# 预期输出:
# NAME PROVISIONER AGE
# nfs (default) cluster.local/nfs-subdir-external-provisioner 24d
# nfs-gitlab cluster.local/gitlab-nfs-provisioner-... 11d
# nfs-nvme cluster.local/nfs-nvme-provisioner 14h
# nfs-sonarqube ...2.3 Harbor 镜像仓库
Harbor 用于存储 Agent 镜像(Jenkins Inbound Agent、Maven、Python、Node、Go 等)。
说明:本环境 Harbor 部署在
192.168.122.156:30000,使用 Docker Registry 兼容 API。
2.3.1 Agent 镜像准备
将以下镜像推送到 Harbor:
bash
# 设置 Harbor 地址
HARBOR=192.168.122.156:30000
# 拉取 & 推送 Jenkins Inbound Agent
docker pull jenkins/inbound-agent:latest-jdk21
docker tag jenkins/inbound-agent:latest-jdk21 $HARBOR/jenkins/inbound-agent:latest-jdk21
docker push $HARBOR/jenkins/inbound-agent:latest-jdk21
# Maven Agent (JDK 21)
docker pull maven:3.9-eclipse-temurin-21
docker tag maven:3.9-eclipse-temurin-21 $HARBOR/jenkins/maven:3.9-eclipse-temurin-21
docker push $HARBOR/jenkins/maven:3.9-eclipse-temurin-21
# Maven Agent (JDK 8)
docker pull maven:3.9-eclipse-temurin-8
docker tag maven:3.9-eclipse-temurin-8 $HARBOR/jenkins/maven:3.9-eclipse-temurin-8
docker push $HARBOR/jenkins/maven:3.9-eclipse-temurin-8
# Python Agent
docker pull python:3.12-alpine
docker tag python:3.12-alpine $HARBOR/jenkins/python:3.12-alpine
docker push $HARBOR/jenkins/python:3.12-alpine
# Node Agent
docker pull node:21-alpine
docker tag node:21-alpine $HARBOR/jenkins/node:21-alpine
docker push $HARBOR/jenkins/node:21-alpine
# Golang Agent
docker pull golang:1.24-alpine
docker tag golang:1.24-alpine $HARBOR/jenkins/golang:1.24-alpine
docker push $HARBOR/jenkins/golang:1.24-alpine
# K8s Sidecar (JCasC 自动重载)
docker pull kiwigrid/k8s-sidecar:2.10.1
docker tag kiwigrid/k8s-sidecar:2.10.1 $HARBOR/jenkins/k8s-sidecar:2.10.1
docker push $HARBOR/jenkins/k8s-sidecar:2.10.1
# Jenkins Controller
docker pull jenkins/jenkins:lts-jdk21
docker tag jenkins/jenkins:lts-jdk21 $HARBOR/jenkins/jenkins:lts-jdk21
docker push $HARBOR/jenkins/jenkins:lts-jdk21第三章:GitLab 搭建与配置
3.1 Helm 安装 GitLab
GitLab 使用官方 Helm Chart 部署,版本 gitlab-10.2.2(GitLab v19.2.2)。
3.1.1 添加 Helm Repo
bash
helm repo add gitlab https://charts.gitlab.io/
helm repo update3.1.2 创建 values.yaml
yaml
# gitlab-values.yaml
global:
hosts:
domain: ai-ear.cn
https: true
externalIP: 192.168.122.9
ingress:
enabled: true
configureCertmanager: false
class: gitlab-nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "512m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
tls:
enabled: true
secretName: gitlab-tls
edition: ce # Community Edition
# 关闭不需要的组件(节省资源)
gitlab-runner:
install: false
prometheus:
install: false
certmanager:
install: false
# Nginx Ingress Controller (GitLab 内置)
nginx-ingress:
enabled: true
controller:
service:
type: NodePort
nodePorts:
http: 30080
https: 30443
ssh: 30022
# PostgreSQL (使用本地 PV)
postgresql:
persistence:
enabled: true
storageClass: local-storage
size: 20Gi
# Redis
redis:
persistence:
enabled: true
storageClass: nfs-gitlab
size: 5Gi
# Gitaly (Git 存储)
gitlab:
gitaly:
persistence:
storageClass: nfs-gitlab
size: 50Gi
# Webservice
webservice:
replicas: 2
ingress:
tls:
secretName: gitlab-tls
# MinIO (对象存储 - Artifacts/LFS)
minio:
persistence:
storageClass: nfs-gitlab
size: 20Gi3.1.3 安装
bash
# 创建 TLS Secret(自签名证书或 Let's Encrypt)
kubectl create secret tls gitlab-tls \
--cert=/path/to/tls.crt \
--key=/path/to/tls.key \
-n gitlab
# 安装
helm upgrade --install gitlab gitlab/gitlab \
--namespace gitlab --create-namespace \
-f gitlab-values.yaml \
--timeout 600s
# 等待 Pod 就绪(约 5-10 分钟)
kubectl get pods -n gitlab -w3.1.4 验证安装
bash
# 获取初始 root 密码
kubectl get secret gitlab-gitlab-initial-root-password -n gitlab \
-o jsonpath='{.data.password}' | base64 -d
# 访问
# https://ai-ear.cn:84433.2 创建项目
bash
# 通过 GitLab API 创建项目
curl -sk -X POST \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-H "Content-Type: application/json" \
"https://ai-ear.cn:8443/api/v4/projects" \
-d '{
"name": "demo-app-v2",
"description": "Demo应用 v2 - Java + Python + AI Review CI/CD",
"visibility": "private",
"initialize_with_readme": true
}'3.3 生成 Access Token
Jenkins 需要两类 Token 与 GitLab 交互:
bash
# 1. API Token (用于 Jenkins → GitLab API 调用)
# 在 GitLab Web UI 中:
# Settings → Access Tokens → Create Personal Access Token
# Name: jenkins-cicd
# Scopes: api, read_repository, write_repository
# 保存 Token: xxx
# 2. Webhook Token (用于 GitLab → Jenkins 触发构建)
# 这个在 Jenkins Helm values 中配置为 additionalSecrets
# 值: glpat-xxx3.4 配置 SSH Key
如果需要 SSH 方式克隆(本教程使用 HTTP + Token 方式,可跳过此步骤):
bash
# 生成 SSH Key
ssh-keygen -t ed25519 -C "jenkins@ai-ear.cn" -f ~/.ssh/jenkins_gitlab -N ""
# 添加到 GitLab Deploy Keys
curl -sk -X POST \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-H "Content-Type: application/json" \
"https://ai-ear.cn:8443/api/v4/projects/4/deploy_keys" \
-d '{
"title": "Jenkins CI",
"key": "'"$(cat ~/.ssh/jenkins_gitlab.pub)"'",
"can_push": false
}'第四章:Jenkins 搭建与配置
4.1 Helm 安装 Jenkins
Jenkins 使用官方 Helm Chart jenkins-5.9.54,通过 JCasC(Jenkins Configuration as Code) 实现全量声明式配置。
4.1.1 添加 Helm Repo
bash
helm repo add jenkins https://charts.jenkins.io
helm repo update4.1.2 创建 jenkins-values.yaml
这是整个 Jenkins 的核心配置文件,分为几个关键部分:
yaml
# jenkins-values.yaml
# ============================================================
# Controller 配置
# ============================================================
controller:
image:
registry: 192.168.122.156:30000 # Harbor 镜像仓库
repository: jenkins/jenkins
tag: lts-jdk21
imagePullPolicy: IfNotPresent
# JVM 调优: 4C10Gi 容器, Heap 6G G1GC
javaOpts: >-
-Xms4g -Xmx6g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+ParallelRefProcEnabled
-XX:+ExplicitGCInvokesConcurrent
-Djava.awt.headless=true
-Djenkins.model.Jenkins.slaveAgentPort=50000
resources:
requests:
cpu: "2"
memory: 6Gi
limits:
cpu: "4"
memory: 10Gi
# 管理员账号
admin:
username: xxx
password: xxx
# 服务暴露: NodePort
serviceType: NodePort
nodePort: 30880
jenkinsUrl: https://ai-ear.cn:9010/
cloudName: kubernetes
numExecutors: 0 # Controller 不执行任务
# 探针配置 (大 JVM 启动较慢)
probes:
startupProbe:
failureThreshold: 60
periodSeconds: 10
livenessProbe:
failureThreshold: 12
periodSeconds: 30
timeoutSeconds: 15
readinessProbe:
failureThreshold: 12
periodSeconds: 30
timeoutSeconds: 15
# 插件列表
additionalPlugins:
- sonar # SonarQube 集成
- job-dsl # Job DSL (自动创建流水线)
- junit # JUnit 测试报告
- timestamper # 时间戳
- gitlab-plugin # GitLab 集成 + Commit Status
- role-strategy # RBAC 权限管理
- generic-webhook-trigger # 通用 Webhook
- pipeline-utility-steps # 流水线工具
- oic-auth # OpenID Connect (GitLab SSO)
- matrix-auth # 矩阵权限
# 敏感信息 (注入为 K8s Secret)
additionalSecrets:
- name: gitlab-token
value: glpat-xxx
- name: gitlab-oauth-client-id
value: xxx
- name: gitlab-oauth-client-secret
value: xxx...
# JCasC 配置自动重载 Sidecar
sidecars:
configAutoReload:
enabled: true
image:
registry: 192.168.122.156:30000
repository: jenkins/k8s-sidecar
tag: 2.10.1
# JCasC 配置 (详见 4.2)
JCasC:
defaultConfig: true
configScripts:
# 见下方各小节
# ============================================================
# Agent 配置
# ============================================================
agent:
enabled: true
containerCap: 200
idleMinutes: 5
maxRequestsPerHostStr: "50"
image:
registry: 192.168.122.156:30000
repository: jenkins/inbound-agent
tag: latest-jdk21
# Pod 模板 (详见 4.3)
podTemplates:
maven8: | ...
maven21: | ...
node21: | ...
golang: | ...
python: | ...
# ============================================================
# 持久化存储
# ============================================================
persistence:
enabled: true
accessMode: ReadWriteMany
storageClass: nfs-nvme # NVME 高速盘
size: 100Gi
# PDB (防止滚动更新时全部驱逐)
podDisruptionBudget:
enabled: true
apiVersion: policy/v1
maxUnavailable: 0
existingSecret: sonar-credentials4.1.3 安装
bash
helm upgrade --install jenkins jenkins/jenkins \
--namespace jenkins --create-namespace \
-f jenkins-values.yaml \
--timeout 600s
# 等待 Controller 启动 (JVM 首次启动约 2-3 分钟)
kubectl get pods -n jenkins -w4.2 JCasC 全量配置
JCasC(Jenkins Configuration as Code)将所有 Jenkins 配置声明化,存储在 K8s ConfigMap 中,实现版本控制和自动同步。
4.2.1 Kubernetes Cloud 配置
yaml
# jcasc 配置段: Kubernetes Cloud
jenkins:
clouds:
- kubernetes:
name: kubernetes
serverUrl: "https://kubernetes.default"
namespace: jenkins
jenkinsUrl: "http://jenkins.jenkins.svc.cluster.local:8080"
jenkinsTunnel: "jenkins-agent.jenkins.svc.cluster.local:50000"
containerCapStr: "200"
connectTimeout: "5"
readTimeout: "15"
retentionTimeout: "5"
waitForPodSec: "600"
maxRequestsPerHostStr: "50"
skipTlsVerify: false
usageRestricted: false
podLabels:
- key: "jenkins/jenkins-jenkins-agent"
value: "true"
templates:
- name: default
namespace: jenkins
containers:
- name: jnlp
image: "192.168.122.156:30000/jenkins/inbound-agent:latest-jdk21"
args: "^${computer.jnlpmac} ^${computer.name}"
envVars:
- envVar:
key: JENKINS_URL
value: "http://jenkins.jenkins.svc.cluster.local:8080/"
resourceRequestCpu: 512m
resourceRequestMemory: 512Mi
resourceLimitCpu: 512m
resourceLimitMemory: 512Mi
workingDir: /home/jenkins/agent
idleMinutes: 5
label: "jenkins-jenkins-agent"
serviceAccount: default
slaveConnectTimeoutStr: "100"
yamlMergeStrategy: override关键参数解释:
jenkinsUrl: Agent Pod 连接 Controller 的地址(K8s 内部 Service)jenkinsTunnel: JNLP 协议隧道地址(端口 50000)waitForPodSec: 等待 Pod 就绪的最大秒数(600s = 10 分钟)containerCapStr: 最大并发 Agent Pod 数量
4.2.2 GitLab 集成配置
yaml
# ConfigMap: jenkins-jenkins-config-gitlab-integration
credentials:
system:
domainCredentials:
- credentials:
# API Token (String 类型)
- string:
scope: GLOBAL
id: "gitlab-api-token"
secret: "${readFile:/run/secrets/additional/gitlab-token}"
description: "GitLab Root PAT"
# Git 克隆凭证 (Username/Password 类型)
- 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: 10
${readFile:/run/secrets/additional/gitlab-token}:这是 JCasC 的 Secret 注入语法,值来自 Helm values 中的additionalSecrets,由 Helm Chart 自动创建为 K8s Secret 并挂载到 Controller Pod。
4.2.3 SonarQube 集成
yaml
credentials:
system:
domainCredentials:
- credentials:
- string:
scope: GLOBAL
id: "sonarqube-token"
secret: "${readFile:/run/secrets/additional/sonar-token}"
description: "SonarQube analysis token"
unclassified:
sonarGlobalConfiguration:
buildWrapperEnabled: false
installations:
- name: "sonarqube"
serverUrl: "http://sonarqube.sonarqube.svc.cluster.local:9000"
credentialsId: "sonarqube-token"4.3 Agent Pod 模板
Agent Pod 是 Jenkins 执行构建任务的"工人"。每个 Pod 模板定义一个特定环境的容器。
4.3.1 Maven 21 Agent
yaml
# Pod 模板: maven21
- name: maven21
label: maven21
inheritFrom: default # 继承 JNLP 容器
containers:
- name: maven
image: "192.168.122.156:30000/jenkins/maven:3.9-eclipse-temurin-21"
command: "sleep"
args: "99d" # 保持容器存活
ttyEnabled: true
resourceRequestCpu: "500m"
resourceRequestMemory: "1Gi"
resourceLimitCpu: "2"
resourceLimitMemory: "2Gi"
volumes:
- configMapVolume:
configMapName: jenkins-examples # 示例代码
mountPath: /examples
- configMapVolume:
configMapName: maven-settings # Maven settings.xml (阿里云镜像)
mountPath: /root/.m2内置工具:java 21, mvn 3.9, git, curl, bash, find, tar
4.3.2 Python Agent
yaml
# Pod 模板: python
- name: python
label: python
inheritFrom: default
containers:
- name: python
image: "192.168.122.156:30000/jenkins/python:3.12-alpine"
command: "sleep"
args: "99d"
ttyEnabled: true
resourceRequestCpu: "250m"
resourceRequestMemory: "512Mi"
resourceLimitCpu: "1"
resourceLimitMemory: "1Gi"
envVars:
- envVar:
key: PIP_INDEX_URL
value: "https://mirrors.aliyun.com/pypi/simple/"
volumes:
- configMapVolume:
configMapName: jenkins-examples
mountPath: /examples内置工具:python3 3.12, pip3缺失工具:curl, bash, wget, jq (Alpine 精简镜像)
⚠️ Alpine 镜像注意:Python Agent 基于 Alpine Linux,缺少
bash和curl。所有 shell 脚本必须使用sh,HTTP 请求必须用 Pythonurllib实现。
4.3.3 其他 Agent 模板
| Label | 镜像 | 用途 |
|---|---|---|
maven8 | maven:3.9-eclipse-temurin-8 | Java 8 项目 |
maven21 | maven:3.9-eclipse-temurin-21 | Java 21 项目 |
node21 | node:21-alpine | Node.js 项目 |
golang | golang:1.24-alpine | Go 项目 |
python | python:3.12-alpine | Python 项目 |
4.4 GitLab 集成
4.4.1 Webhook 配置
在 GitLab 项目中配置 Webhook,每次 Push 自动触发 Jenkins 构建:
bash
# 通过 API 创建 Webhook
curl -sk -X POST \
-H "PRIVATE-TOKEN: xxx" \
-H "Content-Type: application/json" \
"https://ai-ear.cn:8443/api/v4/projects/4/hooks" \
-d '{
"url": "http://admin:WEBHOOK_SECRET@jenkins.jenkins.svc.cluster.local:8080/job/dev/job/gitlab-cicd-demo/build",
"push_events": true,
"merge_requests_events": false,
"enable_ssl_verification": false
}'URL 格式说明:
http://admin:TOKEN@jenkins...→ Basic Auth 方式认证TOKEN= RBAC 中webhook-trigger角色的凭证/job/dev/job/gitlab-cicd-demo/build→ 触发构建的 API 路径
4.4.2 Commit Status 回写
Jenkins Pipeline 中使用 updateGitlabCommitStatus 步骤将构建结果回写到 GitLab:
groovy
post {
success {
updateGitlabCommitStatus name: 'jenkins', state: 'success'
updateGitlabCommitStatus name: 'ai-review', state: 'success'
}
failure {
updateGitlabCommitStatus name: 'jenkins', state: 'failed'
}
}4.5 RBAC 多租户权限
使用 role-strategy 插件实现基于角色的访问控制:
yaml
# ConfigMap: jenkins-jenkins-config-rbac
jenkins:
authorizationStrategy:
roleBased:
roles:
# ===== 全局角色 =====
global:
- name: "admin"
permissions: ["Overall/Administer"]
entries:
- user: "admin"
- group: "jenkins-admins"
- name: "readonly"
permissions: ["Overall/Read"]
entries:
- group: "authenticated"
- name: "webhook-trigger"
permissions: ["Overall/Read", "Job/Build", "Job/Read"]
entries:
- user: "anonymous"
# ===== 项目角色 =====
items:
- name: "dev-developer"
pattern: "dev/.*"
permissions:
- "Job/Build"
- "Job/Cancel"
- "Job/Read"
- "Job/Workspace"
- "Job/Configure"
- "Job/Create"
entries:
- group: "dev-team"权限矩阵:
| 角色 | 范围 | 权限 |
|---|---|---|
| admin | 全局 | 全部管理权限 |
| readonly | 全局 | 只读 |
| webhook-trigger | 全局 | 触发构建 (匿名 Webhook) |
| dev-developer | dev/* | 构建/取消/读取/工作区/配置/创建 |
| test-developer | test/* | 同上 |
| ops-developer | ops/* | 同上 |
4.6 Shared Library
Jenkins Shared Library 存放可复用的 Groovy 函数,在 Pipeline 中通过 @Library('ai-pipeline-lib') _ 引入。
yaml
# ConfigMap: jenkins-jenkins-config-shared-libraries
unclassified:
globalLibraries:
libraries:
- name: "ai-pipeline-lib"
defaultVersion: "main"
implicit: false
allowVersionOverride: true
includeInChangesets: false
retriever:
legacySCM:
scm:
git:
userRemoteConfigs:
- url: "http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/ai-pipeline-lib.git"
credentialsId: "gitlab-git-user"
branches:
- name: "*/main"4.7 Job DSL 自动创建
使用 job-dsl 插件在 Jenkins 启动时自动创建文件夹和流水线 Job:
yaml
# ConfigMap: jenkins-jenkins-config-jobs
jobs:
# 多租户文件夹
- script: |
folder('dev') { description('开发团队工作区') }
folder('test') { description('测试团队工作区') }
folder('ops') { description('运维团队工作区') }
# 工具链验证 Job
- script: |
pipelineJob('dev/toolchain-check') {
description('工具链验证:java8 / java21 / node21 / golang / python')
definition { cps { sandbox(true); script('''
def checks = [
maven8 : ['maven' , 'java -version && mvn -v'],
maven21: ['maven' , 'java -version && mvn -v'],
node21 : ['node' , 'node -v && npm -v'],
golang : ['golang', 'go version'],
python : ['python', 'python3 --version && pip3 --version'],
]
def stages = [:]
checks.each { agentLabel, cfg ->
stages[agentLabel] = {
node(agentLabel) { container(cfg[0]) { sh cfg[1] } }
}
}
parallel stages
''') } }
}
# GitLab SCM 触发的流水线 (Multibranch)
- script: |
multibranchPipelineJob('dev/gitlab-cicd-demo') {
description('GitLab CI/CD Demo: Java + Python + AI Review')
branchSources {
git {
id('gitlab-demo')
remote('http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/demo-app-v2.git')
credentialsId('gitlab-git-user')
traits {
branchDiscovery()
}
}
}
factory {
workflowBranchProjectFactory {
scriptPath('Jenkinsfile')
}
}
triggers {
periodicFolderTrigger { interval('1d') }
}
}第五章:Ollama AI 部署
5.1 安装 Ollama
Ollama 部署在 K8s 集群外的物理机上(192.168.122.9,即 sza122031 控制节点),原因是 GPU/CPU 推理需要独占资源,且 K8s Pod 不方便绑定到 GPU 设备。
bash
# 在 192.168.122.9 上执行
curl -fsSL https://ollama.com/install.sh | sh
# 配置环境变量 (允许外部访问)
cat > /etc/systemd/system/ollama.service.d/override.conf << 'EOF'
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_MAX_LOADED_MODELS=2"
EOF
systemctl daemon-reload
systemctl enable --now ollama5.2 拉取模型
bash
# 拉取 Qwen3.6:27B (17GB)
ollama pull qwen3.6:27b
# 查看已下载的模型
ollama list
# NAME ID SIZE
# qwen3.6:27b a50eda8ed977 17 GB
# qwen3.6:27b-30k 71717dbdb458 17 GB
# qwen3.6:27b-65k 783133eb678b 17 GB模型选择说明:
qwen3.6:27b— 标准版,适合通用代码审查qwen3.6:27b-30k— 30K 上下文窗口版,适合大文件审查qwen3.6:27b-65k— 65K 上下文窗口版,适合超大代码库
5.3 验证 API
bash
# 测试 Ollama 健康检查
curl -s http://192.168.122.9:11434/api/tags | python3 -c "
import json, sys
data = json.load(sys.stdin)
for m in data.get('models', []):
print(f' Model: {m[\"name\"]} Size: {m.get(\"size\",0)/1e9:.1f}GB')
"
# 测试 Chat API (快速验证)
curl -s http://192.168.122.9:11434/api/chat -d '{
"model": "qwen3.6:27b",
"messages": [{"role": "user", "content": "Say hello in one word"}],
"stream": false,
"think": false
}' | python3 -c "import json,sys; print(json.load(sys.stdin)['message']['content'])"5.4 网络连通性
Ollama 部署在 K8s 集群外部,Jenkins Agent Pod 需要通过宿主机 IP 访问:
Jenkins Agent Pod (K8s 内部)
│
│ HTTP: http://192.168.122.9:11434/api/chat
│
▼
Ollama Server (宿主机 192.168.122.9)
│
│ 监听 0.0.0.0:11434
▼
GPU/CPU 推理引擎验证 Agent Pod 到 Ollama 的连通性:
bash
# 找到运行中的 Python Agent Pod
POD=$(kubectl get pods -n jenkins -l jenkins/jenkins-jenkins-agent=true -o name | head -1)
# 从 Pod 内部测试连通性
kubectl exec -n jenkins $POD -c python -- python3 -c "
import urllib.request, json
try:
req = urllib.request.Request('http://192.168.122.9:11434/api/tags')
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
for m in data.get('models', []):
print(f' ✓ {m[\"name\"]}')
except Exception as e:
print(f' ✗ 无法连接: {e}')
"第六章:CI/CD 流水线实战
本章是整个教程的核心,从零开始构建一个 Java + Python + AI 代码审查 的完整 CI/CD 流水线。
6.1 项目结构设计
demo-app-v2/
├── Jenkinsfile # 流水线定义 (核心!)
├── ai-code-review.sh # AI 代码审查脚本
├── pom.xml # Maven 项目配置
├── src/
│ ├── main/
│ │ ├── java/cn/ai_ear/demo/
│ │ │ └── App.java # Java 主类
│ │ └── python/
│ │ ├── calculator.py # 计算器模块 (Python)
│ │ └── utils.py # 工具函数模块 (Python)
│ └── test/
│ ├── java/cn/ai_ear/demo/
│ │ └── AppTest.java # Java 单元测试 (4个)
│ └── python/
│ ├── test_calculator.py # 计算器测试 (12个)
│ └── test_utils.py # 工具函数测试 (24个)
└── target/
└── demo-app-1.0.N.jar # 构建产物 (N = BUILD_NUMBER)目录约定说明:
src/main/java/— Java 源码 (Maven 标准约定)src/main/python/— Python 源码 (扩展约定)src/test/java/— Java 测试src/test/python/— Python 测试
6.2 Java 代码与测试
6.2.1 pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.ai-ear</groupId>
<artifactId>demo-app</artifactId>
<!-- BUILD_NUMBER 由 Jenkins 注入, 每次构建自动递增 -->
<version>1.0.${env.BUILD_NUMBER}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>Maven 镜像加速:Agent Pod 挂载了
maven-settingsConfigMap 到/root/.m2/settings.xml,配置了阿里云 Maven 镜像,加速依赖下载。
6.2.2 App.java
java
package cn.ai_ear.demo;
public class App {
public static void main(String[] args) {
System.out.println("Hello Jenkins CI/CD!");
System.out.println("add(3, 5) = " + add(3, 5));
System.out.println("multiply(4, 6) = " + multiply(4, 6));
System.out.println("isPalindrome('racecar') = " + isPalindrome("racecar"));
System.out.println("fibonacci(10) = " + fibonacci(10));
}
public static int add(int a, int b) {
return a + b;
}
public static int multiply(int a, int b) {
return a * b;
}
public static boolean isPalindrome(String s) {
if (s == null) return false;
String cleaned = s.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
String reverse = new StringBuilder(cleaned).reverse().toString();
return cleaned.equals(reverse);
}
public static int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
}6.2.3 AppTest.java (4 个测试)
java
package cn.ai_ear.demo;
import org.junit.Test;
import static org.junit.Assert.*;
public class AppTest {
@Test
public void testAdd() {
assertEquals(8, App.add(3, 5));
assertEquals(0, App.add(-1, 1));
}
@Test
public void testMultiply() {
assertEquals(24, App.multiply(4, 6));
assertEquals(0, App.multiply(0, 5));
}
@Test
public void testIsPalindrome() {
assertTrue(App.isPalindrome("racecar"));
assertFalse(App.isPalindrome("hello"));
assertTrue(App.isPalindrome("A man a plan a canal Panama"));
}
@Test
public void testFibonacci() {
assertEquals(0, App.fibonacci(0));
assertEquals(1, App.fibonacci(1));
assertEquals(55, App.fibonacci(10));
}
}6.3 Python 代码与测试
6.3.1 calculator.py
python
"""Calculator module - demonstrates basic arithmetic operations."""
class Calculator:
"""A simple calculator class supporting basic math operations."""
def __init__(self):
self.history = []
def add(self, a: float, b: float) -> float:
"""Add two numbers and record in history."""
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def subtract(self, a: float, b: float) -> float:
"""Subtract b from a."""
result = a - b
self.history.append(f"{a} - {b} = {result}")
return result
def multiply(self, a: float, b: float) -> float:
"""Multiply two numbers."""
result = a * b
self.history.append(f"{a} * {b} = {result}")
return result
def divide(self, a: float, b: float) -> float:
"""Divide a by b. Raises ValueError on division by zero."""
if b == 0:
raise ValueError("Cannot divide by zero")
result = a / b
self.history.append(f"{a} / {b} = {result}")
return result
def power(self, base: float, exponent: float) -> float:
"""Raise base to the power of exponent."""
result = base ** exponent
self.history.append(f"{base} ** {exponent} = {result}")
return result
def modulo(self, a: float, b: float) -> float:
"""Return the remainder of a divided by b."""
if b == 0:
raise ValueError("Cannot modulo by zero")
result = a % b
self.history.append(f"{a} % {b} = {result}")
return result
def get_history(self) -> list:
"""Return the calculation history."""
return self.history.copy()
def clear_history(self):
"""Clear the calculation history."""
self.history.clear()6.3.2 utils.py
python
"""Utility functions for data processing and string manipulation."""
import re
from collections import Counter
from typing import Any
def is_palindrome(text: str) -> bool:
"""Check if text is a palindrome."""
cleaned = re.sub(r'[^a-zA-Z0-9]', '', text).lower()
return cleaned == cleaned[::-1]
def word_frequency(text: str) -> dict[str, int]:
"""Return a dictionary of word frequencies."""
words = re.findall(r'\b\w+\b', text.lower())
return dict(Counter(words))
def flatten(nested_list: list) -> list:
"""Flatten a nested list into a single-level list."""
result = []
for item in nested_list:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
def camel_to_snake(name: str) -> str:
"""Convert camelCase to snake_case."""
s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def snake_to_camel(name: str) -> str:
"""Convert snake_case to camelCase."""
components = name.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
def safe_divide(a: float, b: float, default: float = 0.0) -> float:
"""Safely divide a by b, returning default if b is zero."""
try:
return a / b
except (ZeroDivisionError, TypeError):
return default
def chunk_list(lst: list, chunk_size: int) -> list[list]:
"""Split a list into chunks of the specified size."""
if chunk_size <= 0:
raise ValueError("chunk_size must be positive")
return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)]
def deep_get(data: dict, path: str, default: Any = None) -> Any:
"""Safely access nested dictionary values using dot-separated path."""
keys = path.split('.')
current = data
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return default
return current6.3.3 test_calculator.py (12 个测试)
python
"""Unit tests for the Calculator class."""
import sys, os, unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__),
'..', '..', 'main', 'python'))
from calculator import Calculator
class TestCalculator(unittest.TestCase):
def setUp(self):
self.calc = Calculator()
def test_add(self):
self.assertEqual(self.calc.add(2, 3), 5)
self.assertEqual(self.calc.add(-1, 1), 0)
def test_subtract(self):
self.assertEqual(self.calc.subtract(10, 3), 7)
def test_multiply(self):
self.assertEqual(self.calc.multiply(2, 3), 6)
self.assertEqual(self.calc.multiply(0, 5), 0)
def test_divide(self):
self.assertEqual(self.calc.divide(10, 2), 5)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
self.calc.divide(10, 0)
def test_power(self):
self.assertEqual(self.calc.power(2, 8), 256)
self.assertEqual(self.calc.power(5, 0), 1)
def test_modulo(self):
self.assertEqual(self.calc.modulo(10, 3), 1)
def test_modulo_by_zero(self):
with self.assertRaises(ValueError):
self.calc.modulo(10, 0)
def test_history(self):
self.calc.add(1, 2)
history = self.calc.get_history()
self.assertEqual(len(history), 1)
def test_clear_history(self):
self.calc.add(1, 2)
self.calc.clear_history()
self.assertEqual(len(self.calc.get_history()), 0)6.3.4 test_utils.py (24 个测试)
python
"""Unit tests for utility functions."""
import sys, os, unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__),
'..', '..', 'main', 'python'))
from utils import (is_palindrome, word_frequency, flatten,
camel_to_snake, snake_to_camel, safe_divide, chunk_list, deep_get)
class TestIsPalindrome(unittest.TestCase):
def test_simple(self):
self.assertTrue(is_palindrome("racecar"))
def test_non_palindromes(self):
self.assertFalse(is_palindrome("hello"))
def test_case_insensitive(self):
self.assertTrue(is_palindrome("A man a plan a canal Panama"))
def test_special_chars(self):
self.assertTrue(is_palindrome("Was it a car or a cat I saw?"))
def test_empty(self):
self.assertTrue(is_palindrome(""))
class TestWordFrequency(unittest.TestCase):
def test_basic(self):
self.assertEqual(word_frequency("the cat")["the"], 1)
def test_case_insensitive(self):
self.assertEqual(word_frequency("Hello hello")["hello"], 2)
def test_empty(self):
self.assertEqual(word_frequency(""), {})
class TestFlatten(unittest.TestCase):
def test_simple(self):
self.assertEqual(flatten([1, [2, 3], 4]), [1, 2, 3, 4])
def test_nested(self):
self.assertEqual(flatten([1, [2, [3, [4]]]]), [1, 2, 3, 4])
def test_empty(self):
self.assertEqual(flatten([]), [])
class TestCamelToSnake(unittest.TestCase):
def test_basic(self):
self.assertEqual(camel_to_snake("camelCase"), "camel_case")
def test_single(self):
self.assertEqual(camel_to_snake("hello"), "hello")
def test_acronyms(self):
self.assertEqual(camel_to_snake("HTTPResponse"), "http_response")
class TestSnakeToCamel(unittest.TestCase):
def test_basic(self):
self.assertEqual(snake_to_camel("snake_case"), "snakeCase")
def test_single(self):
self.assertEqual(snake_to_camel("hello"), "hello")
class TestSafeDivide(unittest.TestCase):
def test_normal(self):
self.assertEqual(safe_divide(10, 2), 5.0)
def test_zero(self):
self.assertEqual(safe_divide(10, 0), 0.0)
def test_type_error(self):
self.assertEqual(safe_divide("a", "b"), 0.0)
class TestChunkList(unittest.TestCase):
def test_basic(self):
self.assertEqual(chunk_list([1,2,3,4,5], 2), [[1,2],[3,4],[5]])
def test_exact(self):
self.assertEqual(chunk_list([1,2,3,4], 2), [[1,2],[3,4]])
def test_invalid(self):
with self.assertRaises(ValueError):
chunk_list([1, 2], 0)
class TestDeepGet(unittest.TestCase):
def test_basic(self):
self.assertEqual(deep_get({"a":{"b":{"c":42}}}, "a.b.c"), 42)
def test_missing(self):
self.assertIsNone(deep_get({"a":{"b":1}}, "a.c"))
def test_top(self):
self.assertEqual(deep_get({"key":"value"}, "key"), "value")6.4 AI 代码审查脚本
这是整个流水线中最具创新性的部分。脚本使用 Python urllib(因为 Alpine 容器没有 curl)调用 Ollama API,让 AI 模型对代码进行审查。
6.4.1 脚本设计思路
ai-code-review.sh
├── 第1部分: 参数解析 & 环境检测
│ ├── OLLAMA_HOST (默认 127.0.0.1:11434)
│ ├── OLLAMA_MODEL (默认 qwen3.6:27b)
│ └── CODE_DIR (默认 .)
├── 第2部分: 扫描代码文件 (glob + 正则)
├── 第3部分: 构造 Prompt (角色 + 规则 + 代码)
├── 第4部分: 调用 Ollama API (HTTP POST, think=false)
├── 第5部分: 解析 & 输出报告
└── 第6部分: 生成 ai-review-report.md6.4.2 ai-code-review.sh 完整代码
bash
#!/bin/sh
# ai-code-review.sh — AI-powered code review via Ollama API
# Compatible with Alpine Linux (no bash/curl/jq dependencies)
set -e
# ===== 参数解析 =====
OLLAMA_HOST="${OLLAMA_HOST:-127.0.0.1:11434}"
OLLAMA_MODEL="${OLLAMA_MODEL:-qwen3.6:27b}"
CODE_DIR="${CODE_DIR:-.}"
RULES="${RULES:-naming conventions, error handling, security vulnerabilities, performance}"
MAX_FILES="${MAX_FILES:-10}"
MAX_FILE_SIZE="${MAX_FILE_SIZE:-4000}"
echo "===== AI Code Review ====="
echo "Model: $OLLAMA_MODEL"
echo "Host: $OLLAMA_HOST"
echo "Dir: $CODE_DIR"
echo "Rules: $RULES"
echo "=========================="
# ===== 环境检测 =====
if ! which python3 > /dev/null 2>&1; then
echo "[ERROR] python3 not found!"
exit 1
fi
# ===== Python 脚本: 扫描 + 审查 + 报告 =====
python3 - "$CODE_DIR" "$MAX_FILES" "$MAX_FILE_SIZE" "$OLLAMA_HOST" "$OLLAMA_MODEL" "$RULES" << 'PYTHON_SCRIPT'
import sys, os, glob, re, json, urllib.request, urllib.error, time
code_dir = sys.argv[1]
max_files = int(sys.argv[2])
max_file_size = int(sys.argv[3])
ollama_host = sys.argv[4]
ollama_model = sys.argv[5]
rules = sys.argv[6]
# Step 1: 扫描代码文件
extensions = {'.java', '.py', '.js', '.ts', '.go', '.rs', '.sh',
'.yaml', '.yml', '.json', '.xml', '.groovy', '.kt'}
files = []
for root, dirs, filenames in os.walk(code_dir):
dirs[:] = [d for d in dirs if d not in {
'node_modules', '.git', 'target', 'build', '__pycache__', '.m2'}]
for f in filenames:
ext = os.path.splitext(f)[1].lower()
if ext in extensions:
fp = os.path.join(root, f)
sz = os.path.getsize(fp)
if 0 < sz <= max_file_size * 2:
files.append((fp, ext, sz))
files.sort(key=lambda x: x[2], reverse=True)
files = files[:max_files]
print(f"\n[SCAN] Found {len(files)} files:")
for fp, ext, sz in files:
print(f" {ext:8s} {sz:6d}B {os.path.relpath(fp, code_dir)}")
if not files:
print("[SKIP] No code files found.")
sys.exit(0)
# Step 2: 读取代码内容
code_chunks = []
total_chars = 0
for fp, ext, sz in files:
try:
with open(fp, 'r', encoding='utf-8', errors='replace') as fh:
content = fh.read()[:max_file_size]
rel = os.path.relpath(fp, code_dir)
code_chunks.append(f"--- {rel} ({ext}) ---\n{content}")
total_chars += len(content)
except Exception as e:
print(f" [WARN] Cannot read {fp}: {e}")
combined_code = "\n\n".join(code_chunks)
print(f"[INFO] Total code: {total_chars} chars, {len(code_chunks)} files")
# Step 3: 构造 Prompt
prompt = f"""You are a senior code reviewer. Review the following code files.
Review rules: {rules}
For each file, provide:
1. File name
2. Issues found (severity: Critical/Warning/Info)
3. Suggestions for improvement
4. Overall rating (1-10)
Keep review concise. Respond in the same language as the code comments.
Code to review:
{combined_code}
"""
# Step 4: 调用 Ollama API
url = f"http://{ollama_host}/api/chat"
payload = json.dumps({
"model": ollama_model,
"messages": [
{"role": "system", "content": "You are a code review expert. Be concise."},
{"role": "user", "content": prompt}
],
"stream": False,
"think": False, # 关键! 关闭思考模式防止超时
"options": {
"temperature": 0.3, # 低温度 = 更确定性的输出
"num_predict": 2000 # 限制输出长度
}
}).encode('utf-8')
print(f"\n[AI] Sending to {ollama_model} ({total_chars} chars)...")
start = time.time()
try:
req = urllib.request.Request(url, data=payload,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=180) as resp:
result = json.loads(resp.read())
elapsed = time.time() - start
content = result.get('message', {}).get('content', '(empty)')
tokens_eval = result.get('eval_count', 0)
tokens_prompt = result.get('prompt_eval_count', 0)
print(f"[AI] Done in {elapsed:.1f}s "
f"(prompt={tokens_prompt}, eval={tokens_eval})")
# Step 5: 输出报告
print(f"\n{'='*60}")
print(" AI CODE REVIEW REPORT")
print(f"{'='*60}\n")
print(content)
# Step 6: 保存 Markdown 报告
report = f"""# AI Code Review Report
- **Model**: {ollama_model}
- **Files**: {len(code_chunks)}
- **Code size**: {total_chars} chars
- **Review time**: {elapsed:.1f}s
- **Tokens**: prompt={tokens_prompt}, eval={tokens_eval}
## Review
{content}
"""
report_path = os.path.join(code_dir, 'ai-review-report.md')
with open(report_path, 'w') as f:
f.write(report)
print(f"\n[SAVED] Report: {report_path}")
except urllib.error.URLError as e:
print(f"[ERROR] Ollama API unreachable: {e}")
print("[INFO] Continuing pipeline without AI review (non-blocking)")
sys.exit(0)
except Exception as e:
print(f"[ERROR] AI review failed: {e}")
print("[INFO] Continuing pipeline without AI review (non-blocking)")
sys.exit(0)
PYTHON_SCRIPT
echo ""
echo "===== AI Review Complete ====="6.4.3 关键设计决策
| 决策 | 原因 |
|---|---|
使用 #!/bin/sh 而非 #!/bin/bash | Alpine 容器没有 bash |
使用 Python urllib 而非 curl | Alpine 容器没有 curl |
使用 glob + os.walk 而非 find | 跨平台兼容,Alpine 的 find 行为不同 |
"think": false | 关闭 Qwen3 的思考模式,防止 180s 超时 |
"temperature": 0.3 | 低温度产生更一致、更确定的审查结果 |
"num_predict": 2000 | 限制输出长度,防止超长响应 |
AI 失败时 sys.exit(0) | AI 审查是非阻塞的,失败不中断流水线 |
MAX_FILE_SIZE=4000 | 单文件最多 4000 字符,防止超出模型上下文窗口 |
MAX_FILES=10 | 最多审查 10 个文件,防止超时 |
6.5 Jenkinsfile 编写
Jenkinsfile 是流水线的"灵魂",定义了从代码检出到部署的完整流程。
groovy
// Jenkinsfile — Java + Python + AI Review CI/CD Pipeline
pipeline {
agent any
environment {
// AI Review 配置
OLLAMA_HOST = '192.168.122.9:11434'
OLLAMA_MODEL = 'qwen3.6:27b'
// Maven 配置
MAVEN_OPTS = '-Xmx512m'
}
options {
timestamps() // 每行输出带时间戳
buildDiscarder(
logRotator(numToKeepStr: '20') // 只保留最近 20 次构建
)
timeout(time: 30, unit: 'MINUTES') // 全局超时
}
stages {
// ===== Stage 1: 代码检出 =====
stage('Checkout') {
steps {
echo "Checking out branch: ${env.BRANCH_NAME ?: 'main'}"
}
}
// ===== Stage 2: Java 构建 & 测试 (maven21 Agent) =====
stage('Java Build & Test') {
agent { label 'maven21' }
steps {
container('maven') {
// 编译
sh 'mvn clean compile'
// 运行测试 & 生成 JAR
sh 'mvn test package -DskipTests=false'
}
}
post {
always {
// 收集 JUnit 测试报告
junit allowEmptyResults: true,
testResults: '**/surefire-reports/*.xml'
// 归档 JAR 产物
archiveArtifacts allowEmptyArchive: true,
artifacts: '**/target/*.jar'
}
}
}
// ===== Stage 3: Python 静态检查 (python Agent) =====
stage('Python Lint') {
agent { label 'python' }
steps {
container('python') {
// 安装工具
sh 'pip3 install pylint'
// 静态检查
sh '''
pylint src/main/python/*.py \
--output-format=text \
--disable=C0114,C0115,C0116 || true
'''
}
}
}
// ===== Stage 4: Python 单元测试 (python Agent) =====
stage('Python Test') {
agent { label 'python' }
steps {
container('python') {
sh '''
cd src/test/python
python3 -m unittest discover -v 2>&1 | tee test_output.txt
'''
}
}
}
// ===== Stage 5: AI 代码审查 (python Agent) =====
stage('AI Code Review') {
agent { label 'python' }
steps {
container('python') {
sh '''
chmod +x ai-code-review.sh
./ai-code-review.sh
'''
}
}
post {
always {
// 归档 AI 审查报告
archiveArtifacts allowEmptyArchive: true,
artifacts: 'ai-review-report.md'
}
}
}
}
post {
// ===== 构建成功: 回写 GitLab Commit Status =====
success {
echo '✅ Pipeline SUCCESS'
updateGitlabCommitStatus name: 'jenkins', state: 'success'
updateGitlabCommitStatus name: 'ai-review', state: 'success'
}
// ===== 构建失败: 回写失败状态 =====
failure {
echo '❌ Pipeline FAILED'
updateGitlabCommitStatus name: 'jenkins', state: 'failed'
}
// ===== 始终执行: 清理 =====
always {
echo "Build #${env.BUILD_NUMBER} finished"
}
}
}6.5.1 Jenkinsfile 关键概念
pipeline {
agent any ← 主 Agent (仅做调度)
stages {
stage('Java Build') {
agent { label 'maven21' } ← 声明式: 自动分配 maven21 Pod
steps {
container('maven') { ← 指定容器名 (Pod 内的)
sh 'mvn test' ← 在 maven 容器内执行
}
}
}
}
}执行流程:
1. Pipeline 启动 → 分配任意 Agent (jnlp 容器)
2. stage('Java Build') → 新分配 maven21 Pod (jnlp + maven 容器)
3. container('maven') → 切换到 maven 容器执行命令
4. stage 结束 → maven21 Pod 释放 (idle 5 分钟后销毁)
5. stage('Python Lint') → 新分配 python Pod (jnlp + python 容器)
6. ... 以此类推6.6 GitLab Webhook 配置
6.6.1 创建 Webhook
bash
# Webhook URL 格式:
# http://USERNAME:xxx
WEBHOOK_URL="http://admin:113b41e49846a3238ce2f02d3dd23a9476@jenkins.jenkins.svc.cluster.local:8080/job/dev/job/gitlab-cicd-demo/build"
curl -sk -X POST \
-H "PRIVATE-TOKEN: xxx" \
-H "Content-Type: application/json" \
"https://ai-ear.cn:8443/api/v4/projects/4/hooks" \
-d "{
\"url\": \"$WEBHOOK_URL\",
\"push_events\": true,
\"merge_requests_events\": false,
\"enable_ssl_verification\": false
}"6.6.2 验证 Webhook
bash
# 测试 Webhook (发送模拟 Push 事件)
curl -sk -X POST \
-H "PRIVATE-TOKEN: xxx" \
-H "Content-Type: application/json" \
"https://ai-ear.cn:8443/api/v4/projects/4/hooks/2/test/push_events"
# 检查 Jenkins 是否收到触发
# 在 Jenkins Web UI 查看: dev → gitlab-cicd-demo → Build History6.6.3 Webhook 工作原理
开发者 git push
│
▼
GitLab Server
│
│ HTTP POST (Push Event JSON)
│ → Webhook URL
▼
Jenkins Controller
│
│ 解析 Webhook → 匹配 Job
│ → 创建 Build Queue Item
▼
Kubernetes
│
│ 创建 Agent Pod (maven21/python)
▼
Pipeline 执行
│
│ updateGitlabCommitStatus
│ → GitLab API
▼
GitLab Commit 页面显示 ✅/❌第七章:运行与验证
7.1 首次运行流水线
7.1.1 通过 GitLab Push 触发
bash
# 1. 克隆项目
git clone https://oauth2:glpat-xxx@ai-ear.cn:8443/root/demo-app-v2.git
cd demo-app-v2
# 2. 做一次小改动并提交
echo "# Updated $(date)" >> README.md
git add .
git commit -m "feat: trigger CI/CD pipeline"
git push origin main
# 3. Webhook 自动触发 Jenkins 构建
# 等待约 5-10 秒后查看 Jenkins7.1.2 手动触发(备选)
bash
# 通过 Jenkins API 手动触发
curl -s -u 'admin:xxx' \
-X POST \
"https://ai-ear.cn:9010/job/dev/job/gitlab-cicd-demo/build"7.1.3 在 Jenkins Web UI 查看
打开浏览器访问 https://ai-ear.cn:9010,导航到:
Dashboard → dev → gitlab-cicd-demo → Build History → #NPipeline 阶段视图:
Stage Duration Status Agent
─────────────────────────────────────────────────
Checkout 1s ✅ jnlp
Java Build 45s ✅ maven21 (4/4 tests)
Python Lint 15s ✅ python
Python Test 10s ✅ python (36/36 tests)
AI Code Review 90s ✅ python (Ollama)
─────────────────────────────────────────────────
Total ~3min ✅ SUCCESS7.2 查看构建日志
bash
# 查看控制台输出
curl -s -u 'admin:xxx' \
"https://ai-ear.cn:9010/job/dev/job/gitlab-cicd-demo/12/consoleText"
# 查看特定 Stage 的日志
# Jenkins Web UI → Build → Stage View → 点击阶段 → View Logs7.3 查看 AI 审查报告
bash
# 下载 AI Review Report
curl -s -u 'admin:xxx' \
"https://ai-ear.cn:9010/job/dev/job/gitlab-cicd-demo/12/artifact/ai-review-report.md"报告示例输出:
markdown
# AI Code Review Report
- **Model**: qwen3.6:27b
- **Files**: 5
- **Code size**: 3847 chars
- **Review time**: 87.3s
- **Tokens**: prompt=2841, eval=1256
## Review
### App.java
- ✅ `fibonacci()` 使用迭代而非递归,避免栈溢出
- ⚠️ `isPalindrome()` 可考虑使用双指针优化内存
- Overall: 8/10
### calculator.py
- ✅ 除零检查完善,抛出 ValueError
- ✅ 历史记录功能设计良好
- ⚠️ 缺少类型注解的一致性
- Overall: 8/107.4 查看 GitLab Commit Status
在 GitLab 项目页面,每次 Commit 旁边会显示 Jenkins 构建状态:
Commit: feat: add fibonacci function ✅ jenkins ✅ ai-review
Commit: fix: division by zero ✅ jenkins ✅ ai-review
Commit: refactor: rename variables ❌ jenkins7.5 验证测试结果
7.5.1 Java 测试报告
JUnit Tests: 4 tests, 0 failures, 0 errors
✓ testAdd (0.003s)
✓ testMultiply (0.001s)
✓ testIsPalindrome (0.002s)
✓ testFibonacci (0.001s)7.5.2 Python 测试报告
Ran 36 tests in 0.042s
OK
✓ TestCalculator.test_add
✓ TestCalculator.test_subtract
✓ TestCalculator.test_multiply
✓ TestCalculator.test_divide
✓ TestCalculator.test_divide_by_zero
✓ TestCalculator.test_power
✓ TestCalculator.test_modulo
✓ TestCalculator.test_modulo_by_zero
✓ TestCalculator.test_history
✓ TestCalculator.test_clear_history
... (共 36 个测试)7.6 构建产物
每次成功构建会生成:
| 产物 | 路径 | 说明 |
|---|---|---|
| JAR 包 | target/demo-app-1.0.N.jar | N = BUILD_NUMBER |
| AI 报告 | ai-review-report.md | AI 代码审查结果 |
| JUnit XML | **/surefire-reports/*.xml | 测试报告 |
第八章:问题排查与踩坑记录
8.1 Agent Pod 启动超时
现象:Pipeline 卡在 "Preparing" 或 "Agent Pod not ready"
原因:镜像拉取慢或网络问题
排查:
bash
# 检查 Pod 状态
kubectl get pods -n jenkins -l jenkins/jenkins-jenkins-agent=true
# 查看 Pod 事件
kubectl describe pod <pod-name> -n jenkins
# 检查镜像拉取
kubectl events -n jenkins --field-selector reason=Pulled解决:
- 确认 Harbor 可访问:
curl -sk https://192.168.122.156:30000/v2/ - 手动拉取镜像到 Worker 节点:
crictl pull 192.168.122.156:30000/jenkins/python:3.12-alpine - 增大
waitForPodSec(默认 600s)
8.2 Alpine 容器缺少工具
现象:
/bin/bash: not found
curl: not found
jq: not found原因:Python Agent 基于 Alpine Linux,是精简镜像
解决:
| 需要的功能 | Alpine 替代方案 |
|---|---|
#!/bin/bash | #!/bin/sh |
curl URL | python3 -c "import urllib.request; ..." |
find . -name "*.py" | python3 glob.glob("**/*.py", recursive=True) |
jq .field | python3 -c "import json; ..." |
which curl | which python3 (Python 总是可用的) |
8.3 Ollama API 超时
现象:AI Review Stage 报 timeout 或 Connection refused
原因:
- Qwen3 的 "思考模式" 消耗大量 Token 导致超时
- 代码太长超出模型上下文窗口
- Ollama 服务未启动或不可达
排查:
bash
# 1. 检查 Ollama 是否运行
curl -s http://192.168.122.9:11434/api/tags
# 2. 从 Agent Pod 内部测试
kubectl exec -n jenkins <pod> -c python -- python3 -c "
import urllib.request
req = urllib.request.Request('http://192.168.122.9:11434/api/tags')
print(urllib.request.urlopen(req, timeout=5).read()[:200])
"解决:
- 必须 在 API payload 中设置
"think": false - 限制
MAX_FILE_SIZE和MAX_FILES - 增加超时时间 (
timeout=180) - 确认 Ollama 监听
0.0.0.0:11434而非127.0.0.1:11434
8.4 GitLab Webhook 连接失败
现象:Git Push 后 Jenkins 没有触发构建
排查:
bash
# 1. 检查 Webhook 配置
curl -sk -H "PRIVATE-TOKEN: xxx" \
"https://ai-ear.cn:8443/api/v4/projects/4/hooks"
# 2. 测试 Webhook (GitLab → Jenkins)
curl -sk -X POST \
-H "PRIVATE-TOKEN: xxx" \
"https://ai-ear.cn:8443/api/v4/projects/4/hooks/2/test/push_events"
# 3. 检查 Jenkins Webhook 日志
# Jenkins Web UI → Manage Jenkins → System Log常见原因:
- Webhook URL 中的认证 Token 过期
- Jenkins RBAC 未配置
webhook-trigger角色给anonymous - K8s Service DNS 解析失败
8.5 Maven 构建失败: BUILD_NUMBER 未设置
现象:
[ERROR] 'version' contains an expression but should be a constant.原因:pom.xml 使用 ${env.BUILD_NUMBER} 但本地构建时未设置
解决:
bash
# 本地构建时手动设置
export BUILD_NUMBER=0
mvn clean package
# 或在 pom.xml 中添加默认值
<version>1.0.${env.BUILD_NUMBER}</version>
<!-- Jenkins 会自动注入 BUILD_NUMBER -->8.6 Jenkins Controller OOM
现象:Jenkins Pod 被 OOMKilled 重启
排查:
bash
# 检查 OOM 事件
kubectl describe pod jenkins-0 -n jenkins | grep -i oom
kubectl top pod jenkins-0 -n jenkins解决:
- 增大 JVM Heap:
-Xms4g -Xmx6g(已在配置中) - 增大 Pod Memory Limit:
10Gi - 减少
containerCap(默认 200)
8.7 NFS 存储权限问题
现象:Agent Pod 无法写入工作目录
排查:
bash
# 检查 NFS 挂载
kubectl exec -n jenkins <pod> -- df -h
# 检查 NFS 权限
ls -la /data/nfs-nvme/jenkins/解决:确保 NFS exports 配置 no_root_squash
bash
# /etc/exports
/data/nfs-nvme 192.168.122.0/24(rw,sync,no_subtree_check,no_root_squash)附录 A:命令速查表
A.1 K8s 集群管理
bash
# 设置 kubeconfig
export KUBECONFIG=~/.kube/config-122.31
# 查看所有节点
kubectl get nodes -o wide
# 查看所有命名空间的 Pod
kubectl get pods -A
# Jenkins 相关
kubectl get pods -n jenkins
kubectl logs jenkins-0 -n jenkins -c jenkins --tail=50
kubectl exec -n jenkins jenkins-0 -c jenkins -- java -version
# GitLab 相关
kubectl get pods -n gitlab
kubectl logs -n gitlab -l app=webservice --tail=20
# SonarQube
kubectl get pods -n sonarqubeA.2 Helm 管理
bash
# 查看已安装的 Release
helm list -A
# 查看 Jenkins values
helm get values jenkins -n jenkins
# 升级 Jenkins
helm upgrade jenkins jenkins/jenkins -n jenkins -f jenkins-values.yaml
# 回滚
helm rollback jenkins 1 -n jenkins
# 查看历史
helm history jenkins -n jenkinsA.3 Jenkins API
bash
JENKINS="https://ai-ear.cn:9010"
\1:xxx"
# 触发构建
curl -s -u "$AUTH" -X POST "$JENKINS/job/dev/job/gitlab-cicd-demo/build"
# 查看构建状态
curl -s -u "$AUTH" "$JENKINS/job/dev/job/gitlab-cicd-demo/lastBuild/api/json?pretty=true"
# 查看控制台输出
curl -s -u "$AUTH" "$JENKINS/job/dev/job/gitlab-cicd-demo/lastBuild/consoleText"
# 查看所有 Job
curl -s -u "$AUTH" "$JENKINS/api/json?tree=jobs[name,color]"
# 查看 Agent 节点
curl -s -u "$AUTH" "$JENKINS/computer/api/json?depth=1"
# 下载构建产物
curl -s -u "$AUTH" -o ai-report.md \
"$JENKINS/job/dev/job/gitlab-cicd-demo/lastBuild/artifact/ai-review-report.md"
# 查看已安装插件
curl -s -u "$AUTH" "$JENKINS/pluginManager/api/json?depth=1" | \
python3 -c "import json,sys; [print(p['shortName']) for p in json.load(sys.stdin)['plugins']]"
# 查看队列
curl -s -u "$AUTH" "$JENKINS/queue/api/json?pretty=true"
# 重启 Jenkins (安全模式)
curl -s -u "$AUTH" -X POST "$JENKINS/safeRestart"A.4 GitLab API
bash
GITLAB="https://ai-ear.cn:8443"
TOKEN="xxx"
# 列出项目
curl -sk -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/api/v4/projects?per_page=20"
# 查看项目详情
curl -sk -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/api/v4/projects/4"
# 查看 Webhooks
curl -sk -H "PRIVATE-TOKEN: $TOKEN" "$GITLAB/api/v4/projects/4/hooks"
# 测试 Webhook
curl -sk -X POST -H "PRIVATE-TOKEN: $TOKEN" \
"$GITLAB/api/v4/projects/4/hooks/2/test/push_events"
# 查看 Commits
curl -sk -H "PRIVATE-TOKEN: $TOKEN" \
"$GITLAB/api/v4/projects/4/repository/commits?per_page=5"
# 查看 Commit Status (Jenkins 回写的状态)
curl -sk -H "PRIVATE-TOKEN: $TOKEN" \
"$GITLAB/api/v4/projects/4/repository/commits/<SHA>/statuses"
# 创建 Access Token
curl -sk -X POST -H "PRIVATE-TOKEN: $TOKEN" \
-H "Content-Type: application/json" \
"$GITLAB/api/v4/users/1/personal_access_tokens" \
-d '{"name":"ci-token","scopes":["api","read_repository"]}'
# 查看分支
curl -sk -H "PRIVATE-TOKEN: $TOKEN" \
"$GITLAB/api/v4/projects/4/repository/branches"A.5 Ollama API
bash
OLLAMA="http://192.168.122.9:11434"
# 健康检查
curl -s "$OLLAMA/"
# 查看已加载模型
curl -s "$OLLAMA/api/tags" | python3 -m json.tool
# 快速 Chat
curl -s "$OLLAMA/api/chat" -d '{
"model": "qwen3.6:27b",
"messages": [{"role": "user", "content": "Hello"}],
"stream": false,
"think": false
}' | python3 -c "import json,sys; print(json.load(sys.stdin)['message']['content'])"
# 列出本地模型
ollama list
# 拉取模型
ollama pull qwen3.6:27b
# 查看模型信息
ollama show qwen3.6:27b --modelfileA.6 日常运维
bash
# 查看 Jenkins 磁盘使用
kubectl exec -n jenkins jenkins-0 -c jenkins -- df -h /var/jenkins_home
# 清理旧构建
curl -s -u "$AUTH" -X POST \
"$JENKINS/job/dev/job/gitlab-cicd-demo/1/doDelete"
# 查看 Agent Pod 资源使用
kubectl top pods -n jenkins
# 查看 Ollama GPU/CPU 使用情况
ssh 192.168.122.9 'nvidia-smi' 2>/dev/null || \
ssh 192.168.122.9 'cat /proc/loadavg'附录 B:Helm Values 完整配置
B.1 jenkins-values.yaml 完整文件
点击展开完整 YAML
yaml
controller:
image:
registry: 192.168.122.156:30000
repository: jenkins/jenkins
tag: lts-jdk21
imagePullPolicy: IfNotPresent
javaOpts: >-
-Xms4g -Xmx6g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
-XX:+ParallelRefProcEnabled -XX:+ExplicitGCInvokesConcurrent
-Djava.awt.headless=true
-Djenkins.model.Jenkins.slaveAgentPort=50000
resources:
requests: { cpu: "2", memory: 6Gi }
limits: { cpu: "4", memory: 10Gi }
admin:
username: xxx
password: xxx
serviceType: NodePort
nodePort: 30880
jenkinsUrl: https://ai-ear.cn:9010/
cloudName: kubernetes
numExecutors: 0
probes:
startupProbe: { failureThreshold: 60, periodSeconds: 10 }
livenessProbe: { failureThreshold: 12, periodSeconds: 30, timeoutSeconds: 15 }
readinessProbe: { failureThreshold: 12, periodSeconds: 30, timeoutSeconds: 15 }
additionalPlugins:
- sonar
- job-dsl
- junit
- timestamper
- gitlab-plugin
- role-strategy
- generic-webhook-trigger
- pipeline-utility-steps
- oic-auth
- matrix-auth
additionalSecrets:
- name: gitlab-token
value: glpat-xxx
- name: gitlab-oauth-client-id
value: xxx
- name: gitlab-oauth-client-secret
value: xxx
sidecars:
configAutoReload:
enabled: true
image:
registry: 192.168.122.156:30000
repository: jenkins/k8s-sidecar
tag: 2.10.1
JCasC:
defaultConfig: true
configScripts:
gitlab-integration: |
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: 10
jobs: |
jobs:
- script: |
folder('dev') { description('开发团队工作区') }
folder('test') { description('测试团队工作区') }
folder('ops') { description('运维团队工作区') }
- script: |
multibranchPipelineJob('dev/gitlab-cicd-demo') {
description('GitLab CI/CD Demo')
branchSources {
git {
id('gitlab-demo')
remote('http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/demo-app-v2.git')
credentialsId('gitlab-git-user')
traits { branchDiscovery() }
}
}
factory { workflowBranchProjectFactory { scriptPath('Jenkinsfile') } }
}
rbac: |
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" }]
shared-libraries: |
unclassified:
globalLibraries:
libraries:
- name: "ai-pipeline-lib"
defaultVersion: "main"
retriever:
legacySCM:
scm:
git:
userRemoteConfigs:
- url: "http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/ai-pipeline-lib.git"
credentialsId: "gitlab-git-user"
sonar: |
credentials:
system:
domainCredentials:
- credentials:
- string:
scope: GLOBAL
id: "sonarqube-token"
secret: "${readFile:/run/secrets/additional/sonar-token}"
unclassified:
sonarGlobalConfiguration:
installations:
- name: "sonarqube"
serverUrl: "http://sonarqube.sonarqube.svc.cluster.local:9000"
credentialsId: "sonarqube-token"
system: |
jenkins:
systemMessage: |-
Jenkins AI-Enhanced CI/CD Platform
RKE2 + Harbor + JCasC + Ollama
quietPeriod: 0
scmCheckoutRetryCount: 3
agent:
enabled: true
containerCap: 200
idleMinutes: 5
image:
registry: 192.168.122.156:30000
repository: jenkins/inbound-agent
tag: latest-jdk21
podTemplates:
maven21: |
- name: maven21
label: maven21
inheritFrom: default
containers:
- name: maven
image: "192.168.122.156:30000/jenkins/maven:3.9-eclipse-temurin-21"
command: "sleep"
args: "99d"
ttyEnabled: true
resourceRequestCpu: "500m"
resourceRequestMemory: "1Gi"
resourceLimitCpu: "2"
resourceLimitMemory: "2Gi"
volumes:
- configMapVolume:
configMapName: jenkins-examples
mountPath: /examples
- configMapVolume:
configMapName: maven-settings
mountPath: /root/.m2
python: |
- name: python
label: python
inheritFrom: default
containers:
- name: python
image: "192.168.122.156:30000/jenkins/python:3.12-alpine"
command: "sleep"
args: "99d"
ttyEnabled: true
resourceRequestCpu: "250m"
resourceRequestMemory: "512Mi"
resourceLimitCpu: "1"
resourceLimitMemory: "1Gi"
envVars:
- envVar:
key: PIP_INDEX_URL
value: "https://mirrors.aliyun.com/pypi/simple/"
persistence:
enabled: true
accessMode: ReadWriteMany
storageClass: nfs-nvme
size: 100Gi附录 C:网络拓扑图
┌──────────────┐
│ 外部用户/ │
│ 开发者 │
└──────┬───────┘
│ HTTPS
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌───▼────┐ ┌────▼─────┐
│ :8443 │ │ :9010 │ │ :9011 │
│ GitLab │ │ Jenkins│ │ SonarQube│
│ (Ingress) │ │(NodePt)│ │ (NodePt) │
└─────┬─────┘ └───┬────┘ └────┬─────┘
│ │ │
┌───────────▼───────────▼───────────▼───────────┐
│ RKE2 Kubernetes Cluster │
│ 192.168.122.23-31 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ GitLab │ │ Jenkins │ │ SonarQube│ │
│ │ Namespace│ │ Namespace│ │ Namespace│ │
│ │ │ │ │ │ │ │
│ │ webservice│ │ controller│ │ sonarqube│ │
│ │ :8181 │ │ :8080 │ │ :9000 │ │
│ │ sidekiq │ │ agent: │ │ postgres │ │
│ │ gitaly │ │ :50000 │ │ │ │
│ │ shell │ │ │ │ │ │
│ │ :30022 │ │ Pod 模板: │ │ │ │
│ └──────────┘ │ maven21 │ └──────────┘ │
│ │ python │ │
│ │ node21 │ ┌──────────┐ │
│ │ golang │ │ NFS PVC │ │
│ │ maven8 │ │ 100Gi │ │
│ └────┬─────┘ │ (NVME) │ │
│ │ └──────────┘ │
└─────────────────────┼──────────────────────────┘
│
HTTP :11434│
│
┌───────────────▼───────────────┐
│ Ollama Server (宿主机) │
│ 192.168.122.9 (sza122031) │
│ │
│ ollama serve │
│ └─ qwen3.6:27b (17GB) │
│ └─ API: http://0.0.0.0:11434 │
└────────────────────────────────┘
│
┌─────────┼─────────┐
│ │
┌─────▼─────┐ ┌──────▼──────┐
│ NFS Server │ │ Harbor │
│ 192.168. │ │ 192.168. │
│ 122.156 │ │ 122.156 │
│ │ │ :30000 │
│ /data/nfs/ │ │ │
│ jenkins │ │ jenkins/ │
│ gitlab │ │ inbound- │
│ sonarqube │ │ agent │
│ mysql │ │ maven │
│ redis │ │ python │
│ elastic │ │ node │
└────────────┘ │ golang │
└─────────────┘附录 D:完整流水线执行日志示例
以下是 Build #12 的成功执行日志摘要:
[2026-08-25 22:15:32] Started by GitLab push by Igor
[2026-08-25 22:15:32] Obtained Jenkinsfile from git
[2026-08-25 22:15:33] [Pipeline] Start of Pipeline
[2026-08-25 22:15:33] [Pipeline] node
[2026-08-25 22:15:34] Running on JNLP agent
── Stage: Checkout ──
[2026-08-25 22:15:35] Checking out branch: main
── Stage: Java Build & Test ──
[2026-08-25 22:15:36] [Pipeline] node (maven21)
[2026-08-25 22:15:40] Agent Pod ready (jnlp + maven)
[2026-08-25 22:15:41] $ mvn clean compile
[2026-08-25 22:15:58] BUILD SUCCESS
[2026-08-25 22:15:59] $ mvn test package
[2026-08-25 22:16:15] Tests run: 4, Failures: 0, Errors: 0
[2026-08-25 22:16:16] BUILD SUCCESS
[2026-08-25 22:16:17] Recording test results (JUnit)
[2026-08-25 22:16:18] Archiving: demo-app-1.0.12.jar
── Stage: Python Lint ──
[2026-08-25 22:16:20] [Pipeline] node (python)
[2026-08-25 22:16:24] Agent Pod ready (jnlp + python)
[2026-08-25 22:16:25] $ pip3 install pylint
[2026-08-25 22:16:30] $ pylint src/main/python/*.py
[2026-08-25 22:16:35] Your code has been rated at 9.45/10
── Stage: Python Test ──
[2026-08-25 22:16:36] [Pipeline] node (python)
[2026-08-25 22:16:40] $ python3 -m unittest discover -v
[2026-08-25 22:16:42] test_add (TestCalculator) ... ok
[2026-08-25 22:16:42] test_subtract (TestCalculator) ... ok
...
[2026-08-25 22:16:43] test_deep_get_top_level (TestDeepGet) ... ok
[2026-08-25 22:16:43] Ran 36 tests in 0.042s
[2026-08-25 22:16:43] OK
── Stage: AI Code Review ──
[2026-08-25 22:16:44] [Pipeline] node (python)
[2026-08-25 22:16:48] ===== AI Code Review =====
[2026-08-25 22:16:48] Model: qwen3.6:27b
[2026-08-25 22:16:48] Host: 192.168.122.9:11434
[2026-08-25 22:16:49] [SCAN] Found 5 files:
[2026-08-25 22:16:49] .py 3847B src/main/python/calculator.py
[2026-08-25 22:16:49] .py 2156B src/main/python/utils.py
[2026-08-25 22:16:49] .java 1234B src/main/java/.../App.java
[2026-08-25 22:16:49] .py 892B src/test/python/test_calculator.py
[2026-08-25 22:16:49] .py 756B src/test/python/test_utils.py
[2026-08-25 22:16:50] [AI] Sending to qwen3.6:27b (8885 chars)...
[2026-08-25 22:18:17] [AI] Done in 87.3s (prompt=2841, eval=1256)
[2026-08-25 22:18:17] ============================================================
[2026-08-25 22:18:17] AI CODE REVIEW REPORT
[2026-08-25 22:18:17] ============================================================
[2026-08-25 22:18:17] [AI review content...]
[2026-08-25 22:18:18] [SAVED] Report: ai-review-report.md
[2026-08-25 22:18:18] ===== AI Review Complete =====
── Post Actions ──
[2026-08-25 22:18:19] ✅ Pipeline SUCCESS
[2026-08-25 22:18:20] updateGitlabCommitStatus: jenkins=success
[2026-08-25 22:18:21] updateGitlabCommitStatus: ai-review=success
[2026-08-25 22:18:22] Build #12 finished
[2026-08-25 22:18:22] Finished: SUCCESS附录 E:从零开始的快速启动清单
如果你已经完成了所有基础设施搭建,只需按以下步骤操作即可运行第一个流水线:
bash
# ===== Step 1: 克隆项目 =====
git clone https://oauth2:glpat-xxx@ai-ear.cn:8443/root/demo-app-v2.git
cd demo-app-v2
# ===== Step 2: 创建项目文件 =====
# (参考第六章的完整代码,创建以下文件)
# - 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
# ===== Step 3: 提交并推送 =====
git add .
git commit -m "feat: complete CI/CD pipeline with Java + Python + AI review"
git push origin main
# ===== Step 4: 等待 Webhook 触发 (约 5 秒) =====
sleep 5
# ===== Step 5: 查看构建状态 =====
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)
print(f'Build #{d[\"number\"]} - {d[\"result\"] or \"IN PROGRESS\"}')"
# ===== Step 6: 等待完成 (约 3-5 分钟) =====
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; print(json.load(sys.stdin).get('result','RUNNING'))")
echo "$(date '+%H:%M:%S') Status: $STATUS"
if [ "$STATUS" != "RUNNING" ] && [ "$STATUS" != "None" ]; then break; fi
sleep 15
done
# ===== Step 7: 下载 AI 审查报告 =====
curl -s -u 'admin:xxx' \
-o ai-review-report.md \
"https://ai-ear.cn:9010/job/dev/job/gitlab-cicd-demo/lastBuild/artifact/ai-review-report.md"
cat ai-review-report.md附录 F:ConfigMap 管理命令
Jenkins 的 JCasC 配置存储在 K8s ConfigMap 中。以下是常用管理命令:
bash
export KUBECONFIG=~/.kube/config-122.31
# 查看所有 Jenkins 相关 ConfigMap
kubectl get configmap -n jenkins -l jenkins-jenkins-config=true
# 查看特定 ConfigMap 内容
kubectl get configmap jenkins-jenkins-config-gitlab-integration -n jenkins -o yaml
kubectl get configmap jenkins-jenkins-config-jobs -n jenkins -o yaml
kubectl get configmap jenkins-jenkins-config-rbac -n jenkins -o yaml
kubectl get configmap jenkins-jenkins-config-shared-libraries -n jenkins -o yaml
kubectl get configmap jenkins-jenkins-config-system -n jenkins -o yaml
kubectl get configmap jenkins-jenkins-config-sonar -n jenkins -o yaml
# 编辑 ConfigMap (会自动触发 Sidecar 重载)
kubectl edit configmap jenkins-jenkins-config-jobs -n jenkins
# 手动触发 ConfigMap 重载
kubectl exec -n jenkins jenkins-0 -c config-reload -- \
curl -s http://localhost:9533/reload
# 查看 ConfigMap 变更历史
kubectl get configmap jenkins-jenkins-config-jobs -n jenkins -o yaml | \
grep resourceVersion自动重载机制:Jenkins Helm Chart 内置了
k8s-sidecar容器(监听 ConfigMap 变更),当 ConfigMap 被修改时,Sidecar 会自动调用 Jenkins Reload 接口,无需重启 Pod。
附录 G:Agent 镜像自定义
如果需要自定义 Agent 镜像(例如添加额外工具),创建 Dockerfile:
G.1 自定义 Python Agent(添加 curl 和 bash)
dockerfile
# Dockerfile.python-agent
FROM python:3.12-alpine
# 安装常用工具
RUN apk add --no-cache \
bash \
curl \
git \
jq \
&& pip3 install --no-cache-dir \
pylint \
pytest \
requests
# 设置 pip 镜像 (加速)
ENV PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
WORKDIR /home/jenkins/agentbash
# 构建 & 推送
HARBOR=192.168.122.156:30000
docker build -t $HARBOR/jenkins/python:3.12-custom -f Dockerfile.python-agent .
docker push $HARBOR/jenkins/python:3.12-customG.2 自定义 Maven Agent(添加 Node.js)
dockerfile
# Dockerfile.maven-node
FROM maven:3.9-eclipse-temurin-21
# 安装 Node.js
RUN curl -fsSL https://deb.nodesource.com/setup_21.x | bash - \
&& apt-get install -y nodejs \
&& npm config set registry https://registry.npmmirror.com
WORKDIR /home/jenkins/agent附录 H:常见问题 FAQ
Q1: 如何在本地测试 Pipeline?
bash
# 使用 Jenkins Pipeline Linter 验证语法
curl -s -X POST -H "Content-Type: application/x-yaml" \
--data-binary @Jenkinsfile \
"https://ai-ear.cn:9010/pipeline-model-converter/validate" \
-u 'admin:xxx'Q2: 如何查看 Agent Pod 的实时日志?
bash
# 找到当前运行的 Agent Pod
kubectl get pods -n jenkins -l jenkins/jenkins-jenkins-agent=true
# 查看日志
kubectl logs -n jenkins <pod-name> -c maven -f
kubectl logs -n jenkins <pod-name> -c python -fQ3: Pipeline Stage 之间如何传递数据?
groovy
// 方法1: 使用 stash/unstash (小文件)
stage('Build') {
steps {
sh 'mvn package'
stash name: 'jar', includes: 'target/*.jar'
}
}
stage('Deploy') {
steps {
unstash 'jar'
sh 'ls target/*.jar'
}
}
// 方法2: 使用环境变量
environment {
JAR_VERSION = ""
}
stage('Build') {
steps {
script {
env.JAR_VERSION = sh(script: 'mvn help:evaluate -Dexpression=project.version -q -DforceStdout', returnStdout: true)
}
}
}Q4: 如何并行执行多个 Stage?
groovy
stage('Parallel Tests') {
parallel {
stage('Java Tests') {
agent { label 'maven21' }
steps { container('maven') { sh 'mvn test' } }
}
stage('Python Tests') {
agent { label 'python' }
steps { container('python') { sh 'python3 -m unittest discover' } }
}
stage('Node Tests') {
agent { label 'node21' }
steps { container('node') { sh 'npm test' } }
}
}
}Q5: AI 审查总是超时怎么办?
bash
# 1. 减小代码量
export MAX_FILES=5
export MAX_FILE_SIZE=2000
# 2. 增加超时时间
# 在 ai-code-review.sh 中修改:
# urllib.request.urlopen(req, timeout=300) # 5 分钟
# 3. 使用更快的模型
export OLLAMA_MODEL=qwen2.5-coder:7b # 7B 模型更快但质量略低
# 4. 检查 Ollama 资源
ssh 192.168.122.9 'cat /proc/loadavg; free -h'Q6: 如何添加新的 Agent 类型?
yaml
# 在 jenkins-values.yaml 的 agent.podTemplates 中添加:
rust: |
- name: rust
label: rust
inheritFrom: default
containers:
- name: rust
image: "192.168.122.156:30000/jenkins/rust:1.75-alpine"
command: "sleep"
args: "99d"
ttyEnabled: true
# 然后升级 Helm
helm upgrade jenkins jenkins/jenkins -n jenkins -f jenkins-values.yaml📝 文档维护说明:本教程基于实际生产环境 (2026-08-25) 编写,所有命令和配置均已验证。如有更新请修改版本号并通知团队。