Publish to PyPI #19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/' }} |