Skip to content

Commit 52a31b8

Browse files
chaliyclaude
andauthored
feat(python): add PyPI publishing with pre-built wheels (#188)
## Summary - Add `publish-python.yml` workflow: builds pre-compiled wheels for 7 platform variants (Linux glibc/musl x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64) across Python 3.9–3.13, smoke-tests on each OS, publishes to PyPI via trusted publishing (OIDC) - Switch pyproject.toml to dynamic versioning from Cargo.toml — eliminates version drift between Rust and Python packages - Add `specs/013-python-package.md` documenting Python bindings layout, platform matrix, public API, and design decisions - Update `specs/008-release-process.md` with Python publishing workflow and PyPI auth setup ## Setup required before first publish 1. Create GitHub environment `release-python` in repo settings 2. Configure PyPI pending publisher: owner `everruns`, repo `bashkit`, workflow `publish-python.yml`, environment `release-python` ## Test plan - [ ] CI passes (no Rust changes, only new workflow + docs) - [ ] After merge, trigger `publish-python.yml` via workflow_dispatch to build wheels against current `v0.1.4` - [ ] Verify wheels install and smoke test passes on all 3 OS platforms - [ ] Verify PyPI upload succeeds with trusted publishing --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c083f5b commit 52a31b8

6 files changed

Lines changed: 467 additions & 3 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# PyPI publishing workflow for bashkit Python bindings
2+
# Builds pre-compiled wheels for all major platforms and publishes to PyPI.
3+
# Triggered alongside publish.yml on GitHub Release or manual dispatch.
4+
# Adapted from https://github.com/pydantic/monty CI wheel-building pattern.
5+
#
6+
# Prerequisites:
7+
# - PyPI trusted publisher configured for this repo + workflow
8+
# - GitHub environment "release-python" created in repo settings
9+
10+
name: Publish Python
11+
12+
on:
13+
release:
14+
types: [published]
15+
workflow_dispatch:
16+
17+
permissions:
18+
contents: read
19+
20+
env:
21+
CARGO_TERM_COLOR: always
22+
PYTHON_VERSIONS: "3.9 3.10 3.11 3.12 3.13"
23+
24+
jobs:
25+
# Source distribution (platform-independent)
26+
build-sdist:
27+
name: Build sdist
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v6
31+
32+
- uses: actions/setup-python@v6
33+
with:
34+
python-version: "3.12"
35+
36+
- uses: PyO3/maturin-action@v1
37+
with:
38+
command: sdist
39+
args: --out dist
40+
rust-toolchain: stable
41+
working-directory: crates/bashkit-python
42+
43+
- uses: actions/upload-artifact@v6
44+
with:
45+
name: pypi_files-sdist
46+
path: crates/bashkit-python/dist
47+
48+
# Pre-compiled binary wheels for each platform
49+
build:
50+
name: Build wheel - ${{ matrix.os }} (${{ matrix.target }}, ${{ matrix.manylinux || 'auto' }})
51+
strategy:
52+
fail-fast: false
53+
matrix:
54+
include:
55+
# Linux glibc
56+
- os: linux
57+
target: x86_64
58+
runs-on: ubuntu-latest
59+
- os: linux
60+
target: aarch64
61+
runs-on: ubuntu-latest
62+
63+
# Linux musl
64+
- os: linux
65+
target: x86_64
66+
manylinux: musllinux_1_1
67+
runs-on: ubuntu-latest
68+
- os: linux
69+
target: aarch64
70+
manylinux: musllinux_1_1
71+
runs-on: ubuntu-latest
72+
73+
# macOS
74+
- os: macos
75+
target: x86_64
76+
runs-on: macos-latest
77+
- os: macos
78+
target: aarch64
79+
runs-on: macos-latest
80+
81+
# Windows
82+
- os: windows
83+
target: x86_64
84+
runs-on: windows-latest
85+
86+
runs-on: ${{ matrix.runs-on }}
87+
steps:
88+
- uses: actions/checkout@v6
89+
90+
- uses: actions/setup-python@v6
91+
with:
92+
python-version: "3.12"
93+
94+
- name: Build wheels
95+
uses: PyO3/maturin-action@v1
96+
with:
97+
target: ${{ matrix.target }}
98+
manylinux: ${{ matrix.manylinux || 'auto' }}
99+
args: --release --out dist -i ${{ env.PYTHON_VERSIONS }}
100+
rust-toolchain: stable
101+
docker-options: -e CI
102+
working-directory: crates/bashkit-python
103+
104+
- uses: actions/upload-artifact@v6
105+
with:
106+
name: pypi_files-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'manylinux' }}
107+
path: crates/bashkit-python/dist
108+
109+
# Verify built artifacts
110+
inspect:
111+
name: Inspect artifacts
112+
needs: [build, build-sdist]
113+
runs-on: ubuntu-latest
114+
steps:
115+
- uses: actions/download-artifact@v7
116+
with:
117+
pattern: pypi_files-*
118+
merge-multiple: true
119+
path: dist
120+
121+
- name: List dist files
122+
run: |
123+
ls -lhR dist/
124+
echo "---"
125+
echo "Total files: $(ls -1 dist/ | wc -l)"
126+
127+
- uses: astral-sh/setup-uv@v7
128+
- run: uvx twine check dist/*
129+
130+
# Smoke-test wheels on each OS
131+
test-builds:
132+
name: Test wheel on ${{ matrix.os }}
133+
needs: [build]
134+
strategy:
135+
fail-fast: false
136+
matrix:
137+
include:
138+
- os: linux
139+
runs-on: ubuntu-latest
140+
- os: macos
141+
runs-on: macos-latest
142+
- os: windows
143+
runs-on: windows-latest
144+
runs-on: ${{ matrix.runs-on }}
145+
steps:
146+
- uses: actions/setup-python@v6
147+
with:
148+
python-version: "3.12"
149+
150+
- uses: actions/download-artifact@v7
151+
with:
152+
pattern: pypi_files-${{ matrix.os }}-*
153+
merge-multiple: true
154+
path: dist
155+
156+
- name: Install from wheel
157+
run: pip install bashkit --no-index --find-links dist --force-reinstall
158+
159+
- name: Smoke test
160+
run: python -c "from bashkit import BashTool; t = BashTool(); r = t.execute_sync('echo hello'); print(r); assert r.exit_code == 0"
161+
162+
# Publish to PyPI using trusted publishing (OIDC)
163+
publish:
164+
name: Publish to PyPI
165+
needs: [inspect, test-builds]
166+
if: success()
167+
runs-on: ubuntu-latest
168+
169+
environment:
170+
name: release-python
171+
172+
permissions:
173+
id-token: write
174+
175+
steps:
176+
- uses: actions/download-artifact@v7
177+
with:
178+
pattern: pypi_files-*
179+
merge-multiple: true
180+
path: dist
181+
182+
- name: List dist files
183+
run: ls -lhR dist/
184+
185+
- uses: astral-sh/setup-uv@v7
186+
187+
- name: Publish to PyPI
188+
run: uv publish --trusted-publishing always dist/*

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ Fix root cause. Unsure: read more code; if stuck, ask w/ short options. Unrecogn
3232
| 007-parallel-execution | Threading model, Arc usage |
3333
| 008-documentation | Rustdoc guides, embedded markdown |
3434
| 008-posix-compliance | POSIX design rationale, security exclusions |
35-
| 008-release-process | Version tagging, crates.io publishing |
35+
| 008-release-process | Version tagging, crates.io + PyPI publishing |
3636
| 009-implementation-status | Feature status, test coverage, limitations |
3737
| 009-tool-contract | Public LLM Tool trait contract |
3838
| 010-git-support | Sandboxed git operations on VFS |
3939
| 011-python-builtin | Embedded Python via Monty, security, resource limits |
4040
| 012-eval | LLM evaluation harness, dataset format, scoring |
4141
| 012-maintenance | Pre-release maintenance checklist |
42+
| 013-python-package | Python bindings, PyPI wheels, platform matrix |
4243

4344
### Documentation
4445

crates/bashkit-python/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "bashkit"
7-
version = "0.1.2"
7+
dynamic = ["version"]
88
description = "Python bindings for Bashkit - a sandboxed bash interpreter for AI agents"
99
readme = "README.md"
1010
license = { text = "MIT" }
@@ -19,6 +19,7 @@ classifiers = [
1919
"Programming Language :: Python :: 3.10",
2020
"Programming Language :: Python :: 3.11",
2121
"Programming Language :: Python :: 3.12",
22+
"Programming Language :: Python :: 3.13",
2223
"Programming Language :: Rust",
2324
"Topic :: Software Development :: Interpreters",
2425
"Topic :: Security",

specs/008-release-process.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ Example:
145145

146146
- `bashkit` on crates.io (core library)
147147
- `bashkit-cli` on crates.io (CLI tool)
148+
- `bashkit` on PyPI (Python bindings, pre-built wheels)
148149

149150
## Publishing Order
150151

@@ -153,7 +154,9 @@ Crates must be published in dependency order:
153154
1. `bashkit` (core library, no internal deps)
154155
2. `bashkit-cli` (depends on bashkit)
155156

156-
The CI workflow handles this with a dependency chain and wait for index update.
157+
Python wheels are published independently (no crates.io dependency).
158+
159+
The CI workflows handle this automatically on GitHub Release.
157160

158161
## Workflows
159162

@@ -170,6 +173,33 @@ The CI workflow handles this with a dependency chain and wait for index update.
170173
- **File**: `.github/workflows/publish.yml`
171174
- **Secret required**: `CARGO_REGISTRY_TOKEN`
172175

176+
### publish-python.yml
177+
178+
- **Trigger**: GitHub Release published (runs in parallel with publish.yml)
179+
- **Actions**: Builds pre-compiled wheels for all platforms, smoke-tests, publishes to PyPI
180+
- **File**: `.github/workflows/publish-python.yml`
181+
- **Auth**: PyPI trusted publishing (OIDC, no secrets needed)
182+
- **Environment**: `release-python` (must exist in GitHub repo settings)
183+
184+
#### Wheel matrix
185+
186+
| OS | Architecture | Variant |
187+
|----|-------------|---------|
188+
| Linux | x86_64 | manylinux (glibc) |
189+
| Linux | aarch64 | manylinux (glibc) |
190+
| Linux | x86_64 | musllinux |
191+
| Linux | aarch64 | musllinux |
192+
| macOS | x86_64 | universal |
193+
| macOS | aarch64 (Apple Silicon) | universal |
194+
| Windows | x86_64 | MSVC |
195+
196+
Python versions: 3.9, 3.10, 3.11, 3.12, 3.13
197+
198+
#### Version sync
199+
200+
Python package version is read dynamically from `Cargo.toml` via maturin
201+
(`dynamic = ["version"]` in pyproject.toml). No manual version sync needed.
202+
173203
## Authentication
174204

175205
**Required Secrets** (GitHub Settings > Secrets > Actions):
@@ -178,6 +208,11 @@ The CI workflow handles this with a dependency chain and wait for index update.
178208
- Generate at: https://crates.io/settings/tokens
179209
- Scope: Publish new crates, Publish updates
180210

211+
**PyPI Trusted Publishing** (no secret needed):
212+
213+
- Configure at: https://pypi.org/manage/project/bashkit/settings/publishing/
214+
- Add publisher: GitHub, repo `everruns/bashkit`, workflow `publish-python.yml`, environment `release-python`
215+
181216
## Example Conversation
182217

183218
```
@@ -220,3 +255,4 @@ Each release includes:
220255

221256
- **GitHub Release**: Tag, release notes, source archives
222257
- **crates.io**: Published crates for `cargo add bashkit`
258+
- **PyPI**: Pre-built wheels for `pip install bashkit`

0 commit comments

Comments
 (0)