Skip to content

publish wheels

publish wheels #7

Workflow file for this run

# Manually validate or publish a wheelhouse artifact from a completed wheels workflow.
# Validation performs every local check but does not request credentials or upload anything.
name: publish wheels
on:
workflow_dispatch:
inputs:
target:
description: 'Validate only, publish to TestPyPI, or publish to PyPI'
type: choice
default: validate-only
options:
- validate-only
- testpypi
- pypi
wheels_run_id:
description: 'Successful wheels workflow run containing the wheelhouse'
type: string
required: true
release_tag:
description: 'Release tag whose commit produced the wheels (for example v1.0.0-rc1)'
type: string
required: true
project:
description: 'Project to publish (use all after every Trusted Publisher exists)'
type: choice
default: superdex-physics
options:
- all
- superdex-physics
- superdex-mesh-cli
- superdex-studio
- superdex-physics-debugger
- superdex-physics-fp64
- superdex-robotics
- superdex-robotics-fp64
- superdex-lab
- superdex
permissions: {}
concurrency:
group: superdex-publish-${{ inputs.target }}
cancel-in-progress: false
env:
PYTHON_VERSION: '3.12'
TWINE_VERSION: '6.2.0'
jobs:
preflight:
runs-on: ubuntu-22.04
timeout-minutes: 30
outputs:
matrix: ${{ steps.projects.outputs.matrix }}
permissions:
actions: read
contents: read
steps:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Download the selected wheelhouse
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: wheelhouse
path: wheelhouse
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ inputs.wheels_run_id }}
- name: Check the selected wheels run
id: wheels
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ inputs.wheels_run_id }}
TARGET: ${{ inputs.target }}
run: |
python - <<'PY'
import json
import os
import re
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
repository = os.environ["REPOSITORY"]
# Authenticate GitHub API reads with the workflow token.
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {os.environ['GH_TOKEN']}",
"X-GitHub-Api-Version": "2022-11-28",
}
def github_json(path):
request = urllib.request.Request(
f"https://api.github.com/repos/{repository}{path}", headers=headers
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except (OSError, urllib.error.HTTPError) as error:
raise SystemExit(f"GitHub API request failed: {error}") from error
# Bind validation to a successful manual run of the wheels workflow.
run_id = os.environ["RUN_ID"]
if re.fullmatch(r"[1-9][0-9]*", run_id) is None:
raise SystemExit("wheels_run_id must be a positive integer")
run = github_json(f"/actions/runs/{run_id}")
workflow = str(run.get("path", "")).split("@", 1)[0]
if workflow != ".github/workflows/wheels.yml":
raise SystemExit(f"selected run used unexpected workflow {workflow!r}")
if run.get("event") != "workflow_dispatch" or run.get("conclusion") != "success":
raise SystemExit("selected wheels workflow run did not complete successfully")
source_sha = run.get("head_sha")
if not isinstance(source_sha, str) or re.fullmatch(r"[0-9a-f]{40}", source_sha) is None:
raise SystemExit("selected wheels workflow run has an invalid commit SHA")
# A complete release contains exactly 23 wheels and no other files.
entries = sorted(Path("wheelhouse").iterdir())
wheels = [path for path in entries if path.is_file() and path.suffix == ".whl"]
if len(entries) != 23 or len(wheels) != 23:
raise SystemExit(f"wheelhouse must contain exactly 23 wheels; found {len(wheels)}")
# Extract and require the single version shared by every wheel.
versions = set()
for wheel in wheels:
try:
distribution_and_version, _, _, _ = wheel.stem.rsplit("-", 3)
_, version = distribution_and_version.rsplit("-", 1)
except ValueError as error:
raise SystemExit(f"malformed wheel filename: {wheel.name}") from error
versions.add(version)
if len(versions) != 1:
raise SystemExit(f"wheelhouse must contain one version; found {sorted(versions)}")
wheel_version = versions.pop()
# Require an index-appropriate tag whose version matches the wheels.
target = os.environ["TARGET"]
release_tag = os.environ["RELEASE_TAG"]
if target == "testpypi":
match = re.fullmatch(
r"v(?P<version>[0-9]+(?:\.[0-9]+)+)-rc[1-9][0-9]*",
release_tag,
)
expected = "an exact vX.Y.Z-rcN tag"
elif target == "pypi":
match = re.fullmatch(
r"v(?P<version>[0-9]+(?:\.[0-9]+)+)", release_tag
)
expected = "an exact vX.Y.Z tag"
else:
match = re.fullmatch(
r"v(?P<version>[0-9]+(?:\.[0-9]+)+)(?:-rc[1-9][0-9]*)?",
release_tag,
)
expected = "an exact vX.Y.Z or vX.Y.Z-rcN tag"
if match is None:
raise SystemExit(f"{target} requires {expected}")
if match.group("version") != wheel_version:
raise SystemExit(
f"tag version {match.group('version')} does not match wheel version {wheel_version}"
)
# Require the tag to identify the exact commit that built the wheels.
tag_commit = github_json(
f"/commits/{urllib.parse.quote(release_tag, safe='')}"
).get("sha")
if tag_commit != source_sha:
raise SystemExit("release tag must point to the selected wheels run commit")
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
output.write(f"source_sha={source_sha}\n")
output.write(f"version={wheel_version}\n")
output.write(f"wheel_count={len(wheels)}\n")
PY
- name: Select projects to publish
id: projects
shell: bash
env:
PROJECT: ${{ inputs.project }}
run: |
python - <<'PY'
import json
import os
# Map package names to wheel prefixes and expected platform counts.
projects = {
"superdex": ("superdex", 1),
"superdex-lab": ("superdex_lab", 1),
"superdex-mesh-cli": ("superdex_mesh_cli", 3),
"superdex-physics": ("superdex_physics", 3),
"superdex-physics-debugger": ("superdex_physics_debugger", 3),
"superdex-physics-fp64": ("superdex_physics_fp64", 3),
"superdex-robotics": ("superdex_robotics", 3),
"superdex-robotics-fp64": ("superdex_robotics_fp64", 3),
"superdex-studio": ("superdex_studio", 3),
}
# Emit one publishing job, or all nine after bootstrap is complete.
selected = os.environ["PROJECT"]
names = projects if selected == "all" else (selected,)
include = [
{
"project": name,
"wheel_prefix": projects[name][0],
"wheel_count": projects[name][1],
}
for name in names
]
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
output.write(f"matrix={json.dumps({'include': include})}\n")
PY
- name: Install metadata checker
shell: bash
run: python -m pip install --upgrade pip "twine==${TWINE_VERSION}"
- name: Check every wheel's publish metadata
shell: bash
run: python -m twine check wheelhouse/*.whl
- name: Stage the checked wheels for publishing
if: inputs.target != 'validate-only'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: publish-input
path: wheelhouse/*.whl
if-no-files-found: error
retention-days: 7
- name: Summarize preflight
shell: bash
run: |
{
echo '## SuperDex publication preflight'
echo
echo "- Target: \`${{ inputs.target }}\`"
echo "- Selected wheels run: \`${{ inputs.wheels_run_id }}\`"
echo "- Wheels commit: \`${{ steps.wheels.outputs.source_sha }}\`"
echo "- Wheel version: \`${{ steps.wheels.outputs.version }}\`"
echo "- Release tag: \`${{ inputs.release_tag }}\`"
echo "- Wheels: \`${{ steps.wheels.outputs.wheel_count }}\`"
echo "- Selected project: \`${{ inputs.project }}\`"
if [[ '${{ inputs.target }}' == 'validate-only' ]]; then
echo '- Result: validation passed; no upload was attempted'
fi
} >> "$GITHUB_STEP_SUMMARY"
publish:
name: publish ${{ matrix.project }}
needs: preflight
if: inputs.target == 'testpypi' || inputs.target == 'pypi'
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.preflight.outputs.matrix) }}
environment:
name: publish-wheel-${{ matrix.project }}
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
actions: read
id-token: write
steps:
- name: Download the preflight-approved wheels
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: publish-input
path: publish-input
- name: Select ${{ matrix.project }} wheels
shell: bash
env:
EXPECTED_WHEEL_COUNT: ${{ matrix.wheel_count }}
WHEEL_PREFIX: ${{ matrix.wheel_prefix }}
run: |
mkdir project-wheels
shopt -s nullglob
wheels=(publish-input/"${WHEEL_PREFIX}"-*.whl)
if (( ${#wheels[@]} != EXPECTED_WHEEL_COUNT )); then
echo "::error::expected $EXPECTED_WHEEL_COUNT $WHEEL_PREFIX wheels; found ${#wheels[@]}"
exit 1
fi
cp -- "${wheels[@]}" project-wheels/
- name: Publish ${{ matrix.project }} wheels
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
packages-dir: project-wheels
repository-url: ${{ inputs.target == 'testpypi' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
- name: Summarize publication
shell: bash
run: |
{
echo '## SuperDex project published'
echo
echo "- Target: \`${{ inputs.target }}\`"
echo "- Project: \`${{ matrix.project }}\`"
echo "- Wheels run: \`${{ inputs.wheels_run_id }}\`"
} >> "$GITHUB_STEP_SUMMARY"