diff --git a/.gitignore b/.gitignore
index 48bdfc8de..b5c8bd993 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ doc/build
 # pbr generates these
 AUTHORS
 ChangeLog
+dist
diff --git a/doc/source/python-roles.rst b/doc/source/python-roles.rst
index 58cc4d11b..03d910f04 100644
--- a/doc/source/python-roles.rst
+++ b/doc/source/python-roles.rst
@@ -11,6 +11,7 @@ Python Roles
 .. zuul:autorole:: ensure-tox
 .. zuul:autorole:: ensure-twine
 .. zuul:autorole:: ensure-virtualenv
+.. zuul:autorole:: ensure-wheel
 .. zuul:autorole:: fetch-coverage-output
 .. zuul:autorole:: fetch-python-sdist-output
 .. zuul:autorole:: fetch-sphinx-output
diff --git a/playbooks/python/release-pre.yaml b/playbooks/python/release-pre.yaml
new file mode 100644
index 000000000..409944ed0
--- /dev/null
+++ b/playbooks/python/release-pre.yaml
@@ -0,0 +1,4 @@
+- hosts: all
+  roles:
+    - role: ensure-wheel
+      wheel_python: "{{ release_python | default(python) }}"
diff --git a/roles/build-python-release/README.rst b/roles/build-python-release/README.rst
index e67681d37..7e16f3a16 100644
--- a/roles/build-python-release/README.rst
+++ b/roles/build-python-release/README.rst
@@ -5,8 +5,7 @@ Build sdist and wheel for Python projects.
 .. zuul:rolevar:: release_python
    :default: python
 
-   The python interpreter to use. Set it to "python3" to use python 3,
-   for example.
+   The python interpreter to use. Defaults to "python".
 
 .. zuul:rolevar:: bdist_wheel_xargs
    :default: ''
