From 4f63a5fb656d82dda3792bf69b4d178216318cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Thu, 22 Oct 2026 20:47:44 -0700 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20fix(release):=20enforce=20lo?= =?UTF-8?q?ck=20and=20cutoff=20gates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yaml | 8 +- .github/workflows/update-tools.yaml | 2 +- .github/workflows/upgrade-release-tools.yaml | 5 + README.md | 5 +- changelog.d/+lock-cutoff.security.md | 1 + changelog.d/+release-asset-retry.bugfix.md | 1 + changelog.d/+release-tool-setup.bugfix.md | 1 + .../+runtime-secondary-lock.feature.md | 1 + install-nab/action.yml | 5 +- publish-github-release/action.yml | 11 +- pylock.toml | 613 +++++++++++++++++- pyproject.toml | 1 + refresh-release-lock/action.yml | 26 +- tests/build_python_distribution/test_build.py | 5 +- tests/conftest.py | 37 +- tests/install_locked_tools/test_install.py | 45 +- tests/publish_github_release/test_publish.py | 61 +- .../test_recheck.py | 20 +- tests/refresh_release_lock/test_cutoffs.py | 55 +- tests/upgrade_release_tools/test_upgrade.py | 42 +- .../test_distribution.py | 8 +- tools/check_lock_groups.py | 33 +- tools/check_recent_cutoffs.py | 2 +- tools/expire_cutoffs.py | 2 +- tools/nab_cutoffs.py | 36 +- 25 files changed, 934 insertions(+), 92 deletions(-) create mode 100644 changelog.d/+lock-cutoff.security.md create mode 100644 changelog.d/+release-asset-retry.bugfix.md create mode 100644 changelog.d/+release-tool-setup.bugfix.md create mode 100644 changelog.d/+runtime-secondary-lock.feature.md diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index b95d043..eda7222 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -29,6 +29,7 @@ jobs: version: latest-known python-version: "3.15" enable-cache: true + cache-suffix: ${{ github.job }} cache-dependency-glob: | pylock.toml pyproject.toml @@ -58,6 +59,7 @@ jobs: version: latest-known python-version: "3.15" enable-cache: true + cache-suffix: ${{ github.job }} cache-dependency-glob: | pylock.toml pyproject.toml @@ -99,6 +101,7 @@ jobs: version: latest-known python-version: "3.15" enable-cache: true + cache-suffix: ${{ github.job }} cache-dependency-glob: | pylock.toml pyproject.toml @@ -127,6 +130,7 @@ jobs: version: latest-known python-version: "3.15" enable-cache: true + cache-suffix: ${{ github.job }} cache-dependency-glob: | pylock.toml pyproject.toml @@ -145,6 +149,7 @@ jobs: version: latest-known python-version: "3.15" enable-cache: true + cache-suffix: ${{ github.job }} cache-dependency-glob: | pylock.toml pyproject.toml @@ -178,13 +183,14 @@ jobs: version: latest-known python-version: "3.15" enable-cache: true + cache-suffix: ${{ github.job }} cache-dependency-glob: | pylock.toml pyproject.toml - uses: $/refresh-release-lock with: lock-groups: >- - build lock-tools release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check + build lock-tools release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check release-fixture mode: check gate: name: ${{ github.event_name == 'pull_request' && 'Required checks pass' || 'Required checks pass (non-PR)' }} diff --git a/.github/workflows/update-tools.yaml b/.github/workflows/update-tools.yaml index 49a6580..15eff86 100644 --- a/.github/workflows/update-tools.yaml +++ b/.github/workflows/update-tools.yaml @@ -13,7 +13,7 @@ jobs: RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} with: lock-groups: >- - build lock-tools release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check + build lock-tools release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check release-fixture commit-message: "🔧 build: refresh release tool lock" pull-request-title: "🔧 build: refresh release tool lock" pull-request-body: "Refresh the locked tools used by release preparation and CI." diff --git a/.github/workflows/upgrade-release-tools.yaml b/.github/workflows/upgrade-release-tools.yaml index 142ea12..f9059ac 100644 --- a/.github/workflows/upgrade-release-tools.yaml +++ b/.github/workflows/upgrade-release-tools.yaml @@ -14,6 +14,10 @@ on: description: Groups for the secondary lock; empty selects build requirements type: string default: "" + additional-lock-selection: + description: Use project runtime dependencies for the secondary lock + type: string + default: build-or-groups additional-matrix-python: description: Python range for the secondary lock type: string @@ -67,6 +71,7 @@ jobs: lock-groups: ${{ inputs.lock-groups }} additional-lock: ${{ inputs.additional-lock }} additional-lock-groups: ${{ inputs.additional-lock-groups }} + additional-lock-selection: ${{ inputs.additional-lock-selection }} additional-matrix-python: ${{ inputs.additional-matrix-python }} additional-matrix-platforms: ${{ inputs.additional-matrix-platforms }} additional-matrix-implementations: ${{ inputs.additional-matrix-implementations }} diff --git a/README.md b/README.md index a460f71..8eb3361 100644 --- a/README.md +++ b/README.md @@ -161,8 +161,9 @@ For a separately checked-in build matrix lock, also pass `additional-lock` with `additional-matrix-python` with its Python range, and `additional-matrix-platforms` and `additional-matrix-implementations` as space-separated Nab values. The secondary lock keeps its specified environment matrix during refresh. `additional-lock-groups` selects dependency groups for it; when empty, Nab locks the project's -build requirements. These inputs have no effect when `additional-lock` is absent, and matrix inputs without that lock -fail. The writer accepts only the named secondary lock path. +build requirements. Set `additional-lock-selection: project` to lock only the project's runtime dependencies. That +selection cannot be combined with `additional-lock-groups`. Secondary-lock inputs without `additional-lock` fail. The +writer accepts only the named secondary lock path. For a repository that already commits `pylock.build.toml`, the complete caller is: diff --git a/changelog.d/+lock-cutoff.security.md b/changelog.d/+lock-cutoff.security.md new file mode 100644 index 0000000..5e32fef --- /dev/null +++ b/changelog.d/+lock-cutoff.security.md @@ -0,0 +1 @@ +Enforce the seven-day Nab cutoff for global and package rules, including disabled or shortened cutoffs. diff --git a/changelog.d/+release-asset-retry.bugfix.md b/changelog.d/+release-asset-retry.bugfix.md new file mode 100644 index 0000000..6ab5b05 --- /dev/null +++ b/changelog.d/+release-asset-retry.bugfix.md @@ -0,0 +1 @@ +Remove partial GitHub Release assets before retrying an upload in the same run. diff --git a/changelog.d/+release-tool-setup.bugfix.md b/changelog.d/+release-tool-setup.bugfix.md new file mode 100644 index 0000000..d06c8e4 --- /dev/null +++ b/changelog.d/+release-tool-setup.bugfix.md @@ -0,0 +1 @@ +Allow repeated lock-tool setup on Windows and check caller release-creator versions against the selected groups. diff --git a/changelog.d/+runtime-secondary-lock.feature.md b/changelog.d/+runtime-secondary-lock.feature.md new file mode 100644 index 0000000..6829949 --- /dev/null +++ b/changelog.d/+runtime-secondary-lock.feature.md @@ -0,0 +1 @@ +Refresh a secondary pylock for project runtime dependencies, including a separate Python and platform matrix. diff --git a/install-nab/action.yml b/install-nab/action.yml index e2d0788..eea0a4a 100644 --- a/install-nab/action.yml +++ b/install-nab/action.yml @@ -5,10 +5,7 @@ runs: steps: - name: Create lock-tool environment shell: bash - run: | - if [[ ! -x "$RUNNER_TEMP/lock-tools/bin/python" ]]; then - uv venv --python 3.15 "$RUNNER_TEMP/lock-tools" - fi + run: uv venv --allow-existing --python 3.15 "$RUNNER_TEMP/lock-tools" - name: Install lock reader shell: bash run: | diff --git a/publish-github-release/action.yml b/publish-github-release/action.yml index 352cf94..1f91502 100644 --- a/publish-github-release/action.yml +++ b/publish-github-release/action.yml @@ -172,9 +172,18 @@ runs: if jq -e '.[0].draft' "$RUNNER_TEMP/release-current.json" >/dev/null; then for asset in "$ASSETS_DIRECTORY"/*; do name=${asset##*/} - if ! jq -e --arg name "$name" 'any(.[0].assets[]; .name == $name)' \ + if ! jq -e --arg name "$name" --slurpfile assets "$RUNNER_TEMP/release-assets.json" ' + any(.[0].assets[]; .name == $name and .state == "uploaded" and + (.digest as $digest | any($assets[0][]; .name == $name and .digest == $digest))) + ' \ "$RUNNER_TEMP/release-current.json" >/dev/null; then for attempt in {1..6}; do + if ((attempt > 1)); then + while IFS= read -r asset_id; do + gh api -X DELETE "repos/$GH_REPO/releases/assets/$asset_id" >/dev/null + done < <(gh api "repos/$GH_REPO/releases/$release_id" | jq -r --arg name "$name" \ + '.assets[] | select(.name == $name and .state != "uploaded") | .id') + fi if gh release upload "$TAG" "$asset" --repo "$GH_REPO"; then break fi diff --git a/pylock.toml b/pylock.toml index 198455d..5f8aea0 100644 --- a/pylock.toml +++ b/pylock.toml @@ -12,6 +12,7 @@ dependency-groups = [ "project-runtime", "release-attestations", "release-check", + "release-fixture", "release-hook-check", "release-hooks", "release-news", @@ -28,7 +29,7 @@ created-by = "nab" [[packages]] name = "annotated-types" version = "0.8.0" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.10" index = "https://pypi.org/simple/" @@ -75,6 +76,31 @@ size = 43294 [packages.wheels.hashes] sha256 = "6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19" +[[packages]] +name = "attrs" +version = "26.1.0" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "attrs-26.1.0.tar.gz" +upload-time = 2026-03-19 14:22:25.026315+00:00 +url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz" +size = 952055 + +[packages.sdist.hashes] +sha256 = "d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" + +[[packages.wheels]] +name = "attrs-26.1.0-py3-none-any.whl" +upload-time = 2026-03-19 14:22:23.645947+00:00 +url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl" +size = 67548 + +[packages.wheels.hashes] +sha256 = "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" + [[packages]] name = "build" version = "1.6.1" @@ -108,7 +134,7 @@ sha256 = "ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7" [[packages]] name = "certifi" version = "2026.7.22" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.7" index = "https://pypi.org/simple/" @@ -133,7 +159,7 @@ sha256 = "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775" [[packages]] name = "cffi" version = "2.1.1" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or (\"release-fixture\" in dependency_groups and sys_platform == \"linux\")" requires-python = ">=3.10" dependencies = [ { name = "pycparser" }, @@ -188,7 +214,7 @@ sha256 = "e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960" [[packages]] name = "charset-normalizer" version = "3.5.2" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.7" index = "https://pypi.org/simple/" @@ -282,10 +308,42 @@ size = 68872 [packages.wheels.hashes] sha256 = "b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685" +[[packages]] +name = "check-wheel-contents" +version = "0.6.3" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "wheel-filename" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "check_wheel_contents-0.6.3.tar.gz" +upload-time = 2025-08-02 14:01:45.546952+00:00 +url = "https://files.pythonhosted.org/packages/95/37/2b9e0d38c2f668791ffe8b97711185c364d91c027e47f189c371c2348d18/check_wheel_contents-0.6.3.tar.gz" +size = 586023 + +[packages.sdist.hashes] +sha256 = "10e6939e2fe4e6ce1edf2ff6ec6157808677e80782e78021ae139dd88473a442" + +[[packages.wheels]] +name = "check_wheel_contents-0.6.3-py3-none-any.whl" +upload-time = 2025-08-02 14:01:43.968164+00:00 +url = "https://files.pythonhosted.org/packages/be/05/f39fde9f31ef80b285ef5822fad4ddabf73fec62a1f02c5beb4b2f328972/check_wheel_contents-0.6.3-py3-none-any.whl" +size = 27541 + +[packages.wheels.hashes] +sha256 = "5ae39c8c434b972f0740d04610759168590713175aab584b012b1b84f6771874" + [[packages]] name = "click" version = "8.5.0" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.10" index = "https://pypi.org/simple/" @@ -335,7 +393,7 @@ sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" [[packages]] name = "cryptography" version = "50.0.2" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or (\"release-fixture\" in dependency_groups and sys_platform == \"linux\")" requires-python = "!=3.9.0,!=3.9.1,>=3.9" dependencies = [ { name = "cffi" }, @@ -656,7 +714,7 @@ sha256 = "9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9" [[packages]] name = "docutils" version = "0.23" -marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -707,10 +765,42 @@ size = 35604 [packages.wheels.hashes] sha256 = "80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4" +[[packages]] +name = "hatchling" +version = "1.32.4" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "tomlkit" }, + { name = "trove-classifiers" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "hatchling-1.32.4.tar.gz" +upload-time = 2026-09-20 22:48:45.398505+00:00 +url = "https://files.pythonhosted.org/packages/f6/97/b5312f01a8c6daf729a9d272dd442e0c546dbcc630495788786c4b567ed0/hatchling-1.32.4.tar.gz" +size = 58065 + +[packages.sdist.hashes] +sha256 = "c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07" + +[[packages.wheels]] +name = "hatchling-1.32.4-py3-none-any.whl" +upload-time = 2026-09-20 22:48:44.190681+00:00 +url = "https://files.pythonhosted.org/packages/5f/80/91f51f439c05d4ec4623c22928ce16a938d6d793bf709477830823497859/hatchling-1.32.4-py3-none-any.whl" +size = 78748 + +[packages.wheels.hashes] +sha256 = "08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c" + [[packages]] name = "id" version = "1.6.1" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.9" dependencies = [ { name = "urllib3" }, @@ -738,7 +828,7 @@ sha256 = "f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca" [[packages]] name = "idna" version = "3.20" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -785,6 +875,112 @@ size = 464466 [packages.wheels.hashes] sha256 = "f55f5728ff61d3dd54dbc5a397cf3f7307cb64b180c20b72afbebf01c9e2c8a0" +[[packages]] +name = "jaraco-classes" +version = "3.4.0" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.8" +dependencies = [ + { name = "more-itertools" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jaraco.classes-3.4.0.tar.gz" +upload-time = 2024-03-31 07:27:36.643708+00:00 +url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" +size = 11780 + +[packages.sdist.hashes] +sha256 = "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd" + +[[packages.wheels]] +name = "jaraco.classes-3.4.0-py3-none-any.whl" +upload-time = 2024-03-31 07:27:34.792832+00:00 +url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl" +size = 6777 + +[packages.wheels.hashes] +sha256 = "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + +[[packages]] +name = "jaraco-context" +version = "6.1.2" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jaraco_context-6.1.2.tar.gz" +upload-time = 2026-03-20 22:13:33.922545+00:00 +url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz" +size = 16801 + +[packages.sdist.hashes] +sha256 = "f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3" + +[[packages.wheels]] +name = "jaraco_context-6.1.2-py3-none-any.whl" +upload-time = 2026-03-20 22:13:32.808832+00:00 +url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl" +size = 7871 + +[packages.wheels.hashes] +sha256 = "bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535" + +[[packages]] +name = "jaraco-functools" +version = "4.6.0" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "more-itertools" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jaraco_functools-4.6.0.tar.gz" +upload-time = 2026-07-14 01:28:02.544761+00:00 +url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz" +size = 20837 + +[packages.sdist.hashes] +sha256 = "880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280" + +[[packages.wheels]] +name = "jaraco_functools-4.6.0-py3-none-any.whl" +upload-time = 2026-07-14 01:28:01.590470+00:00 +url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl" +size = 11677 + +[packages.wheels.hashes] +sha256 = "99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30" + +[[packages]] +name = "jeepney" +version = "0.9.0" +marker = "\"release-fixture\" in dependency_groups and sys_platform == \"linux\"" +requires-python = ">=3.7" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jeepney-0.9.0.tar.gz" +upload-time = 2025-02-27 18:51:01.684959+00:00 +url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz" +size = 106758 + +[packages.sdist.hashes] +sha256 = "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732" + +[[packages.wheels]] +name = "jeepney-0.9.0-py3-none-any.whl" +upload-time = 2025-02-27 18:51:00.104279+00:00 +url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl" +size = 49010 + +[packages.wheels.hashes] +sha256 = "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" + [[packages]] name = "jinja2" version = "3.1.6" @@ -813,10 +1009,43 @@ size = 134899 [packages.wheels.hashes] sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" +[[packages]] +name = "keyring" +version = "25.7.0" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney" }, + { name = "pywin32-ctypes" }, + { name = "secretstorage" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "keyring-25.7.0.tar.gz" +upload-time = 2025-11-16 16:26:09.482176+00:00 +url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz" +size = 63516 + +[packages.sdist.hashes] +sha256 = "fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b" + +[[packages.wheels]] +name = "keyring-25.7.0-py3-none-any.whl" +upload-time = 2025-11-16 16:26:08.402146+00:00 +url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl" +size = 39160 + +[packages.wheels.hashes] +sha256 = "be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" + [[packages]] name = "markdown-it-py" version = "4.2.0" -marker = "\"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "mdurl" }, @@ -983,7 +1212,7 @@ sha256 = "214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d" [[packages]] name = "mdurl" version = "0.1.2" -marker = "\"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.7" index = "https://pypi.org/simple/" @@ -1005,6 +1234,31 @@ size = 9979 [packages.wheels.hashes] sha256 = "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" +[[packages]] +name = "more-itertools" +version = "11.2.1" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.11" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "more_itertools-11.2.1.tar.gz" +upload-time = 2026-10-14 18:50:30.407709+00:00 +url = "https://files.pythonhosted.org/packages/52/b8/c63995bebbb9ac9c059350e0fb8c82c2420c85aeca7d34e40aecf3df9281/more_itertools-11.2.1.tar.gz" +size = 154133 + +[packages.sdist.hashes] +sha256 = "cbf08fd0af284dc69718b9b76a8e6203df624d70209ea511e4219d350c856f63" + +[[packages.wheels]] +name = "more_itertools-11.2.1-py3-none-any.whl" +upload-time = 2026-10-14 18:50:29.116732+00:00 +url = "https://files.pythonhosted.org/packages/5d/57/3c4b1c6f2e0cc29ba023802458ea3d2a996f87085559e4a5f2192d549532/more_itertools-11.2.1-py3-none-any.whl" +size = 74405 + +[packages.wheels.hashes] +sha256 = "35a7377edd1dd6608dcb2cdf534ded55ea32d49448875ad042cd3f879fb1ded0" + [[packages]] name = "nab" version = "0.0.22" @@ -1189,10 +1443,62 @@ size = 58597 [packages.wheels.hashes] sha256 = "6f77daa98279ef0eedb9e8f2248aee4c50e01b92b470b68886995e5056d79971" +[[packages]] +name = "nh3" +version = "0.3.7" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "nh3-0.3.7.tar.gz" +upload-time = 2026-08-23 14:26:30.728660+00:00 +url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz" +size = 25662 + +[packages.sdist.hashes] +sha256 = "71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848" + +[[packages.wheels]] +name = "nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" +upload-time = 2026-08-23 14:26:09.025294+00:00 +url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" +size = 1493959 + +[packages.wheels.hashes] +sha256 = "6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5" + +[[packages.wheels]] +name = "nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" +upload-time = 2026-08-23 14:26:10.606442+00:00 +url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" +size = 859615 + +[packages.wheels.hashes] +sha256 = "f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479" + +[[packages.wheels]] +name = "nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-08-23 14:26:17.272038+00:00 +url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +size = 832161 + +[packages.wheels.hashes] +sha256 = "70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42" + +[[packages.wheels]] +name = "nh3-0.3.7-cp38-abi3-win_amd64.whl" +upload-time = 2026-08-23 14:26:28.294566+00:00 +url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl" +size = 644462 + +[packages.wheels.hashes] +sha256 = "618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc" + [[packages]] name = "packaging" version = "26.3" -marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -1214,6 +1520,31 @@ size = 129956 [packages.wheels.hashes] sha256 = "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" +[[packages]] +name = "pathspec" +version = "1.1.1" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pathspec-1.1.1.tar.gz" +upload-time = 2026-04-27 01:46:08.907631+00:00 +url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz" +size = 135180 + +[packages.sdist.hashes] +sha256 = "17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a" + +[[packages.wheels]] +name = "pathspec-1.1.1-py3-none-any.whl" +upload-time = 2026-04-27 01:46:07.060850+00:00 +url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl" +size = 57328 + +[packages.wheels.hashes] +sha256 = "a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" + [[packages]] name = "platformdirs" version = "4.13.3" @@ -1239,6 +1570,31 @@ size = 32762 [packages.wheels.hashes] sha256 = "f6ad7f447f24f8a3b82cce5976387428bff894a0eca6c3488f4a17f153c130c4" +[[packages]] +name = "pluggy" +version = "1.7.0" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pluggy-1.7.0.tar.gz" +upload-time = 2026-10-15 09:50:58.343484+00:00 +url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz" +size = 123304 + +[packages.sdist.hashes] +sha256 = "d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8" + +[[packages.wheels]] +name = "pluggy-1.7.0-py3-none-any.whl" +upload-time = 2026-10-15 09:50:56.808462+00:00 +url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl" +size = 27082 + +[packages.wheels.hashes] +sha256 = "7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec" + [[packages]] name = "prek" version = "0.5.5" @@ -1328,7 +1684,7 @@ sha256 = "deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b" [[packages]] name = "pycparser" version = "3.11" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or (\"release-fixture\" in dependency_groups and sys_platform == \"linux\")" requires-python = ">=3.10" index = "https://pypi.org/simple/" @@ -1353,7 +1709,7 @@ sha256 = "51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80" [[packages]] name = "pydantic" version = "2.14.1" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "annotated-types" }, @@ -1385,7 +1741,7 @@ sha256 = "9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454" [[packages]] name = "pydantic-core" version = "2.50.1" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "typing-extensions" }, @@ -1440,7 +1796,7 @@ sha256 = "983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed" [[packages]] name = "pygments" version = "2.21.0" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -1575,6 +1931,31 @@ size = 10724 [packages.wheels.hashes] sha256 = "5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b" +[[packages]] +name = "pywin32-ctypes" +version = "0.2.3" +marker = "\"release-fixture\" in dependency_groups and sys_platform == \"win32\"" +requires-python = ">=3.6" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pywin32-ctypes-0.2.3.tar.gz" +upload-time = 2024-08-14 10:15:34.626878+00:00 +url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz" +size = 29471 + +[packages.sdist.hashes] +sha256 = "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" + +[[packages.wheels]] +name = "pywin32_ctypes-0.2.3-py3-none-any.whl" +upload-time = 2024-08-14 10:15:33.187242+00:00 +url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl" +size = 30756 + +[packages.wheels.hashes] +sha256 = "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" + [[packages]] name = "pyyaml" version = "6.0.3" @@ -1591,6 +1972,36 @@ size = 130960 [packages.sdist.hashes] sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" +[[packages]] +name = "readme-renderer" +version = "46.0" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "readme_renderer-46.0.tar.gz" +upload-time = 2026-08-28 15:18:32.490778+00:00 +url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz" +size = 38382 + +[packages.sdist.hashes] +sha256 = "af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90" + +[[packages.wheels]] +name = "readme_renderer-46.0-py3-none-any.whl" +upload-time = 2026-08-28 15:18:31.132970+00:00 +url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl" +size = 14239 + +[packages.wheels.hashes] +sha256 = "d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560" + [[packages]] name = "release-creator" version = "0.6.0" @@ -1627,7 +2038,7 @@ sha256 = "4b9cd6187c87fcb7bf3aca5f58c67c90703e58875643af633661a0448756d45f" [[packages]] name = "requests" version = "2.34.2" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "certifi" }, @@ -1655,6 +2066,34 @@ size = 73075 [packages.wheels.hashes] sha256 = "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" +[[packages]] +name = "requests-toolbelt" +version = "1.0.0" +marker = "\"release-fixture\" in dependency_groups" +requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +dependencies = [ + { name = "requests" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "requests-toolbelt-1.0.0.tar.gz" +upload-time = 2023-05-01 04:11:33.229998+00:00 +url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz" +size = 206888 + +[packages.sdist.hashes] +sha256 = "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" + +[[packages.wheels]] +name = "requests_toolbelt-1.0.0-py2.py3-none-any.whl" +upload-time = 2023-05-01 04:11:28.427086+00:00 +url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl" +size = 54481 + +[packages.wheels.hashes] +sha256 = "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + [[packages]] name = "rfc3161-client" version = "1.0.9" @@ -1713,7 +2152,7 @@ sha256 = "908f6a775da4bdce1d39f825a4431f389542696078ef3e4ed911a068bcaf1792" [[packages]] name = "rfc3986" version = "2.0.0" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.7" index = "https://pypi.org/simple/" @@ -1763,7 +2202,7 @@ sha256 = "520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48" [[packages]] name = "rich" version = "15.0.0" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.9.0" dependencies = [ { name = "markdown-it-py" }, @@ -1789,6 +2228,35 @@ size = 310654 [packages.wheels.hashes] sha256 = "33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb" +[[packages]] +name = "secretstorage" +version = "3.5.0" +marker = "\"release-fixture\" in dependency_groups and sys_platform == \"linux\"" +requires-python = ">=3.10" +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "secretstorage-3.5.0.tar.gz" +upload-time = 2025-11-23 19:02:53.191898+00:00 +url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz" +size = 19884 + +[packages.sdist.hashes] +sha256 = "f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be" + +[[packages.wheels]] +name = "secretstorage-3.5.0-py3-none-any.whl" +upload-time = 2025-11-23 19:02:51.545472+00:00 +url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl" +size = 15554 + +[packages.wheels.hashes] +sha256 = "0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137" + [[packages]] name = "securesystemslib" version = "1.5.1" @@ -2026,7 +2494,7 @@ sha256 = "188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90" [[packages]] name = "tomlkit" version = "0.15.1" -marker = "\"lock-tools\" in dependency_groups or \"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups" +marker = "\"lock-tools\" in dependency_groups or \"release-fixture\" in dependency_groups or \"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -2077,6 +2545,30 @@ size = 67030 [packages.wheels.hashes] sha256 = "ae4d223e6aadaff98d29b7f09e58d9c9ccf4cd3b455a2d2e0486d432cd8bf660" +[[packages]] +name = "trove-classifiers" +version = "2026.9.21.13" +marker = "\"release-fixture\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "trove_classifiers-2026.9.21.13.tar.gz" +upload-time = 2026-09-21 13:29:07.301512+00:00 +url = "https://files.pythonhosted.org/packages/bf/93/af436dfaa845cab5d96f0adbc1e4f3730532d37fa249e4eb796fb1d7fc82/trove_classifiers-2026.9.21.13.tar.gz" +size = 17085 + +[packages.sdist.hashes] +sha256 = "0a9ebc8d4e2f3e8a22848c5258033035bec17a3012ac3fea16dbaa764489eb71" + +[[packages.wheels]] +name = "trove_classifiers-2026.9.21.13-py3-none-any.whl" +upload-time = 2026-09-21 13:29:06.123578+00:00 +url = "https://files.pythonhosted.org/packages/30/81/0da8afb52a71d0a4f2bd3152357b1a441e393b286374802b9d3addab4ab5/trove_classifiers-2026.9.21.13-py3-none-any.whl" +size = 14232 + +[packages.wheels.hashes] +sha256 = "8b1ff4f9c191b1040b71c37f1e445ab99732911e3cd91de52838453a854d7a17" + [[packages]] name = "truststore" version = "0.10.5" @@ -2131,10 +2623,46 @@ size = 56253 [packages.wheels.hashes] sha256 = "d30434bda6e079ab303fb30d1b3006d939a10ca34783b1573d61cd9b802fa45c" +[[packages]] +name = "twine" +version = "7.0.0" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "id" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "twine-7.0.0.tar.gz" +upload-time = 2026-07-27 15:59:00.825945+00:00 +url = "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz" +size = 215032 + +[packages.sdist.hashes] +sha256 = "85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177" + +[[packages.wheels]] +name = "twine-7.0.0-py3-none-any.whl" +upload-time = 2026-07-27 15:58:59.260418+00:00 +url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl" +size = 43204 + +[packages.wheels.hashes] +sha256 = "b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7" + [[packages]] name = "typing-extensions" version = "4.16.0" -marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups" +marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -2159,7 +2687,7 @@ sha256 = "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" [[packages]] name = "typing-inspection" version = "0.4.4" -marker = "\"release-attestations\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "typing-extensions" }, @@ -2187,7 +2715,7 @@ sha256 = "65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147" [[packages]] name = "urllib3" version = "2.8.0" -marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups" +marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" requires-python = ">=3.10" index = "https://pypi.org/simple/" @@ -2270,6 +2798,31 @@ size = 301667 [packages.wheels.hashes] sha256 = "89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e" +[[packages]] +name = "wheel-filename" +version = "1.4.2" +marker = "\"release-fixture\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "wheel_filename-1.4.2.tar.gz" +upload-time = 2024-12-01 13:03:16.012739+00:00 +url = "https://files.pythonhosted.org/packages/38/be/726dab762b770d0417e505c58e26d661aac1ec0c831e483cda4817ca2417/wheel_filename-1.4.2.tar.gz" +size = 7911 + +[packages.sdist.hashes] +sha256 = "87891c465dcbb40b40394a906f01a93214bdd51aa5d25e3a9a59cae62bc298fd" + +[[packages.wheels]] +name = "wheel_filename-1.4.2-py3-none-any.whl" +upload-time = 2024-12-01 13:03:00.536117+00:00 +url = "https://files.pythonhosted.org/packages/b4/0f/6e97a3bc38cdde32e3ec49f8c0903fe3559ec9ec9db181782f0bb4417717/wheel_filename-1.4.2-py3-none-any.whl" +size = 6195 + +[packages.wheels.hashes] +sha256 = "3fa599046443d4ca830d06e3d180cd0a675d5871af0a68daa5623318bb4d17e3" + [[packages]] name = "xmltodict" version = "1.0.4" @@ -2335,18 +2888,18 @@ command-line = [ "--groups", "build", "lock-tools", - "release-attestations", - "release-check", - "release-hook-check", - "release-hooks", - "release-news", "release-notes", + "release-news", "release-prepare", - "release-shell", "release-validate", + "release-check", + "release-attestations", + "release-shell", + "release-hooks", + "release-hook-check", + "release-fixture", "--output", "pylock.toml", - "--upgrade", ] input-path = "pyproject.toml" mode = "universal" diff --git a/pyproject.toml b/pyproject.toml index b435dec..ce31fee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ release-attestations = [ "pypi-attestations>=0.0.30" ] release-shell = [ "prek>=0.2", "yq>=4.4.5" ] release-hooks = [ "release-creator>=0.6.0", "prek>=0.2" ] release-hook-check = [ "prek>=0.2", "yq>=4.4.5" ] +release-fixture = [ "check-wheel-contents>=0.6.3", "hatchling>=1.28", "twine>=7" ] test = [ "hatchling>=1.28", "pytest>=9", diff --git a/refresh-release-lock/action.yml b/refresh-release-lock/action.yml index f35f3ad..13d35cc 100644 --- a/refresh-release-lock/action.yml +++ b/refresh-release-lock/action.yml @@ -10,6 +10,9 @@ inputs: additional-lock-groups: description: Groups for the secondary lock; empty selects build requirements default: "" + additional-lock-selection: + description: Use project runtime dependencies for the secondary lock + default: build-or-groups additional-matrix-python: description: Python range for the secondary lock default: "" @@ -48,6 +51,7 @@ runs: MODE: ${{ inputs.mode }} ADDITIONAL_LOCK: ${{ inputs.additional-lock }} ADDITIONAL_GROUPS: ${{ inputs.additional-lock-groups }} + ADDITIONAL_SELECTION: ${{ inputs.additional-lock-selection }} MATRIX_PYTHON: ${{ inputs.additional-matrix-python }} MATRIX_PLATFORMS: ${{ inputs.additional-matrix-platforms }} MATRIX_IMPLEMENTATIONS: ${{ inputs.additional-matrix-implementations }} @@ -80,7 +84,8 @@ runs: "$RUNNER_TEMP/pylock-before-check.toml" pylock.toml fi if [[ -z "$ADDITIONAL_LOCK" ]]; then - if [[ -n "$ADDITIONAL_GROUPS$MATRIX_PYTHON$MATRIX_PLATFORMS$MATRIX_IMPLEMENTATIONS" ]]; then + if [[ "$ADDITIONAL_SELECTION" != build-or-groups || + -n "$ADDITIONAL_GROUPS$MATRIX_PYTHON$MATRIX_PLATFORMS$MATRIX_IMPLEMENTATIONS" ]]; then echo '::error::secondary matrix inputs require additional-lock' >&2 exit 1 fi @@ -111,8 +116,23 @@ runs: echo '::error::secondary matrix needs platforms and implementations' >&2 exit 1 fi - selection=(--build-requirements) - if ((${#selected_groups[@]})); then selection=(--groups "${selected_groups[@]}"); fi + case "$ADDITIONAL_SELECTION" in + build-or-groups) + selection=(--build-requirements) + if ((${#selected_groups[@]})); then selection=(--groups "${selected_groups[@]}"); fi + ;; + project) + if ((${#selected_groups[@]})); then + echo '::error::project secondary lock cannot select dependency groups' >&2 + exit 1 + fi + selection=() + ;; + *) + echo '::error::unknown secondary lock selection' >&2 + exit 1 + ;; + esac cp "$ADDITIONAL_LOCK" "$RUNNER_TEMP/additional-lock-before.toml" uv run --no-project --python "$RUNNER_TEMP/lock-tools" nab lock pyproject.toml "${selection[@]}" \ --project-matrix-python "$MATRIX_PYTHON" \ diff --git a/tests/build_python_distribution/test_build.py b/tests/build_python_distribution/test_build.py index 76322fd..4b39a88 100644 --- a/tests/build_python_distribution/test_build.py +++ b/tests/build_python_distribution/test_build.py @@ -11,7 +11,9 @@ @pytest.fixture -def build_repository(source_project: Path, nab_executable: Path, monkeypatch: pytest.MonkeyPatch) -> Path: +def build_repository( + source_project: Path, nab_executable: Path, locked_release_index: str, monkeypatch: pytest.MonkeyPatch +) -> Path: monkeypatch.delenv("SSL_CERT_FILE", raising=False) project = source_project / "pyproject.toml" release_requirement = tomllib.loads((Path(__file__).resolve().parents[2] / "pyproject.toml").read_text())[ @@ -21,6 +23,7 @@ def build_repository(source_project: Path, nab_executable: Path, monkeypatch: py project.read_text() + f"\n[dependency-groups]\nrelease-notes = [{json.dumps(release_requirement)}]\n" + '\n[tool.nab]\nbase-group = "project-runtime"\nbuild-group = "build"\n' + + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{locked_release_index}"\nserialization = "html"\n' ) run( source_project, diff --git a/tests/conftest.py b/tests/conftest.py index c21ef57..20418e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,14 +5,20 @@ import ssl import subprocess import sys +import tomllib from collections.abc import Iterator, Mapping +from hashlib import sha256 +from html import escape from pathlib import Path from textwrap import dedent from typing import Final +from urllib.parse import urlsplit +from urllib.request import urlopen import pytest import trustme import yaml +from packaging.utils import canonicalize_name from pytest_httpserver import HTTPServer from werkzeug.wrappers import Request, Response @@ -27,7 +33,7 @@ def github_api(tmp_path: Path) -> Iterator[tuple[HTTPServer, dict[str, str]]]: server.start() bundle = tmp_path / "ca.pem" authority.cert_pem.write_to_path(bundle) - if system_certs := ssl.get_default_verify_paths().cafile: + if (system_certs := ssl.get_default_verify_paths().cafile) and Path(system_certs).is_file(): bundle.write_bytes(bundle.read_bytes() + Path(system_certs).read_bytes()) yield ( server, @@ -80,6 +86,35 @@ def action_scripts(tmp_path_factory: pytest.TempPathFactory) -> Path: return scripts +@pytest.fixture +def locked_release_index(httpserver: HTTPServer) -> str: + packages = tomllib.loads((Path(__file__).resolve().parents[1] / "pylock.toml").read_text())["packages"] + listings: dict[str, list[str]] = {} + artifacts: dict[str, tuple[str, str]] = {} + for package in packages: + links = listings.setdefault(canonicalize_name(package["name"]), []) + for artifact in [*package.get("wheels", []), *([package["sdist"]] if "sdist" in package else [])]: + filename = artifact.get("name") or urlsplit(artifact["url"]).path.rsplit("/", 1)[-1] + digest = artifact["hashes"]["sha256"] + artifacts[filename] = artifact["url"], digest + links.append( + f'{escape(filename)}' + ) + for name, links in listings.items(): + httpserver.expect_request(f"/simple/{name}/").respond_with_data("\n".join(links), content_type="text/html") + for filename, (url, digest) in artifacts.items(): + + def serve(_request: Request, *, source: str = url, expected: str = digest) -> Response: + with urlopen(source, timeout=30) as stream: + content = stream.read() + assert sha256(content).hexdigest() == expected + return Response(content, mimetype="application/octet-stream") + + httpserver.expect_request(f"/files/{filename}").respond_with_handler(serve) + return httpserver.url_for("/simple/") + + @pytest.fixture(scope="session") def nab_executable(tmp_path_factory: pytest.TempPathFactory) -> Path: root = Path(__file__).resolve().parents[1] diff --git a/tests/install_locked_tools/test_install.py b/tests/install_locked_tools/test_install.py index 3cc274b..496fc91 100644 --- a/tests/install_locked_tools/test_install.py +++ b/tests/install_locked_tools/test_install.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import shutil import subprocess import tomllib @@ -110,6 +111,15 @@ def test_install_lock_tools_rejects_hash_mismatch(tmp_path: Path, action_scripts assert "Hash mismatch" in result.stderr +def test_install_nab_reuses_existing_environment(tmp_path: Path, action_scripts: Path) -> None: + source = Path(__file__).resolve().parents[2] + environment = {"GITHUB_ACTION_PATH": str(source / "install-nab")} + first = action(source, tmp_path, action_scripts, "install-nab", version="", **environment) + second = action(source, tmp_path, action_scripts, "install-nab", version="", **environment) + assert first.returncode == second.returncode == 0, first.stderr + second.stderr + assert (tmp_path / "lock-tools" / ("Scripts/nab.exe" if os.name == "nt" else "bin/nab")).is_file() + + def test_install_locked_tools_rejects_empty_groups(tmp_path: Path, action_scripts: Path) -> None: source = Path(__file__).resolve().parents[2] result = install( @@ -158,6 +168,36 @@ def test_install_locked_tools_checks_caller_release_creator(tmp_path: Path, loca assert (result.returncode == 0) is local_source, result.stderr +def test_caller_version_check_uses_shared_package_marker(tmp_path: Path) -> None: + source = Path(__file__).resolve().parents[2] + shared = tmp_path / "shared.toml" + lock = (source / "pylock.toml").read_text() + marker = next( + package["marker"] for package in tomllib.loads(lock)["packages"] if package["name"] == "release-creator" + ) + shared.write_text(lock.replace(marker.replace('"', '\\"'), '\\"release-shell\\" in dependency_groups')) + caller = tmp_path / "caller.toml" + caller.write_text( + 'lock-version = "1.0"\nrequires-python = ">=3.15"\n' + 'dependency-groups = ["release-shell"]\ncreated-by = "test"\npackages = []\n' + ) + result = subprocess.run( + [ + "python", + str(source / "tools/check_lock_groups.py"), + str(caller), + "release-shell", + "--shared-lock", + str(shared), + ], + text=True, + capture_output=True, + check=False, + ) + assert result.returncode != 0 + assert "relock: release-creator" in result.stderr + + def test_install_locked_tools_shared_group_ignores_missing_caller_group(tmp_path: Path, action_scripts: Path) -> None: source = Path(__file__).resolve().parents[2] lock = (source / "pylock.toml").read_text().replace(' "release-attestations",\n', "", 1) @@ -270,12 +310,13 @@ def test_install_locked_tools_merges_shared_and_caller_build_constraints(tmp_pat def test_install_locked_tools_accepts_lock_without_build_group( - tmp_path: Path, action_scripts: Path, nab_executable: Path + tmp_path: Path, action_scripts: Path, nab_executable: Path, locked_release_index: str ) -> None: (tmp_path / "pyproject.toml").write_text( '[project]\nname = "tool-only"\nversion = "0.1.0"\nrequires-python = ">=3.15"\n' '[dependency-groups]\nrelease-check = ["prek>=0.2"]\n' - '[tool.nab]\nbase-group = "project-runtime"\ndefault-groups = []\n', + '[tool.nab]\nbase-group = "project-runtime"\ndefault-groups = []\n' + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{locked_release_index}"\nserialization = "html"\n', ) generated = subprocess.run( [str(nab_executable), "lock", "--groups", "release-check", "--output", "pylock.toml"], diff --git a/tests/publish_github_release/test_publish.py b/tests/publish_github_release/test_publish.py index 1822fa4..321cb16 100644 --- a/tests/publish_github_release/test_publish.py +++ b/tests/publish_github_release/test_publish.py @@ -436,6 +436,63 @@ def test_draft_release_replaces_incomplete_asset( assert [request.method for request, _ in server.log] == ["GET", "GET", "DELETE", "GET", "GET", "GET"] +def test_failed_upload_deletes_partial_asset_before_retry( + repository: Path, + tmp_path: Path, + action_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + release_cli: dict[str, str], +) -> None: + server, environment = github_api + notes, commit = _notes_and_commit(repository, tmp_path) + assets = tmp_path / "assets" + assets.mkdir() + asset_path = assets / "fixture.zip" + asset_path.write_bytes(b"release archive") + draft = _release(notes, draft=True) + partial = _release( + notes, draft=True, assets=[{"id": "21", "name": asset_path.name, "digest": "", "state": "starter"}] + ) + complete = _release( + notes, + draft=True, + assets=[ + { + "name": asset_path.name, + "digest": f"sha256:{sha256(asset_path.read_bytes()).hexdigest()}", + "state": "uploaded", + } + ], + ) + prefix = "/api/v3/repos/tox-dev/fixture" + server.expect_ordered_request(f"{prefix}/commits/refs/tags/0.1.0", method="GET").respond_with_json({"sha": commit}) + server.expect_ordered_request(f"{prefix}/releases", query_string="per_page=100", method="GET").respond_with_json( + [draft] + ) + for release in (draft, partial, partial): + server.expect_ordered_request(f"{prefix}/releases/42", method="GET").respond_with_json(release) + server.expect_ordered_request(f"{prefix}/releases/assets/21", method="DELETE").respond_with_json({}) + for _ in range(3): + server.expect_ordered_request(f"{prefix}/releases/42", method="GET").respond_with_json(complete) + + result = _request( + repository, + tmp_path, + action_scripts, + notes, + commit, + environment | release_cli | {"GH_UPLOAD_FAILURE": "once"}, + assets=assets, + mode="stage", + ) + assert result.returncode == 0, result.stderr + assert [request.method for request, _ in server.log] == ["GET"] * 5 + ["DELETE"] + ["GET"] * 2 + assert _release_calls(tmp_path) == [ + ("release", "upload", "0.1.0", str(asset_path), "--repo", "tox-dev/fixture"), + ("release", "upload", "0.1.0", str(asset_path), "--repo", "tox-dev/fixture"), + ] + + def test_failed_asset_upload_leaves_draft_unpublished( repository: Path, tmp_path: Path, @@ -454,7 +511,7 @@ def test_failed_asset_upload_leaves_draft_unpublished( server.expect_ordered_request(f"{prefix}/releases", query_string="per_page=100", method="GET").respond_with_json( [draft] ) - for _ in range(7): + for _ in range(14): server.expect_ordered_request(f"{prefix}/releases/42", method="GET").respond_with_json(draft) result = _request( repository, @@ -466,7 +523,7 @@ def test_failed_asset_upload_leaves_draft_unpublished( assets=assets, ) assert result.returncode != 0 - assert [request.method for request, _ in server.log] == ["GET"] * 9 + assert [request.method for request, _ in server.log] == ["GET"] * 14 assert ( _release_calls(tmp_path) == [("release", "upload", "0.1.0", str(assets / "fixture.zip"), "--repo", "tox-dev/fixture")] * 6 diff --git a/tests/recheck_hook_update_cooldown/test_recheck.py b/tests/recheck_hook_update_cooldown/test_recheck.py index af4ef0e..5579a49 100644 --- a/tests/recheck_hook_update_cooldown/test_recheck.py +++ b/tests/recheck_hook_update_cooldown/test_recheck.py @@ -22,14 +22,16 @@ def test_recheck_schedule_has_only_required_permissions() -> None: @pytest.mark.parametrize( - ("observed_age", "job_age", "job_conclusion", "job_name", "reruns"), + ("observed_age", "job_age", "job_conclusion", "job_name", "minimum_days", "reruns"), [ - pytest.param(8, 8, "failure", "hook-cooldown", 1, id="matured-failure"), - pytest.param(8, 8, "failure", "Hook update cooldown", 1, id="named-job"), - pytest.param(6, 6, "failure", "hook-cooldown", 0, id="recent-pull"), - pytest.param(8, 0, "failure", "hook-cooldown", 1, id="failed-rerun"), - pytest.param(31, 31, "failure", "hook-cooldown", 0, id="expired-run"), - pytest.param(8, 8, "success", "hook-cooldown", 0, id="successful-job"), + pytest.param(8, 8, "failure", "hook-cooldown", 7, 1, id="matured-failure"), + pytest.param(8, 8, "failure", "Hook update cooldown", 7, 1, id="named-job"), + pytest.param(6, 6, "failure", "hook-cooldown", 7, 0, id="recent-pull"), + pytest.param(8, 8, "failure", "hook-cooldown", 7, 1, id="failed-rerun"), + pytest.param(31, 31, "failure", "hook-cooldown", 7, 0, id="expired-run"), + pytest.param(8, 8, "success", "hook-cooldown", 7, 0, id="successful-job"), + pytest.param(8, 8, "failure", "hook-cooldown", 14, 0, id="custom-cooldown-pending"), + pytest.param(15, 15, "failure", "hook-cooldown", 14, 1, id="custom-cooldown-matured"), ], ) @pytest.mark.parametrize( @@ -44,6 +46,7 @@ def test_recheck_reruns_failed_job( job_age: int, job_conclusion: str, job_name: str, + minimum_days: int, reruns: int, bot_login: str, ) -> None: @@ -85,7 +88,7 @@ def test_recheck_reruns_failed_job( ] } ) - if 7 <= observed_age < 30: + if minimum_days <= observed_age < 30: server.expect_request("/api/v3/repos/tox-dev/fixture/actions/runs/42/jobs", method="GET").respond_with_json( { "jobs": [ @@ -114,6 +117,7 @@ def test_recheck_reruns_failed_job( version="", GH_TOKEN=environment["GH_ENTERPRISE_TOKEN"], JOB_NAME=job_name, + MINIMUM_DAYS=str(minimum_days), **environment, ) assert result.returncode == 0, result.stderr diff --git a/tests/refresh_release_lock/test_cutoffs.py b/tests/refresh_release_lock/test_cutoffs.py index cd42887..e9cdc86 100644 --- a/tests/refresh_release_lock/test_cutoffs.py +++ b/tests/refresh_release_lock/test_cutoffs.py @@ -163,7 +163,59 @@ def test_recent_cutoff_requires_review(package: str, timestamp: str, accepted: b project = ( f'[tool.nab]\nuploaded-prior-to = "P7D"\n[tool.nab.packages.{package}]\nuploaded-prior-to = "{timestamp}"\n' ) + result = _check_recent_cutoffs(project) + assert (result.returncode == 0) == accepted + if not accepted: + assert "unreviewed recent Nab cutoff" in result.stderr + + +@pytest.mark.parametrize( + ("cutoff", "message"), + [ + pytest.param('uploaded-prior-to = "P0D"', "global Nab cutoff", id="zero-days"), + pytest.param('uploaded-prior-to = "P6D"', "global Nab cutoff", id="six-days"), + pytest.param("uploaded-prior-to = false", "global Nab cutoff", id="disabled"), + ], +) +def test_recent_cutoff_rejects_weak_global_policy(cutoff: str, message: str) -> None: + result = _check_recent_cutoffs(f"[tool.nab]\n{cutoff}\n") + assert result.returncode != 0 + assert message in result.stderr + + +@pytest.mark.parametrize( + ("value", "accepted"), + [ + pytest.param('"P0D"', False, id="zero-days"), + pytest.param('"P6D"', False, id="six-days"), + pytest.param("false", False, id="disabled"), + pytest.param('"P7D"', True, id="seven-days"), + pytest.param('"P14D"', True, id="fourteen-days"), + ], +) +def test_recent_cutoff_checks_package_duration(value: str, accepted: bool) -> None: + project = f'[tool.nab]\nuploaded-prior-to = "P7D"\n[tool.nab.packages.example]\nuploaded-prior-to = {value}\n' + result = _check_recent_cutoffs(project) + assert (result.returncode == 0) is accepted + if not accepted: + assert "weakens the seven-day policy" in result.stderr + + +def test_expire_cutoffs_preserves_disabled_package(cutoff_project: Path) -> None: + cutoff_project.write_text(cutoff_project.read_text().replace('"2999-01-01T00:00:00Z"', "false")) + source = Path(__file__).resolve().parents[2] / "tools" result = subprocess.run( + [sys.executable, str(source / "expire_cutoffs.py"), str(cutoff_project)], + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert tomllib.loads(cutoff_project.read_text())["tool"]["nab"]["packages"]["new"]["uploaded-prior-to"] is False + + +def _check_recent_cutoffs(project: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( [ sys.executable, "-c", @@ -178,6 +230,3 @@ def test_recent_cutoff_requires_review(package: str, timestamp: str, accepted: b text=True, check=False, ) - assert (result.returncode == 0) == accepted - if not accepted: - assert "unreviewed recent Nab cutoff" in result.stderr diff --git a/tests/upgrade_release_tools/test_upgrade.py b/tests/upgrade_release_tools/test_upgrade.py index dcff75e..6cc9c43 100644 --- a/tests/upgrade_release_tools/test_upgrade.py +++ b/tests/upgrade_release_tools/test_upgrade.py @@ -57,7 +57,7 @@ def test_upgrade_pull_request_uses_repository_app() -> None: @pytest.fixture -def lock_repository(tmp_path: Path) -> Path: +def lock_repository(tmp_path: Path, locked_release_index: str) -> Path: repository = tmp_path / "repository" repository.mkdir() (repository / "pyproject.toml").write_text( @@ -66,7 +66,8 @@ def lock_repository(tmp_path: Path) -> Path: '[tool.nab]\nmode = "universal"\nuploaded-prior-to = "P7D"\n' 'decision-order = "stable"\nbase-group = "project-runtime"\ndefault-groups = []\n' '[tool.nab.matrix]\npython = ">=3.15,<3.16"\n' - 'platforms = ["linux_x86_64", "linux_aarch64", "macos_arm64", "windows_amd64"]\n', + 'platforms = ["linux_x86_64", "linux_aarch64", "macos_arm64", "windows_amd64"]\n' + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{locked_release_index}"\nserialization = "html"\n', ) run(repository, "git", "init", "-b", "main") run(repository, "git", "config", "user.name", "Release test") @@ -96,7 +97,10 @@ def test_upgrade_locks_only_selected_groups(lock_repository: Path, tmp_path: Pat def test_upgrade_check_mode_preserves_lock_bytes(lock_repository: Path, tmp_path: Path, upgrade_scripts: Path) -> None: project = lock_repository / "pyproject.toml" - project.write_text(project.read_text().replace("release = []", 'release-check = ["prek>=0.2"]')) + project.write_text( + project.read_text().replace("release = []", 'release-check = ["prek>=0.2"]') + + '[tool.nab.packages.old]\nuploaded-prior-to = "2020-01-01T00:00:00Z"\n' + ) installed = action(lock_repository, tmp_path, upgrade_scripts, "install-nab", version="") assert installed.returncode == 0, installed.stderr refreshed = execute( @@ -108,30 +112,39 @@ def test_upgrade_check_mode_preserves_lock_bytes(lock_repository: Path, tmp_path ) assert refreshed.returncode == 0, refreshed.stderr expected = (lock_repository / "pylock.toml").read_bytes() - checked = execute( + project_bytes = project.read_bytes() + run(lock_repository, "git", "add", ".") + run(lock_repository, "git", "commit", "-m", "Record lock") + checked = action( lock_repository, - upgrade_scripts / "refresh-release-lock-3.sh", tmp_path, + upgrade_scripts, + "refresh-release-lock", + version="", LOCK_GROUPS="release-check", MODE="check", ) assert checked.returncode == 0, checked.stderr assert (lock_repository / "pylock.toml").read_bytes() == expected + assert project.read_bytes() == project_bytes @pytest.mark.parametrize( - ("groups", "selection"), + ("groups", "selection", "source"), [ - pytest.param("", "--build-requirements", id="build-only"), - pytest.param("release-check", "--groups", id="selected-group"), + pytest.param("", "--build-requirements", "build-or-groups", id="build-only"), + pytest.param("release-check", "--groups", "build-or-groups", id="selected-group"), + pytest.param("", "", "project", id="project-runtime"), ], ) def test_upgrade_preserves_secondary_lock_matrix( - lock_repository: Path, tmp_path: Path, upgrade_scripts: Path, groups: str, selection: str + lock_repository: Path, tmp_path: Path, upgrade_scripts: Path, groups: str, selection: str, source: str ) -> None: project = lock_repository / "pyproject.toml" project.write_text( - project.read_text().replace("release = []", 'release-check = ["prek>=0.2"]') + project.read_text() + .replace('version = "0.1.0"', 'version = "0.1.0"\ndependencies = ["packaging>=26"]') + .replace("release = []", 'release-check = ["prek>=0.2"]') + '[build-system]\nrequires = ["setuptools>=80"]\nbuild-backend = "setuptools.build_meta"\n', ) installed = action(lock_repository, tmp_path, upgrade_scripts, "install-nab", version="") @@ -154,8 +167,9 @@ def test_upgrade_preserves_secondary_lock_matrix( "nab", "lock", "pyproject.toml", - selection, ] + if selection: + command.append(selection) if groups: command.append(groups) command.extend( @@ -179,6 +193,7 @@ def test_upgrade_preserves_secondary_lock_matrix( "MODE": "check", "ADDITIONAL_LOCK": additional.name, "ADDITIONAL_GROUPS": groups, + "ADDITIONAL_SELECTION": source, "MATRIX_PYTHON": ">=3.15,<3.16", "MATRIX_PLATFORMS": "linux_x86_64 macos_arm64", "MATRIX_IMPLEMENTATIONS": "cpython", @@ -186,6 +201,10 @@ def test_upgrade_preserves_secondary_lock_matrix( checked = execute(lock_repository, upgrade_scripts / "refresh-release-lock-3.sh", tmp_path, **environment) assert checked.returncode == 0, checked.stderr assert additional.read_bytes() == expected + if source == "project": + names = {package["name"] for package in tomllib.loads(expected.decode())["packages"]} + assert "packaging" in names + assert "setuptools" not in names def test_upgrade_rejects_secondary_matrix_change(lock_repository: Path, tmp_path: Path, upgrade_scripts: Path) -> None: @@ -471,6 +490,7 @@ def execute(repository: Path, script: Path, tmp_path: Path, **environment: str) "GITHUB_ACTION_PATH": str(Path(__file__).resolve().parents[2] / "refresh-release-lock"), "ADDITIONAL_LOCK": "", "ADDITIONAL_GROUPS": "", + "ADDITIONAL_SELECTION": "build-or-groups", "MATRIX_PYTHON": "", "MATRIX_PLATFORMS": "", "MATRIX_IMPLEMENTATIONS": "", diff --git a/tests/validate_python_distribution/test_distribution.py b/tests/validate_python_distribution/test_distribution.py index 48edea9..b822c0d 100644 --- a/tests/validate_python_distribution/test_distribution.py +++ b/tests/validate_python_distribution/test_distribution.py @@ -9,11 +9,15 @@ @pytest.fixture -def distribution(source_project: Path, nab_executable: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[Path, Path]: +def distribution( + source_project: Path, nab_executable: Path, locked_release_index: str, monkeypatch: pytest.MonkeyPatch +) -> tuple[Path, Path]: monkeypatch.delenv("SSL_CERT_FILE", raising=False) project = source_project / "pyproject.toml" project.write_text( - project.read_text() + '\n[dependency-groups]\npkg-meta = ["check-wheel-contents>=0.6.3", "twine>=7"]\n', + project.read_text() + + '\n[dependency-groups]\npkg-meta = ["check-wheel-contents>=0.6.3", "twine>=7"]\n' + + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{locked_release_index}"\nserialization = "html"\n', ) run( source_project, diff --git a/tools/check_lock_groups.py b/tools/check_lock_groups.py index f6341f4..90d6397 100644 --- a/tools/check_lock_groups.py +++ b/tools/check_lock_groups.py @@ -7,7 +7,7 @@ from pathlib import Path from packaging.pylock import Pylock, PylockValidationError -from packaging.version import Version +from packaging.version import InvalidVersion, Version def main() -> None: @@ -18,29 +18,34 @@ def main() -> None: parser.add_argument("--local-tool-source", action="store_true") args = parser.parse_args() try: + groups = set(args.groups) with args.lock.open("rb") as stream: lock = Pylock.from_dict(tomllib.load(stream)) - if missing := set(args.groups) - set(lock.dependency_groups or ()): + if missing := groups - set(lock.dependency_groups or ()): parser.error(f"missing lock dependency groups: {', '.join(sorted(missing))}") if args.shared_lock: with args.shared_lock.open("rb") as stream: shared = Pylock.from_dict(tomllib.load(stream)) - required = next(package.version for package in shared.packages if package.name == "release-creator") - installed = next((package.version for package in lock.packages if package.name == "release-creator"), None) - needs_release_creator = bool( - set(args.groups) - & {"release-news", "release-notes", "release-prepare", "release-validate", "release-hooks"} - ) - if ( - needs_release_creator - and not args.local_tool_source - and (installed is None or Version(str(installed)) < Version(str(required))) - ): + required = _selected_release_creator(shared, groups) + installed = _selected_release_creator(lock, groups) + if required is not None and not args.local_tool_source and (installed is None or installed < required): parser.error(f"relock: release-creator {required} required") - except (OSError, PylockValidationError) as error: + except (InvalidVersion, OSError, PylockValidationError) as error: parser.error(str(error)) +def _selected_release_creator(lock: Pylock, groups: set[str]) -> Version | None: + return next( + ( + Version(str(package.version)) + for package in lock.packages + if package.name == "release-creator" + and (package.marker is None or package.marker.evaluate({"dependency_groups": groups}, context="lock_file")) + ), + None, + ) + + if __name__ == "__main__": main() diff --git a/tools/check_recent_cutoffs.py b/tools/check_recent_cutoffs.py index a1b6cec..8471b00 100644 --- a/tools/check_recent_cutoffs.py +++ b/tools/check_recent_cutoffs.py @@ -15,7 +15,7 @@ def main() -> None: args = parser.parse_args() try: validate_recent_cutoffs(args.project.read_text(), datetime.now(UTC) - timedelta(days=7)) - except (OSError, KeyError, ValueError) as error: + except (OSError, KeyError, TypeError, ValueError) as error: parser.error(str(error)) diff --git a/tools/expire_cutoffs.py b/tools/expire_cutoffs.py index 7b26a44..3ed9857 100644 --- a/tools/expire_cutoffs.py +++ b/tools/expire_cutoffs.py @@ -19,7 +19,7 @@ def main() -> None: updated = remove_expired_cutoffs(source, datetime.now(UTC) - timedelta(days=7)) if source != updated: args.project.write_text(updated) - except (OSError, KeyError, ValueError, ParseError) as error: + except (OSError, KeyError, TypeError, ValueError, ParseError) as error: parser.error(str(error)) diff --git a/tools/nab_cutoffs.py b/tools/nab_cutoffs.py index d65df7f..f0f8521 100644 --- a/tools/nab_cutoffs.py +++ b/tools/nab_cutoffs.py @@ -1,7 +1,19 @@ from __future__ import annotations +import re import tomllib from datetime import UTC, datetime +from typing import Final + +_DURATION: Final = re.compile( + r""" + ^P # ISO duration prefix + (?P[0-9]+) # Nab supports whole days + D$ # Day unit and end + """, + re.VERBOSE, +) +_MINIMUM_DAYS: Final = 7 _REVIEWED_EXCEPTIONS = { **dict.fromkeys( @@ -9,11 +21,17 @@ datetime(2026, 10, 19, tzinfo=UTC), ), "release-creator": datetime(2026, 10, 23, 2, 44, 40, tzinfo=UTC), + "uv-build": datetime(2026, 10, 21, tzinfo=UTC), # toml-fmt requires 0.13.4 until the cutoff matures. } def validate_recent_cutoffs(project: str, threshold: datetime) -> None: nab = tomllib.loads(project)["tool"]["nab"] + global_cutoff = nab.get("uploaded-prior-to") + if not isinstance(global_cutoff, str) or (match := _DURATION.fullmatch(global_cutoff)) is None: + raise ValueError("global Nab cutoff must be a duration of at least P7D") + if int(match["days"]) < _MINIMUM_DAYS: + raise ValueError("global Nab cutoff must be a duration of at least P7D") candidates = [ (name, settings["uploaded-prior-to"]) for name, settings in nab.get("packages", {}).items() @@ -26,8 +44,16 @@ def validate_recent_cutoffs(project: str, threshold: datetime) -> None: for name in rule["match"] ) for name, value in candidates: - if isinstance(value, str) and value.startswith("P"): + if value is False: + if name not in _REVIEWED_EXCEPTIONS: + raise ValueError(f"Nab cutoff for {name} weakens the seven-day policy: {value}") + continue + if isinstance(value, str) and (match := _DURATION.fullmatch(value)) is not None: + if name not in _REVIEWED_EXCEPTIONS and int(match["days"]) < _MINIMUM_DAYS: + raise ValueError(f"Nab cutoff for {name} weakens the seven-day policy: {value}") continue + if isinstance(value, bool): + raise TypeError(f"invalid Nab cutoff for {name}: {value}") timestamp = datetime.fromisoformat(value) if isinstance(value, str) else value if timestamp.tzinfo is None: raise ValueError(f"Nab package cutoff has no timezone: {value}") @@ -66,7 +92,7 @@ def remove_cutoffs(nab: dict, names: set[str], positions: set[int]) -> None: def expired_cutoffs(project: str, threshold: datetime) -> tuple[set[str], set[int]]: nab = tomllib.loads(project)["tool"]["nab"] packages = { - name + str(name) for name, settings in nab.get("packages", {}).items() if (value := settings.get("uploaded-prior-to")) is not None and _expired(value, threshold) } @@ -78,9 +104,11 @@ def expired_cutoffs(project: str, threshold: datetime) -> tuple[set[str], set[in return packages, rules -def _expired(value: str | datetime, threshold: datetime) -> bool: - if isinstance(value, str) and value.startswith("P"): +def _expired(value: str | datetime | bool, threshold: datetime) -> bool: + if value is False or isinstance(value, str) and _DURATION.fullmatch(value) is not None: return False + if isinstance(value, bool): + raise TypeError(f"invalid Nab cutoff: {value}") timestamp = datetime.fromisoformat(value) if isinstance(value, str) else value if timestamp.tzinfo is None: raise ValueError(f"Nab package cutoff has no timezone: {value}") From 850cb550f5ce35fafbd46c37af0873be43b414cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Thu, 22 Oct 2026 20:58:30 -0700 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9D=20docs(lock):=20clarify=20proj?= =?UTF-8?q?ect=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/upgrade-release-tools.yaml | 2 +- README.md | 7 ++++--- refresh-release-lock/action.yml | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/upgrade-release-tools.yaml b/.github/workflows/upgrade-release-tools.yaml index f9059ac..3668ce8 100644 --- a/.github/workflows/upgrade-release-tools.yaml +++ b/.github/workflows/upgrade-release-tools.yaml @@ -15,7 +15,7 @@ on: type: string default: "" additional-lock-selection: - description: Use project runtime dependencies for the secondary lock + description: Select project dependencies and configured groups for the secondary lock type: string default: build-or-groups additional-matrix-python: diff --git a/README.md b/README.md index 8eb3361..1940386 100644 --- a/README.md +++ b/README.md @@ -161,9 +161,10 @@ For a separately checked-in build matrix lock, also pass `additional-lock` with `additional-matrix-python` with its Python range, and `additional-matrix-platforms` and `additional-matrix-implementations` as space-separated Nab values. The secondary lock keeps its specified environment matrix during refresh. `additional-lock-groups` selects dependency groups for it; when empty, Nab locks the project's -build requirements. Set `additional-lock-selection: project` to lock only the project's runtime dependencies. That -selection cannot be combined with `additional-lock-groups`. Secondary-lock inputs without `additional-lock` fail. The -writer accepts only the named secondary lock path. +build requirements. Set `additional-lock-selection: project` to include the project's runtime dependencies and any +configured build group. The consumer must select its runtime group when reading that lock. This selection cannot be +combined with `additional-lock-groups`. Secondary-lock inputs without `additional-lock` fail. The writer accepts only +the named secondary lock path. For a repository that already commits `pylock.build.toml`, the complete caller is: diff --git a/refresh-release-lock/action.yml b/refresh-release-lock/action.yml index 13d35cc..3b8480c 100644 --- a/refresh-release-lock/action.yml +++ b/refresh-release-lock/action.yml @@ -11,7 +11,7 @@ inputs: description: Groups for the secondary lock; empty selects build requirements default: "" additional-lock-selection: - description: Use project runtime dependencies for the secondary lock + description: Select project dependencies and configured groups for the secondary lock default: build-or-groups additional-matrix-python: description: Python range for the secondary lock From 5732e1805a4c70c230209d542d58241037e3dd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Thu, 22 Oct 2026 22:11:22 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20fix(release):=20close=20audi?= =?UTF-8?q?t=20gaps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reject unbounded Nab cutoff overrides and project-level policy bypasses. Keep test-only tools outside the committed release lock. Test the Windows install and lock refresh paths against their failure modes. --- .github/workflows/check.yaml | 2 +- .github/workflows/update-tools.yaml | 2 +- README.md | 9 +- publish-github-release/action.yml | 7 +- pylock.toml | 600 +----------------- pyproject.toml | 4 +- tests/build_python_distribution/test_build.py | 5 +- tests/conftest.py | 41 +- tests/install_locked_tools/test_install.py | 41 +- tests/publish_github_release/test_publish.py | 36 ++ .../test_recheck.py | 2 +- tests/refresh_release_lock/test_cutoffs.py | 53 ++ tests/upgrade_release_tools/test_upgrade.py | 11 +- .../test_distribution.py | 4 +- tools/check_lock_groups.py | 7 +- tools/check_recent_cutoffs.py | 2 + tools/nab_cutoffs.py | 9 +- tools/verify_cutoff_patch.py | 2 +- 18 files changed, 227 insertions(+), 610 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index eda7222..72af74e 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -190,7 +190,7 @@ jobs: - uses: $/refresh-release-lock with: lock-groups: >- - build lock-tools release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check release-fixture + build lock-tools release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check mode: check gate: name: ${{ github.event_name == 'pull_request' && 'Required checks pass' || 'Required checks pass (non-PR)' }} diff --git a/.github/workflows/update-tools.yaml b/.github/workflows/update-tools.yaml index 15eff86..49a6580 100644 --- a/.github/workflows/update-tools.yaml +++ b/.github/workflows/update-tools.yaml @@ -13,7 +13,7 @@ jobs: RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} with: lock-groups: >- - build lock-tools release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check release-fixture + build lock-tools release-notes release-news release-prepare release-validate release-check release-attestations release-shell release-hooks release-hook-check commit-message: "🔧 build: refresh release tool lock" pull-request-title: "🔧 build: refresh release tool lock" pull-request-body: "Refresh the locked tools used by release preparation and CI." diff --git a/README.md b/README.md index 1940386..95e4349 100644 --- a/README.md +++ b/README.md @@ -157,14 +157,17 @@ jobs: The required `lock-groups` input lists the caller's release dependency groups, separated by spaces or newlines. Include `build` and `lock-tools` where declared; exclude test, type, and documentation groups. This example lists this repository's groups; a caller supplies its own. The project configures its Python and platform targets in `[tool.nab]`. -For a separately checked-in build matrix lock, also pass `additional-lock` with an existing `pylock.*.toml` basename, -`additional-matrix-python` with its Python range, and `additional-matrix-platforms` and +Tests install test, type, and documentation groups afresh so they detect upstream dependency changes. The Action tests +fetch immutable, hash-checked release-tool artifacts from the shared lock; their separate package fixture resolves +test-only tools afresh. For a separately checked-in build matrix lock, also pass `additional-lock` with an existing +`pylock.*.toml` basename, `additional-matrix-python` with its Python range, and `additional-matrix-platforms` and `additional-matrix-implementations` as space-separated Nab values. The secondary lock keeps its specified environment matrix during refresh. `additional-lock-groups` selects dependency groups for it; when empty, Nab locks the project's build requirements. Set `additional-lock-selection: project` to include the project's runtime dependencies and any configured build group. The consumer must select its runtime group when reading that lock. This selection cannot be combined with `additional-lock-groups`. Secondary-lock inputs without `additional-lock` fail. The writer accepts only -the named secondary lock path. +the named secondary lock path. Use `project` only when those runtime dependencies become part of the published artifact, +such as a bundled zipapp; tests still install runtime dependencies afresh. For a repository that already commits `pylock.build.toml`, the complete caller is: diff --git a/publish-github-release/action.yml b/publish-github-release/action.yml index 1f91502..f313aac 100644 --- a/publish-github-release/action.yml +++ b/publish-github-release/action.yml @@ -179,10 +179,13 @@ runs: "$RUNNER_TEMP/release-current.json" >/dev/null; then for attempt in {1..6}; do if ((attempt > 1)); then + release=$(gh api "repos/$GH_REPO/releases/$release_id") + asset_ids=$(jq -r --arg name "$name" \ + '.assets[] | select(.name == $name and .state != "uploaded") | .id' <<<"$release") while IFS= read -r asset_id; do + [[ -n "$asset_id" ]] || continue gh api -X DELETE "repos/$GH_REPO/releases/assets/$asset_id" >/dev/null - done < <(gh api "repos/$GH_REPO/releases/$release_id" | jq -r --arg name "$name" \ - '.assets[] | select(.name == $name and .state != "uploaded") | .id') + done <<<"$asset_ids" fi if gh release upload "$TAG" "$asset" --repo "$GH_REPO"; then break diff --git a/pylock.toml b/pylock.toml index 5f8aea0..35bfd47 100644 --- a/pylock.toml +++ b/pylock.toml @@ -12,7 +12,6 @@ dependency-groups = [ "project-runtime", "release-attestations", "release-check", - "release-fixture", "release-hook-check", "release-hooks", "release-news", @@ -29,7 +28,7 @@ created-by = "nab" [[packages]] name = "annotated-types" version = "0.8.0" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.10" index = "https://pypi.org/simple/" @@ -76,31 +75,6 @@ size = 43294 [packages.wheels.hashes] sha256 = "6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19" -[[packages]] -name = "attrs" -version = "26.1.0" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.9" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "attrs-26.1.0.tar.gz" -upload-time = 2026-03-19 14:22:25.026315+00:00 -url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz" -size = 952055 - -[packages.sdist.hashes] -sha256 = "d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" - -[[packages.wheels]] -name = "attrs-26.1.0-py3-none-any.whl" -upload-time = 2026-03-19 14:22:23.645947+00:00 -url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl" -size = 67548 - -[packages.wheels.hashes] -sha256 = "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" - [[packages]] name = "build" version = "1.6.1" @@ -134,7 +108,7 @@ sha256 = "ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7" [[packages]] name = "certifi" version = "2026.7.22" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.7" index = "https://pypi.org/simple/" @@ -159,7 +133,7 @@ sha256 = "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775" [[packages]] name = "cffi" version = "2.1.1" -marker = "\"release-attestations\" in dependency_groups or (\"release-fixture\" in dependency_groups and sys_platform == \"linux\")" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "pycparser" }, @@ -214,7 +188,7 @@ sha256 = "e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960" [[packages]] name = "charset-normalizer" version = "3.5.2" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.7" index = "https://pypi.org/simple/" @@ -308,42 +282,10 @@ size = 68872 [packages.wheels.hashes] sha256 = "b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685" -[[packages]] -name = "check-wheel-contents" -version = "0.6.3" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "attrs" }, - { name = "click" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "wheel-filename" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "check_wheel_contents-0.6.3.tar.gz" -upload-time = 2025-08-02 14:01:45.546952+00:00 -url = "https://files.pythonhosted.org/packages/95/37/2b9e0d38c2f668791ffe8b97711185c364d91c027e47f189c371c2348d18/check_wheel_contents-0.6.3.tar.gz" -size = 586023 - -[packages.sdist.hashes] -sha256 = "10e6939e2fe4e6ce1edf2ff6ec6157808677e80782e78021ae139dd88473a442" - -[[packages.wheels]] -name = "check_wheel_contents-0.6.3-py3-none-any.whl" -upload-time = 2025-08-02 14:01:43.968164+00:00 -url = "https://files.pythonhosted.org/packages/be/05/f39fde9f31ef80b285ef5822fad4ddabf73fec62a1f02c5beb4b2f328972/check_wheel_contents-0.6.3-py3-none-any.whl" -size = 27541 - -[packages.wheels.hashes] -sha256 = "5ae39c8c434b972f0740d04610759168590713175aab584b012b1b84f6771874" - [[packages]] name = "click" version = "8.5.0" -marker = "\"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.10" index = "https://pypi.org/simple/" @@ -393,7 +335,7 @@ sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" [[packages]] name = "cryptography" version = "50.0.2" -marker = "\"release-attestations\" in dependency_groups or (\"release-fixture\" in dependency_groups and sys_platform == \"linux\")" +marker = "\"release-attestations\" in dependency_groups" requires-python = "!=3.9.0,!=3.9.1,>=3.9" dependencies = [ { name = "cffi" }, @@ -714,7 +656,7 @@ sha256 = "9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9" [[packages]] name = "docutils" version = "0.23" -marker = "\"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -765,42 +707,10 @@ size = 35604 [packages.wheels.hashes] sha256 = "80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4" -[[packages]] -name = "hatchling" -version = "1.32.4" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "packaging" }, - { name = "pathspec" }, - { name = "pluggy" }, - { name = "tomlkit" }, - { name = "trove-classifiers" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "hatchling-1.32.4.tar.gz" -upload-time = 2026-09-20 22:48:45.398505+00:00 -url = "https://files.pythonhosted.org/packages/f6/97/b5312f01a8c6daf729a9d272dd442e0c546dbcc630495788786c4b567ed0/hatchling-1.32.4.tar.gz" -size = 58065 - -[packages.sdist.hashes] -sha256 = "c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07" - -[[packages.wheels]] -name = "hatchling-1.32.4-py3-none-any.whl" -upload-time = 2026-09-20 22:48:44.190681+00:00 -url = "https://files.pythonhosted.org/packages/5f/80/91f51f439c05d4ec4623c22928ce16a938d6d793bf709477830823497859/hatchling-1.32.4-py3-none-any.whl" -size = 78748 - -[packages.wheels.hashes] -sha256 = "08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c" - [[packages]] name = "id" version = "1.6.1" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.9" dependencies = [ { name = "urllib3" }, @@ -828,7 +738,7 @@ sha256 = "f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca" [[packages]] name = "idna" version = "3.20" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -875,112 +785,6 @@ size = 464466 [packages.wheels.hashes] sha256 = "f55f5728ff61d3dd54dbc5a397cf3f7307cb64b180c20b72afbebf01c9e2c8a0" -[[packages]] -name = "jaraco-classes" -version = "3.4.0" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.8" -dependencies = [ - { name = "more-itertools" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "jaraco.classes-3.4.0.tar.gz" -upload-time = 2024-03-31 07:27:36.643708+00:00 -url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" -size = 11780 - -[packages.sdist.hashes] -sha256 = "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd" - -[[packages.wheels]] -name = "jaraco.classes-3.4.0-py3-none-any.whl" -upload-time = 2024-03-31 07:27:34.792832+00:00 -url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl" -size = 6777 - -[packages.wheels.hashes] -sha256 = "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" - -[[packages]] -name = "jaraco-context" -version = "6.1.2" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.10" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "jaraco_context-6.1.2.tar.gz" -upload-time = 2026-03-20 22:13:33.922545+00:00 -url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz" -size = 16801 - -[packages.sdist.hashes] -sha256 = "f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3" - -[[packages.wheels]] -name = "jaraco_context-6.1.2-py3-none-any.whl" -upload-time = 2026-03-20 22:13:32.808832+00:00 -url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl" -size = 7871 - -[packages.wheels.hashes] -sha256 = "bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535" - -[[packages]] -name = "jaraco-functools" -version = "4.6.0" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "more-itertools" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "jaraco_functools-4.6.0.tar.gz" -upload-time = 2026-07-14 01:28:02.544761+00:00 -url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz" -size = 20837 - -[packages.sdist.hashes] -sha256 = "880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280" - -[[packages.wheels]] -name = "jaraco_functools-4.6.0-py3-none-any.whl" -upload-time = 2026-07-14 01:28:01.590470+00:00 -url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl" -size = 11677 - -[packages.wheels.hashes] -sha256 = "99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30" - -[[packages]] -name = "jeepney" -version = "0.9.0" -marker = "\"release-fixture\" in dependency_groups and sys_platform == \"linux\"" -requires-python = ">=3.7" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "jeepney-0.9.0.tar.gz" -upload-time = 2025-02-27 18:51:01.684959+00:00 -url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz" -size = 106758 - -[packages.sdist.hashes] -sha256 = "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732" - -[[packages.wheels]] -name = "jeepney-0.9.0-py3-none-any.whl" -upload-time = 2025-02-27 18:51:00.104279+00:00 -url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl" -size = 49010 - -[packages.wheels.hashes] -sha256 = "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" - [[packages]] name = "jinja2" version = "3.1.6" @@ -1009,43 +813,10 @@ size = 134899 [packages.wheels.hashes] sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" -[[packages]] -name = "keyring" -version = "25.7.0" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.9" -dependencies = [ - { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, - { name = "jeepney" }, - { name = "pywin32-ctypes" }, - { name = "secretstorage" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "keyring-25.7.0.tar.gz" -upload-time = 2025-11-16 16:26:09.482176+00:00 -url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz" -size = 63516 - -[packages.sdist.hashes] -sha256 = "fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b" - -[[packages.wheels]] -name = "keyring-25.7.0-py3-none-any.whl" -upload-time = 2025-11-16 16:26:08.402146+00:00 -url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl" -size = 39160 - -[packages.wheels.hashes] -sha256 = "be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" - [[packages]] name = "markdown-it-py" version = "4.2.0" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "mdurl" }, @@ -1212,7 +983,7 @@ sha256 = "214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d" [[packages]] name = "mdurl" version = "0.1.2" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.7" index = "https://pypi.org/simple/" @@ -1234,31 +1005,6 @@ size = 9979 [packages.wheels.hashes] sha256 = "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" -[[packages]] -name = "more-itertools" -version = "11.2.1" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.11" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "more_itertools-11.2.1.tar.gz" -upload-time = 2026-10-14 18:50:30.407709+00:00 -url = "https://files.pythonhosted.org/packages/52/b8/c63995bebbb9ac9c059350e0fb8c82c2420c85aeca7d34e40aecf3df9281/more_itertools-11.2.1.tar.gz" -size = 154133 - -[packages.sdist.hashes] -sha256 = "cbf08fd0af284dc69718b9b76a8e6203df624d70209ea511e4219d350c856f63" - -[[packages.wheels]] -name = "more_itertools-11.2.1-py3-none-any.whl" -upload-time = 2026-10-14 18:50:29.116732+00:00 -url = "https://files.pythonhosted.org/packages/5d/57/3c4b1c6f2e0cc29ba023802458ea3d2a996f87085559e4a5f2192d549532/more_itertools-11.2.1-py3-none-any.whl" -size = 74405 - -[packages.wheels.hashes] -sha256 = "35a7377edd1dd6608dcb2cdf534ded55ea32d49448875ad042cd3f879fb1ded0" - [[packages]] name = "nab" version = "0.0.22" @@ -1443,62 +1189,10 @@ size = 58597 [packages.wheels.hashes] sha256 = "6f77daa98279ef0eedb9e8f2248aee4c50e01b92b470b68886995e5056d79971" -[[packages]] -name = "nh3" -version = "0.3.7" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.8" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "nh3-0.3.7.tar.gz" -upload-time = 2026-08-23 14:26:30.728660+00:00 -url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz" -size = 25662 - -[packages.sdist.hashes] -sha256 = "71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848" - -[[packages.wheels]] -name = "nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" -upload-time = 2026-08-23 14:26:09.025294+00:00 -url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" -size = 1493959 - -[packages.wheels.hashes] -sha256 = "6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5" - -[[packages.wheels]] -name = "nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" -upload-time = 2026-08-23 14:26:10.606442+00:00 -url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" -size = 859615 - -[packages.wheels.hashes] -sha256 = "f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479" - -[[packages.wheels]] -name = "nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" -upload-time = 2026-08-23 14:26:17.272038+00:00 -url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" -size = 832161 - -[packages.wheels.hashes] -sha256 = "70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42" - -[[packages.wheels]] -name = "nh3-0.3.7-cp38-abi3-win_amd64.whl" -upload-time = 2026-08-23 14:26:28.294566+00:00 -url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl" -size = 644462 - -[packages.wheels.hashes] -sha256 = "618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc" - [[packages]] name = "packaging" version = "26.3" -marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" +marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-hooks\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"release-validate\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -1520,31 +1214,6 @@ size = 129956 [packages.wheels.hashes] sha256 = "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" -[[packages]] -name = "pathspec" -version = "1.1.1" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.9" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "pathspec-1.1.1.tar.gz" -upload-time = 2026-04-27 01:46:08.907631+00:00 -url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz" -size = 135180 - -[packages.sdist.hashes] -sha256 = "17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a" - -[[packages.wheels]] -name = "pathspec-1.1.1-py3-none-any.whl" -upload-time = 2026-04-27 01:46:07.060850+00:00 -url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl" -size = 57328 - -[packages.wheels.hashes] -sha256 = "a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" - [[packages]] name = "platformdirs" version = "4.13.3" @@ -1570,31 +1239,6 @@ size = 32762 [packages.wheels.hashes] sha256 = "f6ad7f447f24f8a3b82cce5976387428bff894a0eca6c3488f4a17f153c130c4" -[[packages]] -name = "pluggy" -version = "1.7.0" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.10" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "pluggy-1.7.0.tar.gz" -upload-time = 2026-10-15 09:50:58.343484+00:00 -url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz" -size = 123304 - -[packages.sdist.hashes] -sha256 = "d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8" - -[[packages.wheels]] -name = "pluggy-1.7.0-py3-none-any.whl" -upload-time = 2026-10-15 09:50:56.808462+00:00 -url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl" -size = 27082 - -[packages.wheels.hashes] -sha256 = "7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec" - [[packages]] name = "prek" version = "0.5.5" @@ -1684,7 +1328,7 @@ sha256 = "deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b" [[packages]] name = "pycparser" version = "3.11" -marker = "\"release-attestations\" in dependency_groups or (\"release-fixture\" in dependency_groups and sys_platform == \"linux\")" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.10" index = "https://pypi.org/simple/" @@ -1709,7 +1353,7 @@ sha256 = "51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80" [[packages]] name = "pydantic" version = "2.14.1" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "annotated-types" }, @@ -1741,7 +1385,7 @@ sha256 = "9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454" [[packages]] name = "pydantic-core" version = "2.50.1" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "typing-extensions" }, @@ -1796,7 +1440,7 @@ sha256 = "983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed" [[packages]] name = "pygments" version = "2.21.0" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -1931,31 +1575,6 @@ size = 10724 [packages.wheels.hashes] sha256 = "5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b" -[[packages]] -name = "pywin32-ctypes" -version = "0.2.3" -marker = "\"release-fixture\" in dependency_groups and sys_platform == \"win32\"" -requires-python = ">=3.6" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "pywin32-ctypes-0.2.3.tar.gz" -upload-time = 2024-08-14 10:15:34.626878+00:00 -url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz" -size = 29471 - -[packages.sdist.hashes] -sha256 = "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" - -[[packages.wheels]] -name = "pywin32_ctypes-0.2.3-py3-none-any.whl" -upload-time = 2024-08-14 10:15:33.187242+00:00 -url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl" -size = 30756 - -[packages.wheels.hashes] -sha256 = "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" - [[packages]] name = "pyyaml" version = "6.0.3" @@ -1972,36 +1591,6 @@ size = 130960 [packages.sdist.hashes] sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" -[[packages]] -name = "readme-renderer" -version = "46.0" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "docutils" }, - { name = "nh3" }, - { name = "pygments" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "readme_renderer-46.0.tar.gz" -upload-time = 2026-08-28 15:18:32.490778+00:00 -url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz" -size = 38382 - -[packages.sdist.hashes] -sha256 = "af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90" - -[[packages.wheels]] -name = "readme_renderer-46.0-py3-none-any.whl" -upload-time = 2026-08-28 15:18:31.132970+00:00 -url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl" -size = 14239 - -[packages.wheels.hashes] -sha256 = "d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560" - [[packages]] name = "release-creator" version = "0.6.0" @@ -2038,7 +1627,7 @@ sha256 = "4b9cd6187c87fcb7bf3aca5f58c67c90703e58875643af633661a0448756d45f" [[packages]] name = "requests" version = "2.34.2" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "certifi" }, @@ -2066,34 +1655,6 @@ size = 73075 [packages.wheels.hashes] sha256 = "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" -[[packages]] -name = "requests-toolbelt" -version = "1.0.0" -marker = "\"release-fixture\" in dependency_groups" -requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -dependencies = [ - { name = "requests" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "requests-toolbelt-1.0.0.tar.gz" -upload-time = 2023-05-01 04:11:33.229998+00:00 -url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz" -size = 206888 - -[packages.sdist.hashes] -sha256 = "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" - -[[packages.wheels]] -name = "requests_toolbelt-1.0.0-py2.py3-none-any.whl" -upload-time = 2023-05-01 04:11:28.427086+00:00 -url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl" -size = 54481 - -[packages.wheels.hashes] -sha256 = "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - [[packages]] name = "rfc3161-client" version = "1.0.9" @@ -2152,7 +1713,7 @@ sha256 = "908f6a775da4bdce1d39f825a4431f389542696078ef3e4ed911a068bcaf1792" [[packages]] name = "rfc3986" version = "2.0.0" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.7" index = "https://pypi.org/simple/" @@ -2202,7 +1763,7 @@ sha256 = "520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48" [[packages]] name = "rich" version = "15.0.0" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.9.0" dependencies = [ { name = "markdown-it-py" }, @@ -2228,35 +1789,6 @@ size = 310654 [packages.wheels.hashes] sha256 = "33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb" -[[packages]] -name = "secretstorage" -version = "3.5.0" -marker = "\"release-fixture\" in dependency_groups and sys_platform == \"linux\"" -requires-python = ">=3.10" -dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "secretstorage-3.5.0.tar.gz" -upload-time = 2025-11-23 19:02:53.191898+00:00 -url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz" -size = 19884 - -[packages.sdist.hashes] -sha256 = "f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be" - -[[packages.wheels]] -name = "secretstorage-3.5.0-py3-none-any.whl" -upload-time = 2025-11-23 19:02:51.545472+00:00 -url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl" -size = 15554 - -[packages.wheels.hashes] -sha256 = "0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137" - [[packages]] name = "securesystemslib" version = "1.5.1" @@ -2494,7 +2026,7 @@ sha256 = "188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90" [[packages]] name = "tomlkit" version = "0.15.1" -marker = "\"lock-tools\" in dependency_groups or \"release-fixture\" in dependency_groups or \"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups" +marker = "\"lock-tools\" in dependency_groups or \"release-hook-check\" in dependency_groups or \"release-shell\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -2545,30 +2077,6 @@ size = 67030 [packages.wheels.hashes] sha256 = "ae4d223e6aadaff98d29b7f09e58d9c9ccf4cd3b455a2d2e0486d432cd8bf660" -[[packages]] -name = "trove-classifiers" -version = "2026.9.21.13" -marker = "\"release-fixture\" in dependency_groups" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "trove_classifiers-2026.9.21.13.tar.gz" -upload-time = 2026-09-21 13:29:07.301512+00:00 -url = "https://files.pythonhosted.org/packages/bf/93/af436dfaa845cab5d96f0adbc1e4f3730532d37fa249e4eb796fb1d7fc82/trove_classifiers-2026.9.21.13.tar.gz" -size = 17085 - -[packages.sdist.hashes] -sha256 = "0a9ebc8d4e2f3e8a22848c5258033035bec17a3012ac3fea16dbaa764489eb71" - -[[packages.wheels]] -name = "trove_classifiers-2026.9.21.13-py3-none-any.whl" -upload-time = 2026-09-21 13:29:06.123578+00:00 -url = "https://files.pythonhosted.org/packages/30/81/0da8afb52a71d0a4f2bd3152357b1a441e393b286374802b9d3addab4ab5/trove_classifiers-2026.9.21.13-py3-none-any.whl" -size = 14232 - -[packages.wheels.hashes] -sha256 = "8b1ff4f9c191b1040b71c37f1e445ab99732911e3cd91de52838453a854d7a17" - [[packages]] name = "truststore" version = "0.10.5" @@ -2623,46 +2131,10 @@ size = 56253 [packages.wheels.hashes] sha256 = "d30434bda6e079ab303fb30d1b3006d939a10ca34783b1573d61cd9b802fa45c" -[[packages]] -name = "twine" -version = "7.0.0" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.10" -dependencies = [ - { name = "id" }, - { name = "keyring" }, - { name = "packaging" }, - { name = "readme-renderer" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "rfc3986" }, - { name = "rich" }, - { name = "urllib3" }, -] -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "twine-7.0.0.tar.gz" -upload-time = 2026-07-27 15:59:00.825945+00:00 -url = "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz" -size = 215032 - -[packages.sdist.hashes] -sha256 = "85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177" - -[[packages.wheels]] -name = "twine-7.0.0-py3-none-any.whl" -upload-time = 2026-07-27 15:58:59.260418+00:00 -url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl" -size = 43204 - -[packages.wheels.hashes] -sha256 = "b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7" - [[packages]] name = "typing-extensions" version = "4.16.0" -marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups" requires-python = ">=3.9" index = "https://pypi.org/simple/" @@ -2687,7 +2159,7 @@ sha256 = "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" [[packages]] name = "typing-inspection" version = "0.4.4" -marker = "\"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"release-attestations\" in dependency_groups" requires-python = ">=3.10" dependencies = [ { name = "typing-extensions" }, @@ -2715,7 +2187,7 @@ sha256 = "65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147" [[packages]] name = "urllib3" version = "2.8.0" -marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups or \"release-fixture\" in dependency_groups" +marker = "\"lock-tools\" in dependency_groups or \"release-attestations\" in dependency_groups" requires-python = ">=3.10" index = "https://pypi.org/simple/" @@ -2798,31 +2270,6 @@ size = 301667 [packages.wheels.hashes] sha256 = "89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e" -[[packages]] -name = "wheel-filename" -version = "1.4.2" -marker = "\"release-fixture\" in dependency_groups" -requires-python = ">=3.8" -index = "https://pypi.org/simple/" - -[packages.sdist] -name = "wheel_filename-1.4.2.tar.gz" -upload-time = 2024-12-01 13:03:16.012739+00:00 -url = "https://files.pythonhosted.org/packages/38/be/726dab762b770d0417e505c58e26d661aac1ec0c831e483cda4817ca2417/wheel_filename-1.4.2.tar.gz" -size = 7911 - -[packages.sdist.hashes] -sha256 = "87891c465dcbb40b40394a906f01a93214bdd51aa5d25e3a9a59cae62bc298fd" - -[[packages.wheels]] -name = "wheel_filename-1.4.2-py3-none-any.whl" -upload-time = 2024-12-01 13:03:00.536117+00:00 -url = "https://files.pythonhosted.org/packages/b4/0f/6e97a3bc38cdde32e3ec49f8c0903fe3559ec9ec9db181782f0bb4417717/wheel_filename-1.4.2-py3-none-any.whl" -size = 6195 - -[packages.wheels.hashes] -sha256 = "3fa599046443d4ca830d06e3d180cd0a675d5871af0a68daa5623318bb4d17e3" - [[packages]] name = "xmltodict" version = "1.0.4" @@ -2897,7 +2344,6 @@ command-line = [ "release-shell", "release-hooks", "release-hook-check", - "release-fixture", "--output", "pylock.toml", ] diff --git a/pyproject.toml b/pyproject.toml index ce31fee..054f645 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,8 +19,9 @@ release-attestations = [ "pypi-attestations>=0.0.30" ] release-shell = [ "prek>=0.2", "yq>=4.4.5" ] release-hooks = [ "release-creator>=0.6.0", "prek>=0.2" ] release-hook-check = [ "prek>=0.2", "yq>=4.4.5" ] -release-fixture = [ "check-wheel-contents>=0.6.3", "hatchling>=1.28", "twine>=7" ] test = [ + "certifi", + "check-wheel-contents>=0.6.3", "hatchling>=1.28", "pytest>=9", "pytest-httpserver", @@ -30,6 +31,7 @@ test = [ "tox>=4.64.7", "tox-uv>=1.36.1", "trustme", + "twine>=7", "ty", ] diff --git a/tests/build_python_distribution/test_build.py b/tests/build_python_distribution/test_build.py index 4b39a88..97f7255 100644 --- a/tests/build_python_distribution/test_build.py +++ b/tests/build_python_distribution/test_build.py @@ -12,9 +12,10 @@ @pytest.fixture def build_repository( - source_project: Path, nab_executable: Path, locked_release_index: str, monkeypatch: pytest.MonkeyPatch + source_project: Path, nab_executable: Path, test_package_index: str, monkeypatch: pytest.MonkeyPatch ) -> Path: monkeypatch.delenv("SSL_CERT_FILE", raising=False) + monkeypatch.setenv("UV_INDEX_URL", test_package_index) project = source_project / "pyproject.toml" release_requirement = tomllib.loads((Path(__file__).resolve().parents[2] / "pyproject.toml").read_text())[ "dependency-groups" @@ -23,7 +24,7 @@ def build_repository( project.read_text() + f"\n[dependency-groups]\nrelease-notes = [{json.dumps(release_requirement)}]\n" + '\n[tool.nab]\nbase-group = "project-runtime"\nbuild-group = "build"\n' - + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{locked_release_index}"\nserialization = "html"\n' + + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{test_package_index}"\nserialization = "html"\n' ) run( source_project, diff --git a/tests/conftest.py b/tests/conftest.py index 20418e6..d7c5851 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ from urllib.parse import urlsplit from urllib.request import urlopen +import certifi import pytest import trustme import yaml @@ -33,8 +34,7 @@ def github_api(tmp_path: Path) -> Iterator[tuple[HTTPServer, dict[str, str]]]: server.start() bundle = tmp_path / "ca.pem" authority.cert_pem.write_to_path(bundle) - if (system_certs := ssl.get_default_verify_paths().cafile) and Path(system_certs).is_file(): - bundle.write_bytes(bundle.read_bytes() + Path(system_certs).read_bytes()) + bundle.write_bytes(bundle.read_bytes() + Path(certifi.where()).read_bytes()) yield ( server, { @@ -86,9 +86,30 @@ def action_scripts(tmp_path_factory: pytest.TempPathFactory) -> Path: return scripts +@pytest.fixture(scope="session") +def test_package_lock(tmp_path_factory: pytest.TempPathFactory, nab_executable: Path) -> Path: + fixture = tmp_path_factory.mktemp("fixture-lock") + (fixture / "pyproject.toml").write_text( + '[project]\nname = "release-fixture"\nversion = "0.1.0"\nrequires-python = ">=3.15,<3.16"\n' + '[dependency-groups]\nfixture = ["check-wheel-contents>=0.6.3", "hatchling>=1.28", "twine>=7"]\n' + '[tool.nab]\nmode = "universal"\nbase-group = "project-runtime"\n' + 'default-groups = []\n[tool.nab.matrix]\npython = ">=3.15,<3.16"\nplatforms = ["linux_x86_64"]\n' + ) + subprocess.run( + [str(nab_executable), "lock", "--groups", "fixture", "--output", "pylock.toml"], + cwd=fixture, + check=True, + capture_output=True, + ) + return fixture / "pylock.toml" + + @pytest.fixture -def locked_release_index(httpserver: HTTPServer) -> str: - packages = tomllib.loads((Path(__file__).resolve().parents[1] / "pylock.toml").read_text())["packages"] +def test_package_index(httpserver: HTTPServer, test_package_lock: Path, artifact_cache: dict[str, bytes]) -> str: + packages = [ + *tomllib.loads((Path(__file__).resolve().parents[1] / "pylock.toml").read_text())["packages"], + *tomllib.loads(test_package_lock.read_text())["packages"], + ] listings: dict[str, list[str]] = {} artifacts: dict[str, tuple[str, str]] = {} for package in packages: @@ -106,8 +127,10 @@ def locked_release_index(httpserver: HTTPServer) -> str: for filename, (url, digest) in artifacts.items(): def serve(_request: Request, *, source: str = url, expected: str = digest) -> Response: - with urlopen(source, timeout=30) as stream: - content = stream.read() + if expected not in artifact_cache: + with urlopen(source, timeout=30) as stream: + artifact_cache[expected] = stream.read() + content = artifact_cache[expected] assert sha256(content).hexdigest() == expected return Response(content, mimetype="application/octet-stream") @@ -115,6 +138,11 @@ def serve(_request: Request, *, source: str = url, expected: str = digest) -> Re return httpserver.url_for("/simple/") +@pytest.fixture(scope="session") +def artifact_cache() -> dict[str, bytes]: + return {} + + @pytest.fixture(scope="session") def nab_executable(tmp_path_factory: pytest.TempPathFactory) -> Path: root = Path(__file__).resolve().parents[1] @@ -424,6 +452,7 @@ def _safe_environment() -> dict[str, str]: "LANG", "PATH", "UV_CACHE_DIR", + "UV_INDEX_URL", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", diff --git a/tests/install_locked_tools/test_install.py b/tests/install_locked_tools/test_install.py index 496fc91..1854e1f 100644 --- a/tests/install_locked_tools/test_install.py +++ b/tests/install_locked_tools/test_install.py @@ -120,6 +120,20 @@ def test_install_nab_reuses_existing_environment(tmp_path: Path, action_scripts: assert (tmp_path / "lock-tools" / ("Scripts/nab.exe" if os.name == "nt" else "bin/nab")).is_file() +def test_install_nab_reuses_windows_shaped_environment(tmp_path: Path, action_scripts: Path) -> None: + subprocess.run(["uv", "venv", "--python", "3.15", str(tmp_path / "lock-tools")], check=True, capture_output=True) + (tmp_path / "lock-tools" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).unlink() + result = subprocess.run( + ["bash", str(action_scripts / "install-nab-0.sh")], + env=os.environ | {"RUNNER_TEMP": str(tmp_path)}, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert (tmp_path / "lock-tools/pyvenv.cfg").is_file() + + def test_install_locked_tools_rejects_empty_groups(tmp_path: Path, action_scripts: Path) -> None: source = Path(__file__).resolve().parents[2] result = install( @@ -168,6 +182,29 @@ def test_install_locked_tools_checks_caller_release_creator(tmp_path: Path, loca assert (result.returncode == 0) is local_source, result.stderr +def test_local_tool_source_accepts_versionless_package(tmp_path: Path) -> None: + source = Path(__file__).resolve().parents[2] + project = tomllib.loads((source / "pylock.toml").read_text()) + package = next(item for item in project["packages"] if item["name"] == "release-creator") + lock = (source / "pylock.toml").read_text().replace(f'version = "{package["version"]}"', "", 1) + (tmp_path / "pylock.toml").write_text(lock) + result = subprocess.run( + [ + "python", + str(source / "tools/check_lock_groups.py"), + str(tmp_path / "pylock.toml"), + "release-prepare", + "--shared-lock", + str(source / "pylock.toml"), + "--local-tool-source", + ], + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stderr + + def test_caller_version_check_uses_shared_package_marker(tmp_path: Path) -> None: source = Path(__file__).resolve().parents[2] shared = tmp_path / "shared.toml" @@ -310,13 +347,13 @@ def test_install_locked_tools_merges_shared_and_caller_build_constraints(tmp_pat def test_install_locked_tools_accepts_lock_without_build_group( - tmp_path: Path, action_scripts: Path, nab_executable: Path, locked_release_index: str + tmp_path: Path, action_scripts: Path, nab_executable: Path, test_package_index: str ) -> None: (tmp_path / "pyproject.toml").write_text( '[project]\nname = "tool-only"\nversion = "0.1.0"\nrequires-python = ">=3.15"\n' '[dependency-groups]\nrelease-check = ["prek>=0.2"]\n' '[tool.nab]\nbase-group = "project-runtime"\ndefault-groups = []\n' - f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{locked_release_index}"\nserialization = "html"\n', + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{test_package_index}"\nserialization = "html"\n', ) generated = subprocess.run( [str(nab_executable), "lock", "--groups", "release-check", "--output", "pylock.toml"], diff --git a/tests/publish_github_release/test_publish.py b/tests/publish_github_release/test_publish.py index 321cb16..ba0e4d8 100644 --- a/tests/publish_github_release/test_publish.py +++ b/tests/publish_github_release/test_publish.py @@ -493,6 +493,42 @@ def test_failed_upload_deletes_partial_asset_before_retry( ] +def test_failed_asset_lookup_stops_retry( + repository: Path, + tmp_path: Path, + action_scripts: Path, + github_api: tuple[HTTPServer, dict[str, str]], + release_cli: dict[str, str], +) -> None: + server, environment = github_api + notes, commit = _notes_and_commit(repository, tmp_path) + assets = tmp_path / "assets" + assets.mkdir() + asset_path = assets / "fixture.zip" + asset_path.write_bytes(b"release archive") + draft = _release(notes, draft=True) + prefix = "/api/v3/repos/tox-dev/fixture" + server.expect_ordered_request(f"{prefix}/commits/refs/tags/0.1.0", method="GET").respond_with_json({"sha": commit}) + server.expect_ordered_request(f"{prefix}/releases", query_string="per_page=100", method="GET").respond_with_json( + [draft] + ) + for _ in range(2): + server.expect_ordered_request(f"{prefix}/releases/42", method="GET").respond_with_json(draft) + server.expect_ordered_request(f"{prefix}/releases/42", method="GET").respond_with_data(status=500) + result = _request( + repository, + tmp_path, + action_scripts, + notes, + commit, + environment | release_cli | {"GH_UPLOAD_FAILURE": "once"}, + assets=assets, + mode="stage", + ) + assert result.returncode != 0 + assert _release_calls(tmp_path) == [("release", "upload", "0.1.0", str(asset_path), "--repo", "tox-dev/fixture")] + + def test_failed_asset_upload_leaves_draft_unpublished( repository: Path, tmp_path: Path, diff --git a/tests/recheck_hook_update_cooldown/test_recheck.py b/tests/recheck_hook_update_cooldown/test_recheck.py index 5579a49..62f8920 100644 --- a/tests/recheck_hook_update_cooldown/test_recheck.py +++ b/tests/recheck_hook_update_cooldown/test_recheck.py @@ -27,7 +27,7 @@ def test_recheck_schedule_has_only_required_permissions() -> None: pytest.param(8, 8, "failure", "hook-cooldown", 7, 1, id="matured-failure"), pytest.param(8, 8, "failure", "Hook update cooldown", 7, 1, id="named-job"), pytest.param(6, 6, "failure", "hook-cooldown", 7, 0, id="recent-pull"), - pytest.param(8, 8, "failure", "hook-cooldown", 7, 1, id="failed-rerun"), + pytest.param(8, 0, "failure", "hook-cooldown", 7, 1, id="failed-rerun"), pytest.param(31, 31, "failure", "hook-cooldown", 7, 0, id="expired-run"), pytest.param(8, 8, "success", "hook-cooldown", 7, 0, id="successful-job"), pytest.param(8, 8, "failure", "hook-cooldown", 14, 0, id="custom-cooldown-pending"), diff --git a/tests/refresh_release_lock/test_cutoffs.py b/tests/refresh_release_lock/test_cutoffs.py index e9cdc86..212ae23 100644 --- a/tests/refresh_release_lock/test_cutoffs.py +++ b/tests/refresh_release_lock/test_cutoffs.py @@ -201,6 +201,59 @@ def test_recent_cutoff_checks_package_duration(value: str, accepted: bool) -> No assert "weakens the seven-day policy" in result.stderr +@pytest.mark.parametrize("package", ["nab", "release-creator", "uv-build"], ids=["nab", "creator", "uv-build"]) +@pytest.mark.parametrize("value", ['"P0D"', "false"], ids=["zero-days", "disabled"]) +def test_reviewed_cutoff_rejects_unbounded_override(package: str, value: str) -> None: + project = f'[tool.nab]\nuploaded-prior-to = "P7D"\n[tool.nab.packages.{package}]\nuploaded-prior-to = {value}\n' + result = _check_recent_cutoffs(project) + assert result.returncode != 0 + assert "weakens the seven-day policy" in result.stderr + + +def test_recent_cutoff_rejects_index_override() -> None: + result = _check_recent_cutoffs( + '[tool.nab]\nuploaded-prior-to = "P7D"\n' + '[[tool.nab.indexes]]\nname = "pypi"\nurl = "https://pypi.org/simple/"\n' + "[tool.nab.index.pypi]\nuploaded-prior-to = false\n" + ) + assert result.returncode != 0 + assert "Nab index cutoff for pypi" in result.stderr + + +def test_recent_cutoff_rejects_project_nab_toml(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[tool.nab]\nuploaded-prior-to = "P7D"\n') + (tmp_path / "nab.toml").write_text("[packages.example]\nuploaded-prior-to = false\n") + result = subprocess.run( + [sys.executable, str(Path(__file__).resolve().parents[2] / "tools/check_recent_cutoffs.py"), "pyproject.toml"], + cwd=tmp_path, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode != 0 + assert "project nab.toml can override" in result.stderr + + +def test_verify_cutoff_patch_reports_invalid_boolean(cutoff_project: Path) -> None: + cutoff_project.write_text(cutoff_project.read_text().replace('"2026-01-01T00:00:00Z"', "true", 1)) + subprocess.run(["git", "add", "pyproject.toml"], cwd=cutoff_project.parent, check=True, capture_output=True) + subprocess.run( + ["git", "-c", "user.name=Release test", "-c", "user.email=release@example.org", "commit", "-m", "Invalid"], + cwd=cutoff_project.parent, + check=True, + capture_output=True, + ) + result = subprocess.run( + [sys.executable, str(Path(__file__).resolve().parents[2] / "tools/verify_cutoff_patch.py")], + cwd=cutoff_project.parent, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode != 0 + assert "invalid Nab cutoff: True" in result.stderr + + def test_expire_cutoffs_preserves_disabled_package(cutoff_project: Path) -> None: cutoff_project.write_text(cutoff_project.read_text().replace('"2999-01-01T00:00:00Z"', "false")) source = Path(__file__).resolve().parents[2] / "tools" diff --git a/tests/upgrade_release_tools/test_upgrade.py b/tests/upgrade_release_tools/test_upgrade.py index 6cc9c43..6c58d70 100644 --- a/tests/upgrade_release_tools/test_upgrade.py +++ b/tests/upgrade_release_tools/test_upgrade.py @@ -57,7 +57,7 @@ def test_upgrade_pull_request_uses_repository_app() -> None: @pytest.fixture -def lock_repository(tmp_path: Path, locked_release_index: str) -> Path: +def lock_repository(tmp_path: Path, test_package_index: str) -> Path: repository = tmp_path / "repository" repository.mkdir() (repository / "pyproject.toml").write_text( @@ -67,7 +67,7 @@ def lock_repository(tmp_path: Path, locked_release_index: str) -> Path: 'decision-order = "stable"\nbase-group = "project-runtime"\ndefault-groups = []\n' '[tool.nab.matrix]\npython = ">=3.15,<3.16"\n' 'platforms = ["linux_x86_64", "linux_aarch64", "macos_arm64", "windows_amd64"]\n' - f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{locked_release_index}"\nserialization = "html"\n', + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{test_package_index}"\nserialization = "html"\n', ) run(repository, "git", "init", "-b", "main") run(repository, "git", "config", "user.name", "Release test") @@ -115,6 +115,8 @@ def test_upgrade_check_mode_preserves_lock_bytes(lock_repository: Path, tmp_path project_bytes = project.read_bytes() run(lock_repository, "git", "add", ".") run(lock_repository, "git", "commit", "-m", "Record lock") + (lock_repository / "pylock.toml").write_bytes(expected.replace(b'"release-check"', b'"release-news"', 1)) + stale = (lock_repository / "pylock.toml").read_bytes() checked = action( lock_repository, tmp_path, @@ -124,8 +126,9 @@ def test_upgrade_check_mode_preserves_lock_bytes(lock_repository: Path, tmp_path LOCK_GROUPS="release-check", MODE="check", ) - assert checked.returncode == 0, checked.stderr - assert (lock_repository / "pylock.toml").read_bytes() == expected + assert checked.returncode != 0 + assert "regenerated Nab lock differs" in checked.stderr + assert (lock_repository / "pylock.toml").read_bytes() != stale assert project.read_bytes() == project_bytes diff --git a/tests/validate_python_distribution/test_distribution.py b/tests/validate_python_distribution/test_distribution.py index b822c0d..c124fc8 100644 --- a/tests/validate_python_distribution/test_distribution.py +++ b/tests/validate_python_distribution/test_distribution.py @@ -10,14 +10,14 @@ @pytest.fixture def distribution( - source_project: Path, nab_executable: Path, locked_release_index: str, monkeypatch: pytest.MonkeyPatch + source_project: Path, nab_executable: Path, test_package_index: str, monkeypatch: pytest.MonkeyPatch ) -> tuple[Path, Path]: monkeypatch.delenv("SSL_CERT_FILE", raising=False) project = source_project / "pyproject.toml" project.write_text( project.read_text() + '\n[dependency-groups]\npkg-meta = ["check-wheel-contents>=0.6.3", "twine>=7"]\n' - + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{locked_release_index}"\nserialization = "html"\n', + + f'[[tool.nab.indexes]]\nname = "fixture"\nurl = "{test_package_index}"\nserialization = "html"\n', ) run( source_project, diff --git a/tools/check_lock_groups.py b/tools/check_lock_groups.py index 90d6397..9a73156 100644 --- a/tools/check_lock_groups.py +++ b/tools/check_lock_groups.py @@ -27,9 +27,10 @@ def main() -> None: with args.shared_lock.open("rb") as stream: shared = Pylock.from_dict(tomllib.load(stream)) required = _selected_release_creator(shared, groups) - installed = _selected_release_creator(lock, groups) - if required is not None and not args.local_tool_source and (installed is None or installed < required): - parser.error(f"relock: release-creator {required} required") + if required is not None and not args.local_tool_source: + installed = _selected_release_creator(lock, groups) + if installed is None or installed < required: + parser.error(f"relock: release-creator {required} required") except (InvalidVersion, OSError, PylockValidationError) as error: parser.error(str(error)) diff --git a/tools/check_recent_cutoffs.py b/tools/check_recent_cutoffs.py index 8471b00..b9b816d 100644 --- a/tools/check_recent_cutoffs.py +++ b/tools/check_recent_cutoffs.py @@ -14,6 +14,8 @@ def main() -> None: parser.add_argument("project", type=Path) args = parser.parse_args() try: + if (args.project.parent / "nab.toml").exists(): + parser.error("project nab.toml can override the reviewed cutoff policy") validate_recent_cutoffs(args.project.read_text(), datetime.now(UTC) - timedelta(days=7)) except (OSError, KeyError, TypeError, ValueError) as error: parser.error(str(error)) diff --git a/tools/nab_cutoffs.py b/tools/nab_cutoffs.py index f0f8521..a4a3907 100644 --- a/tools/nab_cutoffs.py +++ b/tools/nab_cutoffs.py @@ -43,13 +43,14 @@ def validate_recent_cutoffs(project: str, threshold: datetime) -> None: if "uploaded-prior-to" in rule for name in rule["match"] ) + for name, settings in nab.get("index", {}).items(): + if "uploaded-prior-to" in settings: + raise ValueError(f"Nab index cutoff for {name} bypasses the seven-day policy") for name, value in candidates: if value is False: - if name not in _REVIEWED_EXCEPTIONS: - raise ValueError(f"Nab cutoff for {name} weakens the seven-day policy: {value}") - continue + raise ValueError(f"Nab cutoff for {name} weakens the seven-day policy: {value}") if isinstance(value, str) and (match := _DURATION.fullmatch(value)) is not None: - if name not in _REVIEWED_EXCEPTIONS and int(match["days"]) < _MINIMUM_DAYS: + if int(match["days"]) < _MINIMUM_DAYS: raise ValueError(f"Nab cutoff for {name} weakens the seven-day policy: {value}") continue if isinstance(value, bool): diff --git a/tools/verify_cutoff_patch.py b/tools/verify_cutoff_patch.py index 57c57d5..1b6c525 100644 --- a/tools/verify_cutoff_patch.py +++ b/tools/verify_cutoff_patch.py @@ -36,7 +36,7 @@ def main() -> None: remove_cutoffs(before, removed_names, removed_positions) if original != actual: parser.error("pyproject.toml has changes beyond expired Nab cutoffs") - except (OSError, KeyError, ValueError, subprocess.CalledProcessError, tomllib.TOMLDecodeError) as error: + except (OSError, KeyError, TypeError, ValueError, subprocess.CalledProcessError, tomllib.TOMLDecodeError) as error: parser.error(str(error))