Jenkins增量代码覆盖率新手实战指南
本文介绍了一种基于Jenkins和Jacoco的增量代码覆盖率测试方案。通过Git差异分析获取增量Java文件,结合Shell脚本实现class文件筛选和报告生成。方案包含以下关键步骤: 使用Git命令获取两次提交间的差异Java文件清单 根据差异文件生成对应的class文件 通过Jacoco生成增量代码覆盖率报告 提供无增量时的友好提示界面 该方案解决了传统全量覆盖率报告的效率问题,通过路径转换
·
1.Jenkins结合Jacoco插件实现覆盖率展示
可以参考另一篇文章:Jenkins集成Jacoco实现覆盖率测试报告-CSDN博客
2.针对增量代码获取覆盖率
查询了很多资料,可以下载Jenkins的新版报告插件“coverage”,内置的好像有diff-report
下面是针对Jenkins版本不够,自定义脚本实现的方案
2.1 原任务(任务创建完整步骤参考上面文章),新增"Execute shell"生成added-files.txt
基于git仓库,获取当前提交与上一次提交的差异文件

#!/bin/bash
# 脚本功能:仅基于本地.git仓库,获取当前提交与上一次提交的Java文件差异
# 输出:added-files.txt(增量Java文件清单) + 打印差异信息
# ==================== 第一步:环境初始化 & 容错 ====================
# 强制UTF-8编码(解决中文文件名/日志乱码)
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# 进入Jenkins工作空间(脚本执行目录)
WORKSPACE=$(pwd)
cd "${WORKSPACE}" || {
echo "ERROR: 无法进入工作空间 ${WORKSPACE}"
exit 1
}
# 检查本地.git目录是否存在
if [ ! -d ".git" ]; then
echo "ERROR: 本地.git目录不存在!"
exit 1
fi
# ==================== 第二步:获取本地提交信息(无分支/远程依赖) ====================
# 获取当前提交的完整Commit ID(本地HEAD)
CURRENT_COMMIT=$(git rev-parse HEAD 2>/dev/null)
if [ -z "${CURRENT_COMMIT}" ]; then
echo "ERROR: 无法获取本地HEAD的Commit ID!"
exit 1
fi
SHORT_CURRENT=${CURRENT_COMMIT:0:8}
# 获取上一次提交的Commit ID(本地HEAD^)
PREV_COMMIT=$(git rev-parse HEAD^ 2>/dev/null)
if [ -z "${PREV_COMMIT}" ]; then
echo "WARNING: 无上次提交记录(当前是首次提交),生成空差异清单"
touch "${WORKSPACE}/added-files.txt"
exit 0
fi
SHORT_PREV=${PREV_COMMIT:0:8}
# ==================== 第三步:获取两次提交的Java文件差异 ====================
# 仅筛选Java文件的差异(新增/修改),输出到added-files.txt
git diff --name-only --diff-filter=AM "${PREV_COMMIT}" "${CURRENT_COMMIT}" -- '*.java' > "${WORKSPACE}/added-files.txt" 2>/dev/null
# 统计差异文件数
DIFF_COUNT=$(wc -l < "${WORKSPACE}/added-files.txt")
# 去除空行(避免统计异常)
sed -i '/^$/d' "${WORKSPACE}/added-files.txt"
REAL_COUNT=$(wc -l < "${WORKSPACE}/added-files.txt")
# ==================== 第四步:输出日志(供Jenkins调用) ====================
echo "✅ 本地Git差异获取完成:"
echo " 当前提交:${SHORT_CURRENT}(完整:${CURRENT_COMMIT})"
echo " 上次提交:${SHORT_PREV}(完整:${PREV_COMMIT})"
echo " 增量Java文件数:${REAL_COUNT}"
if [ ${REAL_COUNT} -gt 0 ]; then
echo " 增量文件清单:"
cat "${WORKSPACE}/added-files.txt" | while read FILE; do
[ -n "${FILE}" ] && echo " - ${FILE}"
done
fi
exit 0
可以执行测试,成功打印差异文件,工作空间成功生成即可