diff --git a/roles/ensure-wheel/README.rst b/roles/ensure-wheel/README.rst
new file mode 100644
index 000000000..f3f49dc07
--- /dev/null
+++ b/roles/ensure-wheel/README.rst
@@ -0,0 +1,9 @@
+Ensure wheel exists for Python projects.
+
+**Role Variables**
+
+.. zuul:rolevar:: wheel_python
+   :default: python3
+
+   The python interpreter to use. Defaults to "python3".
+
diff --git a/roles/ensure-wheel/defaults/main.yaml b/roles/ensure-wheel/defaults/main.yaml
new file mode 100644
index 000000000..8c6056d3b
--- /dev/null
+++ b/roles/ensure-wheel/defaults/main.yaml
@@ -0,0 +1 @@
+wheel_python: python3
diff --git a/roles/ensure-wheel/tasks/main.yaml b/roles/ensure-wheel/tasks/main.yaml
new file mode 100644
index 000000000..cf3fa8690
--- /dev/null
+++ b/roles/ensure-wheel/tasks/main.yaml
@@ -0,0 +1,15 @@
+- name: Check for wheel
+  command: "{{ wheel_python }} -m wheel"
+  failed_when: false
+  register: wheel_exists
+
+- name: Include ensure-pip
+  vars:
+    ensure_pip_from_upstream_interpreters: "{{ wheel_python }}"
+  include_role:
+    name: ensure-pip
+  when: wheel_exists.rc != 0
+
+- name: Install wheel
+  command: "{{ wheel_python }} -m pip install --user wheel"
+  when: wheel_exists.rc != 0
diff --git a/zuul-tests.d/python-roles-jobs.yaml b/zuul-tests.d/python-roles-jobs.yaml
index 20ec3e070..4da6fec6c 100644
--- a/zuul-tests.d/python-roles-jobs.yaml
+++ b/zuul-tests.d/python-roles-jobs.yaml
@@ -1,11 +1,225 @@
+- job:
+    name: zuul-jobs-test-build-python-release-jobs
+    description: Test the build-python-release-jobs role
+    files:
+      - roles/build-python-release/.*
+      - setup.cfg
+      - setup.py
+    vars:
+      role_name: build-python-release
+      release_python: python3
+    pre-run: playbooks/python/release-pre.yaml
+    run: test-playbooks/simple-role-test.yaml
+    tags: all-platforms
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-centos-7
+    description: Test the build-python-release-jobs role on centos-7
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: centos-7
+          label: centos-7
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-centos-8
+    description: Test the build-python-release-jobs role on centos-8
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: centos-8
+          label: centos-8
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-debian-stretch
+    description: Test the build-python-release-jobs role on debian-stretch
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: debian-stretch
+          label: debian-stretch
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-fedora-31
+    description: Test the build-python-release-jobs role on fedora-31
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: fedora-31
+          label: fedora-31
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-gentoo-17-0-systemd
+    description: Test the build-python-release-jobs role on gentoo-17-0-systemd
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: gentoo-17-0-systemd
+          label: gentoo-17-0-systemd
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-opensuse-15
+    description: Test the build-python-release-jobs role on opensuse-15
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: opensuse-15
+          label: opensuse-15
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-opensuse-tumbleweed-nv
+    voting: false
+    description: Test the build-python-release-jobs role on opensuse-tumbleweed
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: opensuse-tumbleweed
+          label: opensuse-tumbleweed
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-ubuntu-bionic
+    description: Test the build-python-release-jobs role on ubuntu-bionic
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: ubuntu-bionic
+          label: ubuntu-bionic
+
+- job:
+    name: zuul-jobs-test-build-python-release-jobs-ubuntu-xenial
+    description: Test the build-python-release-jobs role on ubuntu-xenial
+    parent: zuul-jobs-test-build-python-release-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: ubuntu-xenial
+          label: ubuntu-xenial
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs
+    description: Test the ensure-wheel role
+    files:
+      - roles/ensure-wheel/.*
+      - setup.cfg
+      - setup.py
+    vars:
+      role_name: ensure-wheel
+    run: test-playbooks/simple-role-test.yaml
+    tags: all-platforms
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-centos-7
+    description: Test the ensure-wheel role on centos-7
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: centos-7
+          label: centos-7
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-centos-8
+    description: Test the ensure-wheel role on centos-8
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: centos-8
+          label: centos-8
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-debian-stretch
+    description: Test the ensure-wheel role on debian-stretch
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: debian-stretch
+          label: debian-stretch
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-fedora-31
+    description: Test the ensure-wheel role on fedora-31
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: fedora-31
+          label: fedora-31
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-gentoo-17-0-systemd
+    description: Test the ensure-wheel role on gentoo-17-0-systemd
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: gentoo-17-0-systemd
+          label: gentoo-17-0-systemd
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-opensuse-15
+    description: Test the ensure-wheel role on opensuse-15
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: opensuse-15
+          label: opensuse-15
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-opensuse-tumbleweed-nv
+    voting: false
+    description: Test the ensure-wheel role on opensuse-tumbleweed
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: opensuse-tumbleweed
+          label: opensuse-tumbleweed
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-ubuntu-bionic
+    description: Test the ensure-wheel role on ubuntu-bionic
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: ubuntu-bionic
+          label: ubuntu-bionic
+
+- job:
+    name: zuul-jobs-test-ensure-wheel-jobs-ubuntu-xenial
+    description: Test the ensure-wheel role on ubuntu-xenial
+    parent: zuul-jobs-test-ensure-wheel-jobs
+    tags: auto-generated
+    nodeset:
+      nodes:
+        - name: ubuntu-xenial
+          label: ubuntu-xenial
+
 - job:
     name: zuul-jobs-test-ensure-python-pyenv
     description: Test the ensure-python role with pyenv
     files:
-      - zuul-tests.d/python-roles-jobs.yaml
+      - roles/ensure-python/.*
+      - test-playbooks/ensure-python-pyenv.yaml
     run: test-playbooks/ensure-python-pyenv.yaml
     tags: all-platforms
 
+# -* AUTOGENERATED *-
+#  The following project section is autogenerated by
+#    tools/update-test-platforms.py
+#  Please re-run to generate new job lists
+
 - job:
     name: zuul-jobs-test-ensure-python-pyenv-centos-7
     description: Test the ensure-python role with pyenv on centos-7
