원문: https://til.simonwillison.net/github-actions/uvx-github-actions-cache
Simon Willison 블로그 노트: https://simonwillison.net/2026/Jul/14/
작성일: 2026-07-13 (블로그 노트: 2026-07-14)
GitHub Actions 워크플로우에서 uvx name-of-tool을 빠르게 실행하고 싶을 때가 있다. 하지만 그때마다 PyPI에 네트워크 요청을 보내는 건 원치 않는다. 처음 한 번만 도구를 받아서 이후 실행 시에는 GitHub Actions 캐시에서 재사용하고 싶다.
이런 패턴을 pyproject.toml이나 requirements.txt 파일의 해시값을 캐시 키로 사용하는 방법으로 해결해 왔는데, 간단한 스크립트에서는 캐시 하나 때문에 별도 파일을 관리하고 싶지 않다.
오늘 드디어 마음에 드는 패턴을 찾았다 (GPT-5.6 Sol의 도움으로).
핵심은 UV_EXCLUDE_NEWER 환경 변수다. uvx --exclude-newer DATE와 동일하게 작동하며, 특정 날짜 기준으로 최신 패키지를 설치하도록 uv에 알려준다.
그리고 이 날짜를 GitHub Actions의 캐시 키로도 사용할 수 있다. 즉, 스크립트에서 날짜를 한 곳에만 설정하면 모든 도구에 대해 반복 가능한 버전 세트를 얻을 수 있다. 캐시를 무효화하고 싶을 때는 그 한 곳의 날짜만 올리면 된다.
name: Run tools
on:
workflow_dispatch:
env:
# 이 날짜를 올리면 최신 패키지 릴리스를 허용하고 캐시가 새로 생성됨:
UV_EXCLUDE_NEWER: "2026-07-12"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install uv and restore cache
id: setup-uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
cache-dependency-glob: ""
cache-suffix: "tools-${{ env.UV_EXCLUDE_NEWER }}"
prune-cache: false
- name: Require cache-only uv on cache hits
if: steps.setup-uv.outputs.cache-hit == 'true'
run: echo "UV_OFFLINE=1" >> "$GITHUB_ENV"
- name: Run sqlite-utils
run: uvx sqlite-utils --version
- name: Run datasette
run: uvx --pre datasette --version
- name: Run LLM
run: uvx llm --version
astral-sh/setup-uv는 Astral의 공식 uv 설치 Action이다. Astral 자체 releases.astral.sh 사이트에 매번 요청하는 것이 좀 못마땅하지만, 그게 그들이 원하는 방식이라면 그러려니 한다.
각 설정의 의미:
enable-cache: true: GitHub Actions 캐싱 활성화cache-dependency-glob: "": pyproject.toml 등을 캐시 키로 사용하는 기능 비활성화cache-suffix: "tools-${{ env.UV_EXCLUDE_NEWER }}":UV_EXCLUDE_NEWER날짜값으로 캐시 키 설정prune-cache: false: Astral 기본값은 다운로드된 wheel을 캐시에서 삭제하는 것(내가 원하는 것과 정반대!)이므로 비활성화
uv 공식 문서는 이와 반대 방향을 권장한다:
하지만 지속적 통합 환경에서는 사전 빌드된 wheel을 캐싱하는 것이 바람직하지 않을 수 있습니다. uv의 경우, 캐시에서 사전 빌드된 wheel을 생략하고 매 실행마다 레지스트리에서 새로 다운로드하는 것이 더 빠른 경우가 많습니다.
개인적으로는 GitHub 캐시 복원이 PyPI 신규 설치보다 약간 느릴 수 있더라도, CI 빌드마다 PyPI CDN에 요청을 보내는 것보다 캐시를 쓰는 편이 낫다.
캐시 히트 시에는 다음 블록으로 캐시가 올바르게 사용되는지 강제한다:
- name: Require cache-only uv on cache hits
if: steps.setup-uv.outputs.cache-hit == 'true'
run: echo "UV_OFFLINE=1" >> "$GITHUB_ENV"
UV_OFFLINE=1 환경 변수를 설정하면, 해당 도구가 이전에 설치된 적이 없을 경우 uvx tool-name이 실패한다. 캐시 히트가 있을 때만 실행된다.
즉, UV_EXCLUDE_NEWER 날짜를 올리지 않고 워크플로우에 새 도구를 추가하면 오류가 발생한다.
핵심 요약 (20줄)
- Simon Willison이 GitHub Actions에서
uvx를 캐시 친화적으로 사용하는 방법을 공유 - GPT-5.6 Sol의 도움으로 드디어 마음에 드는 패턴을 찾음
- 핵심은
UV_EXCLUDE_NEWER환경 변수 → 특정 날짜 기준 최신 패키지 설치 - 이 날짜를 GitHub Actions 캐시 키로도 활용 → 날짜 하나로 모든 도구 버전 고정
astral-sh/setup-uvAction의cache-suffix에UV_EXCLUDE_NEWER날짜 삽입cache-dependency-glob: ""→ pyproject.toml 불필요, 별도 파일 관리 없음prune-cache: false→ Astral 기본값(캐시 삭제)을 비활성화하는 핵심 설정- 캐시 히트 시
UV_OFFLINE=1설정 → 캐시에 없는 도구 실행 시 에러로 보호 - 날짜 하나만 올리면 캐시 무효화 + 최신 버전으로 전환
- 새 도구 추가 시 날짜도 함께 올려야 함 (안 올리면 오류 발생)
- uv 공식 문서는 CI에서 wheel 캐싱을 권장하지 않지만 Simon은 반대 입장
- "PyPI CDN에 매번 요청하느니 약간 느린 캐시 복원이 낫다"는 철학
uvx sqlite-utils,uvx --pre datasette,uvx llm등 여러 도구 한 번에 캐싱 가능UV_EXCLUDE_NEWER는 uv 공식 환경 변수 → 문서화된 안정적인 방법- astral-sh/setup-uv가 Astral 자체 서버에 매번 요청하는 점은 여전히 아쉬움
- 같은 날짜 → 같은 버전 → 재현 가능한 빌드 환경 보장
- PyPI 요청 없이 도구 실행 → 네트워크 의존성 최소화, 빌드 신뢰성 향상
- 관련 이슈: astral-sh/setup-uv#745 (기본값을 캐시 방향으로 바꿔달라는 요청)
- 이 패턴은 간단한 스크립트/워크플로우에서 특히 유용
- 전체 예제: https://til.simonwillison.net/github-actions/uvx-github-actions-cache