在日常开发中,Jenkins作为持续集成工具被广泛使用,但很多开发者都会遇到一个令人头疼的问题:构建明明显示成功了,到了部署阶段却发现找不到构建产物。更诡异的是,有时候手动去服务器上看,文件确实存在,但Jenkins就是"不认账"。这种构建产物存储路径配置错误的问题,排查起来往往令人抓狂。本文将系统性地讲解如何定位和解决这类问题,帮你少走弯路。

一、问题现象与常见表现

在深入排查之前,先来看看这个问题通常以什么形式出现。不同表现可能对应不同的根因,搞清楚症状是排查的第一步。

1.1 构建成功但产物列表为空

最常见的表现是:Job页面显示构建结果为蓝色对勾(SUCCESS),但你点进构建历史,"构建产物"那一栏空空如也。这意味着归档步骤要么没执行,要么归档了错误的路径。

1.2 部署阶段报"文件未找到"

另一种常见情况是:构建阶段正常完成,产物也归档了,但到了部署阶段,部署脚本去取产物文件时报错,提示类似"No such file or directory"。这种情况下产物确实归档了,但归档的路径和部署脚本预期的路径不一致。

1.3 产物内容不完整

还有一种隐蔽的问题:产物列表里能看到文件,但内容不对,比如只归档了目录本身而没有归档目录下的文件,或者只归档了编译产物而没有包含配置文件。这类问题往往是因为归档通配符写得不够精确。

1.4 路径中环境变量未展开

当你在归档配置中使用了环境变量,但运行时没有被正确替换,比如归档路径写成了$BUILD_DIR/output,结果实际归档路径就是字面量的$BUILD_DIR/output,而不是你期望的/opt/project/output。

二、构建产物存储路径的底层机制

理解了Jenkins是如何管理和存储构建产物的,排查起来就会胸有成竹。

2.1 工作空间与产物目录的关系

Jenkins每次构建都会分配一个工作空间(workspace),默认路径通常是$JENKINS_HOME/workspace/<job-name>。工作空间是临时的,构建过程中所有编译、打包都发生在这里。但工作空间里的文件并不意味着就是"构建产物"——你需要显式地告诉Jenkins哪些文件需要作为产物被归档保存。

关键概念是:工作空间是"施工地",产物归档目录是"仓库"。施工地的东西会被清理,只有搬进仓库的东西才会被保留。

2.2 归档构建产物的工作机制

在Pipeline中,归档产物通常通过archiveArtifacts步骤完成。这个步骤会扫描指定路径,将匹配的文件复制到Jenkins的构建归档目录中。在传统的Freestyle项目中,则通过"Post-build Actions"中的"Archive the artifacts"来实现。

归档后的文件物理存储路径是$JENKINS_HOME/jobs/<job-name>/builds/<build-number>/artifacts/。这个路径是Jenkins内部管理的,你通常不需要直接操作它,但理解这个结构有助于排查问题。

2.3 路径解析规则

Jenkins在解析归档路径时,有几个关键规则需要注意:

