From b7e5ac24b5d83a221221538ef7782c419fe530ae Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 2 Dec 2025 12:05:28 +0100 Subject: [PATCH] Add a check for accidental cache modifications Add a script and associated CI check that checks whether a new uv build can use the cache of the previous uv version. This prevents accidental changes to cache keys, e.g. by changing nested data structures. I've tested the script by using a previous change of https://github.com/astral-sh/uv/pull/16143. The check can be disabled with a PR label for PRs that change the cache layout. What's missing here is that the base is the last release, meaning that once a PR with that label merges, all following PRs will fail this check, as we currently don't have a good way to ask whether there was a change previously or to download the latest build binary from main as baseline. --- .github/workflows/ci.yml | 23 ++++ .../check-cache-changes.py | 60 ++++++++++ scripts/check-cache-changes/pyproject.toml | 15 +++ scripts/check-cache-changes/uv.lock | 112 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 scripts/check-cache-changes/check-cache-changes.py create mode 100644 scripts/check-cache-changes/pyproject.toml create mode 100644 scripts/check-cache-changes/uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e34876640..86f42e6dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1041,6 +1041,29 @@ jobs: run: | (& ./uvx --generate-shell-completion powershell) | Out-String | Invoke-Expression + # Check for accidental cache layout and cache key changes + integration-test-cache-layout: + timeout-minutes: 10 + needs: build-binary-linux-libc + name: "integration test | cache layout" + runs-on: ubuntu-latest + if: ${{ !contains(github.event.pull_request.labels.*.name, 'cache-change') }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Download binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: uv-linux-libc-${{ github.sha }} + + - name: Prepare binary + run: chmod +x ./uv + + - name: Check for cache layout changes + run: ./uv run scripts/check-cache-changes/check-cache-changes.py ./uv + integration-test-nushell: timeout-minutes: 10 needs: build-binary-linux-libc diff --git a/scripts/check-cache-changes/check-cache-changes.py b/scripts/check-cache-changes/check-cache-changes.py new file mode 100644 index 000000000..e04b7a560 --- /dev/null +++ b/scripts/check-cache-changes/check-cache-changes.py @@ -0,0 +1,60 @@ +""" +Check that the uv cache keys and layout didn't change accidentally. + +This check may fail when intentionally changing the cache layout, it is intended to +catch accidental modification of cache keys. +""" + +import os +import shutil +import sys +from argparse import ArgumentParser +from pathlib import Path +from subprocess import CalledProcessError, check_call + + +def main(): + parser = ArgumentParser() + parser.add_argument("new_uv", help="The new uv binary") + parser.add_argument( + "--old-version", + help="Optionally, compare to a specific uv version instead of the latest version", + ) + + args = parser.parse_args() + # Ensure the uv path, if relative, has the right root + new_uv = Path(os.getcwd()).joinpath(args.new_uv) + work_dir = Path(__file__).parent + cache_dir = work_dir.joinpath(".cache") + if cache_dir.is_dir(): + shutil.rmtree(cache_dir) + env = {"UV_CACHE_DIR": str(cache_dir), **os.environ} + + if args.old_version: + old_uv = f"uv@{args.old_version}" + else: + old_uv = "uv@latest" + + # Prime the cache + print(f"\nPriming the cache with {old_uv}\n") + check_call([new_uv, "tool", "run", old_uv, "sync"], cwd=work_dir, env=env) + # This should always pass, even if the cache changed + print(f"\nUsing the cache offline with {old_uv}\n") + shutil.rmtree(work_dir.joinpath(".venv")) + check_call( + [new_uv, "tool", "run", old_uv, "sync", "--offline"], cwd=work_dir, env=env + ) + # Check that the new uv version can use the old cache + print(f"\nUsing the cache offline with {new_uv}\n") + shutil.rmtree(work_dir.joinpath(".venv")) + try: + check_call([new_uv, "sync", "--offline"], cwd=work_dir, env=env) + except CalledProcessError: + print( + 'Cache layout changed. If this is intentional, add the "cache-change" label to your PR' + ) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/check-cache-changes/pyproject.toml b/scripts/check-cache-changes/pyproject.toml new file mode 100644 index 000000000..285e9093b --- /dev/null +++ b/scripts/check-cache-changes/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "check-cache-changes" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "anyio>=4.12.0", + "package", + "tqdm", +] + +[tool.uv.sources] +tqdm = { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl" } +package = { git = "https://github.com/astral-sh/uv-dynamic-metadata-test", rev = "6c5aa0a65db737c9e7e2e60dc865bd8087012e64" } +# TODO(konsti): Add a case with extra build dependencies +# TODO(konsti): Add a case with config settings diff --git a/scripts/check-cache-changes/uv.lock b/scripts/check-cache-changes/uv.lock new file mode 100644 index 000000000..62e3ac91a --- /dev/null +++ b/scripts/check-cache-changes/uv.lock @@ -0,0 +1,112 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "check-cache-changes" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "anyio" }, + { name = "package" }, + { name = "tqdm" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.12.0" }, + { name = "package", git = "https://github.com/astral-sh/uv-dynamic-metadata-test?rev=6c5aa0a65db737c9e7e2e60dc865bd8087012e64" }, + { name = "tqdm", url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dependency" +version = "0.1.0" +source = { git = "https://github.com/astral-sh/uv-dynamic-metadata-test?subdirectory=dependency&rev=6c5aa0a65db737c9e7e2e60dc865bd8087012e64#6c5aa0a65db737c9e7e2e60dc865bd8087012e64" } +dependencies = [ + { name = "iniconfig" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "package" +version = "0.1.0" +source = { git = "https://github.com/astral-sh/uv-dynamic-metadata-test?rev=6c5aa0a65db737c9e7e2e60dc865bd8087012e64#6c5aa0a65db737c9e7e2e60dc865bd8087012e64" } +dependencies = [ + { name = "dependency" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2" }, +] + +[package.metadata] +requires-dist = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ipywidgets", marker = "extra == 'notebook'", specifier = ">=6" }, + { name = "nbval", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=6" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "pytest-timeout", marker = "extra == 'dev'" }, + { name = "requests", marker = "extra == 'discord'" }, + { name = "requests", marker = "extra == 'telegram'" }, + { name = "slack-sdk", marker = "extra == 'slack'" }, +] +provides-extras = ["dev", "discord", "slack", "telegram", "notebook"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]