基于 curl 实现 Jenkins 上传制品到 JFrog Artifactory 操作文档
一、文档概述
1. 文档目的
本文档详细介绍如何通过 curl 命令(无需 JFrog 插件)实现 Jenkins 构建后将制品上传到 JFrog Artifactory,包含「单个文件上传」和「多个文件批量上传」两种核心场景,脚本基于实际项目验证,适配常见版本号格式(如 v1.0.0、1.0.0-wip、2.1.3-beta),并解决了文件匹配、版本号提取、重复上传等关键问题。
2. 核心优势
- 无插件依赖:仅依赖 Jenkins 节点默认自带的
curl和awk,无需安装 JFrog 插件; - 安全可靠:通过 Jenkins 凭据管理注入认证信息,避免硬编码;
- 灵活适配:支持单个/多个文件、不同制品类型(zip、pom、aar、xcframework.zip 等);
- 容错性强:包含文件存在性检查、版本号提取校验、重复上传处理。
3. 适用场景
- Jenkins 无法安装 JFrog 插件(如权限限制);
- 需自定义上传逻辑(如批量上传、动态路径、上传前校验);
- 制品类型多样(库文件、配置文件等),需统一上传方案。
二、前置准备
1. 环境依赖
| 工具 | 要求 | 说明 |
|---|---|---|
| Jenkins | 2.0+ | 支持 Pipeline 脚本(Declarative 模式) |
| JFrog Artifactory | 6.x+ | 已创建目标仓库(如 suunto-swift、amersports-release-local) |
| 工具依赖 | curl(默认自带)、awk(默认自带) |
Linux/macOS/Windows 节点均兼容 |
2. JFrog Artifactory 配置
2.1 权限配置
- 登录 JFrog Artifactory 后台,进入目标仓库(如
amersports-release-local); - 为 Jenkins 所用账号分配「Deploy」权限(必选,用于上传制品);
- 若需覆盖已存在制品,额外分配「Overwrite」权限(可选,生产环境建议禁用)。
2.2 核心信息收集
| 信息项 | 示例值 | 说明 |
|---|---|---|
| Artifactory 基础 URL | https://your-jfrog-url |
登录 Artifactory 后,首页可查看 |
| 目标仓库名称 | suunto-swift、amersports-release-local |
需提前创建,建议按环境/项目划分 |
| 认证信息 | 用户名 + API Key | API Key 从 Artifactory 个人资料中生成 |
3. Jenkins 配置
3.1 凭据配置(关键)
- 登录 Jenkins → 进入「管理 Jenkins」→「凭据」→「系统」→「全局凭据」;
- 点击「添加凭据」,选择「Username with password」类型;
- Username:JFrog Artifactory 登录用户名;
- Password:JFrog Artifactory API Key(个人资料 → API Key → 生成/复制);
- ID:自定义凭据 ID(如
jfrog-login),脚本中需对应;
- 保存凭据,确保 Jenkins 节点可访问 JFrog Artifactory(网络连通性测试:
curl -I 基础URL)。
三、单个文件上传方案(推荐)
1. 场景说明
适用于仅需上传单个制品(如 libmds-v1.0.0-wip.xcframework.zip),支持上传前检查文件是否已存在,避免重复上传。
2. 完整 Jenkins Pipeline 脚本
groovy
stage('单个文件上传:libmds 制品到 JFrog') {
steps {
script {
// ======================================
// 1. 核心配置(需根据实际环境修改)
// ======================================
def ARTIFACTORY_URL = "https://your-jfrog-url" // JFrog 基础 URL
def ARTIFACTORY_REPO = "suunto-swift" // 目标仓库名称
def LOCAL_FILE_PATTERN = "libmds-*.xcframework.zip" // 本地文件匹配模式(shell 通配符)
def CREDENTIALS_ID = "jfrog-login" // Jenkins 凭据 ID
def REMOTE_BASE_PATH = "suunto/sds" // JFrog 中制品存储的基础路径
// ======================================
// 2. 查找本地制品文件
// ======================================
echo "=== 开始查找本地制品文件 ==="
// 查找当前目录下(含1级子目录)匹配的文件,取第一个避免冲突
def localFile = sh(
script: "find . -maxdepth 1 -type f -name '${LOCAL_FILE_PATTERN}' | head -n 1",
returnStdout: true
).trim()
// 校验文件是否存在
if (localFile == "") {
error "❌ 未找到匹配 ${LOCAL_FILE_PATTERN} 的本地文件!请检查构建是否生成制品"
}
echo "✅ 找到本地文件:${localFile}"
// ======================================
// 3. 提取版本号(适配多种格式:v1.0.0、1.0.0-wip、2.1.3-beta)
// ======================================
echo "=== 开始提取版本号 ==="
// 逻辑:按 "libmds-" 和 ".xcframework.zip" 分隔,取中间部分作为版本号
def version = sh(
script: "echo '${localFile}' | awk -F 'libmds-|.xcframework.zip' '{print \$2}'",
returnStdout: true
).trim()
// 校验版本号提取结果
if (version == "") {
error "❌ 版本号提取失败!请检查文件名格式(示例:libmds-v1.0.0.xcframework.zip)"
}
echo "✅ 提取版本号:${version}"
// ======================================
// 4. 构造 JFrog 目标 URL
// ======================================
def targetFileName = "libmds-${version}.xcframework.zip" // 远程文件名(与本地一致)
def targetUrl = "${ARTIFACTORY_URL}/${ARTIFACTORY_REPO}/${REMOTE_BASE_PATH}/${version}/${targetFileName}"
echo "=== 目标上传 URL:${targetUrl} ==="
// ======================================
// 5. 上传制品到 JFrog(带认证 + 重复检查)
// ======================================
withCredentials([usernamePassword(
credentialsId: CREDENTIALS_ID,
usernameVariable: 'ARTIFACTORY_USER', // 凭据用户名变量
passwordVariable: 'ARTIFACTORY_API_KEY'// 凭据 API Key 变量
)]) {
sh """
# 步骤1:检查 JFrog 中是否已存在该文件(HEAD 请求,高效无带宽消耗)
echo "=== 检查文件是否已存在于 JFrog ==="
if curl -s -L -o /dev/null -w "%{http_code}" \
-u "${ARTIFACTORY_USER}:${ARTIFACTORY_API_KEY}" \
-X HEAD \
"${targetUrl}" | grep -q "200"; then
# 返回 200 → 文件已存在,跳过上传
echo "✅ 文件已存在,跳过重复上传:${targetUrl}"
else
# 返回 404 → 文件不存在,执行上传
echo "=== 开始上传制品 ==="
curl -f -L -v \
-u "${ARTIFACTORY_USER}:${ARTIFACTORY_API_KEY}" \ # 基础认证
-X PUT \ # JFrog 上传请求方法
-T "${localFile}" \ # 本地文件路径
"${targetUrl}" # 远程目标 URL
# 步骤2:上传后验证(确保文件上传成功)
echo "=== 验证上传结果 ==="
if curl -s -L -o /dev/null -w "%{http_code}" \
-u "${ARTIFACTORY_USER}:${ARTIFACTORY_API_KEY}" \
-X HEAD \
"${targetUrl}" | grep -q "200"; then
echo "🎉 制品上传成功!最终路径:${targetUrl}"
else
error "❌ 上传失败!目标路径未找到:${targetUrl}"
fi
fi
"""
}
}
}
}
3. 脚本参数说明
| 参数名 | 示例值 | 说明 |
|---|---|---|
| ARTIFACTORY_URL | https://your-jfrog-url |
JFrog 基础 URL,无需加 /artifactory 后缀(根据实际环境调整) |
| ARTIFACTORY_REPO | suunto-swift |
目标仓库名称,需提前在 JFrog 创建 |
| LOCAL_FILE_PATTERN | libmds-*.xcframework.zip |
本地制品匹配模式,用 * 匹配任意字符 |
| CREDENTIALS_ID | jfrog-login |
Jenkins 中配置的凭据 ID,需与实际一致 |
| REMOTE_BASE_PATH | suunto/sds |
JFrog 中制品的基础存储路径,可按项目划分 |
4. 关键逻辑解释
4.1 文件查找
- 用
find . -maxdepth 1 -type f -name '${LOCAL_FILE_PATTERN}'查找当前目录下的制品文件; maxdepth 1限制搜索深度(避免查找子目录中无关文件),head -n 1取第一个匹配文件(避免多版本冲突)。
4.2 版本号提取
- 用
awk -F 'libmds-|.xcframework.zip' '{print $2}'按「前缀+后缀」分隔文件名; - 适配格式:
libmds-v1.0.0.xcframework.zip→v1.0.0、libmds-1.0.0-wip.xcframework.zip→1.0.0-wip。
4.3 重复上传处理
- 上传前用
curl -X HEAD检查文件是否存在(返回 200 表示存在,404 表示不存在); - 已存在则跳过上传,避免无意义的网络请求和 JFrog 409 冲突错误。
四、多个文件批量上传方案
1. 场景说明
适用于需批量上传多种制品(如 movesense-1.0.0.zip、whiteboard-2.1.3.pom、sds-android-3.0.0.aar),支持按规则动态构造 JFrog 存储路径。
2. 完整 Jenkins Pipeline 脚本
groovy
stage('多个文件批量上传:制品到 JFrog') {
steps {
script {
// ======================================
// 1. 核心配置(需根据实际环境修改)
// ======================================
def ARTIFACTORY_URL = "https://your-jfrog-url" // JFrog 基础 URL
def ARTIFACTORY_REPO = "amersports-release-local" // 目标仓库名称
def CREDENTIALS_ID = "jfrog-artifactory-creds" // Jenkins 凭据 ID
def PUBLISH_DIR = "publish" // 本地制品存放目录(如 publish/)
// ======================================
// 2. 定义文件上传规则(核心)
// 格式:["本地文件模式|JFrog目标路径模板", ...]
// 说明:
// - 本地文件模式:publish/下的文件匹配(用 * 代替 (*))
// - 目标路径模板:{1} 自动替换为提取的版本号
// ======================================
def fileRules = [
// movesense 相关制品
"${PUBLISH_DIR}/movesense-*.zip|com/suunto/sds/movesense/{1}/movesense-{1}.zip",
"${PUBLISH_DIR}/movesense-*.pom|com/suunto/sds/movesense/{1}/movesense-{1}.pom",
// whiteboard 相关制品
"${PUBLISH_DIR}/whiteboard-*.zip|com/suunto/sds/whiteboard/{1}/whiteboard-{1}.zip",
"${PUBLISH_DIR}/whiteboard-*.pom|com/suunto/sds/whiteboard/{1}/whiteboard-{1}.pom",
// sds-android 相关制品
"${PUBLISH_DIR}/sds-android-*.aar|com/suunto/sds/sds-android/{1}/sds-android-{1}.aar",
"${PUBLISH_DIR}/sds-android-*.pom|com/suunto/sds/sds-android/{1}/sds-android-{1}.pom"
]
// ======================================
// 3. 循环处理每个文件规则
// ======================================
fileRules.each { rule ->
// 拆分规则:本地文件模式 + JFrog目标路径模板(用 | 分隔)
def (localPattern, targetTemplate) = rule.split('\\|')
echo "\n=================================================="
echo "=== 开始处理:${localPattern} → ${targetTemplate} ==="
echo "=================================================="
// ======================================
// 3.1 查找本地制品文件
// ======================================
def localFile = sh(
// maxdepth=2 适配 publish/子目录(如 publish/movesense-1.0.0.zip)
script: "find . -maxdepth 2 -type f -name '${localPattern}' | head -n 1",
returnStdout: true
).trim()
// 校验文件是否存在
if (localFile == "") {
error "❌ 未找到匹配 ${localPattern} 的本地文件!"
}
echo "✅ 找到本地文件:${localFile}"
// ======================================
// 3.2 提取文件前缀(用于版本号分隔)
// ======================================
def filePrefix = ""
if (localPattern.contains('movesense')) {
filePrefix = 'movesense-'
} else if (localPattern.contains('whiteboard')) {
filePrefix = 'whiteboard-'
} else if (localPattern.contains('sds-android')) {
filePrefix = 'sds-android-'
}
// ======================================
// 3.3 提取文件后缀(用于版本号分隔)
// ======================================
def fileSuffix = ""
if (localPattern.endsWith('.zip')) {
fileSuffix = '.zip'
} else if (localPattern.endsWith('.pom')) {
fileSuffix = '.pom'
} else if (localPattern.endsWith('.aar')) {
fileSuffix = '.aar'
}
// ======================================
// 3.4 提取版本号
// ======================================
def version = sh(
script: "echo '${localFile}' | awk -F '${filePrefix}|${fileSuffix}' '{print \$2}'",
returnStdout: true
).trim()
if (version == "") {
error "❌ 从文件 ${localFile} 提取版本号失败!请检查文件名格式(示例:${filePrefix}1.0.0${fileSuffix})"
}
echo "✅ 提取版本号:${version}"
// ======================================
// 3.5 构造 JFrog 目标 URL
// ======================================
def targetPath = targetTemplate.replace('{1}', version) // 替换版本号占位符
def targetUrl = "${ARTIFACTORY_URL}/${ARTIFACTORY_REPO}/${targetPath}"
echo "✅ 目标上传 URL:${targetUrl}"
// ======================================
// 3.6 上传制品到 JFrog(带认证 + 验证)
// ======================================
withCredentials([usernamePassword(
credentialsId: CREDENTIALS_ID,
usernameVariable: 'ARTIFACTORY_USER',
passwordVariable: 'ARTIFACTORY_API_KEY'
)]) {
sh """
# 检查文件是否已存在于 JFrog
echo "=== 检查文件是否已存在 ==="
if curl -s -L -o /dev/null -w "%{http_code}" \
-u "${ARTIFACTORY_USER}:${ARTIFACTORY_API_KEY}" \
-X HEAD \
"${targetUrl}" | grep -q "200"; then
echo "✅ 文件已存在,跳过重复上传:${targetUrl}"
else
# 执行上传
echo "=== 开始上传制品 ==="
curl -f -L -v \
-u "${ARTIFACTORY_USER}:${ARTIFACTORY_API_KEY}" \
-X PUT \
-T "${localFile}" \
"${targetUrl}"
# 上传后验证
echo "=== 验证上传结果 ==="
if curl -s -L -o /dev/null -w "%{http_code}" \
-u "${ARTIFACTORY_USER}:${ARTIFACTORY_API_KEY}" \
-X HEAD \
"${targetUrl}" | grep -q "200"; then
echo "🎉 文件上传成功:${localFile} → ${targetUrl}"
else
error "❌ 文件上传失败:${localFile}"
fi
fi
"""
}
}
echo "\n=================================================="
echo "🎉 所有制品上传处理完成!"
echo "=================================================="
}
}
}
3. 脚本核心配置说明
3.1 文件上传规则(fileRules)
- 格式:
"本地文件模式|JFrog目标路径模板",用|分隔; - 本地文件模式:
publish/movesense-*.zip表示匹配publish目录下所有movesense-xxx.zip格式文件; - 目标路径模板:
com/suunto/sds/movesense/{1}/movesense-{1}.zip中{1}自动替换为提取的版本号,最终路径如com/suunto/sds/movesense/1.0.0/movesense-1.0.0.zip。
3.2 动态版本号提取
- 根据文件前缀(
movesense-、whiteboard-等)和后缀(.zip、.pom等)动态分隔,适配不同制品类型; - 无需修改正则,新增制品类型时仅需扩展
fileRules和filePrefix/fileSuffix判断逻辑。
4. 扩展说明
- 新增制品类型:如需添加
xxx-1.0.0.jar上传,只需在fileRules中添加规则"publish/xxx-*.jar|com/suunto/sds/xxx/{1}/xxx-{1}.jar",并在filePrefix中添加else if (localPattern.contains('xxx')) { filePrefix = 'xxx-' }; - 调整存储路径:修改
targetTemplate中的路径前缀(如com/suunto/sds/改为com/company/project/),即可调整 JFrog 中制品的存储结构。
五、常见问题排查
1. 未找到匹配的本地文件
- 检查
LOCAL_FILE_PATTERN或localPattern是否正确(用*而非(*)); - 确认制品是否在
find搜索范围内(如maxdepth限制过严,可调整为maxdepth 3); - 验证构建步骤是否生成制品(在 Jenkins 工作空间查看
publish或目标目录是否有文件)。
2. 版本号提取失败
- 检查文件名格式是否符合「前缀-版本号.后缀」(如
movesense-1.0.0.zip,而非movesense_1.0.0.zip); - 确认
filePrefix和fileSuffix与文件名前缀/后缀完全一致(如无多余空格); - 替换版本号提取命令为
echo '${localFile}' | grep -oP '${filePrefix}\K[vV0-9.+-]+(?=${fileSuffix})'(Perl 正则,兼容性更强)。
3. 凭据类型不匹配
- 错误提示:
Credentials 'xxx' is of type 'Secret text' where 'StandardUsernamePasswordCredentials' was expected; - 解决方案:若凭据是「Secret text」(如 JFrog Access Token),将
withCredentials改为string(credentialsId: CREDENTIALS_ID, variable: 'ARTIFACTORY_TOKEN'),并将 curl 认证改为-H "Authorization: Bearer ${ARTIFACTORY_TOKEN}"。
4. 上传失败(409 Conflict)
- 原因:JFrog 中已存在相同路径文件,且未开启「Overwrite」权限;
- 解决方案:① 生产环境建议创建新版本号(如
1.0.1)而非覆盖;② 测试环境需覆盖时,在 JFrog 中为账号分配「Overwrite」权限,并在 curl 中添加-H "X-Artifactory-Override: true"。
5. 上传失败(401 Unauthorized)
- 检查 Jenkins 凭据的用户名和 API Key 是否正确(重新生成 API Key 并更新凭据);
- 验证 JFrog URL 是否正确(避免多写
/artifactory后缀或路径错误); - 确认网络连通性(Jenkins 节点可访问 JFrog,无防火墙拦截 443 端口)。