第一,支持通配符(如**/*.jar、build/*)。 第二,路径相对于工作空间根目录,除非你用了绝对路径。 第三,支持环境变量展开,但不同上下文中展开时机不同。 第四,如果匹配到的路径不存在,默认不会报错(这也是很多问题的根源)。

三、系统化排查流程

遇到产物路径问题,不要凭感觉乱试,按照下面的流程一步步排查,效率最高。

3.1 第一步:检查配置文件中的归档定义

首先确认你的构建脚本或Job配置中,归档步骤是否真的存在,以及路径定义是否正确。

技术栈:Jenkins Pipeline (Groovy) + Shell

// Jenkinsfile - 构建产物归档配置示例
pipeline {
    agent any

    environment {
        // 定义构建输出目录,统一管理路径
        BUILD_OUTPUT_DIR = "${WORKSPACE}/target"
        DIST_DIR         = "${WORKSPACE}/dist"
    }

    stages {
        stage('编译') {
            steps {
                sh '''
                    # 创建输出目录,避免目录不存在导致后续步骤失败
                    mkdir -p "$BUILD_OUTPUT_DIR"
                    mkdir -p "$DIST_DIR"

                    # 编译项目,产物输出到BUILD_OUTPUT_DIR
                    mvn clean package -DskipTests

                    # 将编译产物复制到标准分发目录
                    cp target/*.jar "$DIST_DIR/"
                    cp -r target/classes/config "$DIST_DIR/config"
                '''
            }
        }

        stage('归档构建产物') {
            steps {
                archiveArtifacts artifacts: 'dist/**', fingerprint: true, allowEmptyArchive: false
            }
        }

        stage('部署') {
            steps {
                sh '''
                    # 从Jenkins产物目录获取文件进行部署
                    cp ${WORKSPACE}/dist/*.jar /opt/app/deploy/
                    echo "部署完成"
                '''
            }
        }
    }
}

检查要点:确认artifacts参数的路径拼写正确,注意相对路径和绝对路径的区别,确认通配符符合预期。

3.2 第二步:查看构建日志中的归档信息

Jenkins在归档产物时会在日志中输出相关信息。找到"Archiving artifacts"关键字后面的日志行,可以确认实际归档了哪些文件。

技术栈:Shell

# 查看最新构建的完整日志
# 获取最新构建编号
BUILD_NUMBER=$(curl -s -u "user:api-token" "http://jenkins-server/job/my-project/lastBuild/buildNumber" | jq -r '.')

# 下载构建日志
curl -s -u "user:api-token" "http://jenkins-server/job/my-project/${BUILD_NUMBER}/logText/pretty" \
    | grep -A 20 "Archiving artifacts"

# 如果日志太大,可以只看最后100行
curl -s -u "user:api-token" "http://jenkins-server/job/my-project/${BUILD_NUMBER}/logText/pretty" \
    | tail -100

如果日志中没有"Archiving artifacts"字样,说明归档步骤根本没有执行。如果有这行但没有列出具体的文件,说明匹配路径下没有文件。

3.3 第三步:通过API查询构建产物列表

即使页面上显示有问题,Jenkins REST API也能给你最准确的信息。

技术栈:Shell + JSON

# 通过Jenkins API查询构建产物列表
# 这个接口会返回构建中实际归档的所有产物信息
curl -s -u "user:api-token" "http://jenkins-server/job/my-project/42/api/json?tree=artifacts[name,relativePath,length]" \
    | jq '.'

# 输出示例:
# {
#   "artifacts": [
#     {
#       "name": "dist/app-1.0.0.jar",
#       "relativePath": "app-1.0.0.jar",
#       "length": 1234567
#     }
#   ]
# }

# 如果需要检查多个构建的产物情况,可以写循环
for i in 40 41 42 43; do
    echo "=== 构建 #${i} 的产物 ==="
    curl -s -u "user:api-token" "http://jenkins-server/job/my-project/${i}/api/json?tree=artifacts[name,relativePath]" \
        | jq -r '.artifacts[] | "\(.relativePath) -> \(.name)"' 2>/dev/null || echo "(无产物)"
    echo ""
done

3.4 第四步:检查服务器文件系统上的实际路径

有时候问题出在Jenkins服务器本身的文件系统上。比如磁盘空间不足导致文件写入失败,或者权限问题导致无法创建目录。

技术栈:Shell

# 登录Jenkins服务器,检查构建产物的实际存储路径
# 首先需要知道JENKINS_HOME的位置
cat /etc/sysconfig/jenkins | grep JENKINS_HOME
# 输出示例:JENKINS_HOME=/var/lib/jenkins

# 检查最近一次构建的artifacts目录
ls -la /var/lib/jenkins/jobs/my-project/builds/42/artifacts/

# 检查工作空间中的文件是否还存在
ls -la /var/lib/jenkins/workspace/my-project/

# 检查磁盘空间
df -h /var/lib/jenkins

# 检查目录权限
ls -laZ /var/lib/jenkins/jobs/my-project/builds/42/

# 如果怀疑是文件损坏,可以检查文件大小
find /var/lib/jenkins/jobs/my-project/builds/42/artifacts/ -type f -exec ls -lh {} \;

3.5 第五步:检查环境变量展开情况

环境变量未正确展开是最容易被忽视的问题。在Pipeline的不同上下文中,变量展开的时机和方式不同。

技术栈:Jenkins Pipeline (Groovy)

// Jenkinsfile - 演示环境变量展开的陷阱
pipeline {
    agent any

    environment {
        // 这里定义的变量在环境上下文中可用
        OUTPUT_PATH = "output"
    }

    stages {
        stage('问题示例') {
            steps {
                // 错误写法:sh指令中使用单引号,Groovy变量不会展开
                sh '''
                    mkdir -p $OUTPUT_PATH
                    # 如果OUTPUT_PATH在sh环境中不存在,这里会创建字面量目录
                    echo "file" > $OUTPUT_PATH/test.txt
                '''

                // 错误写法:archiveArtifacts中使用单引号,环境变量不会展开
                archiveArtifacts artifacts: 'OUTPUT_PATH/*'  // 这里会被当作字面路径
            }
        }

        stage('正确示例') {
            steps {
                // 正确写法1:在sh中用双引号或明确传递环境变量
                sh """
                    mkdir -p ${env.OUTPUT_PATH}
                    echo "file" > ${env.OUTPUT_PATH}/test.txt
                """

                // 正确写法2:archiveArtifacts中使用Groovy字符串插值
                archiveArtifacts artifacts: "${env.OUTPUT_PATH}/*"

                // 正确写法3:提前拼好路径变量,确保在任何上下文中都能正确引用
                def distPath = "${WORKSPACE}/dist"
                sh "mkdir -p ${distPath} && echo hello > ${distPath}/hello.txt"
                archiveArtifacts artifacts: 'dist/*'
            }
        }
    }
}

四、常见错误场景与修复方案

4.1 相对路径与绝对路径混淆

这是最高频的错误。在Pipeline中,archiveArtifacts的默认工作目录是工作空间根目录。如果你用了绝对路径,那Jenkins会按字面绝对路径去找文件;如果你用了相对路径,Jenkins会从工作空间根目录开始找。

// Jenkinsfile - 相对路径与绝对路径的对比
pipeline {
    agent any

    stages {
        stage('相对路径示例') {
            steps {
                // 相对路径:相对于WORKSPACE根目录
                sh '''
                    mkdir -p build/libs
                    echo "binary" > build/libs/app.jar
                    echo "config" > build/libs/config.yml
                '''

                // 使用相对路径归档 - 最常见且推荐的方式
                archiveArtifacts artifacts: 'build/libs/**'
            }
        }

        stage('绝对路径示例') {
            steps {
                // 创建文件在绝对路径下
                sh """
                    mkdir -p /opt/project/output
                    echo "binary" > /opt/project/output/app.jar
                """

                // 使用绝对路径归档 - 需要注意路径必须真实存在
                archiveArtifacts artifacts: "/opt/project/output/**"

                // 错误做法:绝对路径但文件不在那
                // archiveArtifacts artifacts: "/tmp/nonexistent/**"
                // 上面这行如果allowEmptyArchive默认是true,构建会成功但产物为空
            }
        }

        stage('混合路径示例') {
            steps {
                // 工作空间内部创建
                sh '''
                    mkdir -p target
                    echo "build-output" > target/app.jar
                '''

                // 通过WORKSPACE变量构建绝对路径
                archiveArtifacts artifacts: "${WORKSPACE}/target/**"
                // 注意:${WORKSPACE}会被Groovy展开为类似/workspace/jobs/myjob/123的路径
            }
        }
    }
}

修复方案:统一使用相对于工作空间的路径,避免在工作空间之外操作文件,除非有明确需要。

4.2 工作空间被清理导致产物丢失

有些团队配置了"完成后清理工作空间",导致在后续步骤中依赖工作空间文件时找不到。

// Jenkinsfile - 展示工作空间清理问题及解决方案
pipeline {
    agent {
        docker {
            image 'maven:3.9-eclipse-temurin-17'
            // 注意:如果这里配置了cleanup,工作空间在容器销毁时会丢失
            args '--network=host'
        }
    }

    stages {
        stage('编译并产出') {
            steps {
                sh '''
                    mvn clean package -DskipTests
                    ls -la target/
                    echo "构建产物在target目录中"
                '''
            }
        }

        // 问题场景:在不同容器间共享文件
        stage('验证产物 - 错误方式') {
            agent {
                docker {
                    image 'ubuntu:22.04'
                    // 换了新容器,上一步的target目录不存在了!
                }
            }
            steps {
                sh '''
                    # 这里找不到target/目录,因为换了容器
                    # ls target/ 会报错
                    echo "找不到产物,因为换容器了"
                '''
            }
        }

        // 正确方式:用archiveArtifacts传递
        stage('正确传递产物') {
            steps {
                archiveArtifacts artifacts: 'target/**'
            }
        }

        stage('从产物中部署 - 正确方式') {
            agent {
                docker {
                    image 'ubuntu:22.04'
                }
            }
            steps {
                // 产物已经归档,但需要下载到工作空间才能使用
                // 或者通过Jenkins API下载
                sh '''
                    # Jenkins会把归档产物自动下载到工作空间
                    ls -la target/
                    echo "产物文件已就绪"
                '''
            }
        }
    }
}

修复方案:在节点切换或容器切换之前,先执行archiveArtifacts将文件固化到Jenkins归档系统,而不是依赖工作空间的持久化。

4.3 多构建类型产物路径冲突

当你有多个构建任务输出到相同路径时,后构建的任务会覆盖先构建的产物。

// Jenkinsfile - 多构建类型产物管理
pipeline {
    agent any

    environment {
        // 用构建编号和构建原因区分不同构建的产物
        BUILD_TAG = "${env.BUILD_NUMBER}_${env.BUILD_REASON}"
    }

    stages {
        stage('编译Java服务') {
            steps {
                sh '''
                    mkdir -p output/java-service
                    echo "java-service-binary" > output/java-service/app.jar
                    echo "java-service-config" > output/java-service/application.yml
                '''
            }
        }

        stage('编译前端应用') {
            steps {
                sh '''
                    mkdir -p output/frontend-app
                    echo "index.html" > output/frontend-app/index.html
                    echo "bundle.js" > output/frontend-app/bundle.js
                '''
            }
        }

        stage('按模块归档') {
            steps {
                // 分别归档不同模块的产物,使用不同的include/exclude
                archiveArtifacts artifacts: 'output/java-service/**',
                                 allowEmptyArchive: true,
                                 caseSensitive: true,
                                 defaultExcludes: false

                archiveArtifacts artifacts: 'output/frontend-app/**',
                                 allowEmptyArchive: true,
                                 caseSensitive: true,
                                 defaultExcludes: false

                // 也可以一次性归档全部,但需要确保路径不冲突
                // archiveArtifacts artifacts: 'output/**'
            }
        }

        stage('按条件归档') {
            steps {
                // 只归档变更的文件 - 适用于增量部署场景
                sh '''
                    # 模拟只重新编译了部分模块
                    mkdir -p output/changed
                    echo "only-changed" > output/changed/new-feature.jar
                '''

                archiveArtifacts artifacts: 'output/changed/**',
                                 defaultExcludes: false,
                                 fingerprint: true
            }
        }
    }
}

4.4 通配符不匹配

通配符写得不对是导致产物缺失的另一个常见原因。

// Jenkinsfile - 通配符匹配详解
pipeline {
    agent any

    stages {
        stage('创建测试文件') {
            steps {
                sh '''
                    # 创建多层目录结构用于测试通配符
                    mkdir -p build/libs
                    mkdir -p build/docs
                    mkdir -p build/config
                    mkdir -p src/main/java

                    echo "app.jar" > build/libs/app.jar
                    echo "spring.jar" > build/libs/spring-core.jar
                    echo "readme.md" > build/docs/README.md
                    echo "config.yml" > build/config/application.yml
                    echo "Application.java" > src/main/java/Application.java
                '''
            }
        }

        stage('通配符匹配演示') {
            steps {
                // 匹配build下所有jar文件(递归)
                archiveArtifacts artifacts: 'build/**/*.jar'
                // 结果:app.jar, spring-core.jar

                // 匹配build下所有文件(递归)
                archiveArtifacts artifacts: 'build/**'
                // 结果:app.jar, spring-core.jar, README.md, application.yml

                // 精确匹配特定路径
                archiveArtifacts artifacts: 'build/libs/*.jar'
                // 结果:app.jar, spring-core.jar

                // 匹配多个模式(逗号分隔)
                archiveArtifacts artifacts: 'build/libs/*.jar, build/config/*.yml'
                // 结果:app.jar, spring-core.jar, application.yml

                // 排除模式 - 使用exclude参数
                archiveArtifacts artifacts: 'build/**',
                                   exclude: '**/*.md',
                                   defaultExcludes: false
                // 结果:app.jar, spring-core.jar, application.yml
                // 排除了README.md

                // 常见错误:忘记加**
                // archiveArtifacts artifacts: 'build/*.jar'
                // 这只会匹配build/libs/下的jar,不会递归到更深层级
                // 如果文件在build/libs/下,这里就匹配不到
            }
        }
    }
}

