在持续集成(CI)环境中,Jenkins作为最流行的自动化构建工具之一,经常需要处理各种构建产物(Build Artifacts)。特别是在自动化测试场景中,测试报告、日志文件等产出物的可视化展示直接影响着开发团队的问题定位效率。很多团队都会遇到这样的困境:明明测试脚本已经生成了HTML报告或JUnit XML文件,但在Jenkins构建页面却找不到这些关键文件。
这个问题的本质在于Jenkins的Artifacts归档机制与文件路径配置。不同于简单的文件存储,Jenkins需要明确知道哪些文件需要被保留为构建产物,以及这些文件的生成位置。当测试框架(如Pytest、JUnit、TestNG等)自动生成的报告未被正确归档时,开发者就不得不手动登录构建服务器去查找文件,这严重违背了自动化测试的初衷。
Jenkins处理构建产物的流程可以分为三个阶段:
target/surefire-reports)bash复制# 典型测试报告生成路径示例
├── workspace
│ ├── target
│ │ ├── surefire-reports # JUnit测试报告
│ │ │ ├── TEST-*.xml
│ │ ├── allure-results # Allure报告数据
│ ├── build
│ │ ├── reports
│ │ │ ├── tests # Gradle测试报告
在Jenkinsfile或Job配置中,控制产物归档的核心参数包括:
| 参数 | 作用 | 示例值 |
|---|---|---|
artifacts |
定义需要归档的文件路径模式 | **/target/*.xml |
excludes |
排除不需要的文件 | **/temp/* |
allowEmptyArchive |
是否允许空归档 | false |
onlyIfSuccessful |
仅在构建成功时归档 | true |
提示:路径模式使用Ant风格语法,
**表示递归匹配任意子目录
对于Freestyle项目,配置步骤如下:
**/target/surefire-reports/*.xml**/build/reports/tests/**/*.html**/node_modules/**(排除无关目录)对于声明式Pipeline,推荐使用archiveArtifacts指令:
groovy复制pipeline {
agent any
stages {
stage('Test') {
steps {
sh './gradlew test' // 执行测试
}
}
}
post {
always {
archiveArtifacts(
artifacts: '**/build/reports/tests/**/*.*',
excludes: '**/tmp/**',
allowEmptyArchive: true
)
junit '**/build/test-results/test/*.xml' // 同时发布JUnit报告
}
}
}
对于脚本式Pipeline:
groovy复制node {
stage('Build & Test') {
sh 'mvn clean package'
}
stage('Archive') {
archiveArtifacts artifacts: '**/target/*.jar,**/target/site/**/*',
excludes: '**/target/site/apidocs/**'
}
}
不同测试框架的报告路径示例:
| 测试框架 | 默认报告路径 | 归档模式 |
|---|---|---|
| JUnit | target/surefire-reports/*.xml | **/surefire-reports/*.xml |
| TestNG | test-output/*.xml | **/test-output/*.xml |
| Pytest | build/pytest/reports/*.xml | **/pytest/reports/**/* |
| Allure | allure-results/* | **/allure-results/**/* |
| Jacoco | build/jacoco/*.exec | **/jacoco/*.exec |
问题现象:配置了归档规则但页面未显示文件
排查步骤:
echo ${WORKSPACE}bash复制# 在Execute Shell中运行
find . -name "*.xml" -o -name "*.html"
ls命令测试通配符效果:ls -la **/target/*.xml典型错误案例:
./build而非**/build当处理大量测试报告时:
使用精确路径模式替代宽泛匹配:
**/*.xml**/target/surefire-reports/*.xml设置合理的排除规则:
groovy复制archiveArtifacts artifacts: '**/build/**',
excludes: '**/build/tmp/**,**/build/downloads/**'
对大文件启用压缩:
groovy复制archiveArtifacts artifacts: '**/logs/*.log',
fingerprint: true,
onlyIfSuccessful: false
结合常用插件增强展示效果:
JUnit Plugin:
groovy复制post {
always {
junit allowEmptyResults: true,
testResults: '**/test-results/*.xml'
}
}
HTML Publisher Plugin:
groovy复制publishHTML target: [
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: 'build/reports/tests',
reportFiles: 'index.html',
reportName: 'HTML Report'
]
Allure Plugin:
groovy复制allure includeProperties: false,
jdk: '',
results: [[path: 'allure-results']]
推荐的标准测试输出结构:
code复制<module_root>
├── build
│ ├── reports # 聚合报告目录
│ │ ├── junit # 单元测试
│ │ ├── integration # 集成测试
│ │ ├── coverage # 覆盖率报告
│ ├── test-results # 原始测试数据
├── target # Maven标准输出目录
│ ├── surefire-reports
│ ├── allure-results
对应的归档配置:
groovy复制archiveArtifacts artifacts: '''
build/reports/**/*,
target/surefire-reports/*.xml,
target/allure-results/**/*
''', excludes: '**/temp/**'
敏感信息过滤:
groovy复制archiveArtifacts artifacts: '**/logs/*.log',
excludes: '**/*password*.log'
文件大小限制:
groovy复制sh 'find . -name "*.log" -size +10M -exec rm {} \\;'
保留策略:
groovy复制properties([
buildDiscarder(
logRotator(
artifactDaysToKeepStr: '7',
artifactNumToKeepStr: '5'
)
)
])
Windows/Unix兼容写法:
groovy复制def reportPath = isUnix() ? '**/target/reports' : '**\\target\\reports'
archiveArtifacts artifacts: "${reportPath}/*.html"
或者使用平台无关的路径API:
groovy复制archiveArtifacts artifacts: java.nio.file.Paths.get('target', 'reports').toString() + '/**'
在Jenkinsfile中添加Markdown描述:
groovy复制post {
success {
script {
def reportUrl = "${env.BUILD_URL}artifact/build/reports/tests/index.html"
currentBuild.description = """
[](${reportUrl})
"""
}
}
}
使用stash/unstash跨阶段传递产物:
groovy复制stage('Unit Test') {
steps {
sh './gradlew test'
stash includes: 'build/reports/tests/**/*', name: 'unit-reports'
}
}
stage('Integration Test') {
steps {
sh './gradlew integrationTest'
stash includes: 'build/reports/tests/**/*', name: 'integration-reports'
}
}
stage('Merge Reports') {
steps {
unstash 'unit-reports'
unstash 'integration-reports'
archiveArtifacts 'build/reports/**/*'
}
}
根据构建参数决定归档内容:
groovy复制archiveArtifacts artifacts: "${params.REPORT_PATH}/**",
excludes: params.EXCLUDE_PATTERN ?: ''
在Jenkins中配置字符串参数:
REPORT_PATH:默认值build/reportsEXCLUDE_PATTERN:默认空添加构建后检查步骤:
groovy复制post {
always {
script {
def artifacts = currentBuild.rawBuild.getArtifacts()
if (artifacts.isEmpty()) {
unstable("No artifacts were archived")
}
}
}
}
定期清理旧构建的插件推荐:
在Pipeline中添加检查:
groovy复制sh '''
df -h $WORKSPACE
du -sh $WORKSPACE/build/reports
'''
添加自动化验证测试:
groovy复制stage('Verify Artifacts') {
steps {
script {
def expectedFiles = [
'build/reports/tests/test/index.html',
'target/surefire-reports/TEST-*.xml'
]
expectedFiles.each { pattern ->
if (findFiles(glob: pattern).length == 0) {
error "Missing required artifact: ${pattern}"
}
}
}
}
}