Learn (Almost) Anything
КаталогСкачать курс

Автоматизация в GitHub

Механика `actions/cache` и setup-actions cache: `key`, `restore-keys`, cache hit/miss, immutability, eviction, cache poisoning risks и дизайн ключей для lockfiles.

ruЭнциклопедия33 урока6 модулей9
GitHub actionsАвтоматизация в GitHubМодель workflowсостояние и артефактыматрицы и runtimeрелизы и environmentsПереиспользование и собственные actionsgovernance и эксплуатацияgithubactions

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]
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]
Как dependency cache восстанавливается, используется package manager и сохраняется только после успешного job.

Модель: 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

Быстрое повторение #rc-1

Почему cache в GitHub Actions нельзя использовать как источник истины для deploy или handoff между jobs?

Показать ответ

Cache — это только оптимизация для будущих runs: запись может исчезнуть, не совпасть по ключу или восстановиться частично. Для файлов между jobs нужны artifacts, для маленьких значений — step/job outputs.

Источники

  1. GitHub Docs — Dependency caching reference
  2. actions/cache README
  3. actions/setup-node advanced usage — Caching packages data