五、最佳实践与预防措施

5.1 使用标准目录结构

无论你的项目结构如何,建议在Pipeline中统一使用标准目录结构来存放产物,这样归档配置可以标准化。

// Jenkinsfile - 标准目录结构最佳实践
pipeline {
    agent any

    environment {
        // 标准化目录定义,整个Pipeline中使用这些变量
        DIR_SOURCE   = 'src'
        DIR_TARGET   = 'target'
        DIR_DIST     = 'dist'
        DIR_CONFIG   = 'config'
        DIR_LOGS     = 'logs'
    }

    stages {
        stage('初始化目录结构') {
            steps {
                sh """
                    # 确保所有标准目录存在
                    mkdir -p ${DIR_SOURCE} ${DIR_TARGET} ${DIR_DIST} ${DIR_CONFIG} ${DIR_LOGS}

                    # 输出目录结构供调试
                    echo "=== 标准目录结构 ==="
                    find . -type d -not -path './.git*' | sort
                """
            }
        }

        stage('构建') {
            steps {
                sh """
                    # 编译产物统一输出到DIR_TARGET
                    mkdir -p ${DIR_TARGET}/classes
                    mkdir -p ${DIR_TARGET}/lib
                    echo "class-output" > ${DIR_TARGET}/classes/Application.class
                    echo "dependency" > ${DIR_TARGET}/lib/dependency.jar

                    # 可部署的完整包统一输出到DIR_DIST
                    mkdir -p ${DIR_DIST}/app-${BUILD_NUMBER}
                    cp -r ${DIR_TARGET}/* ${DIR_DIST}/app-${BUILD_NUMBER}/
                    cp -r ${DIR_CONFIG}/* ${DIR_DIST}/app-${BUILD_NUMBER}/
                """
            }
        }

        stage('归档') {
            steps {
                // 使用统一的标准路径归档
                archiveArtifacts artifacts: "${DIR_DIST}/**",
                                 allowEmptyArchive: false,
                                 defaultExcludes: false,
                                 fingerprint: true

                // allowEmptyArchive: false 确保产物为空时构建失败,及早发现问题
                // fingerprint: true 记录文件指纹,便于追踪
            }
        }
    }
}

