Skip to content

Publish to PyPI

Publish to PyPI #18

name: Publish to PyPI
on:
workflow_dispatch:
inputs:
repository:
description: "Target index"
type: choice
options: [pypi, testpypi]
default: testpypi
ref:
description: "Git ref to build (branch/tag/SHA). Leave empty to use the UI-selected ref."
required: false
default: ""
concurrency:
group: pypi-publish
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
container:
image: cubertgmbh/cuvis_pyil:3.5.0-ubuntu24.04
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ inputs.ref || github.ref }}
# Optional hard stop: only allow repo admins to proceed
- name: Enforce admin-only trigger
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const username = context.actor;
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner, repo, username
});
core.info(`Actor permission: ${data.permission}`);
if (data.permission !== "admin") {
core.setFailed(`Only repository admins may publish. (${username} has: ${data.permission})`);
}
- name: Install build tooling
run: python3 -m pip install -U build twine
- name: Read package name/version from pyproject.toml
id: meta
run: |
python3 - <<'PY'
import sys, json
try:
import tomllib # py3.11+
except ModuleNotFoundError:
import tomli as tomllib # fallback if needed
from pathlib import Path
data = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
proj = data.get("project", {})
name = proj.get("name")
version = proj.get("version")
if not name or not version:
print("Missing [project].name or [project].version in pyproject.toml", file=sys.stderr)
sys.exit(2)
print(f"name={name}")
print(f"version={version}")
with open("pkg_meta.json", "w", encoding="utf-8") as f:
json.dump({"name": name, "version": version}, f)
PY
echo "name=$(python3 -c "import json; print(json.load(open('pkg_meta.json'))['name'])")" >> "$GITHUB_OUTPUT"
echo "version=$(python3 -c "import json; print(json.load(open('pkg_meta.json'))['version'])")" >> "$GITHUB_OUTPUT"
- name: Abort if this version already exists on the target index
env:
NAME: ${{ steps.meta.outputs.name }}
VERSION: ${{ steps.meta.outputs.version }}
TARGET: ${{ inputs.repository }}
run: |
python3 - <<'PY'
import json, os, sys, urllib.request, urllib.error
name = os.environ["NAME"]
version = os.environ["VERSION"]
target = os.environ["TARGET"]
base = "https://pypi.org/pypi" if target == "pypi" else "https://test.pypi.org/pypi"
url = f"{base}/{name}/json"
try:
with urllib.request.urlopen(url) as resp:
data = json.load(resp)
except urllib.error.HTTPError as e:
if e.code == 404:
print(f"{name} not found on {target}; OK to publish {version}.")
sys.exit(0)
raise
releases = data.get("releases", {})
if version in releases and releases[version]:
print(f"Version {name}=={version} already exists on {target}. Aborting.")
sys.exit(1)
print(f"Version {name}=={version} not present on {target}; OK to publish.")
PY
- name: Build sdist and wheel
run: |
python3 -m build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment: ${{ inputs.repository }}
permissions:
contents: read
id-token: write # required for PyPI Trusted Publishing (OIDC)
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.13.0
with:
repository-url: ${{ inputs.repository == 'pypi' && 'https://upload.pypi.org/legacy/' || 'https://test.pypi.org/legacy/' }}