Collect & Publish uv-ffi to PyPI #30
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
| # publish-collect.yml — wakes when all three builds finish, collects and publishes | |
| name: Collect & Publish uv-ffi to PyPI | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag to collect (e.g. v0.10.8)' | |
| required: true | |
| workflow_run: | |
| workflows: | |
| - "🔧 Build uv-ffi Wheels" | |
| - "🔧 Build uv-ffi Wheels (Extended)" | |
| - "👽 Build uv-ffi Wheels (Ultra-Exotic & Source)" | |
| types: [completed] | |
| branches: [omnipkg-ffi-v0.10.8] | |
| permissions: | |
| contents: write | |
| id-token: write | |
| actions: read | |
| jobs: | |
| check-all-done: | |
| name: "🔍 Check all builds complete" | |
| runs-on: ubuntu-latest | |
| outputs: | |
| all_done: ${{ steps.check.outputs.all_done }} | |
| std_run_id: ${{ steps.check.outputs.std_run_id }} | |
| ext_run_id: ${{ steps.check.outputs.ext_run_id }} | |
| exo_run_id: ${{ steps.check.outputs.exo_run_id }} | |
| steps: | |
| - name: Check completion of all three builds | |
| id: check | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| BRANCH="omnipkg-ffi-v0.10.8" | |
| TRIGGERING_RUN="${{ github.event.workflow_run.id }}" | |
| TRIGGERING_WF="${{ github.event.workflow_run.name }}" | |
| echo "Triggered by: $TRIGGERING_WF ($TRIGGERING_RUN)" | |
| find_latest_run() { | |
| local WF=$1 | |
| gh api "repos/${{ github.repository }}/actions/workflows/$WF/runs?branch=$BRANCH&per_page=5&status=success" \ | |
| --jq ".workflow_runs[0].id" 2>/dev/null | |
| } | |
| STD=$(find_latest_run "build-wheels.yml") | |
| EXT=$(find_latest_run "build-wheels-extended.yml") | |
| EXO=$(find_latest_run "build-wheels-exotic.yml") | |
| # Try inputs first (manual dispatch), then fall back to querying the triggering run | |
| TAG="${{ inputs.tag }}" | |
| if [ -z "$TAG" ]; then | |
| TAG=$(gh api "repos/${{ github.repository }}/actions/runs/$TRIGGERING_RUN" \ | |
| --jq '.inputs.tag // ""' 2>/dev/null) | |
| fi | |
| find_completed_run() { | |
| local WF=$1 | |
| gh api "repos/${{ github.repository }}/actions/workflows/$WF/runs?branch=$BRANCH&per_page=20" \ | |
| --jq ".workflow_runs[] | select(.status == \"completed\") | .id" 2>/dev/null | \ | |
| while read RUN_ID; do | |
| INPUT_TAG=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN_ID" --jq '.inputs.tag // ""' 2>/dev/null) | |
| [ "$INPUT_TAG" = "$TAG" ] && echo "$RUN_ID" && return | |
| done | |
| } | |
| STD=$(find_completed_run "build-wheels.yml") | |
| EXT=$(find_completed_run "build-wheels-extended.yml") | |
| EXO=$(find_completed_run "build-wheels-exotic.yml") | |
| echo "std=$STD ext=$EXT exo=$EXO" | |
| if [ -n "$STD" ] && [ -n "$EXT" ] && [ -n "$EXO" ]; then | |
| echo "✅ All three builds complete" | |
| echo "all_done=true" >> $GITHUB_OUTPUT | |
| echo "std_run_id=$STD" >> $GITHUB_OUTPUT | |
| echo "ext_run_id=$EXT" >> $GITHUB_OUTPUT | |
| echo "exo_run_id=$EXO" >> $GITHUB_OUTPUT | |
| else | |
| echo "⏳ Not all builds done yet — waiting for next workflow_run event" | |
| echo "all_done=false" >> $GITHUB_OUTPUT | |
| fi | |
| publish: | |
| name: "Upload release to PyPI" | |
| needs: check-all-done | |
| if: needs.check-all-done.outputs.all_done == 'true' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/uv-ffi | |
| permissions: | |
| id-token: write | |
| actions: write | |
| contents: write | |
| steps: | |
| - name: Download standard wheels | |
| uses: dawidd6/action-download-artifact@v3 | |
| with: | |
| workflow: build-wheels.yml | |
| run_id: ${{ needs.check-all-done.outputs.std_run_id }} | |
| name: wheels-* | |
| name_is_regexp: true | |
| path: dist/ | |
| if_no_artifact_found: ignore | |
| continue-on-error: true | |
| - name: Download extended wheels | |
| uses: dawidd6/action-download-artifact@v3 | |
| with: | |
| workflow: build-wheels-extended.yml | |
| run_id: ${{ needs.check-all-done.outputs.ext_run_id }} | |
| name: ext-wheels-* | |
| name_is_regexp: true | |
| path: dist/ | |
| if_no_artifact_found: ignore | |
| continue-on-error: true | |
| - name: Download exotic wheels | |
| uses: dawidd6/action-download-artifact@v3 | |
| with: | |
| workflow: build-wheels-exotic.yml | |
| run_id: ${{ needs.check-all-done.outputs.exo_run_id }} | |
| name: exotic-wheels-* | |
| name_is_regexp: true | |
| path: dist/ | |
| if_no_artifact_found: ignore | |
| continue-on-error: true | |
| - name: Download sdist | |
| uses: dawidd6/action-download-artifact@v3 | |
| with: | |
| workflow: publish.yml | |
| run_id: ${{ github.run_id }} | |
| name: sdist | |
| path: dist/ | |
| if_no_artifact_found: ignore | |
| continue-on-error: true | |
| # ── Everything below is identical to your existing publish logic ── | |
| - name: Flatten all wheels into dist/ | |
| run: | | |
| find dist/ -name "*.whl" -exec mv {} dist/ \; 2>/dev/null || true | |
| find dist/ -mindepth 1 -type d -exec rm -rf {} + 2>/dev/null || true | |
| find dist/ -name "*.attestation" -delete 2>/dev/null || true | |
| echo "Total: $(find dist/ -name '*.whl' | wc -l) wheels" | |
| ls -lh dist/ || true | |
| - name: Split wheels — PyPI vs GH Releases | |
| run: | | |
| mkdir -p dist-pypi dist-ghrelease | |
| PYPI_KEEP=( | |
| "*manylinux_2_28_x86_64*" "*manylinux_2_28_aarch64*" | |
| "*manylinux_2_17_x86_64*" "*manylinux_2_17_aarch64*" | |
| "*musllinux_1_2_x86_64*" "*musllinux_1_2_aarch64*" | |
| "*macosx_11_0_arm64*" "*macosx_10_12_x86_64*" | |
| "*win_amd64*" "*win_arm64*" | |
| "*.tar.gz" | |
| ) | |
| for f in dist/*.whl dist/*.tar.gz; do | |
| [ -f "$f" ] || continue | |
| name=$(basename "$f") | |
| case "$name" in | |
| *-pp[0-9]*|*-graalpy*|*-cp313t-*|*-cp3[0-9][0-9]-cp3[0-9][0-9]-*) | |
| cp "$f" dist-ghrelease/; continue ;; | |
| esac | |
| matched=false | |
| for pat in "${PYPI_KEEP[@]}"; do | |
| case "$name" in $pat) matched=true; break ;; esac | |
| done | |
| $matched && cp "$f" dist-pypi/ || cp "$f" dist-ghrelease/ | |
| done | |
| echo "PyPI: $(ls dist-pypi/ | wc -l) GH: $(ls dist-ghrelease/ | wc -l)" | |
| - name: Resolve tag | |
| id: tag | |
| run: | | |
| TAG="${{ inputs.tag }}" | |
| if [ -z "$TAG" ]; then | |
| TAG=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \ | |
| --jq '.inputs.tag // ""' 2>/dev/null) | |
| fi | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Upload exotic wheels to GitHub Release | |
| if: hashFiles('dist-ghrelease/*.whl') != '' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.tag }} | |
| files: dist-ghrelease/*.whl | |
| fail_on_unmatched_files: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Remove wheels already on PyPI | |
| run: | | |
| find dist-pypi/ -name "*.attestation" -delete 2>/dev/null || true | |
| VERSION=$(find dist-pypi/ -name "*.whl" | head -1 | grep -oP '(?<=uv_ffi-)[0-9][^-]+' || true) | |
| [ -z "$VERSION" ] && echo "Nothing to upload" && exit 0 | |
| EXISTING=$(curl -s "https://pypi.org/pypi/uv-ffi/$VERSION/json" \ | |
| | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin).get('urls',[])]" 2>/dev/null || true) | |
| while IFS= read -r fname; do | |
| [ -f "dist-pypi/$fname" ] && rm -f "dist-pypi/$fname" && echo " skip: $fname" | |
| done <<< "$EXISTING" | |
| echo "Uploading $(ls dist-pypi/*.whl 2>/dev/null | wc -l) wheels" | |
| - name: Publish via OIDC | |
| id: oidc | |
| continue-on-error: true | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist-pypi/ | |
| skip-existing: true | |
| - name: Clean attestations after OIDC failure | |
| if: steps.oidc.outcome == 'failure' | |
| run: find dist-pypi/ -name "*.attestation" -delete 2>/dev/null || true | |
| - name: Publish via API token (fallback) | |
| if: steps.oidc.outcome == 'failure' | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist-pypi/ | |
| password: ${{ secrets.PYPI_API_TOKEN }} | |
| update-wheel-index: | |
| name: "📄 Update GH Pages wheel index" | |
| needs: publish | |
| runs-on: ubuntu-latest | |
| if: always() | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: gh-pages | |
| - name: Regenerate PEP 503 index | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| python3 - <<'EOF' | |
| import os, json, urllib.request | |
| repo = os.environ["REPO"] | |
| token = os.environ["GH_TOKEN"] | |
| def gh(url): | |
| req = urllib.request.Request( | |
| url, headers={"Authorization": f"token {token}", | |
| "Accept": "application/vnd.github+json"}) | |
| return json.loads(urllib.request.urlopen(req).read()) | |
| def pypi(pkg): | |
| try: | |
| req = urllib.request.Request(f"https://pypi.org/pypi/{pkg}/json") | |
| data = json.loads(urllib.request.urlopen(req).read()) | |
| out = [] | |
| for ver, files in data["releases"].items(): | |
| for f in files: | |
| if f["filename"].endswith(".whl"): | |
| out.append((ver, f["filename"], f["url"])) | |
| return out | |
| except Exception as e: | |
| print(f"PyPI fetch failed: {e}") | |
| return [] | |
| # GH Release exotic wheels | |
| gh_wheels = [] | |
| page = 1 | |
| while True: | |
| releases = gh(f"https://api.github.com/repos/{repo}/releases?per_page=50&page={page}") | |
| if not releases: | |
| break | |
| for rel in releases: | |
| for asset in rel.get("assets", []): | |
| if asset["name"].endswith(".whl"): | |
| gh_wheels.append((rel["tag_name"], asset["name"], asset["browser_download_url"])) | |
| page += 1 | |
| # PyPI mainstream wheels | |
| pypi_wheels = pypi("uv-ffi") | |
| # Deduplicate by filename — GH Release wins if same name exists both places | |
| seen = {} | |
| for tag, name, url in pypi_wheels: | |
| seen[name] = (tag, name, url) | |
| for tag, name, url in gh_wheels: | |
| seen[name] = (tag, name, url) # GH overrides | |
| all_wheels = sorted(seen.values(), key=lambda x: (x[0], x[1])) | |
| os.makedirs("uv-ffi", exist_ok=True) | |
| links = "\n".join( | |
| f' <a href="{url}" data-requires-python=">=3.8">{name}</a><br>' | |
| for _, name, url in all_wheels | |
| ) | |
| with open("uv-ffi/index.html", "w") as f: | |
| f.write(f"""<!DOCTYPE html> | |
| <html> | |
| <head><title>Links for uv-ffi</title></head> | |
| <body> | |
| <h1>Links for uv-ffi</h1> | |
| {links} | |
| </body> | |
| </html> | |
| """) | |
| with open("index.html", "w") as f: | |
| f.write(f"""<!DOCTYPE html> | |
| <html> | |
| <head><title>uv-ffi wheel index</title></head> | |
| <body> | |
| <h1>uv-ffi wheel index</h1> | |
| <p>Mainstream wheels: <a href="https://pypi.org/project/uv-ffi/">PyPI</a></p> | |
| <p>Exotic platform wheels hosted here. Install any platform:</p> | |
| <pre>pip install uv-ffi --extra-index-url https://exotic-wheels.github.io/</pre> | |
| <p><a href="uv-ffi/">Browse all {len(all_wheels)} wheels →</a></p> | |
| </body> | |
| </html> | |
| """) | |
| print(f"Indexed {len(all_wheels)} total wheels ({len(pypi_wheels)} PyPI + {len(gh_wheels)} GH Releases)") | |
| EOF | |
| - name: Commit and push index | |
| run: | | |
| git config user.name "wheel-index[bot]" | |
| git config user.email "wheel-index[bot]@users.noreply.github.com" | |
| git add index.html uv-ffi/index.html | |
| git diff --staged --quiet && echo "No changes to index" && exit 0 | |
| git commit -m "Update wheel index for ${{ needs.pypi-publish.outputs.tag || github.ref_name }}" | |
| git push origin gh-pages |