5.2 添加构建后验证步骤

在归档之后添加验证步骤,确保产物确实存在且内容正确。

// Jenkinsfile - 产物验证步骤
pipeline {
    agent any

    stages {
        stage('构建') {
            steps {
                sh '''
                    mkdir -p dist
                    echo "version=1.0.0" > dist/version.txt
                    echo "binary-content" > dist/app.jar
                    echo "config-content" > dist/application.yml
                '''
            }
        }

        stage('归档产物') {
            steps {
                archiveArtifacts artifacts: 'dist/**'
            }
        }

        stage('产物完整性验证') {
            steps {
                script {
                    // 通过Jenkins API获取产物列表
                    def artifacts = []
                    try {
                        def response = sh(
                            script: """
                                curl -s -u "${env.USER}:token" \
                                    "${env.JENKINS_URL}job/${env.JOB_NAME}/${env.BUILD_NUMBER}/api/json?tree=artifacts[relativePath,length]"
                            """,
                            returnStdout: true
                        )
                        // 简单验证:检查关键文件是否存在
                        def expectedFiles = ['app.jar', 'version.txt', 'application.yml']
                        expectedFiles.each { file ->
                            if (!response.contains(file)) {
                                error "关键产物 ${file} 未找到!归档可能不完整"
                            }
                        }
                        echo "所有关键产物验证通过"
                    } catch (Exception e) {
                        error "产物验证失败:${e.message}"
                    }
                }

                // 也可以在本地验证(文件在工作空间中的副本)
                sh '''
                    echo "=== 验证工作空间中的产物 ==="
                    for f in dist/app.jar dist/version.txt dist/application.yml; do
                        if [ -f "$f" ]; then
                            echo "OK: $f ($(wc -c < "$f") bytes)"
                        else
                            echo "MISSING: $f"
                            exit 1
                        fi
                    done
                '''
            }
        }
    }
}

