根本原因是jenkins默认不保留vendor目录且不共享composer缓存;应采用分层缓存:全局~/.composer/cache复用+按composer.lock哈希隔离vendor目录,并将php版本和扩展列表纳入缓存key以确保环境一致性。

为什么 vendor 目录在 Jenkins Pipeline 中反复重装?
因为默认 workspace 每次构建都是干净的(除非显式复用),composer install 会从头拉包、解压、生成 autoloader——这既慢又浪费带宽,还可能触发 Packagist 限流。
根本原因不是 Composer 本身,而是 Jenkins 默认不保留 vendor/,也不共享 ~/.composer/cache。缓存得拆成两层:全局 Composer 缓存(下载层) + 项目 vendor 目录(构建层)。
用 archiveArtifacts + unstash 复用 vendor 目录行不行?
不推荐。虽然语法上能 stash vendor/,但实际极易出错:
-
vendor/包含大量软链接和平台相关扩展(如ext-redis编译产物),换 agent 或 PHP 版本后直接报Class not found - stashing 多达数万小文件,I/O 开销大,超时风险高
- 不同分支、不同
composer.lock版本混用vendor/,会导致 autoload 错乱或 class 冲突
结论:只缓存 vendor/ 是伪优化,必须配合 lock 文件哈希校验或隔离存储。
推荐方案:分层缓存 + 路径隔离
核心是两件事:1)复用 Composer 全局 cache;2)按 composer.lock 的 SHA256 哈希值隔离 vendor 目录。这样既安全又快。
在 Jenkinsfile 中这么做:
steps {
script {
// 计算 lock 文件指纹,作为缓存 key
def lockHash = sh(script: 'sha256sum composer.lock | cut -d" " -f1', returnStdout: true).trim()
def cacheKey = "composer-vendor-${lockHash}"
<pre class='brush:php;toolbar:false;'>// 复用全局 Composer cache(挂载到 ~/.composer/cache)
sh 'mkdir -p ~/.composer/cache'
// 尝试恢复 vendor 目录(key 命中才解压)
sh "rm -rf vendor && mkdir -p .cache && cd .cache && if [ -f ../.cache/${cacheKey}.tar.gz ]; then tar -xzf ../.cache/${cacheKey}.tar.gz; fi"
// 执行安装(有 vendor 就跳过下载,没命中就重装)
sh 'composer install --no-interaction --no-progress --optimize-autoloader'
// 缓存 vendor(仅当新生成时才保存)
sh "if [ -d vendor ]; then cd .cache && tar -czf ../.cache/${cacheKey}.tar.gz ../vendor; fi"} }
关键点:
- 必须用
composer.lock哈希作 key,不能用分支名或时间戳 -
~/.composer/cache需在 Jenkins agent 上持久化(比如挂载宿主机目录),否则每次重装包 -
--optimize-autoloader必须加,否则即使 vendor 存在,autoload 仍可能慢 - 别把
.cache/放进archiveArtifacts,它只是中间态,Jenkins workspace 清理时自动丢弃
PHP 版本或扩展变更时,缓存怎么自动失效?
Composer 本身不感知 PHP 环境,所以得靠外部控制。最简单可靠的做法:
- 在
Jenkinsfile中显式读取php -v和php -m输出,拼进 cache key,例如:composer-vendor-${lockHash}-${phpVersion}-${extListHash} - 或者更彻底:把 PHP 版本写进
composer.json的config.platform.php,让composer install自动校验并拒绝不匹配环境 - CI agent 上禁用
ext-*动态加载(比如用docker-php-ext-install静态编译),避免运行时才暴露扩展缺失
漏掉这点,缓存看起来快了,但测试通过、线上却报 Call to undefined function redis_connect()——这种坑没法靠日志提前发现。










