Bump Version and Create Release #46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Bump Version and Create Release | |
| # ============================================================================= | |
| # RELEASE WORKFLOW | |
| # ============================================================================= | |
| # | |
| # This workflow creates releases using CHANGELOG.md as the single source of truth. | |
| # | |
| # BEFORE RUNNING THIS WORKFLOW: | |
| # 1. Update CHANGELOG.md with your changes under the [Unreleased] section | |
| # 2. Follow Keep a Changelog format (https://keepachangelog.com/) | |
| # 3. Use categories: Added, Changed, Deprecated, Removed, Fixed, Security | |
| # | |
| # WHAT THIS WORKFLOW DOES: | |
| # 1. Reads release notes from CHANGELOG.md [Unreleased] section | |
| # 2. Creates a git tag and GitHub release with those notes | |
| # 3. Updates CHANGELOG.md: renames [Unreleased] to [version] - date | |
| # 4. Adds a new empty [Unreleased] section | |
| # 5. Commits the CHANGELOG.md changes | |
| # 6. Triggers downstream workflows (assets, docs, etc.) | |
| # | |
| # See CONTRIBUTING.md for detailed documentation. | |
| # ============================================================================= | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| bump_type: | |
| description: 'Version bump type' | |
| required: true | |
| type: choice | |
| options: | |
| - major | |
| - minor | |
| - patch | |
| default: patch | |
| pre_release: | |
| description: 'Pre-release type (leave empty for stable release)' | |
| required: false | |
| type: choice | |
| options: | |
| - none | |
| - alpha | |
| - beta | |
| - rc | |
| default: none | |
| pre_number: | |
| description: 'Pre-release number (if pre_release is set)' | |
| required: false | |
| type: string | |
| default: '1' | |
| create_release: | |
| description: 'Create GitHub release' | |
| required: false | |
| type: boolean | |
| default: true | |
| jobs: | |
| bump-version: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| outputs: | |
| tag: ${{ steps.new_version.outputs.tag }} | |
| new_version: ${{ steps.new_version.outputs.new_version }} | |
| current_version: ${{ steps.current_version.outputs.current }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Get current version | |
| id: current_version | |
| run: | | |
| # Get latest stable version tag (exclude pre-releases) | |
| CURRENT=$(git tag --sort=-version:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 | sed 's/^v//' | sed 's/^xbbg==//') | |
| if [ -z "$CURRENT" ]; then | |
| CURRENT="0.0.0" | |
| fi | |
| echo "current=$CURRENT" >> "$GITHUB_OUTPUT" | |
| echo "Current version: $CURRENT" | |
| - name: Calculate new version | |
| id: new_version | |
| run: | | |
| CURRENT="${{ steps.current_version.outputs.current }}" | |
| BUMP_TYPE="${{ inputs.bump_type }}" | |
| PRE_RELEASE="${{ inputs.pre_release }}" | |
| PRE_NUM="${{ inputs.pre_number }}" | |
| # Parse current version | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" | |
| # Bump version | |
| case "$BUMP_TYPE" in | |
| major) | |
| MAJOR=$((MAJOR + 1)) | |
| MINOR=0 | |
| PATCH=0 | |
| ;; | |
| minor) | |
| MINOR=$((MINOR + 1)) | |
| PATCH=0 | |
| ;; | |
| patch) | |
| PATCH=$((PATCH + 1)) | |
| ;; | |
| esac | |
| NEW_VERSION="$MAJOR.$MINOR.$PATCH" | |
| # Add pre-release suffix if specified | |
| if [ -n "$PRE_RELEASE" ] && [ "$PRE_RELEASE" != "none" ]; then | |
| # Map alpha/beta/rc to a/b/rc format | |
| case "$PRE_RELEASE" in | |
| alpha) PRE_SUFFIX="a" ;; | |
| beta) PRE_SUFFIX="b" ;; | |
| rc) PRE_SUFFIX="rc" ;; | |
| *) PRE_SUFFIX="$PRE_RELEASE" ;; | |
| esac | |
| NEW_VERSION="${NEW_VERSION}${PRE_SUFFIX}${PRE_NUM}" | |
| fi | |
| echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "New version: $NEW_VERSION" | |
| echo "Tag: v${NEW_VERSION}" | |
| - name: Validate and extract release notes from CHANGELOG.md | |
| id: changelog | |
| run: | | |
| TAG="${{ steps.new_version.outputs.tag }}" | |
| PREV_TAG="v${{ steps.current_version.outputs.current }}" | |
| # Extract content between [Unreleased] and the next ## heading | |
| # This captures all content under [Unreleased] section | |
| RELEASE_NOTES=$(awk ' | |
| /^## \[Unreleased\]/ { capture=1; next } | |
| /^## \[/ { if(capture) exit } | |
| capture { print } | |
| ' CHANGELOG.md | head -100) | |
| # Remove leading/trailing whitespace and empty lines for validation | |
| RELEASE_NOTES_TRIMMED=$(echo "$RELEASE_NOTES" | sed '/^[[:space:]]*$/d' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
| # Validation: Check if [Unreleased] section has meaningful content | |
| if [ -z "$RELEASE_NOTES_TRIMMED" ]; then | |
| echo "::error::CHANGELOG.md [Unreleased] section is empty!" | |
| echo "::error::Please update CHANGELOG.md before releasing." | |
| echo "" | |
| echo "============================================================" | |
| echo "RELEASE BLOCKED: Empty changelog" | |
| echo "============================================================" | |
| echo "" | |
| echo "Before running this workflow, you must update CHANGELOG.md:" | |
| echo "" | |
| echo "1. Add your changes under the [Unreleased] section" | |
| echo "2. Use categories: Added, Changed, Deprecated, Removed, Fixed, Security" | |
| echo "3. Follow Keep a Changelog format (https://keepachangelog.com/)" | |
| echo "" | |
| echo "Example:" | |
| echo " ## [Unreleased]" | |
| echo "" | |
| echo " ### Added" | |
| echo " - New feature X for doing Y (#123)" | |
| echo "" | |
| echo " ### Fixed" | |
| echo " - Bug in Z that caused W (#456)" | |
| echo "" | |
| echo "See CONTRIBUTING.md and .github/RELEASE_INSTRUCTIONS.md for details." | |
| echo "============================================================" | |
| exit 1 | |
| fi | |
| # Validation: Check for placeholder text that shouldn't be released | |
| if echo "$RELEASE_NOTES_TRIMMED" | grep -qiE '(TODO|FIXME|WIP|PLACEHOLDER|TBD|coming soon)'; then | |
| echo "::warning::CHANGELOG.md may contain placeholder text (TODO/FIXME/WIP/TBD)" | |
| echo "::warning::Please review the [Unreleased] section before releasing." | |
| fi | |
| # Validation: Check for minimum content (at least one category header) | |
| if ! echo "$RELEASE_NOTES_TRIMMED" | grep -qE '^### (Added|Changed|Deprecated|Removed|Fixed|Security)'; then | |
| echo "::warning::CHANGELOG.md [Unreleased] section doesn't follow Keep a Changelog format" | |
| echo "::warning::Consider using categories: Added, Changed, Deprecated, Removed, Fixed, Security" | |
| fi | |
| # Write to file for multi-line handling (preserve original formatting) | |
| echo "$RELEASE_NOTES" > /tmp/release_notes.md | |
| # Append full changelog link | |
| echo "" >> /tmp/release_notes.md | |
| echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${TAG}" >> /tmp/release_notes.md | |
| echo "Release notes extracted and validated:" | |
| echo "============================================================" | |
| cat /tmp/release_notes.md | |
| echo "============================================================" | |
| - name: Update CHANGELOG.md and README.md | |
| if: inputs.create_release == true | |
| run: | | |
| VERSION="${{ steps.new_version.outputs.new_version }}" | |
| TAG="${{ steps.new_version.outputs.tag }}" | |
| DATE=$(date -u +"%Y-%m-%d") | |
| # Create the new version header | |
| NEW_HEADER="## [${VERSION}] - ${DATE}" | |
| # Replace [Unreleased] section header with new version, and add new [Unreleased] | |
| # Uses awk to: | |
| # 1. When we see "## [Unreleased]", print new [Unreleased] section + new version header | |
| # 2. Skip the old [Unreleased] line | |
| # 3. Print everything else as-is | |
| awk -v new_header="$NEW_HEADER" ' | |
| /^## \[Unreleased\]/ { | |
| print "## [Unreleased]" | |
| print "" | |
| print new_header | |
| next | |
| } | |
| { print } | |
| ' CHANGELOG.md > /tmp/CHANGELOG.md.new | |
| mv /tmp/CHANGELOG.md.new CHANGELOG.md | |
| # Extract previous tag from the current [Unreleased] compare link | |
| # e.g., [Unreleased]: .../compare/v0.12.0b3...HEAD → v0.12.0b3 | |
| PREV_TAG_LINK=$(grep '^\[Unreleased\]:' CHANGELOG.md | sed 's|.*compare/||;s|\.\.\.HEAD||') | |
| # Add version link using compare format (Keep a Changelog 1.1.0) | |
| if ! grep -q "^\[${VERSION}\]:" CHANGELOG.md; then | |
| sed -i "/^\[Unreleased\]:/a [${VERSION}]: https://github.com/${{ github.repository }}/compare/${PREV_TAG_LINK}...${TAG}" CHANGELOG.md | |
| fi | |
| # Update the [Unreleased] comparison link to compare from new version | |
| sed -i "s|\[Unreleased\]: .*/compare/v.*\.\.\.HEAD|[Unreleased]: https://github.com/${{ github.repository }}/compare/${TAG}...HEAD|" CHANGELOG.md | |
| echo "CHANGELOG.md updated:" | |
| head -50 CHANGELOG.md | |
| python - <<'PY' | |
| from pathlib import Path | |
| version = "${{ steps.new_version.outputs.new_version }}" | |
| tag = "${{ steps.new_version.outputs.tag }}" | |
| readme = Path("README.md") | |
| text = readme.read_text(encoding="utf-8") | |
| start = "<!-- xbbg:latest-release-start -->" | |
| end = "<!-- xbbg:latest-release-end -->" | |
| replacement = ( | |
| f"{start}\n" | |
| f"Latest release: xbbg=={version} (release: [notes](https://github.com/${{ github.repository }}/releases/tag/{tag}))\n" | |
| f"{end}" | |
| ) | |
| if start not in text or end not in text: | |
| raise SystemExit("README.md latest release markers not found") | |
| before, rest = text.split(start, 1) | |
| _, after = rest.split(end, 1) | |
| readme.write_text(before + replacement + after, encoding="utf-8") | |
| PY | |
| echo "README.md release block updated:" | |
| sed -n '20,30p' README.md | |
| - name: Detect latest downloadable Bloomberg C++ SDK version | |
| id: sdk | |
| run: | | |
| INDEX_URL="https://blpapi.bloomberg.com/repository/releases/python/simple/blpapi/" | |
| versions=$(curl -fsSL "$INDEX_URL" \ | |
| | grep -oP 'blpapi-\K[0-9]+\.[0-9]+\.[0-9]+(?:\.[0-9]+)?' \ | |
| | sort -Vr \ | |
| | uniq) | |
| for VERSION in $versions; do | |
| linux_url="https://blpapi.bloomberg.com/download/releases/raw/files/blpapi_cpp_${VERSION}-linux.tar.gz" | |
| macos_url="https://blpapi.bloomberg.com/download/releases/raw/files/blpapi_cpp_${VERSION}-macos-arm64.tar.gz" | |
| windows_url="https://blpapi.bloomberg.com/download/releases/raw/files/blpapi_cpp_${VERSION}-windows.zip" | |
| if curl -fsIL "$linux_url" >/dev/null \ | |
| && curl -fsIL "$macos_url" >/dev/null \ | |
| && curl -fsIL "$windows_url" >/dev/null; then | |
| echo "Detected Bloomberg C++ SDK version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| done | |
| echo "No downloadable Bloomberg C++ SDK archives found" >&2 | |
| exit 1 | |
| - name: Setup Bloomberg SDK | |
| id: sdk-setup | |
| uses: ./.github/actions/setup-blpapi-sdk | |
| with: | |
| version: ${{ steps.sdk.outputs.version }} | |
| os: Linux | |
| - name: Export Bloomberg SDK env | |
| run: | | |
| SDK_ROOT="$GITHUB_WORKSPACE/${{ steps.sdk-setup.outputs.sdk-rel-root }}" | |
| LIB_DIR="$GITHUB_WORKSPACE/${{ steps.sdk-setup.outputs.lib-rel-dir }}" | |
| { | |
| echo "BLPAPI_ROOT=$SDK_ROOT" | |
| echo "LIBRARY_PATH=$LIB_DIR:$LIBRARY_PATH" | |
| echo "LD_LIBRARY_PATH=$LIB_DIR:$LD_LIBRARY_PATH" | |
| } >> "$GITHUB_ENV" | |
| - name: Setup Rust | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| rustflags: "" | |
| - name: Regenerate type stubs | |
| run: | | |
| cargo build -p pyo3-xbbg --bin stub_gen --no-default-features --features live,stub-gen | |
| CARGO_MANIFEST_DIR=${{ github.workspace }}/bindings/pyo3-xbbg ./target/debug/stub_gen | |
| - name: Commit release doc changes | |
| if: inputs.create_release == true | |
| run: | | |
| VERSION="${{ steps.new_version.outputs.new_version }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add CHANGELOG.md README.md py-xbbg/src/xbbg/_core/__init__.pyi py-xbbg/src/xbbg/__init__.pyi py-xbbg/src/xbbg/py.typed | |
| if git diff --cached --quiet; then | |
| echo "No release doc changes" | |
| else | |
| git commit -m "docs(release): update docs and type stubs for ${VERSION} release" | |
| BRANCH="${GITHUB_REF#refs/heads/}" | |
| git push origin HEAD:"${BRANCH}" | |
| fi | |
| - name: Create git tag | |
| run: | | |
| TAG="${{ steps.new_version.outputs.tag }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git tag -a "$TAG" -m "Release $TAG" | |
| git push origin "$TAG" | |
| - name: Create GitHub Release | |
| if: inputs.create_release == true | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.new_version.outputs.tag }} | |
| name: xbbg ${{ steps.new_version.outputs.new_version }} | |
| body_path: /tmp/release_notes.md | |
| prerelease: ${{ inputs.pre_release != 'none' && inputs.pre_release != '' }} | |
| generate_release_notes: false | |
| make_latest: ${{ inputs.pre_release == 'none' || inputs.pre_release == '' }} | |
| draft: false | |
| # Summary job | |
| summary: | |
| needs: [bump-version] | |
| if: always() && inputs.create_release == true | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Output summary | |
| run: | | |
| { | |
| echo "## Release Summary" | |
| echo "" | |
| echo "- **Previous version**: ${{ needs.bump-version.outputs.current_version }}" | |
| echo "- **New version**: ${{ needs.bump-version.outputs.new_version }}" | |
| echo "- **Tag**: ${{ needs.bump-version.outputs.tag }}" | |
| echo "- **Type**: ${{ inputs.bump_type }}${{ inputs.pre_release != 'none' && inputs.pre_release != '' && format(' ({0})', inputs.pre_release) || '' }}" | |
| echo "" | |
| echo "### Next Steps" | |
| echo "The tag push will automatically trigger **pypi_upload.yml** for Python artifacts and **npm-publish.yml** for @xbbg/core native npm packages." | |
| } >> "$GITHUB_STEP_SUMMARY" |