5.3 配置构建产物保留策略

Jenkins默认会保留所有历史构建的产物,这会导致磁盘空间迅速增长。合理的保留策略既能节省空间,又能满足审计需求。

// Jenkinsfile - 产物保留策略配置
pipeline {
    agent any

    options {
        // 只保留最近20次构建
        buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '5'))
        // 或者按时间保留
        // buildDiscarder(logRotator(numDaysToKeepStr: '30', artifactNumToKeepStr: '5'))
    }

    stages {
        stage('构建') {
            steps {
                sh '''
                    mkdir -p dist
                    echo "artifact" > dist/app.jar
                '''
            }
        }

        stage('归档') {
            steps {
                archiveArtifacts artifacts: 'dist/**'
            }
        }
    }
}

// 另外,也可以在Jenkins全局配置中设置
// 路径:Jenkins管理 -> 系统管理 -> 构建丢弃策略
// 建议配置:
// - 最近10次构建保留完整日志和产物
// - 最近30次构建只保留日志
// - 更早的构建删除

5.4 使用Pipeline脚本增强可维护性

将归档配置抽成可复用的Pipeline步骤或共享库,避免每个Job都重复定义。

// src/org/example/jenkins/util/ArtifactUtils.groovy
// 共享库中的产物归档工具类
package org.example.jenkins.util

