基于 curl 实现 Jenkins 上传制品到 JFrog Artifactory

基于 curl 实现 Jenkins 上传制品到 JFrog Artifactory 操作文档

一、文档概述

1. 文档目的

本文档详细介绍如何通过 curl 命令(无需 JFrog 插件)实现 Jenkins 构建后将制品上传到 JFrog Artifactory,包含「单个文件上传」和「多个文件批量上传」两种核心场景,脚本基于实际项目验证,适配常见版本号格式(如 v1.0.01.0.0-wip2.1.3-beta),并解决了文件匹配、版本号提取、重复上传等关键问题。

2. 核心优势

  • 无插件依赖:仅依赖 Jenkins 节点默认自带的 curlawk,无需安装 JFrog 插件;
  • 安全可靠:通过 Jenkins 凭据管理注入认证信息,避免硬编码;
  • 灵活适配:支持单个/多个文件、不同制品类型(zip、pom、aar、xcframework.zip 等);
  • 容错性强:包含文件存在性检查、版本号提取校验、重复上传处理。

3. 适用场景

  • Jenkins 无法安装 JFrog 插件(如权限限制);
  • 需自定义上传逻辑(如批量上传、动态路径、上传前校验);
  • 制品类型多样(库文件、配置文件等),需统一上传方案。

二、前置准备

1. 环境依赖

工具 要求 说明
Jenkins 2.0+ 支持 Pipeline 脚本(Declarative 模式)
JFrog Artifactory 6.x+ 已创建目标仓库(如 suunto-swiftamersports-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-swiftamersports-release-local 需提前创建,建议按环境/项目划分
认证信息 用户名 + API Key API Key 从 Artifactory 个人资料中生成

3. Jenkins 配置

3.1 凭据配置(关键)
  1. 登录 Jenkins → 进入「管理 Jenkins」→「凭据」→「系统」→「全局凭据」;
  2. 点击「添加凭据」,选择「Username with password」类型;
    • Username:JFrog Artifactory 登录用户名;
    • Password:JFrog Artifactory API Key(个人资料 → API Key → 生成/复制);
    • ID:自定义凭据 ID(如 jfrog-login),脚本中需对应;
  3. 保存凭据,确保 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.zipv1.0.0libmds-1.0.0-wip.xcframework.zip1.0.0-wip
4.3 重复上传处理
  • 上传前用 curl -X HEAD 检查文件是否存在(返回 200 表示存在,404 表示不存在);
  • 已存在则跳过上传,避免无意义的网络请求和 JFrog 409 冲突错误。

四、多个文件批量上传方案

1. 场景说明

适用于需批量上传多种制品(如 movesense-1.0.0.zipwhiteboard-2.1.3.pomsds-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 等)动态分隔,适配不同制品类型;
  • 无需修改正则,新增制品类型时仅需扩展 fileRulesfilePrefix/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_PATTERNlocalPattern 是否正确(用 * 而非 (*));
  • 确认制品是否在 find 搜索范围内(如 maxdepth 限制过严,可调整为 maxdepth 3);
  • 验证构建步骤是否生成制品(在 Jenkins 工作空间查看 publish 或目标目录是否有文件)。

2. 版本号提取失败

  • 检查文件名格式是否符合「前缀-版本号.后缀」(如 movesense-1.0.0.zip,而非 movesense_1.0.0.zip);
  • 确认 filePrefixfileSuffix 与文件名前缀/后缀完全一致(如无多余空格);
  • 替换版本号提取命令为 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 端口)。
相关推荐
杨德杰2 小时前
Ubuntu设置VNC远程桌面
linux·运维·ubuntu
写代码的学渣3 小时前
Ubuntu/麒麟默认锁定root账户
linux·运维·ubuntu
刚哥的进化路4 小时前
Linux系统日志管理完全教程:从基础查看 to 集中分析(附实战命令)
运维·自动化运维
互联网小顽童4 小时前
Linux系统进阶管理教程:从基础操作到企业级运维(附实战命令)
运维·自动化运维
面对疾风叭!哈撒给4 小时前
Docker之 Portainer、Node-RED和EMQX安装与配置
运维·docker·容器
RisunJan4 小时前
Linux命令-exportfs命令(管理NFS服务器上共享文件系统)
linux·运维·服务器
小吃饱了4 小时前
docker制作镜像
运维·docker·容器
LSL666_4 小时前
云服务器安装Tomcat
运维·服务器·tomcat
若汝棋茗4 小时前
串口客户端背后的故事:TouchSocket SerialPortClient 探秘
运维·负载均衡