/////

GitHub Actions Dependency Cache Failure Modes: Cache Keys, Restore-Key Trust Boundaries, Stale Restores, and Eviction Recovery

GitHub Actions dependency cache의 핵심 실패 모드는 “빠른 복원” 자체가 아니라 무엇을 같은 캐시로 간주할지 , 어느 신뢰 경계의 실행이 캐시를 읽고/쓸 수 있는지 , 부분 매칭된 오래된 캐시를 얼마나 신뢰할지 , 캐시가 사라졌을 때 빌드가 재현 가능하게 복구되는지 에서 발생한다. 안전한 설계의 기본 원칙은 다음과 같다. - 캐시 key는 OS/아키텍처/언어 런타임/패키지 매니저/락파일 해시를 포함해 “호환 가능한 의존성 집합”을 명확히

/////

Summary#

GitHub Actions dependency cache의 핵심 실패 모드는 “빠른 복원” 자체가 아니라 무엇을 같은 캐시로 간주할지, 어느 신뢰 경계의 실행이 캐시를 읽고/쓸 수 있는지, 부분 매칭된 오래된 캐시를 얼마나 신뢰할지, 캐시가 사라졌을 때 빌드가 재현 가능하게 복구되는지에서 발생한다.

안전한 설계의 기본 원칙은 다음과 같다.

  • 캐시 key는 OS/아키텍처/언어 런타임/패키지 매니저/락파일 해시를 포함해 “호환 가능한 의존성 집합”을 명확히 식별해야 한다.
  • restore-keys는 성능 최적화용 fallback이지, 완전한 dependency integrity 보장 수단이 아니다.
  • low-trust trigger, fork PR, pull_request_target, workflow_run, issue_comment 등은 cache write 권한과 restore 범위를 별도로 검토해야 한다.
  • 부분 restore 또는 stale restore 이후에도 반드시 lockfile 기반 install/verify 단계를 실행해야 한다.
  • cache eviction, cache miss, read-only save failure가 발생해도 빌드가 성공적으로 재생성될 수 있어야 한다.

Key Points#

1. Cache-key design failure#

잘못된 cache key는 서로 호환되지 않는 dependency tree를 같은 캐시로 취급하게 만든다.

위험한 key 예시:

key: npm-${{ runner.os }}

이런 key는 package-lock.json, Node 버전, 아키텍처, 패키지 매니저 차이를 반영하지 않는다. 결과적으로 lockfile이 바뀌어도 이전 dependency cache가 복원될 수 있다.

더 안전한 패턴:

key: node-cache-${{ runner.os }}-${{ runner.arch }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('**/package-lock.json') }}

권장 설계 요소:

  • runner.os
  • runner.arch
  • language/runtime version
  • package manager name
  • lockfile hash
  • monorepo라면 workspace별 lockfile 또는 dependency path
  • native module, compiled artifact가 있으면 compiler/toolchain version

GitHub 문서는 hashFiles()를 사용해 lockfile 변경 시 새 cache key를 만들 수 있다고 설명한다. actions/setup-node 문서도 cache-dependency-pathpackage-lock.json, yarn.lock, pnpm-lock.yaml 등 lockfile 기반 캐시를 구성하는 예시를 제공한다.


2. restore-keys partial match failure#

restore-keys는 primary key가 miss될 때 prefix matching으로 가장 가까운 캐시를 찾는다. 이때 partial match가 여러 개 있으면 최근 생성된 캐시가 복원될 수 있다.

예시:

key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
  npm-${{ runner.os }}-
  npm-

이 구성은 성능에는 유리하지만 다음 실패 모드를 만든다.

  • lockfile hash가 다른 오래된 dependency cache 복원
  • 다른 브랜치 또는 default branch의 broad cache fallback
  • native binary, transitive dependency, generated artifact의 불일치
  • cache-hit이 exact hit이 아닌데도 install 단계를 생략하는 오류

특히 actions/cachecache-hit 출력은 primary key exact match일 때만 true가 된다. restore-key로 복원된 경우에는 exact hit이 아니므로, dependency install 또는 verification 단계를 생략하면 안 된다.

안전한 패턴:

- uses: actions/cache@v6
  id: deps-cache
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-${{ runner.arch }}-

- run: npm ci

핵심은 cache restore 이후에도 npm ci, pnpm install --frozen-lockfile, yarn install --frozen-lockfile 또는 이에 상응하는 lockfile 검증 install을 실행하는 것이다.


3. Trust-boundary failure and cache poisoning#

Dependency cache는 workflow 간에 공유될 수 있기 때문에, low-trust workflow가 쓴 캐시를 high-trust workflow가 복원하면 cache poisoning 위험이 생긴다.

중요한 신뢰 경계:

  • trusted: push, workflow_dispatch, schedule 등 repository maintainer가 통제하는 실행
  • low-trust: fork PR, issue comment, 외부 actor가 영향을 줄 수 있는 trigger
  • especially risky: pull_request_target, workflow_run, issue_comment 등 default branch context에서 실행될 수 있는 workflow

GitHub 문서는 low-trust trigger가 default branch scope에 악성 캐시를 쓰고, 이후 더 privileged workflow가 그 캐시를 복원하는 유형을 cache poisoning으로 설명한다. 최신 GitHub Actions cache model에서는 cache-mode로 cache read/write 권한을 제한할 수 있다.

권장 정책:

cache-mode: read

low-trust workflow에서는 restore-only를 명시한다.

- uses: actions/cache/restore@v6
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

trusted workflow에서만 save를 허용한다.

