Skip to content

Commit c2e7c48

Browse files
joaoh82claude
andauthored
Phase 6f: add publish-python to release.yml (PyPI via OIDC) (#25)
Three new jobs wired into the release pipeline alongside the existing publish-crate / publish-ffi / publish-desktop chain: - build-python-wheels (matrix, 4 cells) - build-python-sdist (single) - publish-python (aggregate + upload + GitHub Release) **Why three jobs instead of one matrix with inline upload:** PyPI expects wheels as a single batch — if each matrix cell published its own wheel independently, a mid-matrix failure would leave PyPI with a partial wave of a release, which is worse than the whole thing failing cleanly. The build/publish split aggregates every wheel + the sdist into one `dist/` directory, then uploads atomically via `pypa/gh-action-pypi-publish`. **Wheel matrix** mirrors publish-ffi + publish-desktop: ubuntu-latest → linux x86_64 (manylinux2014) ubuntu-24.04-arm → linux aarch64 (manylinux2014) macos-latest → macOS aarch64 (Apple Silicon) windows-latest → windows x86_64 abi3-py38 means one wheel per platform works on every CPython ≥ 3.8 — no per-Python-version axis. An sdist is built alongside for platforms not covered by the wheel matrix (FreeBSD, alpine aarch64, etc.); `pip install` falls back to a source build via the user's local Rust toolchain. **Authentication via PyPI trusted publishing** (OIDC). Zero long-lived tokens. The publish-python job has `permissions: id-token: write` and lives in the `release` GitHub environment (required-reviewer gate inherited from publish-crate). PyPI-side config is a one-time web-UI registration documented in docs/release-secrets.md. **Wiring:** - tag-all → pushes sqlrite-py-v<V> - finalize → needs publish-python - umbrella release body → includes 🐍 Python link pointing to the per-product release + PyPI page Verified locally: `cargo check -p sqlrite-python` clean, release.yml parses as valid YAML, pyproject.toml already has name = "sqlrite" (available on PyPI per pypi.org API, no rename dance like crates.io needed). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 61d8d10 commit c2e7c48

2 files changed

Lines changed: 224 additions & 9 deletions

File tree

.github/workflows/release.yml

Lines changed: 216 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
#
77
# Phase 6d: tag-all + publish-crate + publish-ffi + finalize.
88
# Phase 6e adds publish-desktop.
9-
# Phases 6f–6i add publish-python / publish-nodejs / publish-wasm /
10-
# publish-go as separate jobs to this same file.
9+
# Phase 6f adds build-python-wheels + publish-python.
10+
# Phases 6g–6i add publish-nodejs / publish-wasm / publish-go as
11+
# separate jobs to this same file.
1112
#
1213
# Design doc: docs/release-plan.md.
1314
# One-time registry / branch-protection setup: docs/release-secrets.md.
@@ -85,14 +86,15 @@ jobs:
8586
# BEFORE any publish step so a bad version number (e.g., tag
8687
# already exists for some reason) aborts the whole release cleanly.
8788
#
88-
# As of Phase 6e, we tag:
89+
# As of Phase 6f, we tag:
8990
# - sqlrite-v<V> (Rust engine)
9091
# - sqlrite-ffi-v<V> (C FFI prebuilt binaries)
9192
# - sqlrite-desktop-v<V> (Tauri desktop installers)
93+
# - sqlrite-py-v<V> (Python wheels on PyPI)
9294
# - v<V> (umbrella)
9395
#
94-
# Later phases add sqlrite-py-v<V>, sqlrite-node-v<V>,
95-
# sqlrite-wasm-v<V>, sdk/go/v<V> as their publish jobs come online.
96+
# Later phases add sqlrite-node-v<V>, sqlrite-wasm-v<V>,
97+
# sdk/go/v<V> as their publish jobs come online.
9698
#
9799
# Idempotent on re-run: if a tag already exists (partial-failure
98100
# scenario where publish-crate succeeded but publish-ffi failed,
@@ -121,6 +123,7 @@ jobs:
121123
"sqlrite-v$V"
122124
"sqlrite-ffi-v$V"
123125
"sqlrite-desktop-v$V"
126+
"sqlrite-py-v$V"
124127
"v$V"
125128
)
126129
for tag in "${TAGS[@]}"; do
@@ -425,6 +428,211 @@ jobs:
425428
# files to the same release. (action is idempotent on
426429
# this — it uses `tagName` as the identity key.)
427430

