CI比本地先爆冲突,是因为CI从零构建、严格按composer.lock还原并校验平台要求(PHP版本/扩展),而本地常因残留vendor/、过期lock或扩展松散跳过校验。

CI 环境里 Composer 报依赖冲突,不是环境脏了,而是本地没暴露的约束矛盾在干净环境中被严格触发了。
为什么 CI 总比本地先爆冲突?
本地开发机往往残留旧 vendor/、过期 composer.lock,或 PHP 扩展版本松散(比如装了 ext-mbstring 但 CI 没装),导致 Composer 跳过部分校验;CI 是从零构建,composer install 会严格按 composer.lock 还原,同时检查平台要求(PHP 版本、扩展、config.platform)——只要 lock 文件里某行依赖实际需要 ext-gd,而 CI 镜像没装,就直接失败。
- CI 使用的 PHP 版本可能比本地高/低,触发
php约束不匹配(如本地是 PHP 8.1,CI 是 8.2,而某包require了"php": "^8.1.0"但未兼容 8.2) -
composer.lock里记录的包版本,依赖的某个子包在 Packagist 上已被 yank(下架),CI 拉取时找不到,报Could not find package... - 私有库用
path类型配置在本地,但 CI 没挂载对应路径,Composer 回退到 Packagist 查找,结果拉到一个不兼容的稳定版
CI 流程中必须加的三道防线
别等 composer install 在最后一步失败。把验证前移到 pipeline 前半段:
- 运行
composer validate --strict:检查composer.json格式、字段合法性,以及conflict字段是否语法错误(如用了*包名) - 执行
composer install --dry-run --no-interaction:模拟安装全过程,不写文件,只让 SAT 求解器跑一遍——它比真实 install 更早暴露“无解”问题 - 加
composer show -t | head -n 50截图式输出依赖树顶端 50 行:用于快速比对 PR 前后关键包(如laravel/framework、symfony/console)是否意外降级
如何让 composer.lock 成为可信基准?
composer.lock 不是缓存,是契约。CI 失败常因它和 composer.json 不一致,或它本身已过时:
- 每次
composer update后,必须提交更新后的composer.lock——漏提等于把冲突风险甩给 CI - 禁止在 CI 中运行
composer update(除非明确要刷新依赖),它会忽略 lock 文件,触发全量重算,极可能卡住或引入不稳定版本 - 若需支持多 PHP 版本(如 PHP 8.1 + 8.2),在
composer.json的config.platform显式声明最低要求:"php": "8.1",避免 Composer 自动探测 CI 环境里的高版本 PHP 导致选错包 - 私有包发布新 tag 后,立刻在主项目跑
composer update vendor/private-sdk --with-dependencies并提交 lock,而不是等 CI 报错才处理
最易被忽略的一点:CI 镜像里 composer 版本太老(如 2.2.x),无法解析新版 composer.json 的字段(如 conflict 的嵌套写法或 require-dev 的条件加载)。上线前先确认 CI 使用的是 composer self-update 到最新稳定版。











