lenec ru

← все посты

GitHub Actions: правильное кэширование зависимостей pnpm

16K

Скорость CI на простых проектах решает: разработчик ждёт PR-checks, чтобы влить ветку. Если зависимости подтягиваются с нуля каждый раз — добавляются минуты ожидания на ровном месте. Расскажу, как я настраиваю кэш pnpm в GitHub Actions, чтобы он реально помогал, а не делал вид.

Зачем именно pnpm

pnpm в 2026-м стал моим default-выбором: он компактнее, точнее по lockfile, поддерживает workspaces и не страдает от npm ERR! ERESOLVE. Кэш у него тоже устроен иначе: один глобальный store с hard-link на файлы, а не node_modules в каждом проекте. Это влияет на то, что именно нужно кэшировать в CI.

Базовая настройка

Минимальный workflow, который у меня лежит как стартер:

name: CI

on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile
      - run: pnpm typecheck
      - run: pnpm test --reporter=dot
      - run: pnpm build

Что здесь важно:

  • concurrency. Если в один PR кто-то сделал три коммита, отменим лишние прогоны.
  • pnpm/action-setup@v4 до setup-node. Иначе setup-node не найдёт pnpm для кэширования.
  • cache: 'pnpm' в setup-node. Это и есть включение кэша pnpm-store, ничего больше делать не надо.
  • pnpm install --frozen-lockfile. На CI обязательно: лочит версии под lockfile, не позволяет ничего обновить.

На свежем кэше первый прогон идёт около минуты на средний проект. На последующих — секунды.

Что кэшируется и что нет

setup-node кэширует только pnpm-store (или npm cache, или yarn cache, в зависимости от менеджера). node_modules и .pnpm-store в репозитории не кэшируются.

В рабочем каталоге pnpm создаёт node_modules и в нём симлинки/hard-links на файлы из глобального store. На сервере GitHub Actions глобальный store по умолчанию в ~/.local/share/pnpm/store. Именно туда action кладёт восстановленный кэш.

Из-за этого pnpm install при попадании в кэш ускоряется значительно: модули не качаются, симлинки строятся.

Custom-кэш для node_modules

Иногда хочется кэшировать ещё и node_modules, чтобы не ждать построение симлинков. Это делается через actions/cache с ключом по lockfile:

- uses: actions/cache@v4
  with:
    path: |
      node_modules
      **/node_modules
    key: pnpm-modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
    restore-keys: |
      pnpm-modules-${{ runner.os }}-

На моём опыте — выигрывает 5–15 секунд, потому что pnpm install и без того быстрый при попадании в store-кэш. Возьму это, если есть workspaces с десятками пакетов и стройка симлинков заметно тормозит.

Работа с workspaces

На монорепе с pnpm workspaces всё то же самое работает по тем же правилам. Один lockfile в корне — один кэш-ключ. Если кто-то добавил/удалил зависимость в любом пакете — lockfile поменялся — кэш пересобирается.

- run: pnpm install --frozen-lockfile
- run: pnpm -r --filter './apps/api...' build

Фильтры pnpm удобно использовать, чтобы билдить только нужный кусок. ./apps/api... с многоточием означает «api и его внутренние зависимости».

Только нужные jobs запускаются

Если у тебя несколько jobs (frontend, backend, тесты) — каждый из них тратит свои минуты. Я часто использую paths и paths-ignore, чтобы не запускать ненужное:

on:
  push:
    paths:
      - 'apps/api/**'
      - 'packages/**'
      - 'pnpm-lock.yaml'
      - '.github/workflows/api-ci.yml'

На больших монорепо это снимает половину нагрузки на CI. Внимание: на github paths-ignore смотрит на коммит целиком, не на конкретный файл. Если в одном PR изменился код api и код client — оба workflow стартуют.

Подводные камни

Кэш не подхватывается

Самое частое — кэш отдельный для каждого runner.os и каждой версии pnpm. Если у тебя в matrix несколько Node-версий, на каждую будет свой кэш. Это нормально. Если переключился с pnpm 8 на 9 — первый прогон будет долгим, ничего не сделать.

«Lockfile not up to date»

--frozen-lockfile валидно роняет CI, если кто-то добавил пакет, не обновив lockfile. Это не баг, это фича. Лечится pnpm install локально и коммитом lockfile.

setup-node перед action-setup

Если поставить setup-node раньше pnpm/action-setup, опция cache: 'pnpm' упадёт с «pnpm not found». Порядок строгий.

Огромный store

Глобальный store растёт с каждым новым пакетом. На многих ветках с разными версиями он легко уходит за гигабайт. У GitHub Actions есть лимит на размер кэша на репозиторий (10 ГБ суммарно), и старые кэши вытесняются. На моих проектах не упирался — но если у тебя десятки веток в активной разработке, добавь pnpm store prune в шаг билда, чтобы убрать неиспользуемое.

Кэш сбивается из-за хеша

Если используешь ${{ hashFiles('pnpm-lock.yaml') }}, и lockfile меняется на каждом коммите (например, кто-то регенерирует автоматически) — кэш не помогает. Нужна гигиена с lockfile.

Полный пример с тестами и сборкой

name: CI

on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint-and-test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck
      - run: pnpm test --coverage
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage
          path: coverage/
          retention-days: 7

  build:
    runs-on: ubuntu-24.04
    needs: lint-and-test
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

На чистом дереве этот workflow проходит ~2 минуты, на повторных запусках с кэшем — около минуты. Тесты и билд — две отдельные jobs: можно запускать параллельно, если убрать needs, на больших проектах ускоряет ещё раз в полтора.

Self-hosted runners

Если есть self-hosted runner, кэш всё ещё работает: action-setup и setup-node кэшируют в стандартные места внутри runner-а. Преимущество self-hosted — быстрее CPU и сеть, плюс возможность держать дополнительные кэши прямо на диске. Если у тебя self-hosted на VPS, имеет смысл также примонтировать ~/.local/share/pnpm/store как persistent volume — кэш будет переживать пересборки runner-а.

Что у меня в чек-листе

  • concurrency с cancel-in-progress.
  • pnpm/action-setup, потом setup-node с cache: 'pnpm'.
  • --frozen-lockfile в install.
  • На монорепе — фильтры по pnpm.
  • paths в on, чтобы не запускать ненужные jobs.
  • Артефакты coverage и dist через upload-artifact с retention.

Этот шаблон у меня переиспользуется на десятке проектов с минимальными изменениями. Никаких самописных кэш-ключей, никаких хитростей. Простая настройка, которая стабильно работает.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.