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 actions: read steps: - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Check CI status run: | echo "ℹ️ This workflow will download build artifacts from the latest CI run." echo " CI must have completed successfully on the current commit." 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@v5 with: python-version: '3.13' - 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: Push version update run: | git push origin HEAD:main echo "✅ Pushed version update to main branch" COMMIT_SHA=$(git rev-parse HEAD) echo "commit-sha=$COMMIT_SHA" >> $GITHUB_OUTPUT id: push-version - name: Wait for CI to complete id: wait-for-ci run: | echo "⏳ Waiting for CI to build new version..." COMMIT_SHA="${{ steps.push-version.outputs.commit-sha }}" # Wait up to 20 minutes for CI to complete for i in {1..40}; do RUN_ID=$(gh run list \ --workflow="CI - Build Multi-Platform Packages" \ --commit=$COMMIT_SHA \ --json databaseId,status \ --jq '.[] | select(.status == "completed") | .databaseId' | head -1) if [ ! -z "$RUN_ID" ]; then echo "✅ Found completed CI run: $RUN_ID" echo "run-id=$RUN_ID" >> $GITHUB_OUTPUT exit 0 fi echo "⏳ Waiting for CI... (attempt $i/40)" sleep 30 done echo "❌ CI did not complete within 20 minutes" exit 1 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download artifacts from CI run run: | echo "📦 Downloading artifacts from CI run ${{ steps.wait-for-ci.outputs.run-id }}..." # Download all artifacts (not just wheels-*) gh run download ${{ steps.wait-for-ci.outputs.run-id }} \ --dir ./dist-downloads # Consolidate all wheels into packages/*/dist/ mkdir -p packages/leann-core/dist mkdir -p packages/leann-backend-hnsw/dist mkdir -p packages/leann-backend-diskann/dist mkdir -p packages/leann/dist find ./dist-downloads -name "*.whl" -exec cp {} ./packages/ \; # Move wheels to correct package directories for wheel in packages/*.whl; do if [[ $wheel == *"leann_core"* ]]; then mv "$wheel" packages/leann-core/dist/ elif [[ $wheel == *"leann_backend_hnsw"* ]]; then mv "$wheel" packages/leann-backend-hnsw/dist/ elif [[ $wheel == *"leann_backend_diskann"* ]]; then mv "$wheel" packages/leann-backend-diskann/dist/ elif [[ $wheel == *"leann-"* ]] && [[ $wheel != *"backend"* ]] && [[ $wheel != *"core"* ]]; then mv "$wheel" packages/leann/dist/ fi done # List downloaded wheels echo "✅ Downloaded wheels:" find packages/*/dist -name "*.whl" -type f | sort env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Test on TestPyPI (optional) if: inputs.test_pypi continue-on-error: true env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} run: | if [ -z "$TWINE_PASSWORD" ]; then echo "⚠️ TEST_PYPI_API_TOKEN not configured, skipping TestPyPI upload" echo " To enable TestPyPI testing, add TEST_PYPI_API_TOKEN to repository secrets" exit 0 fi pip install twine echo "📦 Uploading to TestPyPI..." twine upload --repository testpypi packages/*/dist/* --verbose || { echo "⚠️ TestPyPI upload failed, but continuing with release" echo " This is optional and won't block the release" exit 0 } 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: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | if [ -z "$TWINE_PASSWORD" ]; then echo "❌ PYPI_API_TOKEN not configured!" echo " Please add PYPI_API_TOKEN to repository secrets" exit 1 fi pip install twine echo "📦 Publishing to PyPI..." # Collect all wheels in one place mkdir -p all_wheels find packages/*/dist -name "*.whl" -exec cp {} all_wheels/ \; find packages/*/dist -name "*.tar.gz" -exec cp {} all_wheels/ \; echo "📋 Packages to publish:" ls -la all_wheels/ # Upload to PyPI twine upload all_wheels/* --skip-existing --verbose echo "✅ Published to PyPI!" echo "🎉 Check packages at: https://pypi.org/project/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 }} ```