431+
# ---------------------------------------------------------------------------
432+
# Step 3d: build Python wheels for every supported platform.
433+
# (Phase 6f — build half; publish half lives in the next job.)
434+
#
435+
# Why the split: PyPI expects wheels to be uploaded as one
436+
# batch — if publish-python-wheels had publish logic inline,
437+
# each matrix cell would race to upload its wheel independently,
438+
# and a failure mid-matrix would leave PyPI with a partial wave.
439+
# The two-job shape lets us aggregate every wheel (+ sdist) into
440+
# one `dist/` directory and do the upload as a single atomic
441+
# step in the `publish-python` job below.
442+
#
443+
# Matrix mirrors publish-ffi / publish-desktop — same four
444+
# OS / arch combinations. abi3-py38 means each platform gets
445+
# ONE wheel that works on every CPython ≥ 3.8, so we don't
446+
# need a per-Python-version matrix axis.
447+
build-python-wheels:
448+
name: Build Python wheel (${{ matrix.platform }})
449+
needs: [detect, tag-all]
450+
if: needs.detect.outputs.should_release == 'true'
451+
runs-on: ${{ matrix.os }}
452+
strategy:
453+
fail-fast: false
454+
matrix:
455+
include:
456+
- os: ubuntu-latest
457+
target: x86_64
458+
platform: linux-x86_64
459+
manylinux: auto
460+
- os: ubuntu-24.04-arm
461+
target: aarch64
462+
platform: linux-aarch64
463+
manylinux: auto
464+
- os: macos-latest
465+
target: aarch64
466+
platform: macos-aarch64
467+
manylinux: ""
468+
- os: windows-latest
469+
target: x64
470+
platform: windows-x86_64
471+
manylinux: ""
472+
steps:
473+
- uses: actions/checkout@v4
474+
475+
- uses: actions/setup-python@v5
476+
with:
477+
# 3.10 is the build interpreter; abi3 means the wheel
478+
# itself runs on every CPython ≥ 3.8. Any recent
479+
# Python on the runner would work — 3.10 is just
480+
# well-supported + cached on all runner images.
481+
python-version: '3.10'
482+
483+
- uses: dtolnay/rust-toolchain@stable
484+
485+
- uses: Swatinem/rust-cache@v2
486+
with:
487+
shared-key: build-python-wheels-${{ matrix.platform }}
488+
489+
# `maturin-action` builds + packages the wheel. For Linux
490+
# `manylinux: auto` means it runs inside a manylinux2014
491+
# container so glibc gets baked at an old-enough version
492+
# that the wheel runs on any distro shipped since ~2014.
493+
# macOS / Windows don't use manylinux — the wheel tags
494+
# reflect the specific OS version it was built on.
495+
- name: Build wheel
496+
uses: PyO3/maturin-action@v1
497+
with:
498+
working-directory: sdk/python
499+
target: ${{ matrix.target }}
500+
args: --release --out dist
501+
manylinux: ${{ matrix.manylinux }}
502+
503+
- name: Upload wheel artifact
504+
uses: actions/upload-artifact@v4
505+
with:
506+
name: python-wheel-${{ matrix.platform }}
507+
path: sdk/python/dist/*.whl
508+
if-no-files-found: error
509+
retention-days: 1
510+
511+
# ---------------------------------------------------------------------------
512+
# Step 3e: build the Python source distribution.
513+
#
514+
# Uploaded alongside the wheels so users on odd platforms not
515+
# covered by our wheel matrix (FreeBSD, alpine aarch64, etc.)
516+
# can still `pip install sqlrite` and get a source build
517+
# (requires their local Rust toolchain, which is the standard
518+
# fallback path for any PyO3 crate).
519+
build-python-sdist:
520+
name: Build Python sdist
521+
needs: [detect, tag-all]
522+
if: needs.detect.outputs.should_release == 'true'
523+
runs-on: ubuntu-latest
524+
steps:
525+
- uses: actions/checkout@v4
526+
527+
- uses: actions/setup-python@v5
528+
with:
529+
python-version: '3.10'
530+
531+
- name: Build sdist
532+
uses: PyO3/maturin-action@v1
533+
with:
534+
working-directory: sdk/python
535+
command: sdist
536+
args: --out dist
537+
538+
- name: Upload sdist artifact
539+
uses: actions/upload-artifact@v4
540+
with:
541+
name: python-sdist
542+
path: sdk/python/dist/*.tar.gz
543+
if-no-files-found: error
544+
retention-days: 1
545+
546+
# ---------------------------------------------------------------------------
547+
# Step 3f: aggregate every wheel + the sdist, upload to PyPI
548+
# via OIDC trusted publishing, and cut the per-product
549+
# `sqlrite-py-v<V>` GitHub Release.
550+
#
551+
# OIDC trusted publishing: no long-lived PyPI API token exists
552+
# anywhere. The `permissions: id-token: write` block below
553+
# lets `pypa/gh-action-pypi-publish` mint a short-lived OIDC
554+
# token, exchange it at PyPI for a one-time upload token, and
555+
# push every wheel. PyPI-side config lives on the `sqlrite`
556+
# project's settings page — see docs/release-secrets.md for
557+
# the one-time trusted-publisher registration.
558+
publish-python:
559+
name: Publish Python wheels to PyPI
560+
needs: [detect, tag-all, build-python-wheels, build-python-sdist]
561+
if: needs.detect.outputs.should_release == 'true'
562+
runs-on: ubuntu-latest
563+
environment: release
564+
permissions:
565+
# OIDC: required for PyPI trusted-publisher token exchange.
566+
id-token: write
567+
# For the softprops/action-gh-release step at the end.
568+
contents: write
569+
steps:
570+
- uses: actions/checkout@v4
571+
572+
# Pull every wheel artifact (one per matrix platform) +
573+
# the sdist into a single `dist/` directory.
574+
- name: Download wheel artifacts
575+
uses: actions/download-artifact@v4
576+
with:
577+
pattern: python-wheel-*
578+
path: dist
579+
merge-multiple: true
580+
581+
- name: Download sdist artifact
582+
uses: actions/download-artifact@v4
583+
with:
584+
name: python-sdist
585+
path: dist
586+
587+
- name: List files about to be uploaded
588+
run: ls -la dist/
589+
590+
# Single atomic upload of all wheels + sdist. If any file
591+
# fails to upload, none are published — no partial wave
592+
# on PyPI.
593+
- name: Publish to PyPI
594+
uses: pypa/gh-action-pypi-publish@release/v1
595+
with:
596+
packages-dir: dist
597+
# Keep `skip-existing: false` so a re-run of this job
598+
# (after a partial-failure scenario) fails loudly rather
599+
# than silently ignoring already-uploaded files.
600+
skip-existing: false
601+
602+
- name: GitHub Release
603+
uses: softprops/action-gh-release@v2
604+
with:
605+
tag_name: sqlrite-py-v${{ needs.detect.outputs.version }}
606+
name: Python v${{ needs.detect.outputs.version }}
607+
body: |
608+
Published to PyPI: https://pypi.org/project/sqlrite/${{ needs.detect.outputs.version }}/
609+
610+
```bash
611+
pip install sqlrite==${{ needs.detect.outputs.version }}
612+
```
613+
614+
```python
615+
import sqlrite
616+
conn = sqlrite.connect(":memory:")
617+
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
618+
conn.execute("INSERT INTO users (name) VALUES (?)", ("alice",))
619+
for row in conn.execute("SELECT * FROM users"):
620+
print(row)
621+
```
622+
623+
**Wheels in this release:**
624+
- Linux x86_64 (manylinux2014 or newer)
625+
- Linux aarch64 (manylinux2014 or newer)
626+
- macOS aarch64 (Apple Silicon)
627+
- Windows x86_64
628+
- Source distribution (`.tar.gz`) — builds from source on other platforms via a local Rust toolchain
629+
630+
All wheels are `abi3-py38`, so one wheel per platform works on every CPython ≥ 3.8.
631+
632+
See the umbrella release [v${{ needs.detect.outputs.version }}](../../releases/tag/v${{ needs.detect.outputs.version }}) for the full changelog.
633+
files: dist/*
634+
generate_release_notes: true
635+
428636
# ---------------------------------------------------------------------------
429637
# Step 4: create the umbrella GitHub Release. Runs after all
430638
# publish-* jobs succeed. Uses GitHub's native auto-generated
@@ -433,7 +641,7 @@ jobs:
433641
# config if we add one later.
434642
finalize:
435643
name: Finalize umbrella release
436-
needs: [detect, publish-crate, publish-ffi, publish-desktop]
644+
needs: [detect, publish-crate, publish-ffi, publish-desktop, publish-python]
437645
if: needs.detect.outputs.should_release == 'true'
438646
runs-on: ubuntu-latest
439647
steps:
@@ -454,8 +662,9 @@ jobs:
454662
- 🦀 [Rust engine](../../releases/tag/sqlrite-v${{ needs.detect.outputs.version }}) → [crates.io](https://crates.io/crates/sqlrite-engine/${{ needs.detect.outputs.version }})
455663
- 🔧 [C FFI](../../releases/tag/sqlrite-ffi-v${{ needs.detect.outputs.version }}) — prebuilt `libsqlrite_c` for Linux x86_64/aarch64, macOS aarch64, Windows x86_64
456664
- 🖥️ [Desktop](../../releases/tag/sqlrite-desktop-v${{ needs.detect.outputs.version }}) — unsigned installers for Linux (AppImage + deb), macOS (dmg aarch64), Windows (msi)
665+
- 🐍 [Python](../../releases/tag/sqlrite-py-v${{ needs.detect.outputs.version }}) → [PyPI](https://pypi.org/project/sqlrite/${{ needs.detect.outputs.version }}/) — abi3-py38 wheels for Linux x86_64/aarch64, macOS aarch64, Windows x86_64 + sdist
457666
458-
_Python / Node.js / WASM / Go SDKs land as their publish jobs come online (Phases 6f–6i)._
667+
_Node.js / WASM / Go SDKs land as their publish jobs come online (Phases 6g–6i)._
459668
460669
---
461670

docs/roadmap.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,15 @@ Release assets land on the `sqlrite-desktop-vX.Y.Z` GitHub Release with a body t
409409

410410
Follow-ups: macOS universal (x86_64 + aarch64 lipo'd — adds one Rust target build + `lipo` step), Linux aarch64 AppImage (adds one matrix cell on `ubuntu-24.04-arm`).
411411

412-
### Phase 6f — Python SDK publish
412+
### Phase 6f — Python SDK publish
413413

414-
Adds `publish-python` job. `maturin-action` builds abi3-py38 wheels; PyPI publish via OIDC trusted publisher.
414+
Adds three jobs to `release.yml``build-python-wheels` (matrix), `build-python-sdist` (single), `publish-python` (aggregator + PyPI upload + GitHub Release).
415+
416+
**Two-job shape (build then publish), not one matrix job with inline upload**, because PyPI expects wheels as a single batch — racing uploads from per-platform matrix cells would leave PyPI with a partial wave if any one cell failed. Artifacts from every matrix cell land in a single aggregated `dist/` directory, which is then atomically uploaded by `pypa/gh-action-pypi-publish`.
417+
418+
Wheel matrix mirrors publish-ffi + publish-desktop: Linux x86_64 (manylinux2014 via the `auto` preset), Linux aarch64 (same preset on `ubuntu-24.04-arm`), macOS aarch64, Windows x86_64. abi3-py38 means one wheel per platform works on every CPython ≥ 3.8 — no per-Python-version axis. An sdist is built alongside for platforms not covered by the wheel matrix.
419+
420+
Authentication via PyPI trusted publishing (OIDC) — zero long-lived tokens. `permissions: id-token: write` on the publish job plus the `release` GitHub environment (one-time trusted-publisher config on PyPI's web UI, documented in `docs/release-secrets.md`).
415421

416422
### Phase 6g — Node.js SDK publish
417423

0 commit comments

Comments
 (0)