feat: implement comprehensive CI/CD pipeline with two-stage release
- Add ci.yml for continuous integration on every commit - Test builds on Ubuntu/macOS with Python 3.9/3.10/3.11 - Ensure code quality before any release - Add release-manual.yml for controlled releases - Manual trigger prevents accidental releases - Version validation and tag creation - Optional TestPyPI testing before production - Only creates tag after validation passes - Keep build-and-publish.yml for automated PyPI deployment - Triggered by new tags (separation of concerns) - Handles multi-platform wheel building - Allows retry if PyPI upload fails - Update RELEASE.md with clear prerequisites and workflow This setup ensures: 1. Every commit is tested (CI) 2. Releases are deliberate (manual trigger) 3. Failed CI won't create broken tags 4. PyPI publish can be retried independently
This commit is contained in:
105
.github/workflows/ci.yml
vendored
Normal file
105
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
name: CI - Build and Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
python-version: ['3.9', '3.10', '3.11']
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
run: |
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Install system dependencies (Ubuntu)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libomp-dev libboost-all-dev libzmq3-dev \
|
||||||
|
pkg-config libopenblas-dev patchelf \
|
||||||
|
libaio-dev protobuf-compiler libprotobuf-dev libabsl-dev
|
||||||
|
|
||||||
|
# Install Intel MKL for DiskANN
|
||||||
|
wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/79153e0f-74d7-45af-b8c2-258941adf58a/intel-onemkl-2025.0.0.940.sh
|
||||||
|
sudo sh intel-onemkl-2025.0.0.940.sh -a --components intel.oneapi.lin.mkl.devel --action install --eula accept -s
|
||||||
|
source /opt/intel/oneapi/setvars.sh
|
||||||
|
echo "MKLROOT=/opt/intel/oneapi/mkl/latest" >> $GITHUB_ENV
|
||||||
|
echo "LD_LIBRARY_PATH=/opt/intel/oneapi/mkl/latest/lib/intel64:$LD_LIBRARY_PATH" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install system dependencies (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew install libomp boost zeromq protobuf
|
||||||
|
|
||||||
|
- name: Build all packages
|
||||||
|
run: |
|
||||||
|
echo "🔨 Building on ${{ matrix.os }} with Python ${{ matrix.python-version }}..."
|
||||||
|
export UV_SYSTEM_PYTHON=1
|
||||||
|
|
||||||
|
# Build each package
|
||||||
|
for pkg in leann-core leann-backend-hnsw leann-backend-diskann leann; do
|
||||||
|
echo "Building $pkg..."
|
||||||
|
cd packages/$pkg
|
||||||
|
rm -rf dist/ build/ _skbuild/
|
||||||
|
uv build --wheel
|
||||||
|
if [ ! -f dist/*.whl ]; then
|
||||||
|
echo "❌ Failed to build $pkg!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ $pkg built successfully"
|
||||||
|
cd ../..
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Install and test packages
|
||||||
|
run: |
|
||||||
|
# Create clean test environment
|
||||||
|
python -m venv test_env
|
||||||
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||||
|
source test_env/Scripts/activate
|
||||||
|
else
|
||||||
|
source test_env/bin/activate
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install built packages
|
||||||
|
pip install packages/*/dist/*.whl
|
||||||
|
|
||||||
|
# Basic import test
|
||||||
|
python -c "import leann; print('✅ LEANN imported successfully')"
|
||||||
|
python -c "import leann_backend_hnsw; print('✅ HNSW backend imported')"
|
||||||
|
python -c "import leann_backend_diskann; print('✅ DiskANN backend imported')"
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wheels-${{ matrix.os }}-py${{ matrix.python-version }}
|
||||||
|
path: packages/*/dist/*.whl
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
# Summary job to ensure all builds pass
|
||||||
|
ci-success:
|
||||||
|
needs: build-test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: CI Success
|
||||||
|
run: |
|
||||||
|
echo "✅ All CI builds passed!"
|
||||||
|
echo "Ready for manual release when needed."
|
||||||
124
.github/workflows/release-manual.yml
vendored
Normal file
124
.github/workflows/release-manual.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
name: Manual Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version to release (e.g., 0.1.1)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
test_pypi:
|
||||||
|
description: 'Test on TestPyPI first'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-and-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Check CI status
|
||||||
|
run: |
|
||||||
|
echo "ℹ️ This workflow assumes CI has already passed on the current commit."
|
||||||
|
echo " If not, please wait for CI to complete before releasing."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Validate version format
|
||||||
|
run: |
|
||||||
|
if ! [[ "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "❌ Invalid version format. Use semantic versioning (e.g., 0.1.1)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Version format valid: ${{ inputs.version }}"
|
||||||
|
|
||||||
|
- name: Check if version already exists
|
||||||
|
run: |
|
||||||
|
if git tag | grep -q "^v${{ inputs.version }}$"; then
|
||||||
|
echo "❌ Version v${{ inputs.version }} already exists!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Version is new"
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
run: |
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Update versions
|
||||||
|
run: |
|
||||||
|
./scripts/bump_version.sh ${{ inputs.version }}
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git add packages/*/pyproject.toml
|
||||||
|
git commit -m "chore: release v${{ inputs.version }}"
|
||||||
|
|
||||||
|
- name: Build packages for TestPyPI
|
||||||
|
run: |
|
||||||
|
echo "🔨 Building packages for testing..."
|
||||||
|
./scripts/build_and_test.sh all
|
||||||
|
echo "✅ Build successful! (Already tested on multiple platforms)"
|
||||||
|
|
||||||
|
- name: Test on TestPyPI (optional)
|
||||||
|
if: inputs.test_pypi
|
||||||
|
env:
|
||||||
|
TWINE_USERNAME: __token__
|
||||||
|
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
pip install twine
|
||||||
|
echo "📦 Uploading to TestPyPI..."
|
||||||
|
twine upload --repository testpypi packages/*/dist/* --verbose
|
||||||
|
echo "✅ Test upload successful!"
|
||||||
|
echo "📋 Check packages at: https://test.pypi.org/user/your-username/"
|
||||||
|
echo ""
|
||||||
|
echo "To test installation:"
|
||||||
|
echo "pip install -i https://test.pypi.org/simple/ leann"
|
||||||
|
|
||||||
|
- name: Create and push tag
|
||||||
|
run: |
|
||||||
|
git tag "v${{ inputs.version }}"
|
||||||
|
git push origin main
|
||||||
|
git push origin "v${{ inputs.version }}"
|
||||||
|
echo "✅ Tag v${{ inputs.version }} created and pushed"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: v${{ inputs.version }}
|
||||||
|
name: Release v${{ inputs.version }}
|
||||||
|
body: |
|
||||||
|
## 🚀 Release v${{ inputs.version }}
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
See the [full changelog](https://github.com/${{ github.repository }}/compare/...v${{ inputs.version }})
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
pip install leann==${{ inputs.version }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Installation (if using TestPyPI)
|
||||||
|
```bash
|
||||||
|
pip install -i https://test.pypi.org/simple/ leann==${{ inputs.version }}
|
||||||
|
```
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Trigger PyPI publish
|
||||||
|
run: |
|
||||||
|
echo "🚀 Triggering PyPI publish workflow..."
|
||||||
|
# The existing build-and-publish.yml will be triggered by the tag push
|
||||||
|
echo "✅ Release process completed! The publish workflow will run automatically."
|
||||||
@@ -1,17 +1,52 @@
|
|||||||
# Release Guide
|
# Release Guide
|
||||||
|
|
||||||
## One-line Release 🚀
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
Before releasing, ensure:
|
||||||
|
1. ✅ All code changes are committed and pushed
|
||||||
|
2. ✅ CI has passed on the latest commit (check [Actions](https://github.com/yichuan-w/LEANN/actions/workflows/ci.yml))
|
||||||
|
3. ✅ You have determined the new version number
|
||||||
|
|
||||||
|
## 🚀 Recommended: Manual Release Workflow
|
||||||
|
|
||||||
|
### Via GitHub UI (Most Reliable)
|
||||||
|
|
||||||
|
1. **Verify CI Status**: Check that the latest commit has a green checkmark ✅
|
||||||
|
2. Go to [Actions → Manual Release](https://github.com/yichuan-w/LEANN/actions/workflows/release-manual.yml)
|
||||||
|
3. Click "Run workflow"
|
||||||
|
4. Enter version (e.g., `0.1.1`)
|
||||||
|
5. Toggle "Test on TestPyPI first" if desired
|
||||||
|
6. Click "Run workflow"
|
||||||
|
|
||||||
|
**What happens:**
|
||||||
|
- ✅ Validates version format
|
||||||
|
- ✅ Updates all package versions
|
||||||
|
- ✅ Optionally tests on TestPyPI
|
||||||
|
- ✅ Creates tag and GitHub release
|
||||||
|
- ✅ Automatically triggers PyPI publish
|
||||||
|
|
||||||
|
### Via Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run release-manual.yml -f version=0.1.1 -f test_pypi=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚡ Quick Release (One-Line)
|
||||||
|
|
||||||
|
For experienced users who want the fastest path:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/release.sh 0.1.1
|
./scripts/release.sh 0.1.1
|
||||||
```
|
```
|
||||||
|
|
||||||
That's it! This script will:
|
This script will:
|
||||||
1. Update all package versions
|
1. Update all package versions
|
||||||
2. Commit and push changes
|
2. Commit and push changes
|
||||||
3. Create GitHub release
|
3. Create GitHub release
|
||||||
4. CI automatically builds and publishes to PyPI
|
4. CI automatically builds and publishes to PyPI
|
||||||
|
|
||||||
|
⚠️ **Note**: If CI fails, you'll need to manually fix and re-tag
|
||||||
|
|
||||||
## Manual Testing Before Release
|
## Manual Testing Before Release
|
||||||
|
|
||||||
For testing specific packages locally (especially DiskANN on macOS):
|
For testing specific packages locally (especially DiskANN on macOS):
|
||||||
@@ -32,13 +67,6 @@ pip install packages/*/dist/*.whl
|
|||||||
./scripts/upload_to_pypi.sh prod
|
./scripts/upload_to_pypi.sh prod
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why Manual Build for DiskANN?
|
|
||||||
|
|
||||||
DiskANN's complex dependencies (protobuf, abseil, etc.) sometimes require local testing before release. The build script will:
|
|
||||||
- Compile the C++ extension
|
|
||||||
- Use `delocate` (macOS) or `auditwheel` (Linux) to bundle system libraries
|
|
||||||
- Create a self-contained wheel with no external dependencies
|
|
||||||
|
|
||||||
## First-time setup
|
## First-time setup
|
||||||
|
|
||||||
1. Install GitHub CLI:
|
1. Install GitHub CLI:
|
||||||
|
|||||||
Reference in New Issue
Block a user