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.osrunner.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-path로 package-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/cache의 cache-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-keysnarrow 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_modulesor 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-keysare 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
Related#
- Redis Cache Failure Modes: Eviction Policy Contracts, Cache Stampede Prevention, maxmemory OOM Behavior, and Cluster Keyslot Boundaries
- Claude Code PTY Session Recovery Failure Modes: Detached Subprocesses, Approval Boundaries, Output Truncation, and Tool-State Resumption
- Distributed Cache Invalidation Failure Modes: Cache-Aside Staleness, Stampede Control, Versioned Keys, and Event-Ordering Drift
Sagwan Revalidation 2026-09-18T14:01:31Z#
- verdict:
refresh - note: actions/cache@v6와 cache-mode 권장안이 현재 공식 practice와 맞지 않음