diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index c6e6ba93b..77247678a 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -132,11 +132,15 @@ jobs: - dev - docs - readme + - type + - type-3.8 - upgrade - zipapp exclude: - - { os: windows-2025, tox_env: readme } - { os: windows-2025, tox_env: docs } + - { os: windows-2025, tox_env: readme } + - { os: windows-2025, tox_env: type } + - { os: windows-2025, tox_env: type-3.8 } steps: - name: 🚀 Install uv uses: astral-sh/setup-uv@v4 diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml new file mode 100644 index 000000000..cbb9dff9e --- /dev/null +++ b/.github/workflows/pre-release.yaml @@ -0,0 +1,42 @@ +name: Pre-release +on: + workflow_dispatch: + inputs: + bump: + description: "Version bump type" + required: true + type: choice + options: + - auto + - major + - minor + - patch + default: auto + +jobs: + pre-release: + runs-on: ubuntu-24.04 + environment: release + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_RELEASE_TOKEN }} + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Python + run: uv python install 3.14 + - name: Configure git identity from token owner + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + run: | + user_info=$(gh api /user) + git config user.name "$(echo "$user_info" | jq -r '.name // .login')" + git config user.email "$(echo "$user_info" | jq -r '.id')+$(echo "$user_info" | jq -r '.login')@users.noreply.github.com" + - name: Generate changelog, commit, tag, and push + run: uv tool run --with tox-uv tox r -e release -- --version "${{ inputs.bump }}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3f601f1fe..7938012a0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,17 +1,7 @@ name: Release on: - workflow_dispatch: - inputs: - bump: - description: "Version bump type" - required: true - type: choice - options: - - auto - - major - - minor - - patch - default: auto + push: + tags: ["*.*.*"] env: dists-artifact-name: python-package-distributions @@ -19,15 +9,11 @@ env: jobs: build: runs-on: ubuntu-24.04 - environment: release - outputs: - version: ${{ steps.resolve.outputs.version }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.GH_RELEASE_TOKEN }} - name: Install the latest version of uv uses: astral-sh/setup-uv@v4 with: @@ -36,26 +22,10 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python run: uv python install 3.14 - - name: Configure git identity from token owner - env: - GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} - run: | - user_info=$(gh api /user) - git config user.name "$(echo "$user_info" | jq -r '.name // .login')" - git config user.email "$(echo "$user_info" | jq -r '.id')+$(echo "$user_info" | jq -r '.login')@users.noreply.github.com" - - name: Generate changelog, commit, and tag locally - id: resolve - run: | - uv tool run --with tox-uv tox r -e release -- --version "${{ inputs.bump }}" --no-push - echo "version=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT" - name: Build sdist and wheel run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist - name: Build zipapp - run: uv tool run --with packaging --with pip tox r -e zipapp - - name: Push release commit and tag - run: | - git push origin HEAD:main - git push origin "${{ steps.resolve.outputs.version }}" + run: uv tool run --with tox-uv tox r -e zipapp - name: Store the distribution packages uses: actions/upload-artifact@v4 with: @@ -72,15 +42,11 @@ jobs: runs-on: ubuntu-24.04 environment: name: release - url: https://pypi.org/project/virtualenv/${{ needs.build.outputs.version }} + url: https://pypi.org/project/virtualenv/${{ github.ref_name }} permissions: contents: write id-token: write steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Download all the dists uses: actions/download-artifact@v4 with: @@ -97,7 +63,7 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ needs.build.outputs.version }} + tag_name: ${{ github.ref_name }} generate_release_notes: true files: virtualenv.pyz - name: Update get-virtualenv @@ -106,13 +72,13 @@ jobs: run: | git clone https://x-access-token:${GH_TOKEN}@github.com/pypa/get-virtualenv.git cp virtualenv.pyz get-virtualenv/public/virtualenv.pyz - echo -n "${{ needs.build.outputs.version }}" > get-virtualenv/public/version.txt + echo -n "${{ github.ref_name }}" > get-virtualenv/public/version.txt cd get-virtualenv user_info=$(gh api /user) git config user.name "$(echo "$user_info" | jq -r '.name // .login')" git config user.email "$(echo "$user_info" | jq -r '.id')+$(echo "$user_info" | jq -r '.login')@users.noreply.github.com" git add public/virtualenv.pyz public/version.txt - git commit -m "update virtualenv to ${{ needs.build.outputs.version }}" + git commit -m "update virtualenv to ${{ github.ref_name }}" git push origin main rollback: @@ -131,9 +97,9 @@ jobs: - name: Delete GitHub Release if created env: GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} - run: gh release delete "${{ needs.build.outputs.version }}" --yes --cleanup-tag || true + run: gh release delete "${{ github.ref_name }}" --yes --cleanup-tag || true - name: Delete remote tag - run: git push origin --delete "${{ needs.build.outputs.version }}" || true + run: git push origin --delete "${{ github.ref_name }}" || true - name: Reset release commit on main run: | git checkout main @@ -146,7 +112,7 @@ jobs: git clone https://x-access-token:${GH_TOKEN}@github.com/pypa/get-virtualenv.git cd get-virtualenv current_version=$(cat public/version.txt) - if [ "$current_version" = "${{ needs.build.outputs.version }}" ]; then + if [ "$current_version" = "${{ github.ref_name }}" ]; then gh release delete "$current_version" --yes --cleanup-tag || true git reset --hard HEAD~1 git push origin main --force diff --git a/docs/development.rst b/docs/development.rst index 121a50743..7eb05cdc4 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -123,64 +123,24 @@ The ``--version`` argument to ``tox r -e release`` controls the version. It defa ``docs/changelog`` directory: if any ``*.feature.rst`` or ``*.removal.rst`` fragments exist, the minor version is bumped, otherwise the patch version is bumped. You can also pass ``major``, ``minor``, or ``patch`` explicitly. +Both methods produce identical results: a release commit and tag on ``main``. Pushing the tag triggers the `Release +workflow `_ which builds the sdist, wheel, and +zipapp, publishes to PyPI via trusted publisher, creates a `GitHub Release +`_ with the zipapp attached, and updates `get-virtualenv +`_. If publish fails, a rollback job automatically reverts everything. + **Via GitHub Actions (recommended)** -1. Go to the `Release workflow `_ on GitHub. +1. Go to the `Pre-release workflow `_ on GitHub. 2. Click **Run workflow** and select the bump type (``auto``, ``major``, ``minor``, or ``patch``). -3. The workflow runs in two phases: - - **Build** (nothing is published yet): - - - Generates the changelog from ``docs/changelog`` fragments via :pypi:`towncrier`. - - Creates the release commit and tag locally. - - Builds the sdist, wheel, and zipapp (``virtualenv.pyz``). - - **Publish** (only if build succeeds): - - - Pushes the release commit and tag to ``main``. - - Publishes the sdist and wheel to PyPI. - - Creates a `GitHub Release `_ with the zipapp attached. - - Pushes the new zipapp and version to ``get-virtualenv``. - - If publish fails, a **rollback** job automatically reverts the release commit, deletes the tag and GitHub Release on - both ``virtualenv`` and ``get-virtualenv``. **Locally** -1. Generate the changelog, create the release commit, tag, and push: - - .. code-block:: console - - tox r -e release - - Pass ``--version `` to override the default ``auto`` behavior (e.g. ``--version minor``). - -2. Build the zipapp: - - .. code-block:: console - - tox r -e zipapp - -3. Create a GitHub Release and attach the zipapp: - - .. code-block:: console - - gh release create virtualenv.pyz --generate-notes - -4. Update ``get-virtualenv`` with the new zipapp: - - .. code-block:: console +.. code-block:: console - git clone https://github.com/pypa/get-virtualenv.git /tmp/get-virtualenv - cp virtualenv.pyz /tmp/get-virtualenv/public/virtualenv.pyz - echo -n "" > /tmp/get-virtualenv/public/version.txt - git -C /tmp/get-virtualenv add public/virtualenv.pyz public/version.txt - git -C /tmp/get-virtualenv commit -m "update virtualenv to " - git -C /tmp/get-virtualenv push origin main + tox r -e release - The push triggers ``get-virtualenv``'s own `release workflow - `_ which automatically creates a tag - and GitHub Release with the zipapp attached. +Pass ``--version `` to override the default ``auto`` behavior (e.g. ``--version minor``). ************** Contributing diff --git a/pyproject.toml b/pyproject.toml index f661e3174..9738e7ded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -216,7 +216,7 @@ html.show_contexts = true html.skip_covered = false [tool.towncrier] -name = "tox" +name = "virtualenv" filename = "docs/changelog.rst" directory = "docs/changelog" title_format = false diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index 823e45a80..5d24363ea 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -277,8 +277,12 @@ def _distutils_install(): with warnings.catch_warnings(): # disable warning for PEP-632 warnings.simplefilter("ignore") try: - from distutils import dist # noqa: PLC0415 - from distutils.command.install import SCHEME_KEYS # noqa: PLC0415 + from distutils import ( # noqa: PLC0415 # ty: ignore[unresolved-import] # https://github.com/astral-sh/ty/issues/708 + dist, + ) + from distutils.command.install import ( # noqa: PLC0415 # ty: ignore[unresolved-import] # https://github.com/astral-sh/ty/issues/708 + SCHEME_KEYS, + ) except ImportError: # if removed or not installed ignore return {} diff --git a/src/virtualenv/run/session.py b/src/virtualenv/run/session.py index 341af5fa4..3077e8b94 100644 --- a/src/virtualenv/run/session.py +++ b/src/virtualenv/run/session.py @@ -2,10 +2,9 @@ import json import logging +import sys from typing import TYPE_CHECKING -from typing_extensions import Self - if TYPE_CHECKING: from types import TracebackType @@ -15,6 +14,11 @@ from virtualenv.discovery.py_info import PythonInfo from virtualenv.seed.seeder import Seeder +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + LOGGER = logging.getLogger(__name__) diff --git a/src/virtualenv/util/path/_sync.py b/src/virtualenv/util/path/_sync.py index a09ecac34..8e531d2d3 100644 --- a/src/virtualenv/util/path/_sync.py +++ b/src/virtualenv/util/path/_sync.py @@ -62,8 +62,10 @@ def onerror(func, path, exc_info): # noqa: ARG001 else: raise # noqa: PLE0704 - kwargs = {"onexc" if sys.version_info >= (3, 12) else "onerror": onerror} - shutil.rmtree(str(dest), ignore_errors=True, **kwargs) + if sys.version_info >= (3, 12): + shutil.rmtree(str(dest), ignore_errors=True, onexc=onerror) + else: + shutil.rmtree(str(dest), ignore_errors=True, onerror=onerror) class _Debug: diff --git a/tasks/release.py b/tasks/release.py index 839ef9128..f876d5cb8 100644 --- a/tasks/release.py +++ b/tasks/release.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path -from subprocess import check_call +from subprocess import call, check_call from git import Commit, Remote, Repo, TagReference from packaging.version import Version @@ -64,7 +64,9 @@ def get_remote(repo: Repo) -> Remote: def release_changelog(repo: Repo, version: Version) -> Commit: print("generate release commit") # noqa: T201 check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S607 + call(["pre-commit", "run", "--all-files"], cwd=str(ROOT_SRC_DIR)) # noqa: S607 repo.git.add(".") + check_call(["pre-commit", "run", "--all-files"], cwd=str(ROOT_SRC_DIR)) # noqa: S607 return repo.index.commit(f"release {version}") diff --git a/tests/unit/discovery/py_info/test_py_info_exe_based_of.py b/tests/unit/discovery/py_info/test_py_info_exe_based_of.py index 90894a59c..20bb28ded 100644 --- a/tests/unit/discovery/py_info/test_py_info_exe_based_of.py +++ b/tests/unit/discovery/py_info/test_py_info_exe_based_of.py @@ -21,7 +21,7 @@ def test_discover_empty_folder(tmp_path, session_app_data): @pytest.mark.skipif(not fs_supports_symlink(), reason="symlink is not supported") -@pytest.mark.parametrize("suffix", sorted({".exe", ".cmd", ""} & set(EXTENSIONS) if IS_WIN else [""])) +@pytest.mark.parametrize("suffix", sorted({".exe", ""} & set(EXTENSIONS) if IS_WIN else [""])) @pytest.mark.parametrize("into", BASE) @pytest.mark.parametrize("arch", [CURRENT.architecture, ""]) @pytest.mark.parametrize("version", [".".join(str(i) for i in CURRENT.version_info[0:i]) for i in range(3, 0, -1)]) diff --git a/tox.ini b/tox.ini index c2e1357ee..32b639289 100644 --- a/tox.ini +++ b/tox.ini @@ -54,11 +54,11 @@ commands = pre-commit run --all-files --show-diff-on-failure [testenv:type] -description = run type checker (ty) on the codebase +description = run type checker (ty) against Python 3.14 deps = ty>=0.0.15 commands = - python -m ty check src/virtualenv + python -m ty check src/virtualenv --python-version 3.14 [testenv:readme] description = check that the long description is valid @@ -90,6 +90,12 @@ base_python = {env:TOX_BASEPYTHON} [testenv:3.13t] base_python = {env:TOX_BASEPYTHON} +[testenv:type-3.8] +description = run type checker (ty) against Python 3.8 +commands = + python -m ty check src/virtualenv --python-version 3.8 +base = type + [testenv:upgrade] description = upgrade pip/wheels/setuptools to latest skip_install = true @@ -107,6 +113,7 @@ description = do a release, required posarg of the version number deps = gitpython>=3.1.44 packaging>=25 + pre-commit-uv>=4.1.4 towncrier>=24.8 change_dir = {toxinidir}/tasks commands =