Skip to content

Collect & Publish uv-ffi to PyPI #30

Collect & Publish uv-ffi to PyPI

Collect & Publish uv-ffi to PyPI #30

# 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