class ArtifactUtils {

    /**
     * 标准化归档构建产物
     * @param pipeline Jenkins Pipeline对象
     * @param patterns 通配符模式列表
     * @param options  额外归档选项
     */
    static void archiveStandard(pipeline, patterns, options = [:]) {
        def defaultOptions = [
            allowEmptyArchive: false,
            defaultExcludes: false,
            fingerprint: true
        ]
        options.each { key, value ->
            defaultOptions[key] = value
        }

        // 确保模式列表是列表类型
        def artifactPatterns = (patterns instanceof List) ? patterns : [patterns]

        // 合并多个通配符模式
        def combinedPattern = artifactPatterns.join(', ')

        pipeline.archiveArtifacts(artifacts: combinedPattern, **defaultOptions)

        // 归档后打印信息
        pipeline.sh '''
            echo "=== 产物归档完成 ==="
            echo "归档模式: ${ARTIFACT_PATTERN}"
        '''
    }
}
// Jenkinsfile - 使用共享库
// @NonCPS
def getBuildDir() {
    // 根据项目类型动态确定构建输出目录
    if (env.PROJECT_TYPE == 'java') {
        return 'target'
    } else if (env.PROJECT_TYPE == 'python') {
        return 'dist'
    } else if (env.PROJECT_TYPE == 'frontend') {
        return 'build'
    }
    return 'output'
}

pipeline {
    agent any

    environment {
        PROJECT_TYPE = 'java'
    }

    stages {
        stage('构建') {
            steps {
                sh '''
                    mkdir -p target
                    echo "java-output" > target/app.jar
                '''
            }
        }

        stage('归档产物') {
            steps {
                script {
                    def buildDir = getBuildDir()
                    echo "构建输出目录: ${buildDir}"

                    // 使用标准化归档
                    archiveArtifacts artifacts: "${buildDir}/**",
                                     allowEmptyArchive: false,
                                     defaultExcludes: false,
                                     fingerprint: true

                    // 验证归档结果
                    sh '''
                        echo "=== 归档结果验证 ==="
                        find dist -type f 2>/dev/null | wc -l
                    '''
                }
            }
        }
    }
}

六、应用场景分析

6.1 单体应用部署场景

对于传统的单体Java应用,构建产物通常是一个WAR或JAR包加上配置文件。路径配置的核心是确保整个可部署包完整归档。

// Jenkinsfile - 单体应用部署场景
pipeline {
    agent any

    stages {
        stage('编译打包') {
            steps {
                sh '''
                    mvn clean package -DskipTests
                    mkdir -p deploy
                    cp target/myapp.war deploy/
                    cp config/application-prod.yml deploy/
                '''
            }
        }

        stage('归档部署包') {
            steps {
                archiveArtifacts artifacts: 'deploy/**'
            }
        }

        stage('部署') {
            steps {
                sh '''
                    # 从归档产物部署到目标服务器
                    scp deploy/myapp.war user@server:/opt/tomcat/webapps/
                    scp deploy/application-prod.yml user@server:/opt/tomcat/conf/
                    ssh user@server "systemctl restart tomcat"
                '''
            }
        }
    }
}

6.2 微服务构建场景

微服务场景下,每个服务独立构建和部署,产物路径需要按服务名组织,避免冲突。

// Jenkinsfile - 微服务构建场景
pipeline {
    agent any

    environment {
        SERVICE_NAME = 'user-service'
        VERSION      = "1.0.${BUILD_NUMBER}"
    }

    stages {
        stage('编译服务') {
            steps {
                sh """
                    mkdir -p build/${SERVICE_NAME}
                    echo "service-binary" > build/${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}.jar
                    echo "docker-config" > build/${SERVICE_NAME}/Dockerfile
                    echo "readme" > build/${SERVICE_NAME}/README.md
                """
            }
        }

        stage('归档服务产物') {
            steps {
                // 按服务名组织归档路径
                archiveArtifacts artifacts: "build/${SERVICE_NAME}/**"

                // 同时归档所有服务的索引文件
                sh '''
                    mkdir -p build/index
                    echo "${SERVICE_NAME} ${VERSION}" >> build/index/services.txt
                '''
                archiveArtifacts artifacts: 'build/index/**'
            }
        }
    }
}

6.3 多平台交叉编译场景

当需要同时产出Linux和Windows版本的构建产物时,路径管理更加复杂。