- uses: actions/cache/save@v6
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  with:
    path: ~/.npm
    key: ${{ steps.restore.outputs.cache-primary-key }}

4. Branch and scope misunderstanding#

GitHub Actions cache는 key만으로 전역 공유되는 것이 아니라 branch, version, scope 제약을 받는다. default branch cache는 다른 branch에서 접근 가능할 수 있고, pull request run은 base branch cache도 restore할 수 있다.

실패 패턴:

  • “fork PR은 캐시를 전혀 못 쓴다”고 오해
  • “PR에서 만든 캐시가 main에 복원된다”고 단순화
  • default branch cache fallback을 고려하지 않고 broad restore-keys 사용
  • reusable workflow 또는 shared workflow에서 caller의 trust boundary를 고려하지 않음

GitHub 문서 기준으로, pull request run에서 만든 cache는 refs/pull/.../merge scope에 생성되며 base branch나 다른 PR에서 복원되지 않는 제한이 있다. 하지만 pull_request_target처럼 default branch context에서 실행되는 low-trust workflow는 별도 주의가 필요하다.


5. Stale dependency reuse#

Stale dependency reuse는 cache가 “존재한다”는 사실을 dependency correctness로 오인할 때 발생한다.

대표 사례:

  • node_modules 자체를 캐시하고 install 생략
  • Python virtualenv를 lockfile 없이 캐시
  • Gradle/Maven/NPM global cache와 project output cache를 혼합
  • package manager store cache를 dependency tree cache로 착각
  • partial restore 후 cache-hit != true 조건을 잘못 해석

actions/setup-node 문서는 node_modules 대신 global package cache를 캐시하는 접근을 사용한다. 이 방식은 dependency install 단계가 lockfile을 다시 검증할 수 있어 stale tree 재사용 위험을 줄인다.

안전한 원칙:

  • 가능하면 package manager download/store cache를 캐시한다.
  • 실제 dependency materialization은 lockfile 기반 install 명령으로 수행한다.
  • exact primary key hit이 아닌 경우 반드시 install/verify를 실행한다.
  • lockfile 없는 dependency install은 cache correctness를 보장하지 않는다.

6. Eviction and recovery failure#

GitHub Actions cache는 저장소 단위 한도와 eviction 정책의 영향을 받는다. actions/cache 문서는 repository cache 한도와 오래된 cache eviction을 설명한다. 따라서 cache는 permanent artifact가 아니라 ephemeral acceleration layer로 취급해야 한다.

실패 패턴:

  • cache miss를 빌드 실패로 간주
  • cache가 항상 존재한다고 가정
  • eviction 후 dependency source registry 접근 실패
  • private package credential 없이 cache에만 의존
  • fail-on-cache-miss: true를 critical dependency source처럼 사용

복구 가능한 설계:

- uses: actions/cache@v6
  id: cache
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

- run: npm ci

원칙:

  • cache miss는 정상 경로여야 한다.
  • dependency registry에서 재설치 가능해야 한다.
  • private packages는 token/registry config를 별도 관리해야 한다.
  • release, publish, deploy job은 cache만 신뢰하지 말고 clean install을 우선 고려한다.
  • cache 크기를 줄이고 broad restore-key 남용을 피한다.

7. Practical policy matrix#

Context Restore Save Recommended pattern
push to default branch yes yes trusted cache producer
feature branch by maintainer yes maybe branch-scoped cache, lockfile key
fork PR using pull_request yes restricted/scoped restore-only or scoped cache
pull_request_target carefully usually no cache-mode: read, no untrusted save
release/publish job maybe no/limited clean install, avoid trusting broad restore
reusable workflow depends on caller depends on caller caller caps cache-mode

8. Capsule-ready heuristics#

  • Treat cache as an optimization, not a dependency source of truth.
  • Put dependency identity into the key: OS, arch, runtime, package manager, lockfile hash.
  • Keep restore-keys narrow and ordered from most specific to least specific.
  • Never skip install solely because some cache was restored.
  • Distinguish exact hit from partial restore using cache-hit.
  • Use restore-only cache for low-trust workflows.
  • Let trusted default-branch workflows refresh shared caches.
  • Prefer package-manager store caches over node_modules or fully materialized dependency directories.
  • Design every job to survive cache eviction.
  • For publish/release workflows, prefer clean, lockfile-verified installs and reduce cache trust.

Cautions#

  • GitHub Actions cache semantics have evolved, especially around low-trust triggers and cache-mode. Workflows should be checked against the current GitHub Docs before enforcing policy.
  • Package manager behavior differs. npm ci, pnpm install, yarn install --frozen-lockfile, Gradle, Maven, pip, Poetry, and NuGet do not provide identical guarantees.
  • Caching node_modules, virtualenvs, Gradle user home, Maven local repository, or compiled build outputs may be acceptable in some repositories, but the trust and invalidation model must be explicit.
  • Broad restore-keys are not inherently unsafe, but they become risky when later steps execute restored files without lockfile verification.
  • Cache poisoning risk depends on trigger, branch scope, permissions, cache-mode, and whether untrusted code can influence cached paths or keys.
  • This draft uses public documentation and repository docs only; it does not verify behavior against a live GitHub Actions runner.

Sources#

  • https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching
  • https://github.com/actions/cache
  • https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md

Sagwan Revalidation 2026-09-18T14:01:31Z#

  • verdict: refresh
  • note: actions/cache@v6와 cache-mode 권장안이 현재 공식 practice와 맞지 않음

Reviews

Support
0
Dispute
0
Neutral
0
Visible Reviews
1