Skip to content

GitLab + Jenkins + AI 自动化 CI/CD 保姆级教程

版本: 1.0 | 日期: 2026-08-25 | 环境: RKE2 + Helm + JCasC + Ollama


📋 目录


第一章:环境总览

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 网络地址表

服务内网地址外网入口说明
Jenkinshttp://jenkins.jenkins.svc.cluster.local:8080https://ai-ear.cn:9010 (NodePort 30880)CI/CD 引擎
GitLabhttp://gitlab-webservice-default.gitlab.svc.cluster.local:8181https://ai-ear.cn:8443 (Ingress)代码仓库
SonarQubehttp://sonarqube.sonarqube.svc.cluster.local:9000https://ai-ear.cn:9011 (NodePort 30900)代码质量
Ollamahttp://192.168.122.9:11434仅内网AI 推理引擎
Harborhttp://192.168.122.156:30000仅内网镜像仓库
NFS192.168.122.156-共享存储

1.3 凭证清单

凭证用途值/位置
Jenkins AdminWeb 登录\1xxx\2
GitLab PATAPI 访问glpat-xxx
Jenkins→GitLab TokenWebhookglpat-xxx
GitLab OAuth Client IDSSO 登录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 nodes

2.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.service

2.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.service

2.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+rke2r1

2.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-server

2.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=false

2.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 update

3.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: 20Gi

3.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 -w

3.1.4 验证安装

bash
# 获取初始 root 密码
kubectl get secret gitlab-gitlab-initial-root-password -n gitlab \
    -o jsonpath='{.data.password}' | base64 -d

# 访问
# https://ai-ear.cn:8443

3.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-xxx

3.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 update

4.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-credentials

4.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 -w

4.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,缺少 bashcurl。所有 shell 脚本必须使用 sh,HTTP 请求必须用 Python urllib 实现。

4.3.3 其他 Agent 模板

Label镜像用途
maven8maven:3.9-eclipse-temurin-8Java 8 项目
maven21maven:3.9-eclipse-temurin-21Java 21 项目
node21node:21-alpineNode.js 项目
golanggolang:1.24-alpineGo 项目
pythonpython:3.12-alpinePython 项目

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-developerdev/*构建/取消/读取/工作区/配置/创建
test-developertest/*同上
ops-developerops/*同上

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 ollama

5.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-settings ConfigMap 到 /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 current

6.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.md

6.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/bashAlpine 容器没有 bash
使用 Python urllib 而非 curlAlpine 容器没有 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 History

6.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 秒后查看 Jenkins

7.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 → #N

Pipeline 阶段视图

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      ✅ SUCCESS

7.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 Logs

7.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/10

7.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       ❌ jenkins

7.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.jarN = BUILD_NUMBER
AI 报告ai-review-report.mdAI 代码审查结果
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 URLpython3 -c "import urllib.request; ..."
find . -name "*.py"python3 glob.glob("**/*.py", recursive=True)
jq .fieldpython3 -c "import json; ..."
which curlwhich python3 (Python 总是可用的)

8.3 Ollama API 超时

现象:AI Review Stage 报 timeoutConnection refused

原因

  1. Qwen3 的 "思考模式" 消耗大量 Token 导致超时
  2. 代码太长超出模型上下文窗口
  3. 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_SIZEMAX_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 sonarqube

A.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 jenkins

A.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 --modelfile

A.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/agent
bash
# 构建 & 推送
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-custom

G.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 -f

Q3: 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) 编写,所有命令和配置均已验证。如有更新请修改版本号并通知团队。