在持续集成(CI)环境中,Jenkins作为最流行的自动化构建工具之一,经常需要处理测试报告等构建产物(Build Artifacts)。许多团队会遇到这样的困境:自动化测试脚本明明生成了HTML报告、日志文件或截图等输出文件,但在Jenkins构建页面的"Build Artifacts"区域却找不到这些文件。这直接影响了测试结果的可见性和团队协作效率。
这个问题的本质在于Jenkins的构建产物收集机制。默认情况下,Jenkins只会归档工作空间(workspace)中特定路径下的文件。如果测试框架的输出目录不在默认收集范围内,或者文件生成时机与归档步骤不匹配,就会导致产物"消失"。
要让测试生成的文件出现在Build Artifacts中,我们需要建立完整的文件收集链路:
下面我们通过一个典型场景来具体说明:假设我们使用Python+pytest框架生成HTML测试报告,需要将其展示在Jenkins构建页面。
首先在pytest配置中明确指定报告输出路径。在pytest.ini中添加:
ini复制[pytest]
addopts = --html=./reports/test-report.html --self-contained-html
关键点:
--html指定报告生成路径--self-contained-html确保HTML报告是单个文件(便于归档)./reports/)而非绝对路径在Jenkins任务的配置页面,找到"构建后操作"(Post-build Actions)部分:
code复制reports/*.html
注意:路径是相对于工作空间的。如果测试报告生成在
target/reports/目录下,则应填写target/reports/*.html
对于声明式Pipeline,配置示例如下:
groovy复制pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'pytest tests/'
}
}
}
post {
always {
archiveArtifacts artifacts: 'reports/*.html', fingerprint: true
}
}
}
关键参数说明:
artifacts:支持Ant风格路径匹配fingerprint:为文件生成唯一标识,便于追踪如果需要收集多种类型的测试输出:
groovy复制archiveArtifacts artifacts: '''
reports/*.html,
logs/*.log,
screenshots/*.png
''', fingerprint: true
当需要保留历史版本时,建议在文件名中加入时间戳:
python复制# 在测试脚本中
import datetime
report_name = f"test-report-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}.html"
然后在Jenkins中配置通配符匹配:
code复制reports/test-report-*.html
对于较大的测试产物(如视频录制):
**/*递归匹配特定类型文件groovy复制parameters {
booleanParam(name: 'ARCHIVE_LARGE_FILES', defaultValue: false, description: '是否归档大文件')
}
// 在归档步骤中
if(params.ARCHIVE_LARGE_FILES) {
archiveArtifacts artifacts: 'videos/**/*.mp4'
}
当Jenkins报错No artifacts found时:
sh 'ls -R reports/'检查sh 'ls reports/*.html'能否列出文件如果遇到权限拒绝错误:
bash复制[html-report] $ /bin/bash -xe /tmp/jenkins12345.sh
/tmp/jenkins12345.sh: line 1: reports/test-report.html: Permission denied
解决方案:
groovy复制sh 'umask 000 && pytest tests/'
groovy复制sh 'chmod -R 755 reports/'
当使用Docker agent时,特别注意:
groovy复制environment {
REPORT_DIR = 'test-reports'
}
steps {
sh "pytest --html=${REPORT_DIR}/output.html"
archiveArtifacts artifacts: "${REPORT_DIR}/*.html"
}
目录结构标准化:团队统一约定测试输出目录,如:
reports/:测试报告logs/:日志文件artifacts/:其他产物命名规范化:采用<测试类型>-<时间戳>.<扩展名>格式,例如:
unit-test-20230815.htmle2e-screenshots-20230815.zip清理策略:在构建开始时清理旧文件:
groovy复制post {
cleanup {
deleteDir() // 或 sh 'rm -rf reports/*'
}
}
groovy复制archiveArtifacts(
artifacts: 'reports/*.html',
fingerprint: true,
onlyIfSuccessful: false,
allowEmptyArchive: true
)
groovy复制junit 'reports/junit-*.xml'
archiveArtifacts 'reports/*.html'
对于企业级需求,可以考虑:
groovy复制rtUpload (
serverId: 'artifactory-server',
spec: '''{
"files": [
{
"pattern": "reports/*.html",
"target": "qa-reports/${BUILD_NUMBER}/"
}
]
}'''
)
这种方案的优点包括:
为了让测试报告更直观:
groovy复制publishHTML(
target: [
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: 'reports',
reportFiles: 'test-report.html',
reportName: 'HTML Report'
]
)
groovy复制step([
$class: 'PlotBuilder',
title: 'Test Duration',
series: [[
file: 'reports/duration.csv',
inclusionFlag: 'INCLUDE_BY_STRING',
url: ''
]],
group: 'performance',
useDescriptions: false
])
不要归档敏感信息:
.gitignore和.jenkinsignore中添加排除规则文件大小限制:
访问控制:
Role Strategy插件控制制品访问权限python复制# pytest-parallel示例
@pytest.fixture(scope='session')
def report_dir(tmp_path_factory):
return tmp_path_factory.mktemp("reports")
find命令筛选:groovy复制sh '''
find reports/ -name "*.html" -mtime -1 -print0 | \
xargs -0 tar -czf new-reports.tgz
'''
archiveArtifacts 'new-reports.tgz'
groovy复制sh 'zip -r reports.zip reports/'
archiveArtifacts 'reports.zip'
针对不同操作系统环境:
groovy复制def reportPath = isUnix() ? 'reports/test.html' : 'reports\\test.html'
archiveArtifacts artifacts: reportPath
groovy复制steps {
script {
if (isUnix()) {
sh 'pytest --html=reports/output.html'
} else {
bat 'pytest --html=reports\\output.html'
}
}
}
groovy复制environment {
// 所有路径使用Unix风格
REPORT_PATH = 'reports/test.html'
}
steps {
// Windows会自动转换路径
bat "pytest --html=%REPORT_PATH%"
}
groovy复制publishBuildAnalysis(
failUnhealthy: true,
healthyThreshold: 90,
unhealthyThreshold: 70,
stabilityThreshold: 5
)
groovy复制post {
failure {
script {
def reportFiles = findFiles(glob: 'reports/*.html')
if (reportFiles.length == 0) {
emailext body: '测试报告生成失败', subject: '构建告警', to: 'team@example.com'
}
}
}
}
groovy复制step([
$class: 'InfluxDbPublisher',
selectedTarget: 'influxdb',
customProjectName: 'QA-Metrics',
customData: [
[measurement: 'test_coverage', value: readFile('reports/coverage.txt').trim()]
]
])
不同Jenkins版本的处理:
旧版本(2.0之前):
新版本(2.3+):
插件版本:
Archive Artifacts插件至少需要2.0版本groovy复制plugins {
id 'org.jenkins-ci.plugins.artifact-manager-s3' version '1.15'
}
groovy复制archiveArtifacts(
artifacts: 'reports/*.html',
fingerprint: true,
onlyIfSuccessful: false,
allowEmptyArchive: true,
defaultExcludes: false
)
groovy复制post {
always {
script {
echo "Workspace contents:"
sh 'ls -R'
}
}
}
bash复制curl -X POST -u USER:TOKEN \
-F "jenkinsfile=<Jenkinsfile" \
http://jenkins-url/pipeline-model-converter/validate
除了原生归档功能,还可以考虑:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 原生归档 | 简单直接 | 存储空间有限 | 小型项目 |
| Artifactory | 企业级功能 | 需要额外配置 | 大型团队 |
| S3存储 | 低成本可扩展 | 访问速度较慢 | 分布式团队 |
| Git LFS | 版本控制集成 | 不适合二进制文件 | 文档类产物 |
某电商平台的UI测试方案:
架构:
Jenkins配置:
groovy复制post {
always {
archiveArtifacts artifacts: '''
cypress/videos/**/*.mp4,
cypress/screenshots/**/*.png,
allure-report/**/*
''', fingerprint: true
allure([
includeProperties: false,
jdk: '',
properties: [],
reportBuildPolicy: 'ALWAYS',
results: [[path: 'allure-results']]
])
}
}
元数据增强:
自动化分析:
云原生方案:
增强可视化:
在多个项目中实施这套方案后,几点关键体会:
一个特别有用的技巧是:在Pipeline开始时就定义好所有路径变量,这样后续步骤都能一致引用:
groovy复制environment {
UNIT_TEST_REPORT = 'reports/unit-test.html'
E2E_SCREENSHOTS = 'e2e/screenshots/'
COVERAGE_DATA = 'coverage/cobertura.xml'
}
stages {
stage('Test') {
steps {
sh "pytest --html=${UNIT_TEST_REPORT}"
sh "npm run e2e --screenshots=${E2E_SCREENSHOTS}"
}
}
}
这种集中管理路径的方式大大减少了后续维护成本。