// Jenkinsfile - 多平台交叉编译场景
pipeline {
    agent {
        label 'cross-compile-node'
    }

    stages {
        stage('交叉编译 - Linux') {
            steps {
                sh '''
                    mkdir -p artifacts/linux-x64
                    mkdir -p artifacts/linux-arm64
                    echo "linux-x64-binary" > artifacts/linux-x64/myapp-x64
                    echo "linux-arm64-binary" > artifacts/linux-arm64/myapp-arm64
                '''
            }
        }

        stage('交叉编译 - Windows') {
            steps {
                sh '''
                    mkdir -p artifacts/windows-x64
                    echo "windows-x64-binary" > artifacts/windows-x64/myapp-x64.exe
                '''
            }
        }

        stage('归档多平台产物') {
            steps {
                // 归档所有平台产物
                archiveArtifacts artifacts: 'artifacts/**'

                // 生成平台清单
                sh '''
                    mkdir -p artifacts/manifest
                    for platform in linux-x64 linux-arm64 windows-x64; do
                        sha256sum artifacts/${platform}/myapp* > artifacts/manifest/${platform}.sha256
                    done
                '''
                archiveArtifacts artifacts: 'artifacts/manifest/**'
            }
        }
    }
}

七、技术优缺点分析

7.1 Jenkins原生归档机制的优势

Jenkins内置的archiveArtifacts功能虽然简单,但有几个显著优势。第一,零配置集成:不需要安装额外的插件或配置外部存储,开箱即用。第二,与Pipeline原生兼容:archiveArtifacts是Pipeline步骤的一部分,可以直接在Jenkinsfile中使用,无需额外的脚本或工具。第三,UI友好:归档的产物会直接展示在构建详情页面上,点击即可下载,对非技术人员也友好。第四,支持指纹追踪:开启fingerprint后,Jenkins会记录文件的哈希值,便于跨项目追踪依赖关系。

7.2 Jenkins原生归档机制的局限

原生归档机制的局限性也很明显。第一,不支持大文件:归档的文件会存储在Jenkins服务器上,文件太大容易导致磁盘爆满。第二,没有版本管理:产物只有"有"和"没有"两种状态,不支持标签、分支等版本管理概念。第三,缺少CDN加速:产物只能通过Jenkins UI或API下载,没有CDN分发能力。第四,清理策略有限:只能按构建次数或时间保留,无法按标签、分支等维度精细化控制。

7.3 替代方案对比

对于大规模或复杂的构建产物管理需求,可以考虑以下替代方案:

// Jenkinsfile - 使用外部存储的替代方案示例
pipeline {
    agent any

    stages {
        stage('构建') {
            steps {
                sh '''
                    mkdir -p dist
                    echo "artifact" > dist/app.jar
                '''
            }
        }

        stage('归档到Nexus') {
            steps {
                sh '''
                    # 使用Nexus作为制品仓库
                    nexusUrl="http://nexus-server:8081/repository/releases"
                    groupId="com.example"
                    artifactId="myapp"
                    version="${BUILD_NUMBER}"

                    curl -u "deployer:password" -F "file=@dist/app.jar" \
                        -F "maven.plugin.groupId=${groupId}" \
                        -F "maven.plugin.artifactId=${artifactId}" \
                        -F "maven.plugin.version=${version}" \
                        "${nexusUrl}/service/local/artifact/deploy"
                '''
            }
        }

        stage('归档到S3') {
            steps {
                sh '''
                    # 使用AWS S3作为制品存储
                    aws s3 cp dist/app.jar \
                        s3://my-bucket/artifacts/myapp/${BUILD_NUMBER}/app.jar \
                        --storage-class STANDARD_IA
                '''
            }
        }
    }
}

八、注意事项

8.1 权限问题

Jenkins进程以特定用户身份运行,该用户必须对工作空间和目标目录有读写权限。如果Pipeline在Docker容器中运行,需要确保容器内的用户有写入权限。

# 检查Jenkins运行用户的权限
ps aux | grep jenkins
# 输出示例:jenkins  12345  0.5  1.2  xxx  jenkins进程

# 检查目录权限
ls -laZ /var/lib/jenkins/workspace/
ls -laZ /var/lib/jenkins/jobs/

# 如果权限不对,修正为:
# chown -R jenkins:jenkins /var/lib/jenkins/
# chmod -R 755 /var/lib/jenkins/

8.2 磁盘空间管理

构建产物是最容易耗尽磁盘空间的因素之一。建议定期清理和监控。

