From 72a7f11fbe84d7f9913c5ba7a07c4532db167613 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Fri, 29 Aug 2025 14:58:49 -0700 Subject: [PATCH 1/8] feat: implement auto version bump and release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add automated version management and release process similar to lance-spark: - Auto-bump workflow detects changes and creates version bump PRs - Release workflow creates GitHub releases with automatic versioning - Enhanced python-release workflow with PyPI availability checking - Python scripts for version calculation and release note generation - Comprehensive release documentation in CONTRIBUTING.md This enables fully automated releases from version bump to PyPI publishing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .bumpversion.toml | 29 ++++ .github/workflows/auto-bump.yml | 189 ++++++++++++++++++++++ .github/workflows/python-release.yml | 50 +++++- .github/workflows/release.yml | 172 ++++++++++++++++++++ CONTRIBUTING.md | 94 +++++++++++ ci/bump_version.py | 68 ++++++++ ci/calculate_version.py | 68 ++++++++ ci/generate_release_notes.py | 231 +++++++++++++++++++++++++++ 8 files changed, 900 insertions(+), 1 deletion(-) create mode 100644 .bumpversion.toml create mode 100644 .github/workflows/auto-bump.yml create mode 100644 .github/workflows/release.yml create mode 100644 ci/bump_version.py create mode 100644 ci/calculate_version.py create mode 100644 ci/generate_release_notes.py diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 0000000..44fc771 --- /dev/null +++ b/.bumpversion.toml @@ -0,0 +1,29 @@ +[tool.bumpversion] +current_version = "0.0.1" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_files = false +ignore_missing_version = false +tag = false +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Release version {new_version}" +allow_dirty = false +commit = false +message = "chore: bump version {current_version} → {new_version}" + +# pyproject.toml - project version +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'version = "{current_version}"' +replace = 'version = "{new_version}"' + +# lance_ray/__init__.py - package version (if exists) +[[tool.bumpversion.files]] +filename = "lance_ray/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' +ignore_missing_files = true \ No newline at end of file diff --git a/.github/workflows/auto-bump.yml b/.github/workflows/auto-bump.yml new file mode 100644 index 0000000..e2c1af1 --- /dev/null +++ b/.github/workflows/auto-bump.yml @@ -0,0 +1,189 @@ +name: Auto Bump Version + +on: + workflow_dispatch: + inputs: + bump_type: + description: 'Type of version bump' + required: false + default: 'auto' + type: choice + options: + - auto + - patch + - minor + - major + +jobs: + check-for-changes: + runs-on: ubuntu-latest + outputs: + should_bump: ${{ steps.check.outputs.should_bump }} + bump_type: ${{ steps.check.outputs.bump_type }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check for unreleased changes + id: check + run: | + # Get the last tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -z "$LAST_TAG" ]; then + echo "No tags found, should create initial release" + echo "should_bump=true" >> $GITHUB_OUTPUT + echo "bump_type=patch" >> $GITHUB_OUTPUT + exit 0 + fi + + # Check for commits since last tag + COMMITS_SINCE_TAG=$(git rev-list --count ${LAST_TAG}..HEAD) + + if [ "$COMMITS_SINCE_TAG" -gt 0 ]; then + echo "Found $COMMITS_SINCE_TAG commits since last tag $LAST_TAG" + + # Determine bump type based on input or commit analysis + if [ "${{ inputs.bump_type }}" != "auto" ] && [ -n "${{ inputs.bump_type }}" ]; then + # Use manual input if provided and not "auto" + BUMP_TYPE="${{ inputs.bump_type }}" + else + # Analyze commit messages to determine bump type + BUMP_TYPE="patch" + + # Check for breaking changes (major bump) + if git log ${LAST_TAG}..HEAD --grep="BREAKING CHANGE" --grep="!:" | grep -q .; then + BUMP_TYPE="major" + # Check for features (minor bump) + elif git log ${LAST_TAG}..HEAD --grep="^feat" --grep="^feature" | grep -q .; then + BUMP_TYPE="minor" + fi + fi + + echo "should_bump=true" >> $GITHUB_OUTPUT + echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT + else + echo "No commits since last tag $LAST_TAG" + echo "should_bump=false" >> $GITHUB_OUTPUT + fi + + - name: Summary + run: | + echo "## Auto Bump Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.check.outputs.should_bump }}" == "true" ]; then + echo "✅ Version bump needed" >> $GITHUB_STEP_SUMMARY + echo "- **Bump Type:** ${{ steps.check.outputs.bump_type }}" >> $GITHUB_STEP_SUMMARY + else + echo "⏭️ No version bump needed" >> $GITHUB_STEP_SUMMARY + fi + + create-bump-pr: + needs: check-for-changes + if: needs.check-for-changes.outputs.should_bump == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install packaging toml bump-my-version + + - name: Get current version + id: current_version + run: | + CURRENT_VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['version'])") + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "Current version: $CURRENT_VERSION" + + - name: Calculate new version + id: new_version + run: | + python ci/calculate_version.py \ + --current "${{ steps.current_version.outputs.version }}" \ + --type "${{ needs.check-for-changes.outputs.bump_type }}" \ + --channel "stable" + + - name: Create feature branch + run: | + BRANCH_NAME="auto-bump-${{ steps.new_version.outputs.version }}" + git checkout -b $BRANCH_NAME + echo "branch=$BRANCH_NAME" >> $GITHUB_ENV + + - name: Bump version + run: | + python ci/bump_version.py --version "${{ steps.new_version.outputs.version }}" + + - name: Configure git + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Commit changes + run: | + git add -A + git commit -m "chore: bump version to ${{ steps.new_version.outputs.version }} + + Automated version bump from ${{ steps.current_version.outputs.version }} to ${{ steps.new_version.outputs.version }}. + Bump type: ${{ needs.check-for-changes.outputs.bump_type }}" + + - name: Push changes + run: | + git push origin ${{ env.branch }} + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ env.branch }} + base: main + title: "chore: bump version to ${{ steps.new_version.outputs.version }}" + body: | + ## Automated Version Bump + + This PR automatically bumps the version from `${{ steps.current_version.outputs.version }}` to `${{ steps.new_version.outputs.version }}`. + + ### Details + - **Bump Type:** ${{ needs.check-for-changes.outputs.bump_type }} + - **Triggered By:** ${{ github.event_name == 'workflow_dispatch' && 'Manual trigger' || 'Automated' }} + + ### Checklist + - [ ] Review version bump changes + - [ ] Verify pyproject.toml is updated + - [ ] Confirm CI checks pass + + ### Next Steps + After merging this PR, you can create a release by: + 1. Going to Actions → Create Release workflow + 2. Selecting the release channel (stable/preview) + 3. Running the workflow + + --- + *This PR was automatically generated by the auto-bump workflow.* + labels: | + version-bump + automated + assignees: ${{ github.actor }} + + - name: Summary + run: | + echo "## Version Bump PR Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Current Version:** ${{ steps.current_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **New Version:** ${{ steps.new_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Bump Type:** ${{ needs.check-for-changes.outputs.bump_type }}" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** ${{ env.branch }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Pull request created successfully!" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 4c8be22..fab60eb 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -29,6 +29,10 @@ on: options: - dry_run - release + ref: + description: 'The branch, tag or SHA to checkout' + required: false + type: string jobs: publish: @@ -39,6 +43,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + # When triggered by a release, use the release tag + # When triggered manually, use the provided ref + ref: ${{ github.event.release.tag_name || inputs.ref }} - name: Set up Python uses: actions/setup-python@v5 @@ -52,9 +60,49 @@ jobs: run: | uv build + - name: Get package version + id: get_version + run: | + VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['version'])") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Package version: $VERSION" + - name: Publish to PyPI if: | (github.event_name == 'release' && github.event.action == 'released') || (github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'release') run: | - uv publish --trusted-publishing always \ No newline at end of file + uv publish --trusted-publishing always + + - name: Wait for PyPI availability + if: | + (github.event_name == 'release' && github.event.action == 'released') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'release') + run: | + VERSION="${{ steps.get_version.outputs.version }}" + PACKAGE_NAME="lance-ray" + + echo "Waiting for ${PACKAGE_NAME}==${VERSION} to be available on PyPI..." + + # Wait up to 5 minutes for the package to be available + MAX_ATTEMPTS=30 + SLEEP_TIME=10 + + for i in $(seq 1 $MAX_ATTEMPTS); do + echo "Attempt $i/$MAX_ATTEMPTS: Checking PyPI..." + + # Check if package is available + if pip index versions ${PACKAGE_NAME} 2>/dev/null | grep -q "${VERSION}"; then + echo "✅ Package ${PACKAGE_NAME}==${VERSION} is now available on PyPI!" + exit 0 + fi + + if [ $i -lt $MAX_ATTEMPTS ]; then + echo "Package not yet available. Waiting ${SLEEP_TIME} seconds..." + sleep $SLEEP_TIME + fi + done + + echo "⚠️ Warning: Package ${PACKAGE_NAME}==${VERSION} is not yet available on PyPI after ${MAX_ATTEMPTS} attempts." + echo "It may still be processing. Please check https://pypi.org/project/${PACKAGE_NAME}/ manually." + exit 1 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a8b4adc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,172 @@ +name: Create Release + +on: + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + release_channel: + description: 'Release channel' + required: true + default: 'preview' + type: choice + options: + - preview + - stable + dry_run: + description: 'Dry run (simulate the release without pushing)' + required: true + default: true + type: boolean + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - name: Output Inputs + run: echo "${{ toJSON(github.event.inputs) }}" + + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install packaging toml bump-my-version + + - name: Get current version + id: current_version + run: | + CURRENT_VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['version'])") + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "Current version: $CURRENT_VERSION" + + - name: Calculate new version + id: new_version + run: | + python ci/calculate_version.py \ + --current "${{ steps.current_version.outputs.version }}" \ + --type "${{ inputs.release_type }}" \ + --channel "${{ inputs.release_channel }}" + + - name: Determine tag name + id: tag_name + run: | + if [ "${{ inputs.release_channel }}" == "stable" ]; then + VERSION="${{ steps.new_version.outputs.version }}" + TAG="v${VERSION}" + else + # For preview releases, use current version with beta suffix + VERSION="${{ steps.current_version.outputs.version }}" + # Find the next beta number for current version + BETA_TAGS=$(git tag -l "v${VERSION}-beta.*" | sort -V) + if [ -z "$BETA_TAGS" ]; then + BETA_NUM=1 + else + LAST_BETA=$(echo "$BETA_TAGS" | tail -n 1) + LAST_NUM=$(echo "$LAST_BETA" | sed "s/v${VERSION}-beta.//") + BETA_NUM=$((LAST_NUM + 1)) + fi + TAG="v${VERSION}-beta.${BETA_NUM}" + fi + + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Tag will be: $TAG" + + - name: Update version (stable releases only) + if: inputs.release_channel == 'stable' + run: | + python ci/bump_version.py --version "${{ steps.new_version.outputs.version }}" + + - name: Configure git identity + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Create release commit (stable releases only) + if: inputs.release_channel == 'stable' + run: | + git add -A + git commit -m "chore: release version ${{ steps.new_version.outputs.version }}" || echo "No changes to commit" + + - name: Create tag + run: | + git tag -a "${{ steps.tag_name.outputs.tag }}" -m "Release ${{ steps.tag_name.outputs.tag }}" + + - name: Push changes (if not dry run) + if: ${{ !inputs.dry_run }} + run: | + if [ "${{ inputs.release_channel }}" == "stable" ]; then + # Push the commit for stable releases + git push origin main + fi + # Always push the tag + git push origin "${{ steps.tag_name.outputs.tag }}" + + - name: Generate release notes + id: release_notes + if: ${{ !inputs.dry_run }} + run: | + python ci/generate_release_notes.py \ + --tag "${{ steps.tag_name.outputs.tag }}" \ + --repo "${{ github.repository }}" \ + --token "${{ secrets.GITHUB_TOKEN }}" + + - name: Create GitHub Release Draft (if not dry run) + if: ${{ !inputs.dry_run }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag_name.outputs.tag }} + name: ${{ steps.tag_name.outputs.tag }} + body_path: release_notes.md + draft: true + prerelease: ${{ inputs.release_channel == 'preview' }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Summary + run: | + echo "## Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Release Type:** ${{ inputs.release_type }}" >> $GITHUB_STEP_SUMMARY + echo "- **Release Channel:** ${{ inputs.release_channel }}" >> $GITHUB_STEP_SUMMARY + echo "- **Current Version:** ${{ steps.current_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.release_channel }}" == "stable" ]; then + echo "- **New Version:** ${{ steps.new_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + fi + echo "- **Tag:** ${{ steps.tag_name.outputs.tag }}" >> $GITHUB_STEP_SUMMARY + echo "- **Dry Run:** ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY + + if [ "${{ inputs.dry_run }}" == "true" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ This was a dry run. No changes were pushed." >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "📝 Draft release created successfully!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps:" >> $GITHUB_STEP_SUMMARY + echo "1. Review the draft release on the [releases page](https://github.com/${{ github.repository }}/releases)" >> $GITHUB_STEP_SUMMARY + echo "2. Edit the release notes if needed" >> $GITHUB_STEP_SUMMARY + echo "3. Publish the release to:" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.release_channel }}" == "stable" ]; then + echo " - Create the official release" >> $GITHUB_STEP_SUMMARY + echo " - Trigger automatic publishing to PyPI" >> $GITHUB_STEP_SUMMARY + else + echo " - Create a preview/beta release" >> $GITHUB_STEP_SUMMARY + echo " - Note: Preview releases are not published to PyPI by default" >> $GITHUB_STEP_SUMMARY + fi + fi \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2351890..6a7d4e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,3 +74,97 @@ uv run mkdocs serve # Documentation will be available at http://localhost:8000 ``` + +## Release Process + +We use an automated release process to manage version bumps and deployments to PyPI. + +### Automatic Version Bumping + +The project uses automatic version detection and bumping based on conventional commits: + +- **`feat:`** commits trigger a minor version bump +- **`fix:`** commits trigger a patch version bump +- **`BREAKING CHANGE:`** or **`!:`** commits trigger a major version bump + +To manually trigger a version bump: + +1. Go to Actions → Auto Bump Version workflow +2. Click "Run workflow" +3. Select the bump type (auto/patch/minor/major) +4. The workflow will create a PR with the version changes + +### Creating a Release + +There are two release channels: + +- **Stable**: Full releases published to PyPI +- **Preview**: Beta/preview releases for testing + +To create a release: + +1. **Ensure version is bumped** (if needed): + - Either wait for automatic bump based on commits + - Or manually trigger the Auto Bump Version workflow + - Review and merge the generated PR + +2. **Create the release**: + - Go to Actions → Create Release workflow + - Select release type (patch/minor/major) + - Select release channel (stable/preview) + - Set dry_run to false for actual release + - Run the workflow + +3. **Review and publish**: + - The workflow creates a draft GitHub release + - Review the auto-generated release notes + - Edit if necessary + - Publish the release + +4. **Automatic PyPI deployment**: + - Once the release is published, the Python Release workflow automatically triggers + - The package is built and published to PyPI using trusted publishing + - No manual intervention required + +### Manual Release (Emergency) + +If needed, you can manually trigger PyPI deployment: + +1. Go to Actions → Python Release workflow +2. Select mode: "release" +3. Optionally specify a ref (branch/tag/SHA) +4. Run the workflow + +### Version Management + +- Version is managed in `pyproject.toml` +- The `.bumpversion.toml` file configures automatic version updates +- All version changes should go through the automated workflows + +### Commit Message Format + +We follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +(): + + + +