name: Publish edgequake-litellm to PyPI
on:
push:
tags:
- "py-v[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:
inputs:
dry-run:
description: "Dry run — build wheels but do NOT upload to PyPI"
required: false
default: "true"
type: choice
options: ["true", "false"]
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
WORKING_DIR: edgequake-litellm
concurrency:
group: python-publish
cancel-in-progress: false
jobs:
preflight:
name: Pre-publish checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install stable Rust + clippy + rustfmt
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-publish-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-publish-
- name: Install Python build tools
working-directory: ${{ env.WORKING_DIR }}
run: pip install maturin "ruff>=0.3" "mypy>=1.8" "pytest>=8.0" "pytest-asyncio>=0.24"
- name: Check Rust formatting
working-directory: ${{ env.WORKING_DIR }}
run: cargo fmt --all -- --check
- name: Clippy
working-directory: ${{ env.WORKING_DIR }}
run: cargo clippy --all-features -- -D warnings
- name: Ruff check
working-directory: ${{ env.WORKING_DIR }}
run: ruff check python/
- name: Build (PEP-517) and run unit tests
working-directory: ${{ env.WORKING_DIR }}
run: |
pip install . -v
pytest tests/ -q -k "not e2e" --tb=short
- name: Verify tag matches pyproject.toml version
if: startsWith(github.ref, 'refs/tags/py-v')
shell: bash
run: |
TAG="${GITHUB_REF_NAME}" # e.g. py-v0.1.0
EXPECTED="py-v$(grep '^version' ${{ env.WORKING_DIR }}/pyproject.toml | head -1 | cut -d'"' -f2)"
echo "Git tag : $TAG"
echo "pyproject.toml: $EXPECTED"
if [ "$TAG" != "$EXPECTED" ]; then
echo "::error::Tag ($TAG) does not match pyproject.toml version ($EXPECTED). Bump version before tagging."
exit 1
fi
- name: Verify pyproject.toml and Cargo.toml versions match
shell: bash
run: |
PY_VER=$(grep '^version' ${{ env.WORKING_DIR }}/pyproject.toml | head -1 | cut -d'"' -f2)
RS_VER=$(grep '^version' ${{ env.WORKING_DIR }}/Cargo.toml | head -1 | cut -d'"' -f2)
echo "pyproject.toml: $PY_VER"
echo "Cargo.toml : $RS_VER"
if [ "$PY_VER" != "$RS_VER" ]; then
echo "::error::Version mismatch — pyproject.toml ($PY_VER) != Cargo.toml ($RS_VER)"
exit 1
fi
- name: Maturin dry-run wheel build
working-directory: ${{ env.WORKING_DIR }}
run: maturin build --release --out /tmp/maturin-check
sdist:
name: Build sdist
runs-on: ubuntu-latest
needs: preflight
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install maturin
run: pip install maturin
- name: Build sdist
working-directory: ${{ env.WORKING_DIR }}
run: maturin sdist --out dist
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: sdist
path: ${{ env.WORKING_DIR }}/dist/*.tar.gz
build-wheels:
name: Build wheel (${{ matrix.label }})
needs: preflight
strategy:
fail-fast: false
matrix:
include:
- label: linux-x86_64-manylinux
os: ubuntu-latest
target: x86_64
manylinux: auto
- label: linux-aarch64-manylinux
os: ubuntu-latest
target: aarch64
manylinux: auto
can-fail: true - label: linux-x86_64-musl
os: ubuntu-latest
target: x86_64
manylinux: musllinux_1_2
- label: linux-aarch64-musl
os: ubuntu-latest
target: aarch64
manylinux: musllinux_1_2
can-fail: true
- label: macos-x86_64
os: macos-latest target: x86_64
manylinux: ""
- label: macos-arm64
os: macos-latest
target: aarch64
manylinux: ""
- label: windows-x86_64
os: windows-latest
target: x86_64
manylinux: ""
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.can-fail == true }}
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Build wheel (maturin-action)
uses: PyO3/maturin-action@v1
with:
working-directory: ${{ env.WORKING_DIR }}
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
args: --release --out dist
sccache: "true"
before-script-linux: |
if [ -d /opt/python/cp39-cp39/bin ]; then
export PATH=/opt/python/cp39-cp39/bin:$PATH
fi
env:
CFLAGS_aarch64_unknown_linux_gnu: "-march=armv8-a -D__ARM_ARCH=8"
CFLAGS_aarch64_unknown_linux_musl: "-march=armv8-a -D__ARM_ARCH=8"
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.label }}
path: ${{ env.WORKING_DIR }}/dist/*.whl
smoke-test:
name: Smoke test (${{ matrix.os }})
needs: build-wheels
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
wheel-pattern: "wheel-linux-x86_64-manylinux"
- os: macos-latest
wheel-pattern: "wheel-macos-arm64"
- os: windows-latest
wheel-pattern: "wheel-windows-x86_64"
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.can-fail == true }}
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Download wheel
uses: actions/download-artifact@v4
with:
name: ${{ matrix.wheel-pattern }}
path: dist
- name: Install wheel
run: pip install edgequake-litellm --find-links dist --no-index
- name: Smoke test — mock provider (no API keys needed)
shell: python
run: |
import edgequake_litellm as eq
# 1. Basic completion
resp = eq.completion("mock/test", [{"role": "user", "content": "hi"}])
assert resp.content is not None, "resp.content is None"
assert isinstance(resp.content, str), f"resp.content is {type(resp.content)}"
# 2. litellm-compatible access path
assert resp.choices[0].message.content is not None, "choices path failed"
# 3. Version
assert eq.__version__ != "0.0.0-dev", f"dev version: {eq.__version__}"
print(f"Smoke test PASSED — version={eq.__version__}, content={resp.content!r}")
- name: Smoke test — unit tests (no API keys)
run: |
pip install "pytest>=8.0" "pytest-asyncio>=0.24"
pytest edgequake-litellm/tests/ -q -k "not e2e" --tb=short
publish:
name: Publish to PyPI
needs: [build-wheels, sdist]
runs-on: ubuntu-latest
if: always() && needs.sdist.result == 'success'
permissions:
id-token: write contents: write
steps:
- name: Download all wheels
uses: actions/download-artifact@v4
with:
pattern: wheel-*
path: dist
merge-multiple: true
- name: Download sdist
uses: actions/download-artifact@v4
with:
name: sdist
path: dist
- name: List dist contents
run: ls -la dist/
- name: Determine publish method
if: >-
startsWith(github.ref, 'refs/tags/py-v') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run == 'false')
id: pub-method
env:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
if [ -n "$PYPI_API_TOKEN" ]; then
echo "method=token" >> "$GITHUB_OUTPUT"
echo "Using API token auth (PYPI_API_TOKEN secret found)"
else
echo "method=oidc" >> "$GITHUB_OUTPUT"
echo "Using OIDC Trusted Publisher auth (no PYPI_API_TOKEN secret)"
fi
- name: Publish to PyPI (OIDC Trusted Publisher)
if: >-
steps.pub-method.outputs.method == 'oidc' &&
(startsWith(github.ref, 'refs/tags/py-v') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run == 'false'))
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
verbose: true
attestations: false
- name: Publish to PyPI (API Token)
if: >-
steps.pub-method.outputs.method == 'token' &&
(startsWith(github.ref, 'refs/tags/py-v') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run == 'false'))
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
verbose: true
attestations: false
- name: Dry-run summary (no upload)
if: >-
github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run == 'true'
run: |
echo "DRY RUN — wheels built but NOT uploaded to PyPI."
echo ""
echo "Artifacts ready to publish:"
ls -la dist/
echo ""
echo "To publish for real, push a tag: git tag py-v<version> && git push --tags"
echo "Or re-run this workflow with dry-run=false."
- name: Extract release notes from changelog
if: startsWith(github.ref, 'refs/tags/py-v')
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#py-v}"
awk -v ver="[$VERSION]" '
$0 ~ "^## \\[" && found { exit }
$0 ~ "^## \\[" ver "\\]" { found=1 }
found { print }
' "${WORKING_DIR}/CHANGELOG.md" > RELEASE_NOTES.md
if [ ! -s RELEASE_NOTES.md ]; then
{
echo "## edgequake-litellm ${VERSION}"
echo
echo "See edgequake-litellm/CHANGELOG.md for release details."
} > RELEASE_NOTES.md
fi
- name: Create GitHub release
if: startsWith(github.ref, 'refs/tags/py-v')
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTES.md
files: dist/*
generate_release_notes: false