Короткий ответ
Скорее всего ключ кэша не включает хэш lock-файла (или использует слишком общий/статичный ключ), поэтому CI восстанавливает устаревший кэш `node_modules`, не замечая, что зависимости изменились; решение — построить ключ кэша на хэше актуального lock-файла (`package-lock.json`/`yarn.lock`) и проверить, что сборка действительно переустанавливает пакеты при изменении этого хэша.
Как это работает подробнее
системы кэширования CI (GitHub Actions cache, GitLab CI cache) сопоставляют текущий ключ с сохранёнными записями; если ключ совпадает — кэш восстанавливается целиком, без проверки его актуальности относительно реального состояния зависимостей. Если ключ не меняется при изменении `package-lock.json`, разработчик получает старые пакеты молча, без явной ошибки.
Пример кода
example.yaml
# Было (проблема):
- uses: actions/cache@v4
with:
path: node_modules
key: node-modules-cache # статичный ключ, никогда не меняется
# Стало (исправлено):
- uses: actions/cache@v4
with:
path: node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}Что использовать на практике
- На практике. для диагностики полезно явно вывести в логе пайплайна хэш lock-файла и факт попадания/непопадания в кэш (`cache hit` / `cache miss`), а также версии реально установленных пакетов (`npm list` или `cat node_modules/.package-lock.json`) перед сборкой — расхождение с ожидаемой версией подтвердит гипотезу. Хорошая практика — также держать отдельный `restore-keys` как fallback, но финальный ключ должен быть точным.
- Пример. Ожидаемое поведение: после изменения `package-lock.json` в логе появляется `Cache not found for input keys: node-modules-<новый-хэш>`, зависимости переустанавливаются, и `npm list <пакет>` в последующей стадии показывает ожидаемую новую версию.
Подводные камни
- Частые ошибки. включают в ключ только имя ветки или статичную строку без привязки к содержимому зависимостей; очищают кэш вручную «иногда», вместо того чтобы доверять автоматической инвалидации по хэшу, что создаёт нестабильное поведение пайплайна.