Publish Python package #61
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 Python package | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| target: | |
| description: "Publish target" | |
| required: true | |
| type: choice | |
| options: | |
| - testpypi | |
| - pypi | |
| default: testpypi | |
| release: | |
| types: [published] | |
| jobs: | |
| build: | |
| name: Build distributions | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.14" | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: Install build tooling | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install ".[dev]" twine | |
| - name: Ruff | |
| run: python -m ruff check . | |
| - name: Type check | |
| run: python -m mypy | |
| - name: Test | |
| run: python -m pytest | |
| - name: Coverage | |
| run: python -m pytest --cov=codex_usage_tracker --cov-report=term-missing | |
| - name: Compile | |
| run: python -m compileall src | |
| - name: Dashboard JavaScript syntax | |
| run: | | |
| for file in src/codex_usage_tracker/plugin_data/dashboard/dashboard*.js; do | |
| node --check "$file" | |
| done | |
| - name: Release readiness | |
| run: python scripts/check_release.py | |
| - name: Whitespace | |
| run: git diff --check | |
| - name: Build wheel and sdist | |
| run: | | |
| rm -rf dist build *.egg-info src/*.egg-info | |
| python -m build | |
| python -m twine check dist/* | |
| python scripts/check_release.py --dist | |
| - name: Show distribution files | |
| run: | | |
| ls -la dist | |
| python - <<'PY' | |
| from pathlib import Path | |
| import hashlib | |
| for path in sorted(Path("dist").iterdir()): | |
| digest = hashlib.sha256(path.read_bytes()).hexdigest() | |
| print(f"{digest} {path}") | |
| PY | |
| - name: Upload dist artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: python-dist | |
| path: dist/* | |
| if-no-files-found: error | |
| publish-testpypi: | |
| name: Publish to TestPyPI | |
| needs: build | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi' | |
| environment: | |
| name: testpypi | |
| url: https://test.pypi.org/project/codex-usage-tracking/ | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Verify PyPI publish ref | |
| run: | | |
| echo "event=$GITHUB_EVENT_NAME" | |
| echo "ref=$GITHUB_REF" | |
| echo "sha=$GITHUB_SHA" | |
| if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then | |
| case "$GITHUB_REF" in | |
| refs/heads/main|refs/tags/*) | |
| ;; | |
| *) | |
| echo "Manual PyPI publishing must run from main or a tag ref." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| fi | |
| - name: Download dist artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: python-dist | |
| path: dist | |
| - name: Check target package version | |
| id: package-version | |
| env: | |
| PACKAGE_INDEX_JSON_URL: https://test.pypi.org/pypi/codex-usage-tracking/json | |
| PACKAGE_INDEX_NAME: TestPyPI | |
| run: | | |
| python - <<'PY' | |
| import json | |
| import os | |
| import urllib.error | |
| import urllib.request | |
| from pathlib import Path | |
| versions = set() | |
| for path in Path("dist").iterdir(): | |
| name = path.name | |
| if name.endswith(".tar.gz") and name.startswith("codex_usage_tracking-"): | |
| versions.add(name.removeprefix("codex_usage_tracking-").removesuffix(".tar.gz")) | |
| elif name.endswith(".whl") and name.startswith("codex_usage_tracking-"): | |
| versions.add(name.split("-")[1]) | |
| if len(versions) != 1: | |
| raise SystemExit(f"expected one distribution version, found {sorted(versions)}") | |
| version = versions.pop() | |
| url = os.environ["PACKAGE_INDEX_JSON_URL"] | |
| index_name = os.environ["PACKAGE_INDEX_NAME"] | |
| try: | |
| with urllib.request.urlopen(url, timeout=30) as response: | |
| payload = json.load(response) | |
| except urllib.error.HTTPError as exc: | |
| if exc.code == 404: | |
| exists = False | |
| else: | |
| raise | |
| else: | |
| exists = version in payload.get("releases", {}) | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: | |
| output.write(f"version={version}\n") | |
| output.write(f"exists={str(exists).lower()}\n") | |
| if exists: | |
| print(f"codex-usage-tracking {version} already exists on {index_name}; skipping upload.") | |
| else: | |
| print(f"codex-usage-tracking {version} is not present on {index_name}; upload will run.") | |
| PY | |
| - name: Publish package distributions to TestPyPI | |
| if: steps.package-version.outputs.exists != 'true' | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| repository-url: https://test.pypi.org/legacy/ | |
| print-hash: true | |
| publish-pypi: | |
| name: Publish to PyPI | |
| needs: build | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi') | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/project/codex-usage-tracking/ | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Verify PyPI publish ref | |
| run: | | |
| echo "event=$GITHUB_EVENT_NAME" | |
| echo "ref=$GITHUB_REF" | |
| echo "sha=$GITHUB_SHA" | |
| if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then | |
| case "$GITHUB_REF" in | |
| refs/heads/main|refs/tags/*) | |
| ;; | |
| *) | |
| echo "Manual PyPI publishing must run from main or a tag ref." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| fi | |
| - name: Download dist artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: python-dist | |
| path: dist | |
| - name: Check target package version | |
| id: package-version | |
| env: | |
| PACKAGE_INDEX_JSON_URL: https://pypi.org/pypi/codex-usage-tracking/json | |
| PACKAGE_INDEX_NAME: PyPI | |
| run: | | |
| python - <<'PY' | |
| import json | |
| import os | |
| import urllib.error | |
| import urllib.request | |
| from pathlib import Path | |
| versions = set() | |
| for path in Path("dist").iterdir(): | |
| name = path.name | |
| if name.endswith(".tar.gz") and name.startswith("codex_usage_tracking-"): | |
| versions.add(name.removeprefix("codex_usage_tracking-").removesuffix(".tar.gz")) | |
| elif name.endswith(".whl") and name.startswith("codex_usage_tracking-"): | |
| versions.add(name.split("-")[1]) | |
| if len(versions) != 1: | |
| raise SystemExit(f"expected one distribution version, found {sorted(versions)}") | |
| version = versions.pop() | |
| url = os.environ["PACKAGE_INDEX_JSON_URL"] | |
| index_name = os.environ["PACKAGE_INDEX_NAME"] | |
| try: | |
| with urllib.request.urlopen(url, timeout=30) as response: | |
| payload = json.load(response) | |
| except urllib.error.HTTPError as exc: | |
| if exc.code == 404: | |
| exists = False | |
| else: | |
| raise | |
| else: | |
| exists = version in payload.get("releases", {}) | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: | |
| output.write(f"version={version}\n") | |
| output.write(f"exists={str(exists).lower()}\n") | |
| if exists: | |
| print(f"codex-usage-tracking {version} already exists on {index_name}; skipping upload.") | |
| else: | |
| print(f"codex-usage-tracking {version} is not present on {index_name}; upload will run.") | |
| PY | |
| - name: Publish package distributions to PyPI | |
| if: steps.package-version.outputs.exists != 'true' | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| print-hash: true |