在GitHub Actions里跑一个Python命令行工具,很多开发者的习惯是直接写一行uvx ruff或uvx black。方便是方便,但uvx默认会去PyPI解析"最新版本",意味着每次workflow触发,不管代码改没改,都要重新联网、重新下载工具本身和它的依赖。独立开发者Simon Willison在自己的TIL博客上给出了一个解法:在workflow开头设一个环境变量UV_EXCLUDE_NEWER="2026-07-12",把这个日期一并塞进GitHub Actions的缓存key。这样uv的解析器就不会再去问"现在最新是哪个版本",而是回答"截止这一天,最新是哪个版本"——答案固定了,缓存key才能固定,缓存才能真正命中。
缓存和"总要最新"天生对着干
这个问题看起来小,但根子在CI机制本身。GitHub Actions的actions/cache靠一个固定的key来判断"这次和上次是不是同一个东西",命中了才跳过下载。可uvx的默认行为恰恰是拒绝固定:它每次都要重新问一遍PyPI"现在最新版本是几",这个问题本身没有确定答案,自然也生成不出稳定的缓存key。于是矛盾摆在那儿——你想要工具永远最新,又想要CI永远快,这两件事在uv的默认设置下互相打架。
Astral(uv的开发公司)自己也注意到了缓存这件事,官方文档给出的方案是用astral-sh/setup-uv这个Action并开启enable-cache: true,或者手动缓存UV_CACHE_DIR目录、在保存前跑一次uv cache prune --ci清理冗余文件。但这套官方方案解决的是"下载结果要不要存起来复用",不是"每次问的问题答案是不是同一个"。两者听起来像近义词,其实是两层完全不同的问题,原文没有把这层区别挑明,但这恰恰是理解这个技巧价值的关键。
- 结论.官方缓存方案管"存不存",Simon的日期技巧管"问什么",叠加使用才是完整方案。
一个环境变量,三行改动的成本
这个技巧本身不复杂,某种程度上也不算新发明。Simon早在2024年就写过uv pip install --exclude-newer的用法,这次只是把同一个思路挪到uvx和CI缓存场景里,算是老工具的新用法,而不是凭空想出来的新概念。
真正值得记住的是它的运维细节:日期一旦写死,就必须有人记得维护它。workflow里新增一个工具,如果忘了把UV_EXCLUDE_NEWER的日期往后调,uv的解析器可能会找不到这个刚发布不久的包——因为它按定义就在"排除范围"之内。这不是bug,是这套设计的必然代价:锁定时间点换来了缓存稳定,但也把"记得升级日期"这件事从机器手里转移到了人的记忆里。
锁定时间点,不锁定版本号,轻,却也容易被人忘掉
对比传统lockfile(比如requirements.txt加精确版本号或uv.lock),这套日期锚定方案更轻量——不需要额外文件,一个环境变量就够了。代价是粒度更粗:它锁的是"某天之前的最新",不是某个具体版本号,理论上如果uv内部解析算法本身发生变化,同一个日期在不同uv版本下也可能给出不完全一致的结果,这一点原文没有展开,使用者需要自己留意。
谁该用这一招
真正会用到这个技巧的,是那些在CI里高频调用小工具、又在意构建速度和网络请求次数的团队——跑linter、格式化工具、代码生成脚本的开源项目和企业流水线,是最直接的受益者。对他们来说,这不是要不要升级uv版本的选择题,而是"要不要在workflow文件里多写一行环境变量、并且把它当成需要定期维护的配置项"的执行细节。
如果团队本来就没感觉到uvx拖慢CI或增加了不必要的PyPI请求,这个技巧的收益并不明显,强行加进去反而多了一处需要记得维护的地方。值得关注的是,Astral会不会把类似的日期锚定思路写进setup-uv官方文档,或者干脆做成工具自带的选项——如果官方把这套模式标准化,现在这种手动维护环境变量的做法很可能会被更省心的方案取代。
