Dependency cache
Механика `actions/cache` и setup-actions cache: `key`, `restore-keys`, cache hit/miss, immutability, eviction, cache poisoning risks и дизайн ключей для lockfiles.
Dependency cache
Dependency cache в GitHub Actions — это сохранённые директории с зависимостями или промежуточными файлами, которые можно восстановить в будущих workflow runs. Его цель — ускорить повторные запуски: не скачивать npm, pnpm, Gradle, Maven, pip или cargo-зависимости с нуля, если lockfile и окружение почти не изменились.
Важно не путать cache с Artifacts и reports. Artifact хранит результат конкретного run: bundle, coverage, screenshots, отчёт. Cache — это оптимизация для будущих runs. Он не является надёжным handoff-механизмом между jobs и не должен быть источником истины для deploy. Для маленьких значений внутри workflow нужны Step outputs и job outputs, для файлов между jobs — artifacts, для ускорения установки — cache.
flowchart TD
A[Job starts on runner] --> B[actions/cache restore]
B --> C{Exact key match?}
C -- yes --> D[Restore cache to path\ncache-hit = true]
C -- no --> E{restore-keys match?}
E -- yes --> F[Restore newest prefix match\nnot an exact hit]
E -- no --> G[No cache restored]
D --> H[Package manager install/checks lockfile]
F --> H
G --> H
H --> I{Job completed successfully?}
I -- yes and no exact cache --> J[Save new immutable cache with primary key]
I -- no --> K[Do not save new cache]Модель: restore, install, save
Типовой cache step стоит до установки зависимостей. actions/cache ищет запись по key, восстанавливает найденные файлы в path, затем package manager всё равно запускается и доводит директорию до состояния lockfile. Если точного совпадения нет и job завершился успешно, action сохраняет новый cache с указанным ключом.
name: Node CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache npm downloads
id: npm-cache
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-node-22-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-node-22-
npm-${{ runner.os }}-
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
- name: Cache status
run: echo "cache-hit=${{ steps.npm-cache.outputs.cache-hit }}"Здесь кэшируется не node_modules, а download cache npm в ~/.npm. Это обычно стабильнее: npm ci всё равно пересоберёт рабочую директорию по lockfile, но возьмёт tarballs из cache. Для pnpm и Yarn идея та же: чаще кэшируют глобальный store, а не установленную директорию проекта.
key и restore-keys
key — главный идентификатор cache. В него обычно включают OS, package manager, версию runtime или toolchain, а также hash lockfile. Lockfile важен: именно он говорит, что набор зависимостей изменился. Если поменялся package-lock.json, pnpm-lock.yaml, yarn.lock, poetry.lock, Cargo.lock или gradle.lockfile, должен появиться новый cache key.
Хороший ключ выглядит скучно:
key: pnpm-${{ runner.os }}-node-22-${{ hashFiles('pnpm-lock.yaml') }}Плохой ключ слишком широкий:
key: pnpm-cacheВо втором варианте разные версии Node, разные lockfiles и разные ветки начнут бороться за один смысловой cache. GitHub не изменит существующую запись на месте, потому cache immutable: старый cache нельзя «дописать» или «исправить», можно только создать новый ключ. Если вы поменяли формат cache, добавьте ручной префикс версии: v2-pnpm-....
restore-keys — это fallback. Если точного cache по key нет, GitHub ищет частичные совпадения в указанном порядке и берёт наиболее свежий подходящий cache. Это полезно после изменения lockfile: старый store может содержать 90% нужных пакетов, а package manager докачает остальное.
restore-keys: |
pnpm-${{ runner.os }}-node-22-
pnpm-${{ runner.os }}-Не делайте fallback слишком широким для чувствительных или плохо изолированных build outputs. Чем шире restore key, тем выше шанс получить мусор из похожего, но не эквивалентного окружения.
cache-hit не заменяет install step
Output cache-hit показывает, было ли точное совпадение по основному ключу. Практичный паттерн — использовать проверку != 'true', если нужно выполнить диагностику или тяжёлый prefetch только при miss. Но установку зависимостей обычно не пропускают: npm ci, pnpm install --frozen-lockfile, bundle install, cargo fetch или gradle build должны сами проверить консистентность.
- name: Explain cache miss
if: ${{ steps.npm-cache.outputs.cache-hit != 'true' }}
run: echo "No exact dependency cache; package manager will populate it."
- run: npm ciЭто защищает от ситуации, где cache восстановился по restore-keys, но не соответствует текущему lockfile полностью. Такой cache полезен как ускоритель, но не как доказательство, что зависимости уже установлены правильно.
Built-in cache в setup-actions
Многие setup-actions умеют включать cache без прямого actions/cache: например, actions/setup-node поддерживает cache: npm, cache: yarn, cache: pnpm и путь к lockfile через cache-dependency-path. Это короче и часто достаточно для обычного CI pipeline.
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm testЯвный actions/cache нужен, когда нужно несколько paths, нестандартный store, ручное разделение restore/save, особые restore keys или cache для toolchain/build outputs. Built-in cache хорош для типового package-manager cache, но не заменяет понимание ключей.
Scope, eviction и matrix
Cache scoped по repository, key, cache version и branch. Cache из default branch доступен другим branches, а cache, созданный для PR merge ref, имеет более узкую область и обычно не становится общим cache для base branch. При Matrix strategy включайте matrix-измерения в ключ: OS, runtime, architecture, compiler, lockfile. Иначе Linux job может попытаться использовать Windows cache, или Node 24 — store, собранный под Node 20.
key: cargo-${{ runner.os }}-${{ matrix.rust }}-${{ hashFiles('Cargo.lock') }}Cache также не вечен. GitHub удаляет давно неиспользуемые entries и применяет лимиты repository storage; при переполнении старые по last access записи вытесняются. Поэтому cache должен быть полезным, но необязательным. Если workflow падает только потому, что cache исчез, это уже баг pipeline. На GitHub-hosted runners особенно важно помнить: runner для job обычно свежий, локальная файловая система не переживает job, а долговременное состояние нужно хранить явно — cache или artifact.
Security: cache poisoning и secrets
Cache не должен содержать secrets, .env, cloud credentials, cookies браузерных тестов, private registry tokens или production config. GitHub прямо предупреждает: anyone with read access and PR capability may be able to access cache contents in allowed scenarios. Маскирование из Variables, env и secrets относится к логам, а не к файлам, которые вы сами положили в cache.
Cache poisoning — это ситуация, когда недоверенный workflow создаёт или подсовывает cache, который позже используется доверенным job. Риск особенно неприятен, если кэшируются исполняемые build outputs, wrappers, tool binaries или директории, которые затем запускаются без проверки. Для обычных dependency stores риск ниже, потому lockfile и package manager проверяют версии и integrity, но он не нулевой.
Практики защиты простые: не кэшировать секреты, не деплоить из cache, не использовать слишком широкие keys, не смешивать untrusted PR и release/deploy pipelines, закреплять third-party actions как обсуждается в Marketplace actions и pinning, а security-sensitive triggers сверять с Secure use и threat model и pull_request_target и untrusted PR.
Когда cache не нужен
Cache не бесплатен: его надо скачать, распаковать, потом иногда загрузить обратно. Для маленьких проектов с быстрым npm ci или go test cache может съесть больше времени, чем сэкономить. Для нестабильных build outputs cache может создать труднообъяснимые flaky failures. Начинайте с lockfile-based dependency cache, измеряйте job duration, затем добавляйте более агрессивные caches только там, где есть реальная экономия.
Хороший dependency cache незаметен: при miss workflow всё равно проходит, при hit становится быстрее, а при eviction не ломает релизный процесс.
See also
- Artifacts и reports — когда нужно сохранить результат конкретного workflow run.
- Step outputs и job outputs — короткие значения между steps и jobs.
- Workflow commands и summaries — как показать cache hit/miss и краткий CI-итог в summary.
- CI pipeline — где dependency cache стоит в обычной build/test цепочке.
- Matrix strategy — как не устроить конфликт cache между вариантами matrix.
- GitHub-hosted runners — почему runner filesystem нельзя считать долговременным состоянием.
- Secure use и threat model — cache как часть supply-chain threat model.
- pull_request_target и untrusted PR — почему untrusted code нельзя смешивать с доверенными secrets и deploy jobs.