@@ -100,6 +314,24 @@
 - project:
     check:
       jobs:
+        - zuul-jobs-test-build-python-release-jobs-centos-7
+        - zuul-jobs-test-build-python-release-jobs-centos-8
+        - zuul-jobs-test-build-python-release-jobs-debian-stretch
+        - zuul-jobs-test-build-python-release-jobs-fedora-31
+        - zuul-jobs-test-build-python-release-jobs-gentoo-17-0-systemd
+        - zuul-jobs-test-build-python-release-jobs-opensuse-15
+        - zuul-jobs-test-build-python-release-jobs-opensuse-tumbleweed-nv
+        - zuul-jobs-test-build-python-release-jobs-ubuntu-bionic
+        - zuul-jobs-test-build-python-release-jobs-ubuntu-xenial
+        - zuul-jobs-test-ensure-wheel-jobs-centos-7
+        - zuul-jobs-test-ensure-wheel-jobs-centos-8
+        - zuul-jobs-test-ensure-wheel-jobs-debian-stretch
+        - zuul-jobs-test-ensure-wheel-jobs-fedora-31
+        - zuul-jobs-test-ensure-wheel-jobs-gentoo-17-0-systemd
+        - zuul-jobs-test-ensure-wheel-jobs-opensuse-15
+        - zuul-jobs-test-ensure-wheel-jobs-opensuse-tumbleweed-nv
+        - zuul-jobs-test-ensure-wheel-jobs-ubuntu-bionic
+        - zuul-jobs-test-ensure-wheel-jobs-ubuntu-xenial
         - zuul-jobs-test-ensure-python-pyenv-centos-7
         - zuul-jobs-test-ensure-python-pyenv-centos-8
         - zuul-jobs-test-ensure-python-pyenv-debian-stretch
@@ -111,6 +343,22 @@
         - zuul-jobs-test-ensure-python-pyenv-ubuntu-xenial
     gate:
       jobs:
+        - zuul-jobs-test-build-python-release-jobs-centos-7
+        - zuul-jobs-test-build-python-release-jobs-centos-8
+        - zuul-jobs-test-build-python-release-jobs-debian-stretch
+        - zuul-jobs-test-build-python-release-jobs-fedora-31
+        - zuul-jobs-test-build-python-release-jobs-gentoo-17-0-systemd
+        - zuul-jobs-test-build-python-release-jobs-opensuse-15
+        - zuul-jobs-test-build-python-release-jobs-ubuntu-bionic
+        - zuul-jobs-test-build-python-release-jobs-ubuntu-xenial
+        - zuul-jobs-test-ensure-wheel-jobs-centos-7
+        - zuul-jobs-test-ensure-wheel-jobs-centos-8
+        - zuul-jobs-test-ensure-wheel-jobs-debian-stretch
+        - zuul-jobs-test-ensure-wheel-jobs-fedora-31
+        - zuul-jobs-test-ensure-wheel-jobs-gentoo-17-0-systemd
+        - zuul-jobs-test-ensure-wheel-jobs-opensuse-15
+        - zuul-jobs-test-ensure-wheel-jobs-ubuntu-bionic
+        - zuul-jobs-test-ensure-wheel-jobs-ubuntu-xenial
         - zuul-jobs-test-ensure-python-pyenv-centos-7
         - zuul-jobs-test-ensure-python-pyenv-centos-8
         - zuul-jobs-test-ensure-python-pyenv-debian-stretch
diff --git a/zuul.d/python-jobs.yaml b/zuul.d/python-jobs.yaml
index db4cc61d4..204069969 100644
--- a/zuul.d/python-jobs.yaml
+++ b/zuul.d/python-jobs.yaml
@@ -249,7 +249,7 @@
     name: build-python-release
     description: |
       Build a source tarball and a bdist wheel for uploading.
-    pre-run: playbooks/python/pre.yaml
+    pre-run: playbooks/python/release-pre.yaml
     run: playbooks/python/release.yaml
     post-run: playbooks/python/tarball-post.yaml