Skip to content

📦 Build release artifacts in CI workflow #4544

📦 Build release artifacts in CI workflow

📦 Build release artifacts in CI workflow #4544

Workflow file for this run

name: CI
on:
merge_group:
pull_request:
push:
branches:
- main
tags:
- v*
workflow_call:
inputs:
cpython-pip-version:
description: >-
A JSON string with pip versions
to test against under CPython.
required: false
type: string
cpython-versions:
description: >-
A JSON string with CPython versions
to test against.
required: false
type: string
release-version:
description: >-
Target PEP440-compliant version to release.
Please, don't prepend `v`.
required: false
type: string
release-committish:
default: ''
description: >-
The commit to be released to PyPI and tagged
in Git as `release-version`. Normally, you
should keep this empty.
type: string
outputs:
dists-artifact-name:
description: Workflow artifact name containing dists.
value: ${{ jobs.pre-setup.outputs.dists-artifact-name }}
is-upstream-repository:
description: >-
A flag representing whether the workflow runs in the upstream
repository or a fork.
value: ${{ jobs.pre-setup.outputs.is-upstream-repository }}
project-name:
description: PyPI project name.
value: ${{ jobs.pre-setup.outputs.project-name }}
project-version:
description: PyPI project version string.
value: ${{ jobs.pre-setup.outputs.dist-version }}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
FORCE_COLOR: 1 # Request colored output from CLI tools supporting it
MYPY_FORCE_COLOR: 1 # MyPy's color enforcement
PIP_DISABLE_PIP_VERSION_CHECK: 1
PIP_NO_PYTHON_VERSION_WARNING: 1
PIP_NO_WARN_SCRIPT_LOCATION: 1
PRE_COMMIT_COLOR: 1
PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest`
TOX_PARALLEL_NO_SPINNER: 1
TOX_TESTENV_PASSENV: >-
FORCE_COLOR
MYPY_FORCE_COLOR
NO_COLOR
PY_COLORS
PYTEST_THEME
PYTEST_THEME_MODE
PRE_COMMIT_COLOR
UPSTREAM_REPOSITORY_ID: >-
5746963
jobs:
pre-setup:
name: ⚙️ Pre-set global build settings
runs-on: ubuntu-latest
timeout-minutes: 2 # network is slow sometimes when fetching from Git
defaults:
run:
shell: python
outputs:
# NOTE: These aren't env vars because the `${{ env }}` context is
# NOTE: inaccessible when passing inputs to reusable workflows.
dists-artifact-name: python-package-distributions
dist-version: >-
${{
steps.request-check.outputs.release-requested == 'true'
&& inputs.release-version
|| steps.scm-version.outputs.dist-version
}}
project-name: ${{ steps.metadata.outputs.project-name }}
release-requested: >-
${{
steps.request-check.outputs.release-requested || false
}}
cache-key-for-dep-files: >-
${{ steps.calc-cache-key-files.outputs.cache-key-for-dep-files }}
sdist-artifact-name: ${{ steps.artifact-name.outputs.sdist }}
wheel-artifact-name: ${{ steps.artifact-name.outputs.wheel }}
is-upstream-repository: >-
${{ toJSON(env.UPSTREAM_REPOSITORY_ID == github.repository_id) }}
steps:
- name: Switch to using Python 3.14 by default
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: >-
Mark the build as untagged '${{
github.event.repository.default_branch
}}' branch build
id: untagged-check
if: >-
github.event_name == 'push' &&
github.ref_type == 'branch' &&
github.ref_name == github.event.repository.default_branch
run: |
from os import environ
from pathlib import Path
FILE_APPEND_MODE = 'a'
with Path(environ['GITHUB_OUTPUT']).open(
mode=FILE_APPEND_MODE,
) as outputs_file:
print('is-untagged-devel=true', file=outputs_file)
- name: Mark the build as "release request"
id: request-check
if: inputs.release-version != ''
run: |
from os import environ
from pathlib import Path
FILE_APPEND_MODE = 'a'
with Path(environ['GITHUB_OUTPUT']).open(
mode=FILE_APPEND_MODE,
) as outputs_file:
print('release-requested=true', file=outputs_file)
- name: Check out src from Git
uses: actions/checkout@v4
with:
fetch-depth: >-
${{
steps.request-check.outputs.release-requested == 'true'
&& 1 || 0
}}
ref: ${{ inputs.release-committish }}
- name: Scan static PEP 621 core packaging metadata
id: metadata
run: |
from os import environ
from pathlib import Path
from tomllib import loads as parse_toml_from_string
FILE_APPEND_MODE = 'a'
pyproject_toml_txt = Path('pyproject.toml').read_text()
metadata = parse_toml_from_string(pyproject_toml_txt)['project']
project_name = metadata["name"]
with Path(environ['GITHUB_OUTPUT']).open(
mode=FILE_APPEND_MODE,
) as outputs_file:
print(f'project-name={project_name}', file=outputs_file)
- name: >-
Calculate dependency files' combined hash value
for use in the cache key
if: >-
steps.request-check.outputs.release-requested != 'true'
id: calc-cache-key-files
uses: ./.github/actions/cache-keys
- name: Set up pip cache
if: >-
steps.request-check.outputs.release-requested != 'true'
uses: re-actors/cache-python-deps@release/v1
with:
cache-key-for-dependency-files: >-
${{ steps.calc-cache-key-files.outputs.cache-key-for-dep-files }}
- name: Drop Git tags from HEAD for non-release requests
if: >-
steps.request-check.outputs.release-requested != 'true'
run: >-
git tag --points-at HEAD
|
xargs git tag --delete
shell: bash -eEuxo pipefail {0}
- name: Set up versioning prerequisites
if: >-
steps.request-check.outputs.release-requested != 'true'
run: >-
python -m
pip install
--user
setuptools-scm
shell: bash -eEuxo pipefail {0}
- name: Set the current dist version from Git
if: steps.request-check.outputs.release-requested != 'true'
id: scm-version
run: |
from os import environ
from pathlib import Path
import setuptools_scm
FILE_APPEND_MODE = 'a'
ver = setuptools_scm.get_version(
${{
steps.untagged-check.outputs.is-untagged-devel == 'true'
&& 'local_scheme="no-local-version"' || ''
}}
)
with Path(environ['GITHUB_OUTPUT']).open(
mode=FILE_APPEND_MODE,
) as outputs_file:
print(f'dist-version={ver}', file=outputs_file)
print(
f'dist-version-for-filenames={ver.replace("+", "-")}',
file=outputs_file,
)
- name: Set the expected dist artifact names
id: artifact-name
env:
PROJECT_NAME: ${{ steps.metadata.outputs.project-name }}
run: |
from os import environ
from pathlib import Path
FILE_APPEND_MODE = 'a'
whl_file_prj_base_name = environ['PROJECT_NAME'].replace('-', '_')
sdist_file_prj_base_name = (
whl_file_prj_base_name.
replace('.', '_').
lower()
)
with Path(environ['GITHUB_OUTPUT']).open(
mode=FILE_APPEND_MODE,
) as outputs_file:
print(
f"sdist={sdist_file_prj_base_name !s}-${{
steps.request-check.outputs.release-requested == 'true'
&& inputs.release-version
|| steps.scm-version.outputs.dist-version
}}.tar.gz",
file=outputs_file,
)
print(
f"wheel={whl_file_prj_base_name !s}-${{
steps.request-check.outputs.release-requested == 'true'
&& inputs.release-version
|| steps.scm-version.outputs.dist-version
}}-py3-none-any.whl",
file=outputs_file,
)
build:
name: >-
📦 Build dists
needs:
- pre-setup # transitive, for accessing settings
uses: tox-dev/workflow/.github/workflows/reusable-tox.yml@617ca35caa695c572377861016677905e58a328c # yamllint disable-line rule:line-length
with:
cache-key-for-dependency-files: >-
${{ needs.pre-setup.outputs.cache-key-for-dep-files }}
check-name: Build dists under 🐍3.12
checkout-src-git-committish: >-
${{ inputs.release-committish }}
checkout-src-git-fetch-depth: >-
${{
fromJSON(needs.pre-setup.outputs.release-requested)
&& 1
|| 0
}}
job-dependencies-context: >- # context for hooks
${{ toJSON(needs) }}
python-version: 3.12
runner-vm-os: ubuntu-latest
timeout-minutes: 2
toxenv: build-dists
xfail: false
linters:
name: Linters
uses: ./.github/workflows/reusable-qa.yml
test:
name: ${{ matrix.os }} / ${{ matrix.python-version }} / ${{ matrix.pip-version }}
runs-on: ${{ matrix.os }}-latest
timeout-minutes: 9
strategy:
fail-fast: false
matrix:
os:
- Ubuntu
- Windows
- macOS
python-version: >-
${{
fromJSON(
inputs.cpython-versions
&& inputs.cpython-versions
|| '["3.9", "3.10", "3.11", "3.12", "3.13"]'
)
}}
pip-version: >-
${{
fromJSON(
inputs.cpython-pip-version
&& inputs.cpython-pip-version
|| '["supported", "lowest"]'
)
}}
env:
TOXENV: >-
pip${{ matrix.pip-version }}${{
!inputs.cpython-pip-version
&& '-coverage'
|| ''
}}
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }} from GitHub
id: python-install
if: "!endsWith(matrix.python-version, '-dev')"
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }} from deadsnakes
if: endsWith(matrix.python-version, '-dev')
uses: deadsnakes/[email protected]
with:
python-version: ${{ matrix.python-version }}
- name: Log python version info (${{ matrix.python-version }})
run: python --version --version
- name: Get pip cache dir
id: pip-cache
shell: bash
run: |
echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}"
- name: Pip cache
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: >-
${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}-${{
hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }}-${{
hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install test dependencies
run: python -m pip install -U tox virtualenv
- name: Prepare test environment
# NOTE: `--parallel-live` is a workaround for the regression in
# NOTE: the upstream tox project that made the
# NOTE: `TOX_PARALLEL_NO_SPINNER=1` env var auto-enable parallelism
# NOTE: and disable output from the tox environments.
#
# Ref: https://github.com/tox-dev/tox/issues/3193
run: tox -vv --notest -p auto --parallel-live
- name: Test pip ${{ matrix.pip-version }}
# NOTE: `--parallel-live` is a workaround for the regression in
# NOTE: the upstream tox project that made the
# NOTE: `TOX_PARALLEL_NO_SPINNER=1` env var auto-enable parallelism
# NOTE: and disable output from the tox environments.
#
# Ref: https://github.com/tox-dev/tox/issues/3193
run: tox --skip-pkg-install --parallel-live
- name: Re-run the failing tests with maximum verbosity
if: >-
!cancelled()
&& failure()
run: >- # `exit 1` makes sure that the job remains red with flaky runs
python -Xutf8 -Im
tox
--parallel=auto
--parallel-live
--skip-missing-interpreters=false
--skip-pkg-install
-vvvvv
--
--continue-on-collection-errors
--full-trace
--last-failed
${{ !inputs.cpython-pip-version && '--no-cov' || '' }}
--numprocesses=0
--showlocals
--trace-config
-rA
-vvvvv
&& exit 1
shell: bash
- name: Upload coverage to Codecov
if: >-
!cancelled()
&& !inputs.cpython-pip-version
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
flags: >-
CI-GHA,
OS-${{ runner.os }},
VM-${{ matrix.os }},
Py-${{ steps.python-install.outputs.python-version }},
Pip-${{ matrix.pip-version }}
name: >-
OS-${{ runner.os }},
VM-${{ matrix.os }},
Py-${{ steps.python-install.outputs.python-version }},
Pip-${{ matrix.pip-version }}
pypy:
name: ${{ matrix.os }} / ${{ matrix.python-version }} / ${{ matrix.pip-version }}
runs-on: ${{ matrix.os }}-latest
timeout-minutes: 9
strategy:
fail-fast: false
matrix:
os:
- Ubuntu
- MacOS
- Windows
python-version:
- pypy-3.10
pip-version:
- supported
env:
TOXENV: pip${{ matrix.pip-version }}
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
shell: bash
run: |
echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}"
- name: Pip cache
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: >-
${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}-${{
hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }}-${{
hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install tox
run: pip install tox
- name: Prepare test environment
# NOTE: `--parallel-live` is a workaround for the regression in
# NOTE: the upstream tox project that made the
# NOTE: `TOX_PARALLEL_NO_SPINNER=1` env var auto-enable parallelism
# NOTE: and disable output from the tox environments.
#
# Ref: https://github.com/tox-dev/tox/issues/3193
run: tox --notest -p auto --parallel-live
- name: Test pip ${{ matrix.pip-version }}
# NOTE: `--parallel-live` is a workaround for the regression in
# NOTE: the upstream tox project that made the
# NOTE: `TOX_PARALLEL_NO_SPINNER=1` env var auto-enable parallelism
# NOTE: and disable output from the tox environments.
#
# Ref: https://github.com/tox-dev/tox/issues/3193
run: tox --skip-pkg-install --parallel-live
- name: Re-run the failing tests with maximum verbosity
if: >-
!cancelled()
&& failure()
run: >- # `exit 1` makes sure that the job remains red with flaky runs
python -Xutf8 -Im
tox
--parallel=auto
--parallel-live
--skip-missing-interpreters=false
--skip-pkg-install
-vvvvv
--
--continue-on-collection-errors
--full-trace
--last-failed
--numprocesses=0
--showlocals
--trace-config
-rA
-vvvvv
&& exit 1
shell: bash
coverage-summary:
name: Coverage processing
if: >-
!cancelled()
runs-on: ubuntu-latest
timeout-minutes: 1
needs:
- test
steps:
- name: Notify Codecov that all coverage reports have been uploaded
if: >-
!cancelled()
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
run_command: send-notifications
check: # This job does nothing and is only used for the branch protection
if: always()
needs:
- linters
- pypy
- test
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@afee1c1eac2a506084c274e9c02c8e0687b48d9e
with:
jobs: ${{ toJSON(needs) }}