diff --git a/.bumpversion.toml b/.bumpversion.toml index 25cc338fcaa..025ec87014e 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,8 +1,8 @@ [tool.bumpversion] -current_version = "0.32.1" -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(-(?P.+))?" +current_version = "0.40.0-beta.1" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(-(?P(beta|rc))\\.(?P\\d+))?" serialize = [ - "{major}.{minor}.{patch}-{prerelease}", + "{major}.{minor}.{patch}-{prerelease}.{prerelease_num}", "{major}.{minor}.{patch}" ] search = "{current_version}" @@ -18,6 +18,13 @@ allow_dirty = false commit = false message = "chore: bump version {current_version} → {new_version}" +[tool.bumpversion.parts.prerelease] +optional_value = "stable" +values = ["beta", "rc", "stable"] + +[tool.bumpversion.parts.prerelease_num] +first_value = "0" + [[tool.bumpversion.files]] filename = "Cargo.toml" search = 'version = "{current_version}"' @@ -108,96 +115,8 @@ filename = "Cargo.toml" search = 'lance-bitpacking = {{ version = "={current_version}"' replace = 'lance-bitpacking = {{ version = "={new_version}"' -# Update all rust crate Cargo.toml files -[[tool.bumpversion.files]] -filename = "rust/lance/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-arrow/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-core/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-datafusion/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-datagen/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-encoding/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-file/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-index/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-io/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-linalg/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-namespace/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-namespace-impls/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-table/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/compression/bitpacking/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/compression/fsst/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-test-macros/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/lance-testing/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "rust/examples/Cargo.toml" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' +# Note: Individual rust crate Cargo.toml files use workspace = true, +# so we don't need to update them individually # Python Cargo.toml [[tool.bumpversion.files]] diff --git a/.github/actions/setup-release-env/action.yml b/.github/actions/setup-release-env/action.yml new file mode 100644 index 00000000000..a7b08d70e23 --- /dev/null +++ b/.github/actions/setup-release-env/action.yml @@ -0,0 +1,26 @@ +name: 'Setup Release Environment' +description: 'Sets up Python, Rust, and dependencies for release workflows (assumes repo is already checked out)' +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + shell: bash + run: | + pip install bump-my-version packaging PyGithub + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Configure git identity + shell: bash + run: | + git config user.name 'Lance Release Bot' + git config user.email 'lance-dev@lancedb.com' diff --git a/.github/workflows/approve-rc.yml b/.github/workflows/approve-rc.yml new file mode 100644 index 00000000000..7e1a6627526 --- /dev/null +++ b/.github/workflows/approve-rc.yml @@ -0,0 +1,113 @@ +name: Approve RC + +on: + workflow_dispatch: + inputs: + rc_tag: + description: 'RC tag to approve (e.g., v1.3.0-rc.2 or v1.3.1-rc.1)' + required: true + type: string + dry_run: + description: 'Dry run (simulate without pushing)' + required: true + default: false + type: boolean + +jobs: + approve-rc: + runs-on: ubuntu-latest + outputs: + stable_version: ${{ steps.approve.outputs.STABLE_VERSION }} + stable_tag: ${{ steps.approve.outputs.STABLE_TAG }} + release_branch: ${{ steps.approve.outputs.RELEASE_BRANCH }} + is_major_minor: ${{ steps.approve.outputs.IS_MAJOR_MINOR }} + previous_tag: ${{ steps.approve.outputs.PREVIOUS_TAG }} + steps: + - name: Output Inputs + run: echo "${{ toJSON(github.event.inputs) }}" + + - name: Check out repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.LANCE_RELEASE_TOKEN }} + fetch-depth: 0 + lfs: true + + - name: Setup release environment + uses: ./.github/actions/setup-release-env + + - name: Approve RC + id: approve + run: | + bash ci/approve_rc.sh "${{ inputs.rc_tag }}" + + - name: Push changes (if not dry run) + if: ${{ !inputs.dry_run }} + run: | + git push origin "${{ steps.approve.outputs.RELEASE_BRANCH }}" + git push origin "${{ steps.approve.outputs.STABLE_TAG }}" + + - name: Generate Release Notes (if not dry run) + if: ${{ !inputs.dry_run }} + id: release_notes + env: + GH_TOKEN: ${{ secrets.LANCE_RELEASE_TOKEN }} + run: | + PREVIOUS_TAG="${{ steps.approve.outputs.PREVIOUS_TAG }}" + STABLE_TAG="${{ steps.approve.outputs.STABLE_TAG }}" + + if [ -n "${PREVIOUS_TAG}" ]; then + echo "Generating release notes from ${PREVIOUS_TAG} to ${STABLE_TAG}" + NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \ + -f tag_name="${STABLE_TAG}" \ + -f previous_tag_name="${PREVIOUS_TAG}" \ + --jq .body) + else + echo "No previous tag found, using automatic generation" + NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \ + -f tag_name="${STABLE_TAG}" \ + --jq .body) + fi + + # Save to output + echo "notes<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Release (if not dry run) + if: ${{ !inputs.dry_run }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.approve.outputs.STABLE_TAG }} + name: ${{ steps.approve.outputs.STABLE_TAG }} + draft: false + prerelease: false + body: ${{ steps.release_notes.outputs.notes }} + token: ${{ secrets.LANCE_RELEASE_TOKEN }} + + - name: Summary + run: | + echo "## Stable Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **RC Tag:** ${{ inputs.rc_tag }}" >> $GITHUB_STEP_SUMMARY + echo "- **Stable Version:** ${{ steps.approve.outputs.STABLE_VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "- **Stable Tag:** ${{ steps.approve.outputs.STABLE_TAG }}" >> $GITHUB_STEP_SUMMARY + echo "- **Release Branch:** ${{ steps.approve.outputs.RELEASE_BRANCH }}" >> $GITHUB_STEP_SUMMARY + echo "- **Next Version:** ${{ steps.approve.outputs.NEXT_BETA_VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "- **Release Type:** $( [ "${{ steps.approve.outputs.IS_MAJOR_MINOR }}" == "true" ] && echo "Major/Minor" || echo "Patch" )" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ steps.approve.outputs.PREVIOUS_TAG }}" ]; then + echo "- **Release Notes From:** ${{ steps.approve.outputs.PREVIOUS_TAG }}" >> $GITHUB_STEP_SUMMARY + fi + 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 "✅ Stable release ${{ steps.approve.outputs.STABLE_TAG }} complete!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Publishing:** Stable artifacts will be published to PyPI, crates.io, Maven Central" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Auto-bumped:** Release branch bumped to ${{ steps.approve.outputs.NEXT_BETA_VERSION }}" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/bump-version/action.yml b/.github/workflows/bump-version/action.yml deleted file mode 100644 index e71a7db79d3..00000000000 --- a/.github/workflows/bump-version/action.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Bump version using bump-my-version -description: "Automated version bumping using bump-my-version tool" -inputs: - bump_type: - description: "Type of version bump (major, minor, patch)" - required: true - default: "patch" - dry_run: - description: "Perform a dry run without making changes" - required: false - default: "false" -runs: - using: "composite" - steps: - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - shell: bash - run: | - pip install bump-my-version - - - name: Set git configs - shell: bash - run: | - git config user.name 'Lance Release Bot' - git config user.email 'lance-dev@lancedb.com' - - - name: Run version bump - shell: bash - run: | - if [ "${{ inputs.dry_run }}" == "true" ]; then - python ci/bump_version.py ${{ inputs.bump_type }} --dry-run - else - python ci/bump_version.py ${{ inputs.bump_type }} - fi - - - name: Show changes - shell: bash - run: | - echo "## Version changes:" - git diff --name-only - echo "" - echo "## Detailed changes:" - git diff --stat \ No newline at end of file diff --git a/.github/workflows/cargo-publish.yml b/.github/workflows/cargo-publish.yml index 347604fbf9b..876b43b1e55 100644 --- a/.github/workflows/cargo-publish.yml +++ b/.github/workflows/cargo-publish.yml @@ -35,11 +35,32 @@ jobs: working-directory: . steps: - uses: actions/checkout@v4 + - name: Check if stable release + id: check_version + run: | + # Get the tag from the event + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + TAG="${{ github.event.inputs.tag }}" + else + TAG="${{ github.ref_name }}" + fi + echo "Checking tag: $TAG" + + # Skip if tag contains -beta or -rc (not a stable release) + if [[ "$TAG" == *-beta.* ]] || [[ "$TAG" == *-rc.* ]]; then + echo "Skipping cargo publish for non-stable version: $TAG" + echo "Only stable versions (without -beta or -rc) are published to crates.io" + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "Stable version detected: $TAG" + echo "skip=false" >> $GITHUB_OUTPUT + fi - uses: Swatinem/rust-cache@v2 + if: steps.check_version.outputs.skip != 'true' with: workspaces: rust - name: Verify and checkout specified tag - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && steps.check_version.outputs.skip != 'true' run: | git fetch --all --tags if git rev-parse ${{ github.event.inputs.tag }} >/dev/null 2>&1; then @@ -52,6 +73,7 @@ jobs: exit 1 fi - name: Install dependencies + if: steps.check_version.outputs.skip != 'true' run: | sudo apt update sudo apt install -y protobuf-compiler libssl-dev @@ -59,6 +81,7 @@ jobs: # - uses: rust-lang/crates-io-auth-action@v1 # id: auth - uses: albertlockett/publish-crates@v2.2 + if: steps.check_version.outputs.skip != 'true' with: # registry-token: ${{ steps.auth.outputs.token }} registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/create-rc.yml b/.github/workflows/create-rc.yml new file mode 100644 index 00000000000..67e179436f8 --- /dev/null +++ b/.github/workflows/create-rc.yml @@ -0,0 +1,120 @@ +name: Create RC on Release Branch + +on: + workflow_dispatch: + inputs: + release_branch: + description: 'Release branch (e.g., release/v1.3)' + required: true + type: string + dry_run: + description: 'Dry run (simulate without pushing)' + required: true + default: false + type: boolean + +jobs: + create-rc: + runs-on: ubuntu-latest + outputs: + rc_version: ${{ steps.create.outputs.RC_VERSION }} + rc_tag: ${{ steps.create.outputs.RC_TAG }} + steps: + - name: Output Inputs + run: echo "${{ toJSON(github.event.inputs) }}" + + - name: Check out repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.LANCE_RELEASE_TOKEN }} + fetch-depth: 0 + lfs: true + + - name: Setup release environment + uses: ./.github/actions/setup-release-env + + - name: Create RC + id: create + run: | + bash ci/create_rc.sh "${{ inputs.release_branch }}" + + - name: Create GitHub Discussion for voting + if: ${{ !inputs.dry_run }} + id: create_discussion + env: + GH_TOKEN: ${{ secrets.LANCE_RELEASE_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + DISCUSSION_URL=$(bash ci/create_rc_discussion.sh \ + "${{ steps.create.outputs.RC_TAG }}" \ + "${{ steps.create.outputs.RC_VERSION }}" \ + "${{ inputs.release_branch }}" \ + "${{ steps.create.outputs.RELEASE_TYPE }}") + echo "DISCUSSION_URL=$DISCUSSION_URL" >> $GITHUB_OUTPUT + + - name: Push changes (if not dry run) + if: ${{ !inputs.dry_run }} + run: | + git push origin "${{ inputs.release_branch }}" + git push origin "${{ steps.create.outputs.RC_TAG }}" + + - name: Generate Release Notes (if not dry run) + if: ${{ !inputs.dry_run }} + id: rc_release_notes + env: + GH_TOKEN: ${{ secrets.LANCE_RELEASE_TOKEN }} + run: | + PREVIOUS_TAG="${{ steps.create.outputs.PREVIOUS_TAG }}" + RC_TAG="${{ steps.create.outputs.RC_TAG }}" + + if [ -n "${PREVIOUS_TAG}" ]; then + echo "Generating release notes from ${PREVIOUS_TAG} to ${RC_TAG}" + NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \ + -f tag_name="${RC_TAG}" \ + -f previous_tag_name="${PREVIOUS_TAG}" \ + --jq .body) + else + echo "No previous tag found, using automatic generation" + NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \ + -f tag_name="${RC_TAG}" \ + --jq .body) + fi + + # Save to output + echo "notes<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Pre-Release (if not dry run) + if: ${{ !inputs.dry_run }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.create.outputs.RC_TAG }} + name: ${{ steps.create.outputs.RC_TAG }} + draft: false + prerelease: true + body: ${{ steps.rc_release_notes.outputs.notes }} + token: ${{ secrets.LANCE_RELEASE_TOKEN }} + + - name: Summary + run: | + echo "## RC Creation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Release Branch:** ${{ inputs.release_branch }}" >> $GITHUB_STEP_SUMMARY + echo "- **RC Version:** ${{ steps.create.outputs.RC_VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "- **RC Tag:** ${{ steps.create.outputs.RC_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 "✅ RC ${{ steps.create.outputs.RC_TAG }} created!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Next steps:**" >> $GITHUB_STEP_SUMMARY + echo "1. Check the GitHub Discussion thread for voting" >> $GITHUB_STEP_SUMMARY + echo "2. Test RC artifacts" >> $GITHUB_STEP_SUMMARY + echo "3. Vote on the RC" >> $GITHUB_STEP_SUMMARY + echo "4. If approved, use approve-rc workflow" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/create-release-branch.yml b/.github/workflows/create-release-branch.yml new file mode 100644 index 00000000000..253a534435c --- /dev/null +++ b/.github/workflows/create-release-branch.yml @@ -0,0 +1,135 @@ +name: Create Release Branch + +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run (simulate without pushing)' + required: true + default: false + type: boolean + +jobs: + create-release-branch: + runs-on: ubuntu-latest + outputs: + rc_tag: ${{ steps.create_branch.outputs.RC_TAG }} + rc_version: ${{ steps.create_branch.outputs.RC_VERSION }} + release_branch: ${{ steps.create_branch.outputs.RELEASE_BRANCH }} + main_version: ${{ steps.create_branch.outputs.MAIN_VERSION }} + release_root_tag: ${{ steps.create_branch.outputs.RELEASE_ROOT_TAG }} + steps: + - name: Output Inputs + run: echo "${{ toJSON(github.event.inputs) }}" + + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: main + token: ${{ secrets.LANCE_RELEASE_TOKEN }} + fetch-depth: 0 + lfs: true + + - name: Setup release environment + uses: ./.github/actions/setup-release-env + + - name: Create release branch + id: create_branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + bash ci/create_release_branch.sh + + - name: Push changes (if not dry run) + if: ${{ !inputs.dry_run }} + run: | + git push origin "${{ steps.create_branch.outputs.RELEASE_BRANCH }}" + git push origin main + git push origin "${{ steps.create_branch.outputs.RC_TAG }}" + # Push release root tag (may already exist remotely if created during beta publish) + git push origin "${{ steps.create_branch.outputs.RELEASE_ROOT_TAG }}" || echo "Release root tag already exists remotely" + + - name: Generate Release Notes (if not dry run) + if: ${{ !inputs.dry_run }} + id: rc_release_notes + env: + GH_TOKEN: ${{ secrets.LANCE_RELEASE_TOKEN }} + run: | + PREVIOUS_TAG="${{ steps.create_branch.outputs.PREVIOUS_TAG }}" + RC_TAG="${{ steps.create_branch.outputs.RC_TAG }}" + + if [ -n "${PREVIOUS_TAG}" ]; then + echo "Generating release notes from ${PREVIOUS_TAG} to ${RC_TAG}" + NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \ + -f tag_name="${RC_TAG}" \ + -f previous_tag_name="${PREVIOUS_TAG}" \ + --jq .body) + else + echo "No previous tag found, using automatic generation" + NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \ + -f tag_name="${RC_TAG}" \ + --jq .body) + fi + + # Save to output + echo "notes<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Pre-Release (if not dry run) + if: ${{ !inputs.dry_run }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.create_branch.outputs.RC_TAG }} + name: ${{ steps.create_branch.outputs.RC_TAG }} + draft: false + prerelease: true + body: ${{ steps.rc_release_notes.outputs.notes }} + token: ${{ secrets.LANCE_RELEASE_TOKEN }} + + - name: Create GitHub Discussion for RC Vote (if not dry run) + if: ${{ !inputs.dry_run }} + id: create_discussion + env: + GH_TOKEN: ${{ secrets.LANCE_RELEASE_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + DISCUSSION_URL=$(bash ci/create_rc_discussion.sh \ + "${{ steps.create_branch.outputs.RC_TAG }}" \ + "${{ steps.create_branch.outputs.RC_VERSION }}" \ + "${{ steps.create_branch.outputs.RELEASE_BRANCH }}" \ + "${{ steps.create_branch.outputs.RELEASE_TYPE }}") + echo "DISCUSSION_URL=$DISCUSSION_URL" >> $GITHUB_OUTPUT + + - name: Summary + run: | + echo "## Release Branch Creation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Target Version:** ${{ steps.create_branch.outputs.RC_VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "- **RC Tag:** ${{ steps.create_branch.outputs.RC_TAG }}" >> $GITHUB_STEP_SUMMARY + echo "- **Release Branch:** ${{ steps.create_branch.outputs.RELEASE_BRANCH }}" >> $GITHUB_STEP_SUMMARY + echo "- **Release Root Tag:** ${{ steps.create_branch.outputs.RELEASE_ROOT_TAG }}" >> $GITHUB_STEP_SUMMARY + echo "- **Main Version:** ${{ steps.create_branch.outputs.MAIN_VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "- **Source Branch:** main (HEAD)" >> $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 "✅ Release candidate ${{ steps.create_branch.outputs.RC_TAG }} created!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Voting Discussion**: ${{ steps.create_discussion.outputs.DISCUSSION_URL }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**What happened:**" >> $GITHUB_STEP_SUMMARY + echo "1. Created release branch ${{ steps.create_branch.outputs.RELEASE_BRANCH }} at ${{ steps.create_branch.outputs.RC_TAG }}" >> $GITHUB_STEP_SUMMARY + echo "2. Bumped main to ${{ steps.create_branch.outputs.MAIN_VERSION }} (unreleased)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Next steps:**" >> $GITHUB_STEP_SUMMARY + echo "1. Review and vote in the discussion thread" >> $GITHUB_STEP_SUMMARY + echo "2. Test the RC artifacts" >> $GITHUB_STEP_SUMMARY + echo "3. If issues found, fix on release branch and use create-rc workflow" >> $GITHUB_STEP_SUMMARY + echo "4. If approved, use approve-rc workflow" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/make-release-commit.yml b/.github/workflows/make-release-commit.yml deleted file mode 100644 index bd2f73f4963..00000000000 --- a/.github/workflows/make-release-commit.yml +++ /dev/null @@ -1,210 +0,0 @@ -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: false - type: boolean - draft_release: - description: 'Create a draft release on GitHub' - required: true - default: false - type: boolean - -jobs: - validate-and-release: - runs-on: ubuntu-latest - steps: - - name: Output Inputs - run: echo "${{ toJSON(github.event.inputs) }}" - - - name: Check out main - uses: actions/checkout@v4 - with: - ref: main - token: ${{ secrets.LANCE_RELEASE_TOKEN }} - fetch-depth: 0 - lfs: true - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - - name: Install dependencies - run: | - pip install bump-my-version packaging PyGithub - - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Validate release type against breaking changes - env: - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_SHA: ${{ github.sha }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - python ci/check_breaking_changes.py --release-type ${{ inputs.release_type }} - - - name: Get current version - id: current_version - run: | - CURRENT_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - echo "Current version: $CURRENT_VERSION" - - - name: Calculate new version - id: new_version - run: | - CURRENT="${{ steps.current_version.outputs.version }}" - TYPE="${{ inputs.release_type }}" - CHANNEL="${{ inputs.release_channel }}" - - # Strip any prerelease suffix to get base version - BASE_VERSION=$(echo "$CURRENT" | sed 's/-.*$//') - IFS='.' read -r major minor patch <<< "$BASE_VERSION" - - # Determine if we need to bump the base version - if [[ "$CHANNEL" == "stable" && "$CURRENT" =~ -beta\. ]]; then - # Stable release from beta: use base version without bumping - NEW_VERSION="$BASE_VERSION" - elif [[ "$CHANNEL" == "preview" && "$CURRENT" =~ -beta\. ]]; then - # Preview from preview: keep the same base version, only beta number changes - NEW_VERSION="$BASE_VERSION" - else - # All other cases: bump according to type - case "$TYPE" in - major) - NEW_VERSION="$((major + 1)).0.0" - ;; - minor) - NEW_VERSION="${major}.$((minor + 1)).0" - ;; - patch) - NEW_VERSION="${major}.${minor}.$((patch + 1))" - ;; - esac - fi - - echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "New version will be: $NEW_VERSION" - - - name: Determine tag name and prerelease suffix - id: tag_name - run: | - if [ "${{ inputs.release_channel }}" == "stable" ]; then - VERSION="${{ steps.new_version.outputs.version }}" - TAG="v${VERSION}" - PRERELEASE="" - else - # For preview releases, base the beta tag on the next release version - VERSION="${{ steps.new_version.outputs.version }}" - # Find the next beta number for upcoming 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}" - PRERELEASE="beta.${BETA_NUM}" - fi - - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT - echo "Tag will be: $TAG" - - - name: Update version - run: | - if [ "${{ inputs.release_channel }}" == "stable" ]; then - python ci/bump_version.py --new-version "${{ steps.new_version.outputs.version }}" - else - python ci/bump_version.py --new-version "${{ steps.new_version.outputs.version }}-${{ steps.tag_name.outputs.prerelease }}" - fi - - - name: Configure git identity - run: | - git config user.name 'Lance Release Bot' - git config user.email 'lance-dev@lancedb.com' - - - name: Create release commit - run: | - git add -A - if [ "${{ inputs.release_channel }}" == "stable" ]; then - git commit -m "chore: release version ${{ steps.new_version.outputs.version }}" - else - git commit -m "chore: release version ${{ steps.tag_name.outputs.tag }}" - fi - - - 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: | - # Push the commit to main - git push origin main - # Push the tag - git push origin "${{ steps.tag_name.outputs.tag }}" - - - name: Create GitHub Release (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 }} - draft: ${{ inputs.draft_release }} - prerelease: ${{ inputs.release_channel == 'preview' }} - generate_release_notes: true - token: ${{ secrets.LANCE_RELEASE_TOKEN }} - - - name: Next steps - if: ${{ !inputs.dry_run }} - run: | - if [ "${{ inputs.release_channel }}" == "stable" ]; then - echo "Stable release complete. Version bumped to ${{ steps.new_version.outputs.version }}" - else - echo "Preview release complete. Version bumped to ${{ steps.new_version.outputs.version }}-${{ steps.tag_name.outputs.prerelease }}" - fi - - - 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 - echo "- **New Version:** ${{ steps.new_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY - 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 - fi diff --git a/.github/workflows/publish-beta.yml b/.github/workflows/publish-beta.yml new file mode 100644 index 00000000000..8824a8e17d6 --- /dev/null +++ b/.github/workflows/publish-beta.yml @@ -0,0 +1,120 @@ +name: Publish Beta Preview Release + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch to publish beta from (e.g., main or release/v1.3)' + required: true + default: 'main' + type: string + dry_run: + description: 'Dry run (simulate without pushing)' + required: true + default: false + type: boolean + +jobs: + publish-beta: + runs-on: ubuntu-latest + outputs: + beta_version: ${{ steps.publish.outputs.BETA_VERSION }} + beta_tag: ${{ steps.publish.outputs.BETA_TAG }} + release_root_tag: ${{ steps.publish.outputs.RELEASE_ROOT_TAG }} + release_notes_from: ${{ steps.publish.outputs.RELEASE_NOTES_FROM }} + steps: + - name: Output Inputs + run: echo "${{ toJSON(github.event.inputs) }}" + + - name: Check out repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.LANCE_RELEASE_TOKEN }} + fetch-depth: 0 + lfs: true + + - name: Setup release environment + uses: ./.github/actions/setup-release-env + + - name: Publish beta release + id: publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + bash ci/publish_beta.sh "${{ inputs.branch }}" + + - name: Push changes (if not dry run) + if: ${{ !inputs.dry_run }} + run: | + git push origin "${{ inputs.branch }}" + git push origin "${{ steps.publish.outputs.BETA_TAG }}" + # Push release root tag if it was created (only when breaking changes bump major version) + if [ -n "${{ steps.publish.outputs.RELEASE_ROOT_TAG }}" ]; then + git push origin "${{ steps.publish.outputs.RELEASE_ROOT_TAG }}" + fi + + - name: Generate Release Notes (if not dry run) + if: ${{ !inputs.dry_run }} + id: beta_release_notes + env: + GH_TOKEN: ${{ secrets.LANCE_RELEASE_TOKEN }} + run: | + RELEASE_NOTES_FROM="${{ steps.publish.outputs.RELEASE_NOTES_FROM }}" + BETA_TAG="${{ steps.publish.outputs.BETA_TAG }}" + + if [ -n "${RELEASE_NOTES_FROM}" ]; then + echo "Generating release notes from ${RELEASE_NOTES_FROM} to ${BETA_TAG}" + NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \ + -f tag_name="${BETA_TAG}" \ + -f previous_tag_name="${RELEASE_NOTES_FROM}" \ + --jq .body) + else + echo "No release-root tag found, using automatic generation" + NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \ + -f tag_name="${BETA_TAG}" \ + --jq .body) + fi + + # Save to output + echo "notes<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Pre-Release (if not dry run) + if: ${{ !inputs.dry_run }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.publish.outputs.BETA_TAG }} + name: ${{ steps.publish.outputs.BETA_TAG }} + draft: false + prerelease: true + body: ${{ steps.beta_release_notes.outputs.notes }} + token: ${{ secrets.LANCE_RELEASE_TOKEN }} + + - name: Summary + run: | + echo "## Beta Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** ${{ inputs.branch }}" >> $GITHUB_STEP_SUMMARY + echo "- **Beta Version:** ${{ steps.publish.outputs.BETA_VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "- **Beta Tag:** ${{ steps.publish.outputs.BETA_TAG }}" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ steps.publish.outputs.RELEASE_ROOT_TAG }}" ]; then + echo "- **Release Root Tag:** ${{ steps.publish.outputs.RELEASE_ROOT_TAG }} (breaking changes detected)" >> $GITHUB_STEP_SUMMARY + fi + 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 "✅ Beta release ${{ steps.publish.outputs.BETA_TAG }} published!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Publishing:** Beta artifacts will be published to fury.io" >> $GITHUB_STEP_SUMMARY + echo "**GitHub Pre-Release:** Created with release notes" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ steps.publish.outputs.RELEASE_ROOT_TAG }}" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**⚠️ Breaking Changes:** Major version bumped due to breaking changes. New release root tag created." >> $GITHUB_STEP_SUMMARY + fi + fi diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 17db39cd6ad..8b6510a5c80 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -61,9 +61,9 @@ jobs: - name: Handle tag id: handle_tag run: | - # If the tag ends with -beta.N, we need to call setup_version.py + # If the tag ends with -beta.N or -rc.N, we need to call setup_version.py # and export repo as "fury" instead of "pypi" - if [[ ${{ github.ref }} == refs/tags/*-beta.* ]]; then + if [[ ${{ github.ref }} == refs/tags/*-beta.* ]] || [[ ${{ github.ref }} == refs/tags/*-rc.* ]]; then TAG=$(echo ${{ github.ref }} | sed 's/refs\/tags\///') pip install packaging python ci/setup_version.py $TAG @@ -116,9 +116,9 @@ jobs: - name: Handle tag id: handle_tag run: | - # If the tag ends with -beta.N, we need to call setup_version.py + # If the tag ends with -beta.N or -rc.N, we need to call setup_version.py # and export repo as "fury" instead of "pypi" - if [[ ${{ github.ref }} == refs/tags/*-beta.* ]]; then + if [[ ${{ github.ref }} == refs/tags/*-beta.* ]] || [[ ${{ github.ref }} == refs/tags/*-rc.* ]]; then TAG=$(echo ${{ github.ref }} | sed 's/refs\/tags\///') pip install packaging python ci/setup_version.py $TAG diff --git a/ci/approve_rc.sh b/ci/approve_rc.sh new file mode 100644 index 00000000000..141e0da49a8 --- /dev/null +++ b/ci/approve_rc.sh @@ -0,0 +1,85 @@ +#!/bin/bash +set -e + +# Script to approve RC and promote to stable release +# Works for both major/minor and patch releases +# Usage: approve_rc.sh +# Example: approve_rc.sh v1.3.0-rc.2 + +RC_TAG=${1:?"Error: RC tag required (e.g., v1.3.0-rc.2)"} +TAG_PREFIX=${2:-"v"} + +readonly SELF_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +# Source common release functions +source "${SELF_DIR}/release_common.sh" + +echo "Promoting RC tag ${RC_TAG} to stable release" + +# Parse version from RC tag (v1.3.0-rc.2 → 1.3.0) +RC_VERSION=$(echo "${RC_TAG}" | sed "s/^${TAG_PREFIX}//") +STABLE_VERSION=$(echo "${RC_VERSION}" | sed 's/-rc\.[0-9]*$//') + +echo "Stable version will be: ${STABLE_VERSION}" + +# Parse major.minor.patch +read MAJOR MINOR PATCH <<< $(parse_version_components "${STABLE_VERSION}") +RELEASE_BRANCH="release/v${MAJOR}.${MINOR}" + +echo "Release branch: ${RELEASE_BRANCH}" + +# Checkout release branch +git checkout "${RELEASE_BRANCH}" + +# Verify we're at the correct RC version +CURRENT_VERSION=$(get_version_from_cargo) +if [ "${CURRENT_VERSION}" != "${RC_VERSION}" ]; then + echo "ERROR: Branch is at ${CURRENT_VERSION}, expected ${RC_VERSION}" + echo "Make sure the RC tag matches the branch state" + exit 1 +fi + +# Bump from RC to stable +echo "Bumping version from ${RC_VERSION} to ${STABLE_VERSION}" +bump_and_commit_version "${STABLE_VERSION}" "chore: release version ${STABLE_VERSION} + +Promoted from ${RC_TAG}" + +# Create stable tag +STABLE_TAG="${TAG_PREFIX}${STABLE_VERSION}" +echo "Creating stable tag: ${STABLE_TAG}" +git tag -a "${STABLE_TAG}" -m "Release version ${STABLE_VERSION}" + +# Determine if this is a major/minor release or patch release +if [ "${PATCH}" = "0" ]; then + echo "This is a major/minor release (${STABLE_VERSION})" + IS_MAJOR_MINOR="true" +else + echo "This is a patch release (${STABLE_VERSION})" + IS_MAJOR_MINOR="false" +fi + +# Determine previous tag for release notes +PREVIOUS_TAG=$(determine_previous_tag "${MAJOR}" "${MINOR}" "${PATCH}" "${TAG_PREFIX}") +if [ -n "${PREVIOUS_TAG}" ]; then + echo "Release notes will compare against: ${PREVIOUS_TAG}" +else + echo "Warning: Previous tag not found" +fi + +# Always auto-bump to next patch beta.0 after stable release +NEXT_PATCH=$((PATCH + 1)) +NEXT_BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.0" + +echo "Bumping to ${NEXT_BETA_VERSION} for next patch development" +bump_and_commit_version "${NEXT_BETA_VERSION}" "chore: bump to ${NEXT_BETA_VERSION} for next patch development" + +echo "Successfully promoted to stable release: ${STABLE_TAG}" +echo "Release branch bumped to ${NEXT_BETA_VERSION}" + +echo "STABLE_TAG=${STABLE_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "STABLE_VERSION=${STABLE_VERSION}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RELEASE_BRANCH=${RELEASE_BRANCH}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "IS_MAJOR_MINOR=${IS_MAJOR_MINOR}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "NEXT_BETA_VERSION=${NEXT_BETA_VERSION}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true diff --git a/ci/bump_version.py b/ci/bump_version.py deleted file mode 100644 index a66147d9c04..00000000000 --- a/ci/bump_version.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -""" -Version management script for Lance project. -Handles version bumping across all project components. -""" - -import argparse -import subprocess -import sys -import json -import re -from pathlib import Path -from typing import Tuple, Optional - - -def run_command(cmd: list[str], capture_output: bool = True, cwd: Optional[Path] = None) -> subprocess.CompletedProcess: - """Run a command and return the result.""" - print(f"Running: {' '.join(cmd)}") - result = subprocess.run(cmd, capture_output=capture_output, text=True, cwd=cwd) - if result.returncode != 0: - print(f"Error running command: {' '.join(cmd)}") - if capture_output: - print(f"stderr: {result.stderr}") - sys.exit(result.returncode) - return result - - -def get_current_version() -> str: - """Get the current version from Cargo.toml.""" - cargo_toml = Path("Cargo.toml") - with open(cargo_toml, "r") as f: - for line in f: - if line.strip().startswith('version = "'): - return line.split('"')[1] - raise ValueError("Could not find version in Cargo.toml") - - -def parse_version(version: str) -> Tuple[int, int, int, Optional[str]]: - """Parse a version string into major, minor, patch, and optional prerelease components.""" - match = re.match(r"(\d+)\.(\d+)\.(\d+)(?:-(.+))?", version) - if not match: - raise ValueError(f"Invalid version format: {version}") - return int(match.group(1)), int(match.group(2)), int(match.group(3)), match.group(4) - - -def bump_version(current: str, bump_type: str, prerelease: Optional[str] = None) -> str: - """Calculate the new version based on bump type. - - Args: - current: Current version string - bump_type: Type of bump (major, minor, patch) - prerelease: Optional prerelease suffix (e.g., "beta.1") - - Returns: - New version string - """ - major, minor, patch, _ = parse_version(current) - - if bump_type == "major": - new_version = f"{major + 1}.0.0" - elif bump_type == "minor": - new_version = f"{major}.{minor + 1}.0" - elif bump_type == "patch": - new_version = f"{major}.{minor}.{patch + 1}" - else: - raise ValueError(f"Invalid bump type: {bump_type}") - - if prerelease: - new_version = f"{new_version}-{prerelease}" - - return new_version - - -def update_cargo_lock_files(): - """Update all Cargo.lock files after version change.""" - lock_files = [ - "Cargo.lock", - "python/Cargo.lock", - "java/lance-jni/Cargo.lock", - ] - - for lock_file in lock_files: - if Path(lock_file).exists(): - directory = Path(lock_file).parent - print(f"Updating {lock_file}...") - run_command(["cargo", "update", "-p", "lance"], cwd=directory if directory != Path(".") else None) - - -def validate_version_consistency(): - """Validate that all versions are consistent across the project.""" - version = get_current_version() - errors = [] - - # Check all creates with explicit versioning - rust_crates = [ - "python/Cargo.toml", - "java/lance-jni/Cargo.toml", - ] - - for crate_path in rust_crates: - if Path(crate_path).exists(): - with open(crate_path, "r") as f: - content = f.read() - if f'version = "{version}"' not in content: - errors.append(f"{crate_path} has inconsistent version") - - if errors: - print("Version consistency check failed:") - for error in errors: - print(f" - {error}") - return False - - print(f"All components are at version {version}") - return True - - -def main(): - parser = argparse.ArgumentParser(description="Bump Lance project version") - parser.add_argument( - "bump_type", - choices=["major", "minor", "patch"], - nargs='?', - help="Type of version bump to perform (not needed with --new-version)" - ) - parser.add_argument( - "--new-version", - type=str, - default=None, - help="Set exact new version (e.g., '0.38.3' or '0.38.3-beta.1')" - ) - parser.add_argument( - "--prerelease", - type=str, - default=None, - help="Prerelease suffix (e.g., 'beta.1')" - ) - parser.add_argument( - "--dry-run", - action="store_true", - help="Show what would be done without making changes" - ) - parser.add_argument( - "--no-validate", - action="store_true", - help="Skip version consistency validation" - ) - - args = parser.parse_args() - - # Get current version - current_version = get_current_version() - - if args.new_version: - # Use exact version provided - new_version = args.new_version - else: - # Calculate new version from bump type - if not args.bump_type: - parser.error("Either bump_type or --new-version must be provided") - new_version = bump_version(current_version, args.bump_type, args.prerelease) - - print(f"Current version: {current_version}") - print(f"New version: {new_version}") - - if args.dry_run: - print("Dry run - no changes made") - return - - # Use bump-my-version to update all files - print("\nUpdating version in all files...") - run_command(["bump-my-version", "bump", "--current-version", current_version, "--new-version", new_version, "--ignore-missing-version", "--ignore-missing-files"]) - - # Update Cargo.lock files - print("\nUpdating Cargo.lock files...") - update_cargo_lock_files() - - # Validate consistency - if not args.no_validate: - print("\nValidating version consistency...") - if not validate_version_consistency(): - print("Version update may have failed. Please check manually.") - sys.exit(1) - - print(f"\nSuccessfully bumped version from {current_version} to {new_version}") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/ci/check_breaking_changes.py b/ci/check_breaking_changes.py index 4bb78c60bb5..aa83d1ae7ee 100644 --- a/ci/check_breaking_changes.py +++ b/ci/check_breaking_changes.py @@ -1,112 +1,70 @@ -#!/usr/bin/env python3 """ -Check for breaking changes by examining GitHub PR labels. +Check whether there are any breaking changes in the PRs between the base and head commits. +If there are, assert that we have incremented the minor version. -This script is used during the release process to ensure we don't accidentally -release breaking changes as a patch version. +Can also be used as a library to detect breaking changes without version validation. """ - import argparse -import sys import os +import sys +from packaging.version import parse + from github import Github -def check_github_pr_labels() -> bool: - """Check for breaking-change labels in PRs between last release and current commit.""" - # Require GitHub environment variables - if not os.environ.get("GITHUB_REPOSITORY"): - print("Error: GITHUB_REPOSITORY environment variable not set") - sys.exit(1) - - try: - # Initialize GitHub client - token = os.environ.get("GITHUB_TOKEN") - g = Github(token) if token else Github() - repo = g.get_repo(os.environ["GITHUB_REPOSITORY"]) - - # Get the latest release - try: - latest_release = repo.get_latest_release() - last_tag = latest_release.tag_name - except: - print("No previous releases found, skipping breaking change check") - return False - - print(f"Checking for breaking changes since {last_tag}") - - # Get commits between last release and current SHA - sha = os.environ.get("GITHUB_SHA", "HEAD") - comparison = repo.compare(last_tag, sha) - - # Check all PRs for breaking-change label - breaking_prs = [] - checked_prs = set() - - for commit in comparison.commits: - # Get PRs associated with this commit - prs = list(commit.get_pulls()) - - for pr in prs: - # Skip if we've already checked this PR - if pr.number in checked_prs: - continue - checked_prs.add(pr.number) - - # Check for breaking-change label - pr_labels = [label.name for label in pr.labels] - if "breaking-change" in pr_labels: - breaking_prs.append(pr) - print(f" Found breaking change in PR #{pr.number}: {pr.title}") - print(f" {pr.html_url}") - - if breaking_prs: +def detect_breaking_changes(repo, base, head): + """ + Detect if there are any breaking changes between base and head commits. + + Args: + repo: GitHub repository object + base: Base commit/tag + head: Head commit/tag + + Returns: + bool: True if breaking changes found, False otherwise + """ + commits = repo.compare(base, head).commits + prs = (pr for commit in commits for pr in commit.get_pulls()) + + for pr in prs: + if any(label.name == "breaking-change" for label in pr.labels): + print(f"Breaking change in PR: {pr.html_url}") return True - else: - print(" No breaking changes found in PR labels") - return False - - except Exception as e: - print(f"Error checking GitHub PR labels: {e}") - # If we can't check, assume no breaking changes to avoid blocking releases - print("Warning: Could not verify breaking changes, proceeding anyway") - return False - - -def main(): - """Main function to check for breaking changes.""" - parser = argparse.ArgumentParser( - description="Check for breaking changes and validate release type" - ) - parser.add_argument( - "--release-type", - choices=["patch", "minor", "major"], - required=True, - help="Type of release being performed" - ) + + print("No breaking changes found.") + return False + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("base", help="Base commit/tag for comparison") + parser.add_argument("head", help="Head commit/tag for comparison") + parser.add_argument("last_stable_version", nargs="?", help="Last stable version (for validation)") + parser.add_argument("current_version", nargs="?", help="Current version (for validation)") + parser.add_argument("--detect-only", action="store_true", + help="Only detect breaking changes, don't validate version") args = parser.parse_args() - - print(f"Checking for breaking changes (Release type: {args.release_type})...") - print("-" * 50) - - has_breaking_changes = check_github_pr_labels() - - print("-" * 50) - - if has_breaking_changes: - if args.release_type == "patch": - print("✗ Breaking changes detected but patch release requested!") - print("Please use 'minor' or 'major' version bump for the release.") - sys.exit(1) - else: - print(f"⚠️ Breaking changes detected, proceeding with {args.release_type} release") - print("This is allowed since you're using a minor or major version bump.") - sys.exit(0) - else: - print("✓ No breaking changes detected") - print(f"Proceeding with {args.release_type} release") + + repo = Github(os.environ["GITHUB_TOKEN"]).get_repo(os.environ["GITHUB_REPOSITORY"]) + + has_breaking_changes = detect_breaking_changes(repo, args.base, args.head) + + if args.detect_only: + # Exit with 1 if breaking changes found, 0 if not + sys.exit(1 if has_breaking_changes else 0) + + # Original behavior: validate version bump if breaking changes found + if not has_breaking_changes: sys.exit(0) + # Breaking changes found, validate version was bumped appropriately + if not args.last_stable_version or not args.current_version: + print("Error: last_stable_version and current_version required for validation") + sys.exit(1) -if __name__ == "__main__": - main() \ No newline at end of file + last_stable_version = parse(args.last_stable_version) + current_version = parse(args.current_version) + if current_version.minor <= last_stable_version.minor: + print("Minor version is not greater than the last stable version.") + sys.exit(1) diff --git a/ci/create_rc.sh b/ci/create_rc.sh new file mode 100644 index 00000000000..6dcc53ae3cf --- /dev/null +++ b/ci/create_rc.sh @@ -0,0 +1,85 @@ +#!/bin/bash +set -e + +# Script to create RC on an existing release branch +# Works for patch rc.1 and iteration rc.2, rc.3, etc. +# Usage: create_rc.sh +# Example: create_rc.sh release/v1.3 + +RELEASE_BRANCH=${1:?"Error: release branch required (e.g., release/v1.3)"} +TAG_PREFIX=${2:-"v"} + +readonly SELF_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +# Source common release functions +source "${SELF_DIR}/release_common.sh" + +echo "Creating RC on release branch: ${RELEASE_BRANCH}" + +# Checkout release branch +git checkout "${RELEASE_BRANCH}" + +# Read current version from Cargo.toml +CURRENT_VERSION=$(get_version_from_cargo) +echo "Current version on branch: ${CURRENT_VERSION}" + +# Validate version format - should be beta.N or rc.N +if [[ "${CURRENT_VERSION}" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-beta\.([0-9]+)$ ]]; then + # At beta version, determine next RC number + BASE_VERSION="${BASH_REMATCH[1]}" + + # Find highest RC tag for this base version + HIGHEST_RC=$(git tag -l "${TAG_PREFIX}${BASE_VERSION}-rc.*" | sed "s/^${TAG_PREFIX}${BASE_VERSION}-rc\.//" | sort -n | tail -n1) + + if [ -z "${HIGHEST_RC}" ]; then + # No RC exists yet, start with rc.1 + RC_NUMBER=1 + else + # Increment the highest RC + RC_NUMBER=$((HIGHEST_RC + 1)) + fi + + RC_VERSION="${BASE_VERSION}-rc.${RC_NUMBER}" + +elif [[ "${CURRENT_VERSION}" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-rc\.([0-9]+)$ ]]; then + # At rc.N version, increment RC number + BASE_VERSION="${BASH_REMATCH[1]}" + CURRENT_RC="${BASH_REMATCH[2]}" + RC_NUMBER=$((CURRENT_RC + 1)) + RC_VERSION="${BASE_VERSION}-rc.${RC_NUMBER}" +elif [[ "${CURRENT_VERSION}" =~ ^([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + # At stable version - this shouldn't happen as approve-rc workflow auto-bumps to beta.0 + echo "ERROR: Release branch is at stable version ${CURRENT_VERSION}" + echo "Expected format: X.Y.Z-beta.N or X.Y.Z-rc.N" + echo "The release branch should have been auto-bumped to beta.0 after RC approval" + exit 1 +else + echo "ERROR: Unexpected version format: ${CURRENT_VERSION}" + echo "Expected format: X.Y.Z-beta.N or X.Y.Z-rc.N" + exit 1 +fi + +echo "Creating RC version: ${RC_VERSION}" +bump_and_commit_version "${RC_VERSION}" "chore: release candidate ${RC_VERSION}" + +# Create the RC tag +RC_TAG="${TAG_PREFIX}${RC_VERSION}" +echo "Creating tag ${RC_TAG}" +git tag -a "${RC_TAG}" -m "Release candidate ${RC_VERSION}" + +# Determine comparison base for release notes +read MAJOR MINOR PATCH <<< $(parse_version_components "${BASE_VERSION}") + +# Determine previous tag for release notes +PREVIOUS_TAG=$(determine_previous_tag "${MAJOR}" "${MINOR}" "${PATCH}" "${TAG_PREFIX}") +if [ -n "${PREVIOUS_TAG}" ]; then + echo "Release notes will compare against: ${PREVIOUS_TAG}" +else + echo "Warning: Previous tag not found" +fi + +echo "Successfully created RC tag: ${RC_TAG}" +echo "RC_TAG=${RC_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RC_VERSION=${RC_VERSION}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RELEASE_TYPE=patch" >> $GITHUB_OUTPUT 2>/dev/null || true diff --git a/ci/create_rc_discussion.sh b/ci/create_rc_discussion.sh new file mode 100755 index 00000000000..0343c5b380e --- /dev/null +++ b/ci/create_rc_discussion.sh @@ -0,0 +1,146 @@ +#!/bin/bash +set -e + +# Script to create a GitHub Discussion for RC voting +# Usage: create_rc_discussion.sh [release_branch] [release_type] +# Environment variables required: GH_TOKEN, GITHUB_REPOSITORY + +RC_TAG=${1} +RC_VERSION=${2} +RELEASE_BRANCH=${3:-""} +RELEASE_TYPE=${4:-"minor"} # major, minor, or patch + +if [ -z "$RC_TAG" ] || [ -z "$RC_VERSION" ]; then + echo "Error: RC_TAG and RC_VERSION are required" + echo "Usage: create_rc_discussion.sh [release_branch]" + exit 1 +fi + +DISCUSSION_TITLE="[VOTE] Release Candidate ${RC_TAG}" + +# Determine vote duration based on release type +case "$RELEASE_TYPE" in + major) + VOTE_DURATION_DAYS=7 + ;; + minor) + VOTE_DURATION_DAYS=3 + ;; + patch) + VOTE_DURATION_DAYS=0 + ;; + *) + VOTE_DURATION_DAYS=3 + ;; +esac + +# Calculate vote end time in both UTC and Pacific +if [ "$VOTE_DURATION_DAYS" -gt 0 ]; then + # Try macOS date format first, then GNU date format + VOTE_END_TIME_UTC=$(date -u -v+${VOTE_DURATION_DAYS}d '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -u -d "+${VOTE_DURATION_DAYS} days" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "") + VOTE_END_TIME_PT=$(TZ='America/Los_Angeles' date -v+${VOTE_DURATION_DAYS}d '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || TZ='America/Los_Angeles' date -d "+${VOTE_DURATION_DAYS} days" '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo "") +fi + +# Build discussion body with testing instructions +DISCUSSION_BODY="## Release Candidate: ${RC_TAG} + +This is a release candidate for version **${RC_VERSION}**. + +### Release Information +- **RC Tag**: ${RC_TAG}" + +if [ -n "$RELEASE_BRANCH" ]; then + DISCUSSION_BODY="${DISCUSSION_BODY} +- **Release Branch**: ${RELEASE_BRANCH}" +fi + +DISCUSSION_BODY="${DISCUSSION_BODY} +- **Release Notes**: https://github.com/lancedb/lance/releases/tag/${RC_TAG} + +### Testing Instructions + +#### Python +\`\`\`bash +pip install --pre --extra-index-url https://pypi.fury.io/lancedb/ pylance==${RC_VERSION} +\`\`\` + +#### Java (Maven) +Add to your \`pom.xml\`: +\`\`\`xml + + com.lancedb + lance + ${RC_VERSION} + +\`\`\` + +#### Rust (Cargo) +Add to your \`Cargo.toml\`: +\`\`\`toml +[dependencies] +lance = { version = \"=${RC_VERSION}\", git = \"https://github.com/lancedb/lance\", tag = \"${RC_TAG}\" } +\`\`\` + +### Voting Instructions +Please test the RC artifacts and vote by commenting: +- **+1** to approve +- **0** to abstain or neutral +- **-1** if issues found (please include details)" + +if [ "$VOTE_DURATION_DAYS" -gt 0 ] && [ -n "$VOTE_END_TIME_UTC" ]; then + DISCUSSION_BODY="${DISCUSSION_BODY} + +**Vote Duration**: If there are enough binding votes and no vetoes, the vote will end at **${VOTE_END_TIME_UTC} UTC**" + + if [ -n "$VOTE_END_TIME_PT" ]; then + DISCUSSION_BODY="${DISCUSSION_BODY} (Pacific time: ${VOTE_END_TIME_PT})." + else + DISCUSSION_BODY="${DISCUSSION_BODY}." + fi +else + DISCUSSION_BODY="${DISCUSSION_BODY} + +**Patch Release**: For patch releases, there is no duration requirement. The release will be cut as soon as there are enough binding votes and no vetoes." +fi + +DISCUSSION_BODY="${DISCUSSION_BODY} + +### Next Steps +- If approved: Approve RC using \`approve-rc\` workflow +- If issues found: Fix on release branch and create new RC using \`create-rc\` workflow" + +# Get repository and category IDs using "Release Vote" category +REPO_DATA=$(gh api graphql -f query=' + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + id + discussionCategory(slug: "release-vote") { + id + } + } + } +' -f owner="$(echo ${GITHUB_REPOSITORY} | cut -d'/' -f1)" -f name="$(echo ${GITHUB_REPOSITORY} | cut -d'/' -f2)") + +REPO_ID=$(echo "$REPO_DATA" | jq -r '.data.repository.id') +CATEGORY_ID=$(echo "$REPO_DATA" | jq -r '.data.repository.discussionCategory.id') + +if [ -z "$CATEGORY_ID" ] || [ "$CATEGORY_ID" = "null" ]; then + echo "Error: Discussion category 'Release Vote' not found. Please create it in repository settings." + exit 1 +fi + +# Create discussion +DISCUSSION_URL=$(gh api graphql -f query=' + mutation($repositoryId: ID!, $categoryId: ID!, $body: String!, $title: String!) { + createDiscussion(input: {repositoryId: $repositoryId, categoryId: $categoryId, body: $body, title: $title}) { + discussion { + url + } + } + } +' -f repositoryId="$REPO_ID" -f categoryId="$CATEGORY_ID" \ + -f title="$DISCUSSION_TITLE" -f body="$DISCUSSION_BODY" \ + --jq '.data.createDiscussion.discussion.url') + +echo "Created discussion: $DISCUSSION_URL" >&2 +echo "$DISCUSSION_URL" diff --git a/ci/create_release_branch.sh b/ci/create_release_branch.sh new file mode 100755 index 00000000000..594709bcb15 --- /dev/null +++ b/ci/create_release_branch.sh @@ -0,0 +1,181 @@ +#!/bin/bash +set -e + +# Script to create a release branch with initial RC for major/minor release +# Always creates RC from the tip of main branch +# Checks for breaking changes and bumps major version if needed +# The version is automatically determined from main branch HEAD +# Usage: create_release_branch.sh +# Example: create_release_branch.sh + +TAG_PREFIX=${1:-"v"} + +readonly SELF_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +git checkout main +MAIN_VERSION=$(grep '^version = ' Cargo.toml | head -n1 | cut -d'"' -f2) +echo "Main branch current version: ${MAIN_VERSION}" + +# Extract the base version from main (remove beta suffix if present) +if [[ "${MAIN_VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-beta\.([0-9]+))?$ ]]; then + CURR_MAJOR="${BASH_REMATCH[1]}" + CURR_MINOR="${BASH_REMATCH[2]}" + CURR_PATCH="${BASH_REMATCH[3]}" + BASE_VERSION="${CURR_MAJOR}.${CURR_MINOR}.${CURR_PATCH}" +else + echo "ERROR: Cannot parse version from main branch: ${MAIN_VERSION}" + exit 1 +fi + +echo "Current base version on main: ${BASE_VERSION}" + +# Check for existing release-root tag to find comparison base +CURR_RELEASE_ROOT_TAG="release-root/${BASE_VERSION}-beta.N" + +if git rev-parse "${CURR_RELEASE_ROOT_TAG}" >/dev/null 2>&1; then + echo "Found release root tag: ${CURR_RELEASE_ROOT_TAG}" + COMPARE_TAG="${CURR_RELEASE_ROOT_TAG}" + COMPARE_COMMIT=$(git rev-parse "${CURR_RELEASE_ROOT_TAG}") + echo "Will compare against: ${COMPARE_TAG} (commit: ${COMPARE_COMMIT})" +else + echo "No release root tag found for current version series" + COMPARE_TAG="" +fi + +# Check for breaking changes +BREAKING_CHANGES="false" +if [ -n "${COMPARE_TAG}" ]; then + if python3 "${SELF_DIR}/check_breaking_changes.py" --detect-only "${COMPARE_TAG}" "HEAD"; then + echo "No breaking changes detected" + BREAKING_CHANGES="false" + else + echo "Breaking changes detected" + BREAKING_CHANGES="true" + fi +fi + +# Determine RC version based on breaking changes +if [ "${BREAKING_CHANGES}" = "true" ]; then + # Extract base RC version from release-root tag message + TAG_MESSAGE=$(git tag -l --format='%(contents)' "${CURR_RELEASE_ROOT_TAG}") + BASE_RC_VERSION=$(echo "${TAG_MESSAGE}" | head -n1 | sed 's/Base: //') + BASE_RC_MAJOR=$(echo "${BASE_RC_VERSION}" | cut -d. -f1 | sed 's/^v//') + + echo "Base RC version: ${BASE_RC_VERSION} (major: ${BASE_RC_MAJOR})" + + if [ "${CURR_MAJOR}" -gt "${BASE_RC_MAJOR}" ]; then + echo "Major version already bumped from ${BASE_RC_MAJOR} to ${CURR_MAJOR}" + RC_VERSION="${BASE_VERSION}-rc.1" + else + echo "Breaking changes require major version bump" + RC_MAJOR=$((CURR_MAJOR + 1)) + RC_VERSION="${RC_MAJOR}.0.0-rc.1" + fi +else + # No breaking changes, use current base version + RC_VERSION="${BASE_VERSION}-rc.1" +fi + +echo "Creating RC version: ${RC_VERSION}" + +# Determine release type (major if X.0.0, otherwise minor) +RC_MINOR=$(echo "${RC_VERSION}" | cut -d. -f2 | cut -d- -f1) +if [ "${RC_MINOR}" = "0" ]; then + RELEASE_TYPE="major" +else + RELEASE_TYPE="minor" +fi +echo "Release type: ${RELEASE_TYPE}" + +# Parse RC version for release branch +RC_MAJOR=$(echo "${RC_VERSION}" | cut -d. -f1) +RC_MINOR=$(echo "${RC_VERSION}" | cut -d. -f2) +RELEASE_BRANCH="release/v${RC_MAJOR}.${RC_MINOR}" + +echo "Will create release branch: ${RELEASE_BRANCH}" + +# Create release branch from main HEAD +echo "Creating release branch ${RELEASE_BRANCH} from main HEAD" +git checkout -b "${RELEASE_BRANCH}" + +# Set version to RC version +echo "Setting version to ${RC_VERSION}" +bump-my-version bump -vv --new-version "${RC_VERSION}" --no-tag patch + +# Update Cargo.lock files after version bump +cargo update +(cd python && cargo update) +(cd java/lance-jni && cargo update) + +# Commit the RC version +git add -A +git commit -m "chore: release candidate ${RC_VERSION}" + +# Create the RC tag +RC_TAG="${TAG_PREFIX}${RC_VERSION}" +echo "Creating tag ${RC_TAG}" +git tag -a "${RC_TAG}" -m "Release candidate ${RC_VERSION}" + +echo "Successfully created RC tag: ${RC_TAG} on branch ${RELEASE_BRANCH}" + +# Now bump main to next unreleased version (beta.0) +echo "Bumping main to next version beta.0" +git checkout main + +# Determine next version for main based on RC version +# Always bump minor from the RC version +NEXT_MAJOR="${RC_MAJOR}" +NEXT_MINOR=$((RC_MINOR + 1)) +NEXT_VERSION="${NEXT_MAJOR}.${NEXT_MINOR}.0-beta.0" + +echo "Bumping main to ${NEXT_VERSION} (unreleased)" + +bump-my-version bump -vv --new-version "${NEXT_VERSION}" --no-tag patch + +# Update Cargo.lock files after version bump +cargo update +(cd python && cargo update) +(cd java/lance-jni && cargo update) + +git add -A +git commit -m "chore: bump main to ${NEXT_VERSION} + +Unreleased version after creating ${RC_TAG}" + +echo "Main branch bumped to ${NEXT_VERSION}" + +# Create release-root tag for the new beta series on main (points to commit before RC branch) +# Strip the prerelease suffix from NEXT_VERSION for the tag name +NEXT_BASE_VERSION="${NEXT_MAJOR}.${NEXT_MINOR}.0" +RELEASE_ROOT_TAG="release-root/${NEXT_BASE_VERSION}-beta.N" +echo "Creating release root tag ${RELEASE_ROOT_TAG} pointing to RC ${RC_VERSION}" +git tag -a "${RELEASE_ROOT_TAG}" "${RC_TAG}^" -m "Base: ${RC_VERSION} +Release root for ${NEXT_BASE_VERSION}-beta.N series" + +# Determine comparison base for RC release notes +# For major/minor RC, we want to compare against the OLD release-root tag (the one for the main version before bump) +# which points to the previous RC base +OLD_RELEASE_ROOT_TAG="release-root/${BASE_VERSION}-beta.N" + +if git rev-parse "${OLD_RELEASE_ROOT_TAG}" >/dev/null 2>&1; then + PREVIOUS_TAG="${OLD_RELEASE_ROOT_TAG}" + echo "Release notes will compare against previous release-root: ${PREVIOUS_TAG}" +else + echo "Warning: Release root tag ${OLD_RELEASE_ROOT_TAG} not found" + PREVIOUS_TAG="" +fi + +# Output for GitHub Actions +echo "RC_TAG=${RC_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RC_VERSION=${RC_VERSION}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RELEASE_BRANCH=${RELEASE_BRANCH}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "MAIN_VERSION=${NEXT_VERSION}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RELEASE_ROOT_TAG=${RELEASE_ROOT_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RELEASE_TYPE=${RELEASE_TYPE}" >> $GITHUB_OUTPUT 2>/dev/null || true + +echo "Successfully created major/minor RC!" +echo " RC Tag: ${RC_TAG}" +echo " Release Branch: ${RELEASE_BRANCH}" +echo " Main Version: ${NEXT_VERSION}" +echo " Release Root Tag: ${RELEASE_ROOT_TAG}" diff --git a/ci/publish_beta.sh b/ci/publish_beta.sh new file mode 100644 index 00000000000..43747f32cd8 --- /dev/null +++ b/ci/publish_beta.sh @@ -0,0 +1,208 @@ +#!/bin/bash +set -e + +# Script to publish a beta preview release +# Usage: publish_beta.sh [branch_name] +# Example: publish_beta.sh main +# Example: publish_beta.sh release/v1.3 + +BRANCH=${1:-$(git branch --show-current)} +TAG_PREFIX=${2:-"v"} + +readonly SELF_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +echo "Publishing beta release from branch: ${BRANCH}" + +# Ensure we're on the specified branch +git checkout "${BRANCH}" + +# Read current version from Cargo.toml +CURRENT_VERSION=$(grep '^version = ' Cargo.toml | head -n1 | cut -d'"' -f2) +echo "Current version: ${CURRENT_VERSION}" + +# Validate current version is a beta version +if [[ ! "${CURRENT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]]; then + echo "ERROR: Current version ${CURRENT_VERSION} is not a beta version" + echo "Expected format: X.Y.Z-beta.N" + exit 1 +fi + +# Breaking change detection for main branch +if [[ "${BRANCH}" == "main" ]] && [[ "${CURRENT_VERSION}" =~ -beta\.[0-9]+$ ]]; then + echo "Checking for breaking changes on main branch..." + + # Parse current version + CURR_MAJOR=$(echo "${CURRENT_VERSION}" | cut -d. -f1) + CURR_MINOR=$(echo "${CURRENT_VERSION}" | cut -d. -f2) + CURR_PATCH=$(echo "${CURRENT_VERSION}" | cut -d. -f3 | cut -d- -f1) + CURR_BETA=$(echo "${CURRENT_VERSION}" | sed 's/.*-beta\.//') + + # Find the release-root tag for the current version series + CURR_RELEASE_ROOT_TAG="release-root/${CURR_MAJOR}.${CURR_MINOR}.${CURR_PATCH}-beta.N" + + if git rev-parse "${CURR_RELEASE_ROOT_TAG}" >/dev/null 2>&1; then + echo "Found release root tag for current version: ${CURR_RELEASE_ROOT_TAG}" + COMPARE_TAG="${CURR_RELEASE_ROOT_TAG}" + COMPARE_COMMIT=$(git rev-parse "${CURR_RELEASE_ROOT_TAG}") + else + # No release-root tag found - skip breaking change detection for first time + # But create the release-root tag at current HEAD for future comparisons + echo "Release root tag ${CURR_RELEASE_ROOT_TAG} not found" + echo "First time: skipping breaking change detection and creating release-root tag at current HEAD" + echo "Future beta releases will compare against this tag" + + # We'll create the release-root tag after the beta increment below + COMPARE_TAG="" + COMPARE_COMMIT="" + CREATE_INITIAL_RELEASE_ROOT="true" + fi + + if [ -n "${COMPARE_TAG}" ]; then + echo "Comparing against: ${COMPARE_TAG} (commit: ${COMPARE_COMMIT})" + + # Check for breaking changes + BREAKING_CHANGES="false" + if python3 "${SELF_DIR}/check_breaking_changes.py" --detect-only "${COMPARE_TAG}" "HEAD"; then + echo "No breaking changes detected" + BREAKING_CHANGES="false" + else + echo "Breaking changes detected" + BREAKING_CHANGES="true" + fi + + if [ "${BREAKING_CHANGES}" = "true" ]; then + # Extract base RC version from release-root tag message + TAG_MESSAGE=$(git tag -l --format='%(contents)' "${CURR_RELEASE_ROOT_TAG}") + BASE_RC_VERSION=$(echo "${TAG_MESSAGE}" | head -n1 | sed 's/Base: //') + BASE_VERSION=$(echo "${BASE_RC_VERSION}" | sed 's/-rc\.[0-9]*$//') + BASE_MAJOR=$(echo "${BASE_VERSION}" | cut -d. -f1) + + echo "Base RC version: ${BASE_RC_VERSION} (major: ${BASE_MAJOR})" + + # Check if major already bumped from base + if [ "${CURR_MAJOR}" -gt "${BASE_MAJOR}" ]; then + echo "Breaking changes exist, but major version already bumped from ${BASE_MAJOR} to ${CURR_MAJOR}" + echo "No additional major version bump needed" + else + echo "Breaking changes detected since ${BASE_VERSION}, bumping major version" + NEXT_MAJOR=$((CURR_MAJOR + 1)) + NEXT_VERSION="${NEXT_MAJOR}.0.0-beta.1" + echo "Bumping to ${NEXT_VERSION}" + + echo "Updating version from ${CURRENT_VERSION} to ${NEXT_VERSION}" + bump-my-version bump -vv --new-version "${NEXT_VERSION}" --no-tag patch + + # Update Cargo.lock files after version bump + cargo update + (cd python && cargo update) + (cd java/lance-jni && cargo update) + + git add -A + git commit -m "chore: bump to ${NEXT_VERSION} based on breaking change detection" + + CURRENT_VERSION="${NEXT_VERSION}" + + # Create new release-root tag pointing to same commit (same base for comparison) + NEW_RELEASE_ROOT_TAG="release-root/${NEXT_MAJOR}.0.0-beta.N" + if git rev-parse "${NEW_RELEASE_ROOT_TAG}" >/dev/null 2>&1; then + echo "Release root tag ${NEW_RELEASE_ROOT_TAG} already exists" + else + echo "Creating new release root tag: ${NEW_RELEASE_ROOT_TAG} pointing to commit ${COMPARE_COMMIT}" + git tag -a "${NEW_RELEASE_ROOT_TAG}" "${COMPARE_COMMIT}" -m "Base: ${BASE_RC_VERSION} +Release root for ${NEXT_MAJOR}.0.0-beta.N series (same base as ${CURR_MAJOR}.${CURR_MINOR}.${CURR_PATCH}-beta.N)" + fi + BETA_TAG="${TAG_PREFIX}${CURRENT_VERSION}" + echo "Creating beta tag: ${BETA_TAG}" + git tag -a "${BETA_TAG}" -m "Beta release version ${CURRENT_VERSION}" + + echo "Successfully published beta release: ${BETA_TAG}" + echo "BETA_TAG=${BETA_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true + echo "BETA_VERSION=${CURRENT_VERSION}" >> $GITHUB_OUTPUT 2>/dev/null || true + echo "RELEASE_ROOT_TAG=${NEW_RELEASE_ROOT_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true + echo "RELEASE_NOTES_FROM=${NEW_RELEASE_ROOT_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true + exit 0 + fi + fi + else + echo "Warning: No compare tag found for breaking change detection" + fi +fi + +# Bump beta version (beta.N → beta.N+1) +echo "Bumping beta version" +bump-my-version bump -vv prerelease_num + +# Update Cargo.lock files after version bump +cargo update +(cd python && cargo update) +(cd java/lance-jni && cargo update) + +# Get new version +NEW_VERSION=$(grep '^version = ' Cargo.toml | head -n1 | cut -d'"' -f2) +echo "New version: ${NEW_VERSION}" + +# Commit the version change +git add -A +git commit -m "chore: release beta version ${NEW_VERSION}" + +# Create beta tag +BETA_TAG="${TAG_PREFIX}${NEW_VERSION}" +echo "Creating beta tag: ${BETA_TAG}" +git tag -a "${BETA_TAG}" -m "Beta release version ${NEW_VERSION}" + +# Create initial release-root tag if this is the first time +CREATED_RELEASE_ROOT_TAG="" +if [ "${CREATE_INITIAL_RELEASE_ROOT:-false}" = "true" ]; then + BETA_MAJOR=$(echo "${NEW_VERSION}" | cut -d. -f1) + BETA_MINOR=$(echo "${NEW_VERSION}" | cut -d. -f2) + BETA_PATCH=$(echo "${NEW_VERSION}" | cut -d. -f3 | cut -d- -f1) + INITIAL_RELEASE_ROOT_TAG="release-root/${BETA_MAJOR}.${BETA_MINOR}.${BETA_PATCH}-beta.N" + + echo "Creating initial release-root tag: ${INITIAL_RELEASE_ROOT_TAG} at HEAD" + git tag -a "${INITIAL_RELEASE_ROOT_TAG}" "HEAD" -m "Base: ${NEW_VERSION} +Release root for ${BETA_MAJOR}.${BETA_MINOR}.${BETA_PATCH}-beta.N series (initial)" + echo "Created initial release-root tag for future breaking change detection" + CREATED_RELEASE_ROOT_TAG="${INITIAL_RELEASE_ROOT_TAG}" +fi + +# Determine release notes comparison base +BETA_MAJOR=$(echo "${NEW_VERSION}" | cut -d. -f1) +BETA_MINOR=$(echo "${NEW_VERSION}" | cut -d. -f2) +BETA_PATCH=$(echo "${NEW_VERSION}" | cut -d. -f3 | cut -d- -f1) + +if [[ "${BRANCH}" == "main" ]]; then + # For main branch: compare against release-root tag + BETA_RELEASE_ROOT_TAG="release-root/${BETA_MAJOR}.${BETA_MINOR}.${BETA_PATCH}-beta.N" + + if git rev-parse "${BETA_RELEASE_ROOT_TAG}" >/dev/null 2>&1; then + echo "Release notes will compare from ${BETA_RELEASE_ROOT_TAG} to ${BETA_TAG}" + RELEASE_NOTES_FROM="${BETA_RELEASE_ROOT_TAG}" + else + echo "Warning: Release root tag ${BETA_RELEASE_ROOT_TAG} not found" + RELEASE_NOTES_FROM="" + fi +elif [[ "${BRANCH}" =~ ^release/ ]]; then + # For release branch: compare against last stable tag + PREV_PATCH=$((BETA_PATCH - 1)) + if [ "${PREV_PATCH}" -ge 0 ]; then + PREV_STABLE_TAG="${TAG_PREFIX}${BETA_MAJOR}.${BETA_MINOR}.${PREV_PATCH}" + if git rev-parse "${PREV_STABLE_TAG}" >/dev/null 2>&1; then + echo "Release notes will compare from ${PREV_STABLE_TAG} to ${BETA_TAG}" + RELEASE_NOTES_FROM="${PREV_STABLE_TAG}" + else + echo "Warning: Previous stable tag ${PREV_STABLE_TAG} not found" + RELEASE_NOTES_FROM="" + fi + else + echo "Warning: No previous patch to compare against (patch is 0)" + RELEASE_NOTES_FROM="" + fi +else + RELEASE_NOTES_FROM="" +fi + +echo "Successfully published beta release: ${BETA_TAG}" +echo "BETA_TAG=${BETA_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "BETA_VERSION=${NEW_VERSION}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RELEASE_NOTES_FROM=${RELEASE_NOTES_FROM}" >> $GITHUB_OUTPUT 2>/dev/null || true +echo "RELEASE_ROOT_TAG=${CREATED_RELEASE_ROOT_TAG}" >> $GITHUB_OUTPUT 2>/dev/null || true diff --git a/ci/release_common.sh b/ci/release_common.sh new file mode 100644 index 00000000000..bea245919fa --- /dev/null +++ b/ci/release_common.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Common functions for release scripts + +# Gets the current version from Cargo.toml +# Returns: version string (e.g., "1.3.0-beta.1") +get_version_from_cargo() { + grep '^version = ' Cargo.toml | head -n1 | cut -d'"' -f2 +} + +# Parses version components from a version string +# Args: VERSION_STRING +# Returns: three values separated by spaces: MAJOR MINOR PATCH +# Example: parse_version_components "1.3.0-rc.2" returns "1 3 0" +parse_version_components() { + local VERSION=$1 + local MAJOR=$(echo "${VERSION}" | cut -d. -f1 | sed 's/^v//') + local MINOR=$(echo "${VERSION}" | cut -d. -f2) + local PATCH=$(echo "${VERSION}" | cut -d. -f3 | cut -d- -f1) + echo "${MAJOR} ${MINOR} ${PATCH}" +} + +# Bumps version and commits the change +# Args: NEW_VERSION COMMIT_MESSAGE +bump_and_commit_version() { + local NEW_VERSION=$1 + local COMMIT_MESSAGE=$2 + + bump-my-version bump -vv --new-version "${NEW_VERSION}" --no-tag patch + + # Update Cargo.lock files after version bump + cargo update + (cd python && cargo update) + (cd java/lance-jni && cargo update) + + git add -A + git commit -m "${COMMIT_MESSAGE}" +} + +# Determines the previous tag for release notes comparison +# Args: MAJOR MINOR PATCH [TAG_PREFIX] +# Returns: previous tag name or empty string +determine_previous_tag() { + local MAJOR=$1 + local MINOR=$2 + local PATCH=$3 + local TAG_PREFIX=${4:-"v"} + + if [ "${PATCH}" = "0" ]; then + # Major/Minor release: compare against release-root tag + local RELEASE_ROOT_TAG="release-root/${MAJOR}.${MINOR}.${PATCH}-beta.N" + if git rev-parse "${RELEASE_ROOT_TAG}" >/dev/null 2>&1; then + echo "${RELEASE_ROOT_TAG}" + else + echo "" + fi + else + # Patch release: compare against previous stable tag + local PREV_PATCH=$((PATCH - 1)) + local PREV_TAG="${TAG_PREFIX}${MAJOR}.${MINOR}.${PREV_PATCH}" + if git rev-parse "${PREV_TAG}" >/dev/null 2>&1; then + echo "${PREV_TAG}" + else + echo "" + fi + fi +} diff --git a/release_process.md b/release_process.md index f6b6f56dee0..0a97e3afaf5 100644 --- a/release_process.md +++ b/release_process.md @@ -1,162 +1,477 @@ -# Release process +# Release Process + +Lance uses [semantic versioning](https://semver.org/) and maintains a linear commit history. +* All pull requests are merged into the `main` branch. +* Beta releases (or preview releases) are created on-demand from the `main` branch. +* Stable releases (non-prereleases) are created only after a voting process and come from a release branch `release/vX.Y`. These are typically created once every two weeks. +* Release Candidates (RC) are created from release branches prior to voting. +* Patch releases are created by committing fixes directly to the release branch, voting on a new RC, and releasing. + +```mermaid +gitGraph + commit + branch feature + checkout feature + commit + checkout main + merge feature + branch bugfix + checkout bugfix + commit id: "bugfix" + checkout main + branch "release/v1.4" + checkout "release/v1.4" + commit tag: "1.4.0-rc.1" + commit tag: "1.4.0" + checkout main + merge bugfix + commit id: "merged" + checkout "release/v1.4" + cherry-pick id: "merged" + commit tag: "1.4.1-rc.1" + commit tag: "1.4.1" + checkout main + commit tag: "1.5.0-beta.1" -We create a full release of Lance up to every 2 weeks. In between full releases, -we make preview releases of the latest features and bug fixes, which are hosted -on fury.io. This allows us to release frequently and get feedback on new features -while keeping under the PyPI project size limits. - -## Overview of Automated Release Process - -The Lance release process is now automated using `bump-my-version` to eliminate -manual version updates. The workflow handles version bumping, breaking change -validation, and release creation automatically. - -## Choosing a full versus preview release - -There are three conditions that can trigger a full release: - -1. There's a bugfix we urgently want to get out to a broad audience -2. We want to make a release of LanceDB that requires new features from Lance - (LanceDB can't depend on a preview release of Lance) -3. It's been two weeks since we last released a full release. - -Otherwise, we should make a preview release. - -## Make a preview release - -First, make sure the CI on main is green. - -Trigger the `Create release` action with the following parameters: -- **release_type**: Choose based on changes (patch/minor/major) -- **release_channel**: `preview` -- **dry_run**: `false` (use `true` to test first) -- **draft_release**: `true` (to review release notes before publishing) - -This will create a tag on the current main with format `vX.Y.Z-beta.N`. After -creating the tag, the action will create a GitHub release for the new tag. -Once that release is published, it will trigger publish jobs for Python. - -The action will automatically generate release notes. **Please review these -and make any necessary edits.** - -> [!NOTE] -> Preview releases are not published to crates.io, since Rust is a source -> distribution. Users can simply point to the tag on GitHub in their `Cargo.toml`. - -## Make a full release - -First, make sure the CI on main is green. - -Trigger the `Create release` action with the following parameters: -- **release_type**: Choose based on changes (patch/minor/major) -- **release_channel**: `stable` -- **dry_run**: `false` (use `true` to test first) -- **draft_release**: `true` (to review release notes before publishing) - -The workflow will: -1. Check for breaking changes automatically -2. Update all version numbers using bump-my-version -3. Create a commit with the version update -4. Create a tag with format `vX.Y.Z` -5. Push both the commit and tag -6. Create a GitHub release - -The action will automatically generate release notes. **Please review these -and make any necessary edits.** - -Once that release is published, it will trigger publish jobs for Rust, Python, and Java. - -## Version Management - -### Automated Version Bumping +``` -The release process now uses `bump-my-version` configured in `.bumpversion.toml` to: -- Synchronize versions across all Rust crates -- Update Python and Java package versions -- Update all Cargo.lock files automatically +## Version Semantics + +### Version Format + +Lance uses semantic versioning with prerelease identifiers: +- **Stable**: `X.Y.Z` (e.g., `1.3.0`) +- **Beta**: `X.Y.Z-beta.N` (e.g., `1.3.0-beta.1`, `1.3.0-beta.2`) +- **RC**: `X.Y.Z-rc.N` (e.g., `1.3.0-rc.1`, `1.3.0-rc.2`) + +### Beta Version States + +- **beta.0**: Unreleased version (exists on branch but not published) + - Created after cutting an RC to mark the next unreleased version + - Indicates no preview has been published yet +- **beta.1+**: Published preview releases + - Created when publishing beta preview artifacts + +### Publishing Channels + +| Language | Stable release | RC release | Beta release | +|--------------|---------------------|-----------------------------|-----------------------------| +| **Rust** | crates.io | Not published (use git tag) | Not published (use git tag) | +| **Python** | PyPI | fury.io | fury.io | +| **Java** | Maven Central | Maven Central | Maven Central | +| **Protobuf** | Buf Schema Registry | Buf Schema Registry | Buf Schema Registry | + +### GitHub Releases and Release Notes + +| Release Type | GitHub Release Type | Start Commit (exclusive) | End Commit (inclusive) | Explanation | +|---------------------------|---------------------|-----------------------------|------------------------|----------------------------------------------------------------------| +| **Stable (Major/Minor)** | Release | `release-root/X.Y.0-beta.N` | `vX.Y.0` | All changes from main + RC fixes | +| **Stable (Patch)** | Release | `vX.Y.(Z-1)` | `vX.Y.Z` | Only changes in this patch release | +| **RC (Major/Minor)** | Pre-Release | `release-root/X.Y.0-beta.N` | `vX.Y.0-rc.N` | All changes for the release | +| **RC (Patch)** | Pre-Release | `vX.Y.(Z-1)` | `vX.Y.Z-rc.N` | Only changes in this patch release | +| **RC (Iterations)** | Pre-Release | `vX.Y.(Z-1)` | `vX.Y.Z-rc.N` | Only changes in this patch release (not changes against previous RC) | +| **Beta (Main branch)** | Pre-Release | `release-root/X.Y.Z-beta.N` | `vX.Y.Z-beta.N` | Changes since last stable release RC cut in main branch | +| **Beta (Release branch)** | Pre-Release | `vX.Y.(Z-1)` | `vX.Y.Z-beta.N` | Changes since last stable release | + +## Branching Strategy + +### Main Branch +- Always contains the latest development work +- Version format: `X.Y.Z-beta.N` +- After RC creation, bumped to next minor version with `-beta.0` (unreleased) + - Beta previews published by bumping to `-beta.1+` + +### Release Branches +- Format: `release/v{major}.{minor}` (e.g., `release/v1.3`) +- Created when cutting initial RC for major/minor release +- Maintained for patch releases +- Version progression: `rc.1` → `rc.2` → stable → `beta.0` → `rc.1` (for patches) + +## Version Flow + +```mermaid +%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%% +flowchart LR + subgraph main["Main Branch"] + direction LR + M0["1.3.0-beta.2
📍 release-root/1.4.0-beta.N
📍 release-root/2.0.0-beta.N"] --> M1["1.4.0-beta.0"] + M1 --> M2["1.4.0-beta.1
🏷️ v1.4.0-beta.1"] + M2 --> M3["2.0.0-beta.1
🏷️ v2.0.0-beta.1"] + end + + subgraph release["Release Branch: release/v1.3"] + direction LR + R1["1.3.0-rc.1
🏷️ v1.3.0-rc.1"] --> R2["1.3.0
🏷️ v1.3.0"] + R2 --> R3["1.3.1-beta.0"] + R3 --> R4["1.3.1-rc.1
🏷️ v1.3.1-rc.1"] + R4 --> R5["1.3.1
🏷️ v1.3.1"] + R5 --> R6["1.3.2-beta.0"] + end + + M0 -.->|"create RC
(no breaking changes)"| R1 +``` -### Release Types +**Flow explanation:** +- **Main branch**: Commit M0 at `1.3.0-beta.2` has `release-root/1.4.0-beta.N` (created when cutting v1.3.0-rc.1, pointing to this commit) and `release-root/2.0.0-beta.N` (created when breaking changes bumped major version, pointing to same commit) → M1 bumps to `1.4.0-beta.0` (unreleased) → M2 publishes `1.4.0-beta.1` (preview, tagged) → M3 publishes `2.0.0-beta.1` after detecting breaking changes (tagged) +- **Release branch** `release/v1.3` created from M0, starts at `1.3.0-rc.1` (tagged) → `1.3.0` (stable, tagged) → `1.3.1-beta.0` → `1.3.1-rc.1` (tagged) → `1.3.1` (stable, tagged) → `1.3.2-beta.0` +- **Tags**: 🏷️ = version tag (points to tagged commit), 📍 = release-root tag (points to commit before RC was created, used for breaking change detection) +- **Breaking changes**: Both `release-root/1.4.0-beta.N` and `release-root/2.0.0-beta.N` point to M0 (same commit), showing that 2.0.0 is a major version bump from the 1.3.0-rc.1 baseline -- **patch**: Bug fixes and minor improvements (0.32.1 → 0.32.2) -- **minor**: New features or breaking changes (0.32.1 → 0.33.0) -- **major**: Major breaking changes (0.32.1 → 1.0.0) +**Note**: All commits are linear on their respective branches. `beta.0` = unreleased, `beta.1+` = published previews. -The breaking change detection script (`scripts/check_breaking_changes.py`) will -prevent patch releases when breaking changes are detected. +## Workflows -## Breaking Change policy +### User-Facing Workflows -We try to avoid breaking changes, but sometimes they are necessary. When there -are breaking changes, we will increment the minor version. (This is valid -semantic versioning because we are still in `0.x` versions.) +1. **publish-beta.yml** - Publish beta preview releases from any branch +2. **create-release-branch.yml** - Create release branch with initial RC for new major/minor version +3. **create-rc.yml** - Create RC on existing release branch (for new patch release RC or iterations of an existing RC) +4. **approve-rc.yml** - Approve any RC to stable (works for all release types) -### Automatic Breaking Change Detection +## Create a Beta / Preview Release -The release workflow automatically detects breaking changes by: -- Analyzing commit messages for breaking change indicators -- Checking for changes in public Rust APIs -- Detecting migration files +**Purpose**: Publish preview releases for testing before creating release candidates. -When a PR makes a breaking change, the PR author should mark the PR using the -conventional commit markers: either exclamation mark after the type -(such as `feat!: change signature of func`) or have `BREAKING CHANGE` in the -body of the PR. +**Steps**: +1. Trigger **"Publish Beta"** workflow +2. Set **branch**: `main` (or any release branch) +3. Set **dry_run**: `true` (test first) +4. Review results, then run with **dry_run**: `false` -### What Constitutes a Breaking Change +**Result**: Creates a beta tag (e.g., `v1.4.0-beta.1`) and publishes preview artifacts to fury.io, Maven Central, and Buf Schema Registry. -Some things that are considered breaking changes: +
+How beta versioning works -* Upgrading a dependency pin that is in the Rust API. In particular, upgrading - `DataFusion` and `Arrow` are breaking changes. Changing dependencies that are - not exposed in our public API are not considered breaking changes. -* Changing the signature of a public function or method. -* Removing a public function or method. +**For main branch**: Automatically checks for breaking changes and bumps version: +- **No breaking changes**: Increments beta (e.g., `1.4.0-beta.0` → `1.4.0-beta.1`) +- **Breaking changes found**: Bumps major and resets beta (e.g., `1.4.0-beta.1` → `2.0.0-beta.1`) +- **Already bumped**: Just increments beta (e.g., `2.0.0-beta.1` → `2.0.0-beta.2`) -We do make exceptions for APIs that are marked as experimental. These are APIs -that are under active development and not in major use. These changes should not -receive the `breaking-change` label. +**For release branches**: Bumps beta number (`beta.N` → `beta.N+1`) -## Local Testing +**Use cases**: +- Testing features before RC +- Regular preview releases for early adopters +- Automatic breaking change detection +
+ +## Breaking Change Detection + +**How it works**: Mark PRs with the `breaking-change` label in GitHub. The workflow automatically detects these and bumps the major version when publishing beta releases from main. -To test the release process locally: +**What counts as breaking**: +- Upgrading pinned dependencies in public API (DataFusion, Arrow) +- Changing signatures of public functions/methods +- Removing public functions/methods +- Changing public data structures +- **Exception**: Experimental APIs (marked as such in docs) are not considered breaking + +
+Technical details: Release root tags and version bumping + +### Release Root Tag + +Release root tags mark the base commits for breaking change detection. The tag naming reflects the **beta version series on main**, while the tag points to the **RC commit being compared against**. + +**Tag Format**: `release-root/{major}.{minor}.{patch}-beta.N` +- The tag name indicates which beta version series uses this base +- The tag points to the commit on main branch before the RC was created (the comparison base) +- The tag message stores the base RC version (e.g., "Base: 1.3.0-rc.1") - this is what we compare against to detect major version bumps +- The base RC version in the message stays constant even when multiple release-root tags point to the same commit + +**When created**: +1. **When creating a major/minor RC**: After bumping main to the next version + - Example: After cutting v1.3.0-rc.1, create `release-root/1.4.0-beta.N` pointing to the commit before the RC branch was created +2. **When breaking changes bump major version**: When major version is bumped during beta publish + - Example: When bumping 1.4.0-beta.5 → 2.0.0-beta.1, create `release-root/2.0.0-beta.N` pointing to the SAME commit with the SAME base RC version + +**Key properties**: +- **Multiple tags, same commit**: `release-root/1.4.0-beta.N` and `release-root/2.0.0-beta.N` point to the same commit on main (the commit before the RC branch was created) +- **Major version bumped once**: Both tags store same base RC version (1.3.0-rc.1), so we know 2.x is already a major bump from 1.3.0 +- **No additional bumps**: When at 2.0.0-beta.1, we detect breaking changes but see major already bumped (2 > 1), so just increment beta +- **Beta reset on major bump**: When bumping major version, beta number resets to 1 (e.g., 1.4.0-beta.5 → 2.0.0-beta.1) + +### Detection Process + +Breaking change detection happens **on every beta publish from main branch**: + +1. **Find release-root tag**: Look for `release-root/{current_version}-beta.N` + - If NOT found → Bump minor only (no comparison base exists, skip breaking change detection) +2. **Extract base RC version**: Read from tag message (e.g., "Base: 1.3.0-rc.1" → base major is `1`) +3. **Compare**: Check for breaking changes since the commit pointed to by the release-root tag +4. **Determine action**: + - If breaking changes AND current_major == base_major → bump to next major + - If breaking changes AND current_major > base_major → no bump (already bumped) + - If no breaking changes → no major bump + +### Examples + +Starting from v1.3.0-rc.1 cut, main at 1.4.0-beta.0 with `release-root/1.4.0-beta.N` (Base: 1.3.0-rc.1): +- `1.4.0-beta.0` + no breaking → `1.4.0-beta.1` +- `1.4.0-beta.1` + breaking → `2.0.0-beta.1` + - Creates `release-root/2.0.0-beta.N` pointing to same commit, message still "Base: 1.3.0-rc.1" + - Base major from tag message is 1, current major is 1, so bump to 2 +- `2.0.0-beta.1` + breaking → `2.0.0-beta.2` + - Base major is 1, current major is 2 (already bumped), so just increment beta +- `2.0.0-beta.2` + no breaking → `2.0.0-beta.3` + +**Key insight**: Multiple beta version series can share the same release-root commit, with major version bumped only once when first detected. +
+ +## Create a Major / Minor Release + +**Purpose**: Create a new major or minor release from the main branch. + +**Steps**: +1. Ensure CI on main is green +2. Trigger **"Create Release Branch"** workflow with **dry_run**: `true` +3. Review the RC version and changes +4. Run with **dry_run**: `false` to create release branch and RC +5. Test RC artifacts (published to fury.io, Maven Central) +6. Vote in the GitHub Discussion thread (created automatically) +7. **If issues found**: Fix on release branch, run **"Create RC"** workflow to create `rc.2`, `rc.3`, etc. +8. **If approved**: Trigger **"Approve RC"** workflow with **rc_tag** (e.g., `v1.3.0-rc.2`) + +**Result**: +- Creates release branch (e.g., `release/v1.3`) with RC tag (e.g., `v1.3.0-rc.1`) +- Bumps main to next minor (e.g., `1.4.0-beta.0`) +- After approval: Creates stable tag (e.g., `v1.3.0`) and publishes to PyPI, crates.io, Maven Central + +
+What happens under the hood + +**Create Release Branch workflow**: +- Reads current version from main (e.g., `1.3.0-beta.2`) +- Checks for breaking changes since release-root tag +- If breaking changes: Creates RC with bumped major (e.g., `2.0.0-rc.1`), bumps main to `2.1.0-beta.0` +- If no breaking changes: Creates RC with current version (e.g., `1.3.0-rc.1`), bumps main to `1.4.0-beta.0` +- Creates `release/v{major}.{minor}` branch from main HEAD +- Creates GitHub Discussion for voting + +**Approve RC workflow**: +- Bumps version from `rc.N` to stable +- Generates release notes comparing against `release-root/{version}-beta.N` tag +- Creates GitHub Release and publishes stable artifacts +- Auto-bumps release branch to next patch `beta.0` (e.g., `1.3.0` → `1.3.1-beta.0`) +- Main branch is NOT affected (already bumped in step 1) +
+ +## Create a Patch / Bugfix Release + +**Purpose**: Release critical bug fixes for an existing release. + +**Steps**: +1. Checkout the release branch (e.g., `release/v1.3`) +2. Create and test your fix (ensure no breaking changes) +3. Create a PR to merge into the release branch +4. Trigger **"Create RC"** workflow with **release_branch** (e.g., `release/v1.3`) and **dry_run**: `true` +5. Review the patch RC version +6. Run with **dry_run**: `false` to create the patch RC +7. Test RC artifacts and vote in the GitHub Discussion +8. **If issues found**: Fix and run **"Create RC"** again to create `rc.2`, `rc.3`, etc. +9. **If approved**: Trigger **"Approve RC"** workflow with **rc_tag** (e.g., `v1.3.1-rc.1`) + +**Result**: +- Creates patch RC tag (e.g., `v1.3.1-rc.1`) on release branch +- After approval: Creates stable tag (e.g., `v1.3.1`) and publishes to PyPI, crates.io, Maven Central +- Auto-bumps release branch to next patch `beta.0` (e.g., `1.3.2-beta.0`) +- Main branch is NOT affected + +
+Important notes + +- **Breaking changes not allowed**: Release branches are for patch releases only +- **Beta versions**: Release branches stay at `X.Y.Z-beta.N` between releases (auto-bumped after stable) +- **Release notes**: Compares against previous stable tag (e.g., `v1.3.0`) +- **Allowed changes**: Correctness bugs, security fixes, major performance regressions, unintentional breaking change reverts +
+ +## Example Workflows + +### Beta Preview Release ```bash -# Install bump-my-version -pip install bump-my-version - -# Test version bumping (dry run) -python ci/bump_version.py patch --dry-run - -# Check for breaking changes -python ci/check_breaking_changes.py +# 1. Main at 1.4.0-beta.0 (unreleased after RC cut for v1.3.0) +# 2. Want to publish preview for testing +Workflow: Publish Beta + branch: main + Result: + - Looks for release-root/1.4.0-beta.N → found + - Extracts base: 1.3.0-rc.1 (major: 1) from tag message + - No breaking changes detected + - Bumped to 1.4.0-beta.1 + - Tagged v1.4.0-beta.1 + - Created GitHub Pre-Release with release notes from release-root/1.4.0-beta.N to v1.4.0-beta.1 + - Published artifacts to fury.io + +# 3. More changes, publish again (with breaking changes) +Workflow: Publish Beta + branch: main + Result: + - Looks for release-root/1.4.0-beta.N → found + - Extracts base: 1.3.0-rc.1 (major: 1) from tag message + - Breaking changes detected, current major (1) == base major (1) + - Bumped to 2.0.0-beta.1 (beta resets on major bump) + - Created release-root/2.0.0-beta.N → same commit, message "Base: 1.3.0-rc.1" + - Tagged v2.0.0-beta.1 + - Created GitHub Pre-Release with release notes from release-root/2.0.0-beta.N to v2.0.0-beta.1 + - Published artifacts to fury.io + +# 4. More changes, publish again (still has breaking changes) +Workflow: Publish Beta + branch: main + Result: + - Looks for release-root/2.0.0-beta.N → found + - Extracts base: 1.3.0-rc.1 (major: 1) from tag message + - Breaking changes detected, but current major (2) > base major (1) + - No major bump needed (already bumped) + - Bumped to 2.0.0-beta.2 + - Tagged v2.0.0-beta.2 + - Created GitHub Pre-Release with release notes from release-root/2.0.0-beta.N to v2.0.0-beta.2 + - Published artifacts to fury.io ``` -## Troubleshooting +### Standard Major/Minor Release -### Version Mismatch -If versions become out of sync: ```bash -python ci/bump_version.py patch --no-validate +# 1. Main is at 1.3.0-beta.2 +# 2. Create release branch (version auto-determined from main) +Workflow: Create Release Branch + Result: + - Checks for breaking changes since release-root/1.3.0-beta.N + - No breaking changes detected + - Created release/v1.3 at 1.3.0-rc.1 + - Tagged v1.3.0-rc.1 + - Created GitHub Pre-Release with release notes from release-root/1.3.0-beta.N to v1.3.0-rc.1 + - Bumped main to 1.4.0-beta.0 (unreleased) + - Tagged release-root/1.4.0-beta.N → points to commit before RC branch, message "Base: 1.3.0-rc.1" + - GitHub Discussion created + +# 3. Vote on RC + - Navigate to Discussion thread + - Test RC artifacts + - Vote with +1, 0, -1 + +# 4. Approve RC +Workflow: Approve RC + rc_tag: v1.3.0-rc.1 + Result: + - release/v1.3 @ 1.3.0 (stable) + - Tagged v1.3.0 + - Generated release notes comparing v1.3.0 vs release-root/1.3.0-beta.N + - Created GitHub Release (not pre-release) + - Stable artifacts published + - Release branch auto-bumped to 1.3.1-beta.0 + - Main unchanged (already at 1.4.0-beta.0) + +# 5. Later: Publish first beta after RC (no breaking changes) +Workflow: Publish Beta + branch: main + Result: + - Looks for release-root/1.4.0-beta.N → found + - Extracts base: 1.3.0-rc.1 (major: 1) from tag message + - No breaking changes detected + - Bumped from 1.4.0-beta.0 to 1.4.0-beta.1 + - Tagged v1.4.0-beta.1 + - Created GitHub Pre-Release with release notes from release-root/1.4.0-beta.N to v1.4.0-beta.1 + - Published artifacts to fury.io + +# 6. More changes, publish second beta (breaking changes introduced!) +Workflow: Publish Beta + branch: main + Result: + - Looks for release-root/1.4.0-beta.N → found + - Extracts base: 1.3.0-rc.1 (major: 1) from tag message + - Breaking changes detected, current major (1) == base major (1) + - Bumped from 1.4.0-beta.1 to 2.0.0-beta.1 (beta resets on major bump) + - Created release-root/2.0.0-beta.N → same commit, message "Base: 1.3.0-rc.1" + - Tagged v2.0.0-beta.1 + - Created GitHub Pre-Release with release notes from release-root/2.0.0-beta.N to v2.0.0-beta.1 + - Published artifacts to fury.io + +# 7. More changes, publish third beta (still has breaking changes) +Workflow: Publish Beta + branch: main + Result: + - Looks for release-root/2.0.0-beta.N → found + - Extracts base: 1.3.0-rc.1 (major: 1) from tag message + - Breaking changes detected, but current major (2) > base major (1) + - No major bump needed (already bumped from base) + - Bumped to 2.0.0-beta.2 + - Tagged v2.0.0-beta.2 + - Published artifacts + +# 8. Eventually: Cut RC for v2.0.0 +Workflow: Create Release Branch + Result: + - Main at 2.0.0-beta.2 + - Checks for breaking changes since release-root/2.0.0-beta.N + - No additional breaking changes (major already bumped) + - Created release/v2.0 at 2.0.0-rc.1 + - Tagged v2.0.0-rc.1 + - Bumped main to 2.1.0-beta.0 + - Tagged release-root/2.1.0-beta.N → points to commit before RC branch, message "Base: 2.0.0-rc.1" ``` -### Failed Release -If a release fails: -1. Check the GitHub Actions logs -2. Fix any issues -3. Re-run with `dry_run: true` first -4. Once successful, run with `dry_run: false` +### Patch Release -### Manual Version Update -If you need to update versions manually: ```bash -bump-my-version bump --new-version 0.33.0 -cargo update -p lance # Update lock files +# 1. Start with release/v1.3 @ 1.3.1-beta.0 (auto-bumped after previous stable release) +# 2. Critical bug found in 1.3.0 +# 3. Fix committed to release/v1.3 +# 4. Create patch RC +Workflow: Create RC + release_branch: release/v1.3 + Result: + - Branch at 1.3.1-beta.0 + - Created 1.3.1-rc.1 + - Tagged v1.3.1-rc.1 + - Created GitHub Pre-Release with release notes from v1.3.0 to v1.3.1-rc.1 + - GitHub Discussion created + +# 5. Vote passes +# 6. Approve patch RC +Workflow: Approve RC + rc_tag: v1.3.1-rc.1 + Result: + - release/v1.3 @ 1.3.1 + - Tagged v1.3.1 + - Generated release notes comparing v1.3.1 vs v1.3.0 + - Created GitHub Release (not pre-release) + - Stable artifacts published + - Auto-bumped to 1.3.2-beta.0 (ready for next patch) + - Main unchanged ``` -## Key Files +### RC Iteration Due to Issues -- `.bumpversion.toml` - Configuration for version management -- `ci/bump_version.py` - Version update orchestration -- `ci/check_breaking_changes.py` - Breaking change detection -- `.github/workflows/make-release-commit.yml` - Main release workflow -- `.github/workflows/bump-version/action.yml` - Version bump action \ No newline at end of file +```bash +# 1. Create initial RC +RC: v1.3.0-rc.1 on release/v1.3 + +# 2. Issue found during testing +# 3. Fix committed to release/v1.3 branch +# 4. Create new RC +Workflow: Create RC + release_branch: release/v1.3 + Result: + - Branch at 1.3.0-rc.1 + - Auto-incremented to 1.3.0-rc.2 + - Tagged v1.3.0-rc.2 + - Created GitHub Pre-Release with release notes from release-root/1.3.0-beta.N to v1.3.0-rc.2 (same comparison as rc.1, showing all changes) + - GitHub Discussion created + +# 5. Vote passes +# 6. Approve rc.2 +Workflow: Approve RC + rc_tag: v1.3.0-rc.2 + Result: + - release/v1.3 @ 1.3.0 + - Tagged v1.3.0 + - Generated release notes comparing v1.3.0 vs release-root/1.3.0-beta.N (includes fixes from rc.1 and rc.2) + - Created GitHub Release (not pre-release) + - Stable artifacts published + - Release branch auto-bumped to 1.3.1-beta.0 + - Main unchanged +```