Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/pre-release.yaml
Original file line number Diff line number Diff line change
@@ -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 }}"
54 changes: 10 additions & 44 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
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

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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
60 changes: 10 additions & 50 deletions docs/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/pypa/virtualenv/actions/workflows/release.yaml>`_ which builds the sdist, wheel, and
zipapp, publishes to PyPI via trusted publisher, creates a `GitHub Release
<https://github.com/pypa/virtualenv/releases>`_ with the zipapp attached, and updates `get-virtualenv
<https://github.com/pypa/get-virtualenv>`_. If publish fails, a rollback job automatically reverts everything.

**Via GitHub Actions (recommended)**

1. Go to the `Release workflow <https://github.com/pypa/virtualenv/actions/workflows/release.yaml>`_ on GitHub.
1. Go to the `Pre-release workflow <https://github.com/pypa/virtualenv/actions/workflows/pre-release.yaml>`_ 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 <https://github.com/pypa/virtualenv/releases>`_ 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 <bump>`` 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 <version> 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 "<version>" > /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 <version>"
git -C /tmp/get-virtualenv push origin main
tox r -e release

The push triggers ``get-virtualenv``'s own `release workflow
<https://github.com/pypa/get-virtualenv/blob/main/.github/workflows/release.yml>`_ which automatically creates a tag
and GitHub Release with the zipapp attached.
Pass ``--version <bump>`` to override the default ``auto`` behavior (e.g. ``--version minor``).

**************
Contributing
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/virtualenv/discovery/py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down
8 changes: 6 additions & 2 deletions src/virtualenv/run/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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__)


Expand Down
6 changes: 4 additions & 2 deletions src/virtualenv/util/path/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion tasks/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}")


Expand Down
2 changes: 1 addition & 1 deletion tests/unit/discovery/py_info/test_py_info_exe_based_of.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)])
Expand Down
11 changes: 9 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 =
Expand Down
Loading