基于git仓库,获取当前最新提交与dev最新提交的差异文件
#!/bin/bash
# 脚本功能:
# 1. 自动识别Jenkins当前构建的分支(适配Branches to build配置的任意分支)
# 2. 对比远程origin/dev与当前构建分支的Java文件差异
# 3. 兼容detached HEAD状态,输出added-files.txt(增量/全量Java文件清单)
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# ==================== 基础配置 & 容错 ====================
WORKSPACE=$(pwd)
cd "${WORKSPACE}" || {
echo "ERROR: 无法进入工作空间 ${WORKSPACE}"
exit 1
}
if [ ! -d ".git" ]; then
echo "ERROR: 本地.git目录不存在!"
exit 1
fi
# 远程仓库名称(固定为origin,匹配Jenkins拉取的远程)
REMOTE_NAME="origin"
# 基准对比分支(固定为远程dev)
BASE_BRANCH="${REMOTE_NAME}/dev"
# ==================== 核心:自动获取Jenkins当前构建分支 ====================
# 方案1:优先读取Jenkins内置环境变量(最准确)
TARGET_BRANCH=""
if [ -n "${GIT_BRANCH}" ]; then
# Jenkins Git插件自动设置的环境变量,格式如:origin/ticket/13
TARGET_BRANCH="${GIT_BRANCH}"
echo "INFO: 从Jenkins环境变量获取当前构建分支:${TARGET_BRANCH}"
else
# 方案2:环境变量不存在时,从Git信息中推导(兼容detached HEAD)
echo "INFO: 未检测到Jenkins环境变量,从Git推导当前分支..."
# 获取当前checkout的引用(如refs/remotes/origin/ticket/13)
CURRENT_REF=$(git symbolic-ref -q HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ "${CURRENT_REF}" = "HEAD" ] || [ -z "${CURRENT_REF}" ]; then
# detached HEAD状态:从reflog找最新的远程分支引用
CURRENT_REF=$(git reflog show -1 | awk '{print $3}' | sed 's/^origin\///' 2>/dev/null)
if [ -n "${CURRENT_REF}" ]; then
TARGET_BRANCH="${REMOTE_NAME}/${CURRENT_REF}"
else
# 终极兜底:用HEAD代表当前代码
TARGET_BRANCH="HEAD"
fi
else
# 正常分支状态:拼接远程分支名
TARGET_BRANCH="${REMOTE_NAME}/${CURRENT_REF}"
fi
fi
# 最终校验:确保目标分支格式正确(origin/xxx)
if [[ ! "${TARGET_BRANCH}" =~ ^${REMOTE_NAME}/ ]]; then
TARGET_BRANCH="${REMOTE_NAME}/${TARGET_BRANCH}"
fi
echo "INFO: 最终确定对比的目标分支:${TARGET_BRANCH}"
# ==================== 拉取远程最新引用(复用Jenkins已拉取的缓存) ====================
# 注释冗余的git fetch(Jenkins已提前拉取,避免权限问题)
# echo "INFO: 拉取远程仓库(${REMOTE_NAME})最新分支信息..."
# git fetch --quiet --prune "${REMOTE_NAME}" 2>/dev/null || {
# echo "WARNING: 拉取远程引用失败,使用本地缓存(仍尝试对比)"
# }
# ==================== 验证远程分支是否存在 ====================
# 检查远程dev分支是否存在
if ! git show-ref --verify --quiet "refs/remotes/${BASE_BRANCH}" 2>/dev/null; then
echo "ERROR: 远程仓库无${BASE_BRANCH}分支!"
touch "${WORKSPACE}/added-files.txt"
exit 0
fi
# 检查目标分支是否存在(兼容HEAD)
if [ "${TARGET_BRANCH}" != "HEAD" ] && ! git show-ref --verify --quiet "refs/remotes/${TARGET_BRANCH}" 2>/dev/null; then
echo "WARNING: 远程仓库无${TARGET_BRANCH}分支,将使用当前HEAD对比"
TARGET_BRANCH="HEAD"
fi
# ==================== 获取两个分支的最新Commit ====================
# 远程dev分支最新Commit
DEV_COMMIT=$(git rev-parse "${BASE_BRANCH}" 2>/dev/null)
SHORT_DEV=${DEV_COMMIT:0:8}
# 当前目标分支最新Commit
TARGET_COMMIT=$(git rev-parse "${TARGET_BRANCH}" 2>/dev/null)
SHORT_TARGET=${TARGET_COMMIT:0:8}
if [ -z "${DEV_COMMIT}" ] || [ -z "${TARGET_COMMIT}" ]; then
echo "WARNING: 无法获取分支Commit ID,输出全量Java文件"
GET_FULL_LIST=1
else
GET_FULL_LIST=0
echo "INFO: 对比基准 - ${BASE_BRANCH} (${SHORT_DEV}) vs ${TARGET_BRANCH} (${SHORT_TARGET})"
fi
# ==================== 生成差异文件(核心) ====================
if [ ${GET_FULL_LIST} -eq 0 ]; then
# 对比远程dev和当前构建分支的Java文件差异(新增/修改)
git diff --name-only --diff-filter=AM "${DEV_COMMIT}" "${TARGET_COMMIT}" -- '*.java' > "${WORKSPACE}/added-files.txt" 2>/dev/null
else
# 容错:对比失败时输出全量Java文件
find "${WORKSPACE}" -path "${WORKSPACE}/.git" -prune -o -name "*.java" -type f | sed "s|^${WORKSPACE}/||" > "${WORKSPACE}/added-files.txt" 2>/dev/null
fi
# ==================== 清理 & 统计 & 日志 ====================
sed -i '/^$/d' "${WORKSPACE}/added-files.txt"
REAL_COUNT=$(wc -l < "${WORKSPACE}/added-files.txt")
echo -e "\n✅ 差异文件生成完成:"
echo " 对比分支:${BASE_BRANCH} ↔ ${TARGET_BRANCH}"
echo " 增量Java文件数:${REAL_COUNT}"
if [ ${REAL_COUNT} -gt 0 ] && [ ${REAL_COUNT} -le 50 ]; then
echo " 增量文件清单:"
cat "${WORKSPACE}/added-files.txt" | while read FILE; do
[ -n "${FILE}" ] && echo " - ${FILE}"
done
elif [ ${REAL_COUNT} -gt 50 ]; then
echo " 增量文件清单:共${REAL_COUNT}个文件(数量过多,不展示)"
else
echo " 提示:${TARGET_BRANCH}与${BASE_BRANCH}无新增/修改的Java文件"
fi
exit 0
执行成功的打印