# 磁盘空间监控脚本 - 建议加入定时任务
#!/bin/bash
JENKINS_HOME="/var/lib/jenkins"
WARN_THRESHOLD=80   # 警告阈值80%
CRIT_THRESHOLD=90   # 临界阈值90%

# 获取磁盘使用率
USAGE=$(df "$JENKINS_HOME" | tail -1 | awk '{print $5}' | tr -d '%')

if [ "$USAGE" -ge "$CRIT_THRESHOLD" ]; then
    echo "CRITICAL: 磁盘使用率 ${USAGE}%,需要立即清理!"
    # 自动清理30天前的旧构建
    find "$JENKINS_HOME"/jobs/*/builds/ -mindepth 1 -maxdepth 1 \
        -type d -mtime +30 -exec rm -rf {} +
elif [ "$USAGE" -ge "$WARN_THRESHOLD" ]; then
    echo "WARNING: 磁盘使用率 ${USAGE}%,建议清理"
fi

echo "当前磁盘使用率: ${USAGE}%"

8.3 并发构建时的路径冲突

如果多个构建同时运行且使用相同路径,可能产生文件冲突。使用lock步骤或unique目录可以避免。

// Jenkinsfile - 处理并发构建路径冲突
pipeline {
    agent any

    options {
        // 限制并发构建数
        disableConcurrentBuilds()
    }

    stages {
        stage('构建') {
            options {
                // 使用锁确保同一时刻只有一个构建在操作共享目录
                lock(resource: 'shared-output-dir')
            }
            steps {
                sh '''
                    mkdir -p shared_output
                    echo "unique-build-${BUILD_NUMBER}" > shared_output/build.txt
                '''
            }
        }
    }
}

// 或者使用唯一目录,允许并发构建
pipeline {
    agent any

    stages {
        stage('构建') {
            steps {
                sh """
                    # 每个构建使用唯一的临时目录
                    WORK_DIR="/tmp/build-${BUILD_NUMBER}-$$"
                    mkdir -p \${WORK_DIR}/output
                    echo "build content" > \${WORK_DIR}/output/artifact.jar

                    # 归档后清理
                    rm -rf \${WORK_DIR}
                """

                archiveArtifacts artifacts: '/tmp/build-*/output/**'
            }
        }
    }
}

8.4 安全性考虑

构建产物中可能包含敏感信息(如数据库密码、API密钥),需要特别注意:

// Jenkinsfile - 安全处理产物中的敏感信息
pipeline {
    agent any

    environment {
        // 敏感变量使用Jenkins Credentials管理,不要硬编码
        DB_PASSWORD = credentials('db-credentials')
    }

    stages {
        stage('构建') {
            steps {
                sh '''
                    mkdir -p dist
                    echo "app-binary" > dist/app.jar

                    # 生成配置文件时,敏感信息从环境变量注入
                    echo "database.url=jdbc:mysql://prod:3306/db" > dist/application.yml
                    echo "database.password=${DB_PASSWORD}" >> dist/application.yml

                    # 注意:上面的配置文件中包含密码,归档后任何人都可能看到!
                    # 更好的做法是不要在产物中包含明文密码
                '''
            }
        }

        stage('安全检查') {
            steps {
                sh '''
                    # 检查产物中是否包含常见敏感模式
                    if grep -rE "(password|secret|key|token)" dist/ | grep -v "^\s*#" > /dev/null 2>&1; then
                        echo "WARNING: 产物中可能包含敏感信息,请检查!"
                        # 可选:阻断构建
                        # exit 1
                    else
                        echo "安全检查通过"
                    fi
                '''
            }
        }

        stage('归档') {
            steps {
                archiveArtifacts artifacts: 'dist/**'
            }
        }
    }
}

九、文章总结

构建产物存储路径配置错误是Jenkins日常使用中最常见的问题之一。它的表现形式多样——可能是产物列表为空、文件内容不完整、路径找不到、环境变量未展开等。排查这类问题没有捷径,核心思路是:从配置定义出发,沿着"配置 → 日志 → API → 文件系统"的路径逐步缩小范围。

预防胜于排查。通过采用标准目录结构、添加验证步骤、配置合理的保留策略、使用共享库统一管理归档配置,可以从根本上减少路径配置错误的发生概率。对于更复杂的场景,可以考虑将产物管理迁移到专业的制品仓库(如Nexus、Artifactory)或云存储(如S3),但这需要根据团队规模和实际需求来决定。

最后,记住一个原则:当不确定归档是否正确时,总是加一个验证步骤。一个构建后检查产物的stage,可能帮你省下好几个小时的排查时间。