2.2 根据差异的文件的class文件生成差异报告
添加获取差异class文件的命令
# ---------------------- 3. Jenkins 端:生成报告(核心修复) ----------------------
log "INFO" "开始 Jenkins 端生成报告..."
JENKINS_SCRIPT=$(cat <<'EOF'
# 基础环境配置
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
unset PYENV_ROOT
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 核心路径(关键修复:移除末尾/)
WORKSPACE="/var/lib/jenkins/workspace/gf-busniess-test-report"
JACOCO_CLI="/opt/jacoco/lib/jacococli.jar"
DIFF_FILE="${WORKSPACE}/added-files.txt"
COV_FILE="${WORKSPACE}/coverage.exec"
REPORT_DIR="${WORKSPACE}/diff-report"
TEMP_FILTER_DIR="${WORKSPACE}/temp-incremental-classes"
# 日志函数
jenkins_log() {
local LEVEL=$1
local MESSAGE=$2
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [Jenkins] [${LEVEL}] ${MESSAGE}"
}
# ===================== 前置校验 =====================
jenkins_log "INFO" "启动增量覆盖率报告生成流程"
if [ ! -d "${WORKSPACE}" ]; then
jenkins_log "ERROR" "工作空间不存在:${WORKSPACE}"
exit 1
fi
cd "${WORKSPACE}" || { jenkins_log "ERROR" "无法进入工作空间"; exit 1; }
# ===================== 校验diff文件 =====================
FILE_COUNT=0
if [ -f "${DIFF_FILE}" ]; then
sed -i '/^$/d; /^#/d' "${DIFF_FILE}" 2>/dev/null
FILE_COUNT=$(wc -l < "${DIFF_FILE}")
jenkins_log "INFO" "加载增量文件完成,有效文件数:${FILE_COUNT}"
else
jenkins_log "WARN" "增量文件不存在,创建空文件"
touch "${DIFF_FILE}"
fi
# ===================== 清理旧文件 =====================
rm -rf "${REPORT_DIR}" "${TEMP_FILTER_DIR}"
mkdir -p "${REPORT_DIR}" || { jenkins_log "ERROR" "创建报告目录失败"; exit 1; }
# ===================== 生成报告 =====================
if [ ${FILE_COUNT} -eq 0 ]; then
# 无增量:友好报告
jenkins_log "INFO" "无增量文件,生成友好提示报告"
cat > "${REPORT_DIR}/index.html" <<HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>增量代码覆盖率报告</title>
<style>
body {font-family: Arial;margin:40px auto;text-align:center;max-width:800px;}
.box {padding:20px;border:1px solid #eee;border-radius:8px;}
h1 {color:#666;}
.time {color:#999;font-size:14px;margin-top:15px;}
</style>
</head>
<body>
<div class="box">
<h1>当前构建无增量 Java 代码</h1>
<p>本次无需要统计覆盖率的变更文件</p>
<p class="time">生成时间:$(date +'%Y-%m-%d %H:%M:%S')</p>
</div>
</body>
</html>
HTML
jenkins_log "INFO" "✅ 无增量报告生成完成"
else
# 有增量:过滤class + 生成报告(核心修复路径转换)
jenkins_log "INFO" "开始过滤增量class文件"
mkdir -p "${TEMP_FILTER_DIR}" || { jenkins_log "ERROR" "创建临时目录失败"; exit 1; }
# 遍历拷贝class(关键修复:路径转换逻辑)
COPY_COUNT=0
while IFS= read -r JAVA_FILE; do
[ -z "${JAVA_FILE}" ] && continue
# 核心修复:分步转换路径,避免拼接错误
# 步骤1:去掉src/main/java/前缀
RELATIVE_JAVA=${JAVA_FILE#src/main/java/}
# 步骤2:替换后缀为.class
RELATIVE_CLASS=${RELATIVE_JAVA/.java/.class}
# 步骤3:拼接完整路径(无//)
FULL_CLASS="${WORKSPACE}/target/classes/${RELATIVE_CLASS}"
if [ -f "${FULL_CLASS}" ]; then
# 创建目标目录+拷贝(保留属性)
TARGET_DIR="${TEMP_FILTER_DIR}/$(dirname "${RELATIVE_CLASS}")"
mkdir -p "${TARGET_DIR}" && cp -p "${FULL_CLASS}" "${TARGET_DIR}/"
COPY_COUNT=$((COPY_COUNT + 1))
jenkins_log "INFO" "✅ 拷贝class:${RELATIVE_CLASS}"
else
jenkins_log "WARN" "❌ class不存在:${FULL_CLASS}"
# 兜底:打印Jenkins端实际的class文件路径,便于排查
jenkins_log "INFO" " Jenkins端target/classes目录文件:$(ls ${WORKSPACE}/target/classes/com/cffex/business/service/ 2>/dev/null | head -5)"
fi
done < "${DIFF_FILE}"
# 校验拷贝结果
if [ ${COPY_COUNT} -eq 0 ]; then
jenkins_log "ERROR" "❌ 无有效class文件,生成报告失败"
# 兜底:打印Jenkins端class目录结构
jenkins_log "INFO" "Jenkins端target/classes完整目录:$(ls -R ${WORKSPACE}/target/classes/ 2>/dev/null | head -20)"
exit 1
fi
# 核心:官方标准report命令(无--diff)
jenkins_log "INFO" "执行Jacoco report命令(官方0.8.14)"
java -jar "${JACOCO_CLI}" report \
"${COV_FILE}" \
--classfiles "${TEMP_FILTER_DIR}" \
--sourcefiles "${WORKSPACE}/src/main/java" \
--html "${REPORT_DIR}" \
--quiet 2>/tmp/jacoco_error.log
# 结果校验
if [ $? -eq 0 ] && [ -f "${REPORT_DIR}/index.html" ]; then
jenkins_log "INFO" "✅ 增量覆盖率报告生成成功"
jenkins_log "INFO" "📄 报告路径:${REPORT_DIR}/index.html"
else
jenkins_log "ERROR" "❌ 报告生成失败,错误日志:$(cat /tmp/jacoco_error.log 2>/dev/null)"
exit 1
fi
fi
# ===================== 权限修复 =====================
chown -R jenkins:jenkins "${REPORT_DIR}" "${DIFF_FILE}" "${COV_FILE}" "${WORKSPACE}/target"
chmod -R 755 "${REPORT_DIR}"
rm -rf "${TEMP_FILTER_DIR}" # 清理临时文件
jenkins_log "INFO" "🎉 Jenkins端全流程执行完成"
exit 0
EOF
)
执行成功会生成diff-report文件
完整的dump脚本
#!/bin/bash
# ===================== 自定义配置区(仅需维护这里) ======================
# JaCoCo基础配置(应用服务器)
JACOCO_CLI_JAR="/opt/jacoco/lib/jacococli.jar"
COV_FILE="/root/app-service/gf-business/coverage.exec"
JACOCO_PORT=6300
JAVA_HOME="/usr/local/jdk-8"
APP_NAME="gf-business.jar"
# Jenkins配置
JENKINS_SERVER_IP="你的ip"
JENKINS_USER="root"
JENKINS_WORKSPACE="/var/lib/jenkins/workspace/gf-busniess-test-report"
JENKINS_JACOCO_CLI_JAR="/opt/jacoco/lib/jacococli.jar"
# 应用Jar包配置
APP_JAR_PATH="/root/app-service/gf-business/gf-business.jar"
TEMP_CLASS_DIR="/tmp/gf-business-classes"
# ==============================================================================
# ---------------------- 工具函数 ----------------------
log() {
local LEVEL=$1
local MESSAGE=$2
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$LEVEL] $MESSAGE"
}
error_exit() {
log "ERROR" "$1"
[ -d "$TEMP_CLASS_DIR" ] && rm -rf "$TEMP_CLASS_DIR"
[ -f "$COV_FILE" ] && rm -f "$COV_FILE"
exit 1
}
print_sep() {
log "INFO" "======================================================"
}
# ---------------------- 初始化 ----------------------
print_sep
log "INFO" "开始执行 JaCoCo 覆盖率采集 + 增量报告流程"
print_sep
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export PATH=$JAVA_HOME/bin:$PATH
# ---------------------- 前置检查 ----------------------
log "INFO" "前置检查..."
! command -v java &>/dev/null && error_exit "Java 未找到"
[ ! -f "$JACOCO_CLI_JAR" ] && error_exit "JaCoCo CLI 不存在"
[ ! -f "$APP_JAR_PATH" ] && error_exit "应用jar包不存在"
! command -v scp &>/dev/null || ! command -v ssh &>/dev/null && error_exit "请安装 openssh-clients"
APP_PID=$(ps -ef | grep -E "[j]ava.*$APP_NAME" | awk '{print $2}')
[ -z "$APP_PID" ] && error_exit "未找到应用进程 $APP_NAME"
log "INFO" "✅ 前置检查通过"
print_sep
# ---------------------- 1. 生成 coverage.exec ----------------------
log "INFO" "执行 JaCoCo dump..."
[ -f "$COV_FILE" ] && rm -f "$COV_FILE"
java -jar "$JACOCO_CLI_JAR" dump \
--address localhost \
--port "$JACOCO_PORT" \
--destfile "$COV_FILE" \
--quiet || error_exit "dump 失败"
[ ! -s "$COV_FILE" ] && error_exit "coverage.exec 为空"
log "INFO" "✅ coverage.exec 生成成功"
print_sep
# ---------------------- 2. 推送到 Jenkins ----------------------
log "INFO" "推送 coverage.exec 到 Jenkins..."
scp -q -o StrictHostKeyChecking=no -o ConnectTimeout=10 \
"$COV_FILE" "$JENKINS_USER@$JENKINS_SERVER_IP:$JENKINS_WORKSPACE/" \
|| error_exit "推送 exec 失败"
log "INFO" "解压 jar 并推送 class..."
rm -rf "$TEMP_CLASS_DIR" && mkdir -p "$TEMP_CLASS_DIR"
# 关键修复:解压时保留完整目录结构,且只解压class文件
unzip -q -o "$APP_JAR_PATH" "BOOT-INF/classes/**/*.class" -d "$TEMP_CLASS_DIR" || error_exit "解压失败"
CLASS_DIR="$TEMP_CLASS_DIR/BOOT-INF/classes"
[ ! -d "$CLASS_DIR" ] && error_exit "无 BOOT-INF/classes 目录"
# 关键修复:推送class文件到Jenkins的target/classes(和路径转换逻辑匹配)
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \
"$JENKINS_USER@$JENKINS_SERVER_IP" \
"mkdir -p '$JENKINS_WORKSPACE/target/classes' && chown -R jenkins:jenkins '$JENKINS_WORKSPACE/target'"
# 关键修复:递归推送所有class文件(保留目录结构)
scp -r -q -o StrictHostKeyChecking=no "$CLASS_DIR/"* \
"$JENKINS_USER@$JENKINS_SERVER_IP:$JENKINS_WORKSPACE/target/classes/" \
|| error_exit "推送 class 失败"
log "INFO" "✅ 文件推送完成(exec + class)"
print_sep
# ---------------------- 3. Jenkins 端:生成报告(核心修复) ----------------------
log "INFO" "开始 Jenkins 端生成报告..."
JENKINS_SCRIPT=$(cat <<'EOF'
# 基础环境配置
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
unset PYENV_ROOT
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WORKSPACE="/var/lib/jenkins/workspace/gf-busniess-test-report"
JACOCO_CLI="/opt/jacoco/lib/jacococli.jar"
DIFF_FILE="${WORKSPACE}/added-files.txt"
COV_FILE="${WORKSPACE}/coverage.exec"
REPORT_DIR="${WORKSPACE}/diff-report"
TEMP_FILTER_DIR="${WORKSPACE}/temp-incremental-classes"
# 日志函数
jenkins_log() {
local LEVEL=$1
local MESSAGE=$2
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [Jenkins] [${LEVEL}] ${MESSAGE}"
}
# ===================== 前置校验 =====================
jenkins_log "INFO" "启动增量覆盖率报告生成流程"
if [ ! -d "${WORKSPACE}" ]; then
jenkins_log "ERROR" "工作空间不存在:${WORKSPACE}"
exit 1
fi
cd "${WORKSPACE}" || { jenkins_log "ERROR" "无法进入工作空间"; exit 1; }
# ===================== 校验diff文件 =====================
FILE_COUNT=0
if [ -f "${DIFF_FILE}" ]; then
sed -i '/^$/d; /^#/d' "${DIFF_FILE}" 2>/dev/null
FILE_COUNT=$(wc -l < "${DIFF_FILE}")
jenkins_log "INFO" "加载增量文件完成,有效文件数:${FILE_COUNT}"
else
jenkins_log "WARN" "增量文件不存在,创建空文件"
touch "${DIFF_FILE}"
fi
# ===================== 清理旧文件 =====================
rm -rf "${REPORT_DIR}" "${TEMP_FILTER_DIR}"
mkdir -p "${REPORT_DIR}" || { jenkins_log "ERROR" "创建报告目录失败"; exit 1; }
# ===================== 生成报告 =====================
if [ ${FILE_COUNT} -eq 0 ]; then
# 无增量:友好报告
jenkins_log "INFO" "无增量文件,生成友好提示报告"
cat > "${REPORT_DIR}/index.html" <<HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>增量代码覆盖率报告</title>
<style>
body {font-family: Arial;margin:40px auto;text-align:center;max-width:800px;}
.box {padding:20px;border:1px solid #eee;border-radius:8px;}
h1 {color:#666;}
.time {color:#999;font-size:14px;margin-top:15px;}
</style>
</head>
<body>
<div class="box">
<h1>当前构建无增量 Java 代码</h1>
<p>本次无需要统计覆盖率的变更文件</p>
<p class="time">生成时间:$(date +'%Y-%m-%d %H:%M:%S')</p>
</div>
</body>
</html>
HTML
jenkins_log "INFO" "✅ 无增量报告生成完成"
else
# 有增量:过滤class + 生成报告(核心修复路径转换)
jenkins_log "INFO" "开始过滤增量class文件"
mkdir -p "${TEMP_FILTER_DIR}" || { jenkins_log "ERROR" "创建临时目录失败"; exit 1; }
# 遍历拷贝class(关键修复:路径转换逻辑)
COPY_COUNT=0
while IFS= read -r JAVA_FILE; do
[ -z "${JAVA_FILE}" ] && continue
# 核心修复:分步转换路径,避免拼接错误
# 步骤1:去掉src/main/java/前缀
RELATIVE_JAVA=${JAVA_FILE#src/main/java/}
# 步骤2:替换后缀为.class
RELATIVE_CLASS=${RELATIVE_JAVA/.java/.class}
# 步骤3:拼接完整路径(无//)
FULL_CLASS="${WORKSPACE}/target/classes/${RELATIVE_CLASS}"
if [ -f "${FULL_CLASS}" ]; then
# 创建目标目录+拷贝(保留属性)
TARGET_DIR="${TEMP_FILTER_DIR}/$(dirname "${RELATIVE_CLASS}")"
mkdir -p "${TARGET_DIR}" && cp -p "${FULL_CLASS}" "${TARGET_DIR}/"
COPY_COUNT=$((COPY_COUNT + 1))
jenkins_log "INFO" "✅ 拷贝class:${RELATIVE_CLASS}"
else
jenkins_log "WARN" "❌ class不存在:${FULL_CLASS}"
# 兜底:打印Jenkins端实际的class文件路径,便于排查
jenkins_log "INFO" " Jenkins端target/classes目录文件:$(ls ${WORKSPACE}/target/classes/com/cffex/business/service/ 2>/dev/null | head -5)"
fi
done < "${DIFF_FILE}"
# 校验拷贝结果
if [ ${COPY_COUNT} -eq 0 ]; then
jenkins_log "ERROR" "❌ 无有效class文件,生成报告失败"
# 兜底:打印Jenkins端class目录结构
jenkins_log "INFO" "Jenkins端target/classes完整目录:$(ls -R ${WORKSPACE}/target/classes/ 2>/dev/null | head -20)"
exit 1
fi
# 核心:官方标准report命令(无--diff)
jenkins_log "INFO" "执行Jacoco report命令(官方0.8.14)"
java -jar "${JACOCO_CLI}" report \
"${COV_FILE}" \
--classfiles "${TEMP_FILTER_DIR}" \
--sourcefiles "${WORKSPACE}/src/main/java" \
--html "${REPORT_DIR}" \
--quiet 2>/tmp/jacoco_error.log
# 结果校验
if [ $? -eq 0 ] && [ -f "${REPORT_DIR}/index.html" ]; then
jenkins_log "INFO" "✅ 增量覆盖率报告生成成功"
jenkins_log "INFO" "📄 报告路径:${REPORT_DIR}/index.html"
else
jenkins_log "ERROR" "❌ 报告生成失败,错误日志:$(cat /tmp/jacoco_error.log 2>/dev/null)"
exit 1
fi
fi
# ===================== 权限修复 =====================
chown -R jenkins:jenkins "${REPORT_DIR}" "${DIFF_FILE}" "${COV_FILE}" "${WORKSPACE}/target"
chmod -R 755 "${REPORT_DIR}"
rm -rf "${TEMP_FILTER_DIR}" # 清理临时文件
jenkins_log "INFO" "🎉 Jenkins端全流程执行完成"
exit 0
EOF
)
# 执行远程脚本
ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o ConnectTimeout=20 \
"$JENKINS_USER@$JENKINS_SERVER_IP" "$JENKINS_SCRIPT" | while read line; do
log "INFO" "$line"
done
JENKINS_RET=${PIPESTATUS[0]}
[ $JENKINS_RET -ne 0 ] && error_exit "Jenkins 执行失败"
print_sep
log "INFO" "清理临时文件..."
rm -rf "$TEMP_CLASS_DIR"
log "INFO" "🎉 全流程执行完成!报告路径:${JENKINS_WORKSPACE}/diff-report/index.html"
print_sep
exit 0
生成的报告没有高亮提示
2.3 仅生成增量的jacoco报告
dump脚本仅需要生成exec文件,以及筛选出来增量的class文件,推到工作台
#!/bin/bash
# ===================== 自定义配置区 ======================
# JaCoCo基础配置
COV_FILE="/root/app-service/gf-business/coverage.exec"
JACOCO_CLI_JAR="/opt/jacoco/lib/jacococli.jar"
JACOCO_PORT=6300
JAVA_HOME="/usr/local/jdk-8"
APP_NAME="gf-business.jar"
# Jenkins配置
JENKINS_SERVER_IP="你的ip"
JENKINS_USER="root"
JENKINS_WORKSPACE="/var/lib/jenkins/workspace/gf-busniess-test-report"
JENKINS_CLASS_DIR="${JENKINS_WORKSPACE}/incremental-classes" # 统一用这个目录(增量/全量都放这)
JENKINS_RUN_USER="jenkins"
DIFF_FILE_PATH="${JENKINS_WORKSPACE}/added-files.txt"
# 应用Jar包配置
APP_JAR_PATH="/root/app-service/gf-business/gf-business.jar"
TEMP_CLASS_DIR="/tmp/gf-business-classes"
TEMP_INCREMENTAL_CLASS_DIR="/tmp/gf-incremental-classes"
# ==============================================================================
# 工具函数
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$1] $2"
}
error_exit() {
log "ERROR" "$1"
[ -d "$TEMP_CLASS_DIR" ] && rm -rf "$TEMP_CLASS_DIR"
[ -d "$TEMP_INCREMENTAL_CLASS_DIR" ] && rm -rf "$TEMP_INCREMENTAL_CLASS_DIR"
[ -f "$COV_FILE" ] && rm -f "$COV_FILE"
[ -f "/tmp/added-files.txt" ] && rm -f "/tmp/added-files.txt"
exit 1
}
# 初始化
log "INFO" "===== 开始执行 JaCoCo 数据采集(适配Jenkins插件) ====="
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export PATH=$JAVA_HOME/bin:$PATH
# 前置检查
log "INFO" "前置检查..."
! command -v java &>/dev/null && error_exit "Java 未找到"
[ ! -f "$JACOCO_CLI_JAR" ] && error_exit "JaCoCo CLI 不存在"
[ ! -f "$APP_JAR_PATH" ] && error_exit "应用jar包不存在"
! command -v scp &>/dev/null || ! command -v ssh &>/dev/null && error_exit "请安装 openssh-clients"
APP_PID=$(ps -ef | grep -E "[j]ava.*$APP_NAME" | awk '{print $2}')
[ -z "$APP_PID" ] && error_exit "未找到应用进程 $APP_NAME"
log "INFO" "✅ 前置检查通过"
# 1. 生成 coverage.exec
log "INFO" "执行 JaCoCo dump..."
[ -f "$COV_FILE" ] && rm -f "$COV_FILE"
java -jar "$JACOCO_CLI_JAR" dump --address localhost --port "$JACOCO_PORT" --destfile "$COV_FILE" --quiet || error_exit "dump 失败"
[ ! -s "$COV_FILE" ] && error_exit "coverage.exec 为空"
log "INFO" "✅ coverage.exec 生成成功"
# 2. 解压应用Jar(先拿到全量class)
log "INFO" "解压应用Jar包(全量class)..."
rm -rf "$TEMP_CLASS_DIR" && mkdir -p "$TEMP_CLASS_DIR"
unzip -q -o "$APP_JAR_PATH" "BOOT-INF/classes/**/*.class" -d "$TEMP_CLASS_DIR" || error_exit "解压失败"
FULL_CLASS_DIR="$TEMP_CLASS_DIR/BOOT-INF/classes"
[ ! -d "$FULL_CLASS_DIR" ] && error_exit "无 BOOT-INF/classes 目录"
# 3. 拉取增量文件列表(解决pyenv报错+文件不存在)
log "INFO" "拉取增量文件列表..."
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \
"$JENKINS_USER@$JENKINS_SERVER_IP" \
"unset PYENV_ROOT && source /dev/null && \
if [ -f '${DIFF_FILE_PATH}' ]; then
cat '${DIFF_FILE_PATH}' | sed '/^$/d; /^#/d'
else
touch '${DIFF_FILE_PATH}'
echo ''
fi" > /tmp/added-files.txt || {
log "WARN" "拉取增量文件列表失败,将推送全量class"
> /tmp/added-files.txt
}
# 4. 筛选class(增量/全量逻辑)
log "INFO" "筛选class文件..."
rm -rf "$TEMP_INCREMENTAL_CLASS_DIR" && mkdir -p "$TEMP_INCREMENTAL_CLASS_DIR"
COPY_COUNT=0
# 先尝试读取增量文件
while IFS= read -r JAVA_FILE; do
[ -z "${JAVA_FILE}" ] && continue
RELATIVE_JAVA=${JAVA_FILE#src/main/java/}
RELATIVE_CLASS=${RELATIVE_JAVA/.java/.class}
FULL_CLASS="${FULL_CLASS_DIR}/${RELATIVE_CLASS}"
if [ -f "${FULL_CLASS}" ]; then
TARGET_DIR="${TEMP_INCREMENTAL_CLASS_DIR}/$(dirname "${RELATIVE_CLASS}")"
mkdir -p "${TARGET_DIR}" && cp -p "${FULL_CLASS}" "${TARGET_DIR}/"
COPY_COUNT=$((COPY_COUNT + 1))
fi
done < /tmp/added-files.txt
# 无增量时推送全量class
if [ ${COPY_COUNT} -eq 0 ]; then
log "INFO" "未找到增量文件,推送全量class文件"
# 拷贝全量class到临时目录
cp -rp "${FULL_CLASS_DIR}/"* "${TEMP_INCREMENTAL_CLASS_DIR}/"
COPY_COUNT=$(find "${TEMP_INCREMENTAL_CLASS_DIR}" -name "*.class" | wc -l)
log "INFO" "✅ 全量class文件数:${COPY_COUNT}"
else
log "INFO" "✅ 筛选出增量class文件数:${COPY_COUNT}"
fi
# 5. 推送数据到Jenkins
log "INFO" "推送 coverage.exec 到 Jenkins..."
scp -q -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$COV_FILE" "$JENKINS_USER@$JENKINS_SERVER_IP:$JENKINS_WORKSPACE/" || error_exit "推送exec失败"
log "INFO" "推送class文件到 Jenkins..."
# 清理旧目录+重建
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$JENKINS_USER@$JENKINS_SERVER_IP" "rm -rf ${JENKINS_CLASS_DIR} && mkdir -p ${JENKINS_CLASS_DIR}" || error_exit "初始化class目录失败"
# 推送筛选后的class(增量/全量)
scp -r -q -o StrictHostKeyChecking=no "$TEMP_INCREMENTAL_CLASS_DIR/"* "$JENKINS_USER@$JENKINS_SERVER_IP:${JENKINS_CLASS_DIR}/" || error_exit "推送class失败"
# 6. 统一赋权
log "INFO" "配置Jenkins工作空间权限..."
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$JENKINS_USER@$JENKINS_SERVER_IP" \
"chown -R ${JENKINS_RUN_USER} ${JENKINS_WORKSPACE} && \
chmod -R 755 ${JENKINS_WORKSPACE}" || error_exit "赋权失败"
# 7. 收尾清理
log "INFO" "清理本地临时文件..."
rm -rf "$TEMP_CLASS_DIR" "$TEMP_INCREMENTAL_CLASS_DIR" /tmp/added-files.txt
log "INFO" "===== 执行完成! ====="
log "INFO" " - 覆盖率数据:${JENKINS_WORKSPACE}/coverage.exec"
log "INFO" " - Class文件目录:${JENKINS_CLASS_DIR}(${COPY_COUNT}个文件)"
exit 0
构建后配置:扫描增量的class文件

3.构建后生成增量代码报告
更多推荐
所有评论(0)