CD: Bump Version #133
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: "CD: Bump Version" | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| service: | |
| description: "Service/Package to bump" | |
| required: true | |
| type: choice | |
| options: | |
| - pypi/agent | |
| - pypi/bench | |
| - pypi/bench-ui | |
| - pypi/computer | |
| - pypi/computer-server | |
| - pypi/core | |
| - pypi/mcp-server | |
| - pypi/som | |
| - npm/cli | |
| - npm/computer | |
| - npm/core | |
| - lume | |
| - docker/kasm | |
| - docker/xfce | |
| - docker/lumier | |
| - docker/qemu-android | |
| - docker/qemu-linux | |
| - docker/qemu-windows | |
| bump_type: | |
| description: "Version bump type" | |
| required: true | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| permissions: | |
| contents: write | |
| jobs: | |
| bump-version: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Set package directory and type | |
| id: package | |
| run: | | |
| case "${{ inputs.service }}" in | |
| "pypi/agent") | |
| echo "directory=libs/python/agent" >> $GITHUB_OUTPUT | |
| echo "type=python" >> $GITHUB_OUTPUT | |
| ;; | |
| "pypi/bench") | |
| echo "directory=libs/cua-bench" >> $GITHUB_OUTPUT | |
| echo "type=python" >> $GITHUB_OUTPUT | |
| ;; | |
| "pypi/bench-ui") | |
| echo "directory=libs/python/bench-ui" >> $GITHUB_OUTPUT | |
| echo "type=python" >> $GITHUB_OUTPUT | |
| ;; | |
| "pypi/computer") | |
| echo "directory=libs/python/computer" >> $GITHUB_OUTPUT | |
| echo "type=python" >> $GITHUB_OUTPUT | |
| ;; | |
| "pypi/computer-server") | |
| echo "directory=libs/python/computer-server" >> $GITHUB_OUTPUT | |
| echo "type=python" >> $GITHUB_OUTPUT | |
| ;; | |
| "pypi/core") | |
| echo "directory=libs/python/core" >> $GITHUB_OUTPUT | |
| echo "type=python" >> $GITHUB_OUTPUT | |
| ;; | |
| "pypi/mcp-server") | |
| echo "directory=libs/python/mcp-server" >> $GITHUB_OUTPUT | |
| echo "type=python" >> $GITHUB_OUTPUT | |
| ;; | |
| "pypi/som") | |
| echo "directory=libs/python/som" >> $GITHUB_OUTPUT | |
| echo "type=python" >> $GITHUB_OUTPUT | |
| ;; | |
| "npm/cli") | |
| echo "directory=libs/typescript/cua-cli" >> $GITHUB_OUTPUT | |
| echo "type=npm" >> $GITHUB_OUTPUT | |
| ;; | |
| "npm/computer") | |
| echo "directory=libs/typescript/computer" >> $GITHUB_OUTPUT | |
| echo "type=npm" >> $GITHUB_OUTPUT | |
| ;; | |
| "npm/core") | |
| echo "directory=libs/typescript/core" >> $GITHUB_OUTPUT | |
| echo "type=npm" >> $GITHUB_OUTPUT | |
| ;; | |
| "lume") | |
| echo "directory=libs/lume" >> $GITHUB_OUTPUT | |
| echo "type=lume" >> $GITHUB_OUTPUT | |
| ;; | |
| "docker/kasm") | |
| echo "directory=libs/kasm" >> $GITHUB_OUTPUT | |
| echo "type=docker" >> $GITHUB_OUTPUT | |
| ;; | |
| "docker/xfce") | |
| echo "directory=libs/xfce" >> $GITHUB_OUTPUT | |
| echo "type=docker" >> $GITHUB_OUTPUT | |
| ;; | |
| "docker/lumier") | |
| echo "directory=libs/lumier" >> $GITHUB_OUTPUT | |
| echo "type=docker" >> $GITHUB_OUTPUT | |
| ;; | |
| "docker/qemu-android") | |
| echo "directory=libs/qemu-docker/android" >> $GITHUB_OUTPUT | |
| echo "type=docker" >> $GITHUB_OUTPUT | |
| ;; | |
| "docker/qemu-linux") | |
| echo "directory=libs/qemu-docker/linux" >> $GITHUB_OUTPUT | |
| echo "type=docker" >> $GITHUB_OUTPUT | |
| ;; | |
| "docker/qemu-windows") | |
| echo "directory=libs/qemu-docker/windows" >> $GITHUB_OUTPUT | |
| echo "type=docker" >> $GITHUB_OUTPUT | |
| ;; | |
| *) | |
| echo "Unknown service: ${{ inputs.service }}" | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Install bump2version | |
| run: pip install bump2version | |
| - name: Configure Git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Clean existing tags if present | |
| run: | | |
| # Function to clean a tag for a given package directory | |
| clean_tag() { | |
| local dir=$1 | |
| cd "$dir" | |
| NEW_VERSION=$(bump2version --dry-run --list ${{ inputs.bump_type }} 2>/dev/null | grep -m1 '^new_version=' | sed 's/new_version=//') | |
| TAG_NAME=$(grep -m1 '^tag_name' .bumpversion.cfg | sed 's/tag_name *= *//' | sed "s|{new_version}|$NEW_VERSION|") | |
| echo "Checking for existing tag: $TAG_NAME" | |
| git tag -d "$TAG_NAME" 2>/dev/null || true | |
| git push origin ":refs/tags/$TAG_NAME" 2>/dev/null || true | |
| cd - > /dev/null | |
| } | |
| # Clean tag for the primary package | |
| clean_tag "${{ steps.package.outputs.directory }}" | |
| # Clean cascade tags when bumping pypi/core (also bumps computer and agent) | |
| if [ "${{ inputs.service }}" == "pypi/core" ]; then | |
| clean_tag "libs/python/computer" | |
| clean_tag "libs/python/agent" | |
| fi | |
| # Clean cascade tags when bumping pypi/computer (also bumps agent) | |
| if [ "${{ inputs.service }}" == "pypi/computer" ]; then | |
| clean_tag "libs/python/agent" | |
| fi | |
| # Clean cascade tags when bumping pypi/som (also bumps agent) | |
| if [ "${{ inputs.service }}" == "pypi/som" ]; then | |
| clean_tag "libs/python/agent" | |
| fi | |
| # Clean cascade tags when bumping npm/core (also bumps npm/computer) | |
| if [ "${{ inputs.service }}" == "npm/core" ]; then | |
| clean_tag "libs/typescript/computer" | |
| fi | |
| - name: Save starting commit | |
| id: start_commit | |
| run: | | |
| echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT | |
| - name: Run bump2version | |
| run: | | |
| cd ${{ steps.package.outputs.directory }} | |
| bump2version ${{ inputs.bump_type }} | |
| - name: Also bump cua-computer (when bumping cua-core) | |
| if: ${{ inputs.service == 'pypi/core' }} | |
| run: | | |
| cd libs/python/computer | |
| bump2version ${{ inputs.bump_type }} | |
| - name: Also bump cua-agent (when bumping cua-computer or cua-core) | |
| if: ${{ inputs.service == 'pypi/computer' || inputs.service == 'pypi/core' }} | |
| run: | | |
| cd libs/python/agent | |
| bump2version ${{ inputs.bump_type }} | |
| - name: Also bump cua-agent (when bumping cua-som) | |
| if: ${{ inputs.service == 'pypi/som' }} | |
| run: | | |
| cd libs/python/agent | |
| bump2version ${{ inputs.bump_type }} | |
| - name: Also bump @trycua/computer (when bumping @trycua/core) | |
| if: ${{ inputs.service == 'npm/core' }} | |
| run: | | |
| cd libs/typescript/computer | |
| bump2version ${{ inputs.bump_type }} | |
| - name: Collect all created tags | |
| id: collect_tags | |
| run: | | |
| # Find all tags pointing to commits after the starting commit | |
| START_SHA="${{ steps.start_commit.outputs.sha }}" | |
| echo "Starting commit: $START_SHA" | |
| # Get all local tags and filter to those pointing to commits after START_SHA | |
| ALL_TAGS="" | |
| for tag in $(git tag --points-at HEAD) $(git tag --points-at HEAD~1 2>/dev/null) $(git tag --points-at HEAD~2 2>/dev/null); do | |
| if [ -n "$tag" ]; then | |
| TAG_SHA=$(git rev-parse "$tag^{commit}" 2>/dev/null || true) | |
| if [ -n "$TAG_SHA" ]; then | |
| # Check if this tag's commit is a descendant of (or equal to) START_SHA | |
| if git merge-base --is-ancestor "$START_SHA" "$TAG_SHA" 2>/dev/null; then | |
| ALL_TAGS="$ALL_TAGS $tag" | |
| fi | |
| fi | |
| fi | |
| done | |
| # Remove duplicates and trim | |
| ALL_TAGS=$(echo "$ALL_TAGS" | tr ' ' '\n' | sort -u | tr '\n' ' ' | xargs) | |
| echo "Tags to push: $ALL_TAGS" | |
| echo "tags=$ALL_TAGS" >> $GITHUB_OUTPUT | |
| - name: Capture bumped agent version | |
| if: ${{ inputs.service == 'pypi/agent' || inputs.service == 'pypi/computer' || inputs.service == 'pypi/core' || inputs.service == 'pypi/som' }} | |
| id: agent_version | |
| run: | | |
| cd libs/python/agent | |
| VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])") | |
| echo "Agent version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped bench version | |
| if: ${{ inputs.service == 'pypi/bench' }} | |
| id: bench_version | |
| run: | | |
| cd libs/cua-bench | |
| VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])") | |
| echo "Bench version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped bench-ui version | |
| if: ${{ inputs.service == 'pypi/bench-ui' }} | |
| id: bench_ui_version | |
| run: | | |
| cd libs/python/bench-ui | |
| VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])") | |
| echo "Bench-ui version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped computer version | |
| if: ${{ inputs.service == 'pypi/computer' || inputs.service == 'pypi/core' }} | |
| id: computer_version | |
| run: | | |
| cd libs/python/computer | |
| VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])") | |
| echo "Computer version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped computer-server version | |
| if: ${{ inputs.service == 'pypi/computer-server' }} | |
| id: computer_server_version | |
| run: | | |
| cd libs/python/computer-server | |
| VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])") | |
| echo "Computer-server version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped core version | |
| if: ${{ inputs.service == 'pypi/core' }} | |
| id: core_version | |
| run: | | |
| cd libs/python/core | |
| VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])") | |
| echo "Core version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped mcp-server version | |
| if: ${{ inputs.service == 'pypi/mcp-server' }} | |
| id: mcp_server_version | |
| run: | | |
| cd libs/python/mcp-server | |
| VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])") | |
| echo "MCP-server version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped som version | |
| if: ${{ inputs.service == 'pypi/som' }} | |
| id: som_version | |
| run: | | |
| cd libs/python/som | |
| VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])") | |
| echo "SOM version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped npm/cli version | |
| if: ${{ inputs.service == 'npm/cli' }} | |
| id: npm_cli_version | |
| run: | | |
| VERSION=$(grep -oP '"version": "\K[^"]+' libs/typescript/cua-cli/package.json) | |
| echo "npm/cli version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped npm/computer version | |
| if: ${{ inputs.service == 'npm/computer' || inputs.service == 'npm/core' }} | |
| id: npm_computer_version | |
| run: | | |
| VERSION=$(grep -oP '"version": "\K[^"]+' libs/typescript/computer/package.json) | |
| echo "npm/computer version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped npm/core version | |
| if: ${{ inputs.service == 'npm/core' }} | |
| id: npm_core_version | |
| run: | | |
| VERSION=$(grep -oP '"version": "\K[^"]+' libs/typescript/core/package.json) | |
| echo "npm/core version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Capture bumped lume version | |
| if: ${{ inputs.service == 'lume' }} | |
| id: lume_version | |
| run: | | |
| VERSION=$(grep -oP 'static let current: String = "\K[^"]+' libs/lume/src/Main.swift) | |
| echo "lume version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.RELEASE_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| repositories: ${{ github.event.repository.name }} | |
| - name: Push release to main | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| # Configure git to use the app token | |
| git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" | |
| # Verify the token works | |
| echo "Verifying GitHub App token..." | |
| gh auth status | |
| # Get all tags created by bump2version | |
| ALL_TAGS="${{ steps.collect_tags.outputs.tags }}" | |
| echo "Tags to push: $ALL_TAGS" | |
| # Use the first tag for naming the temp branch | |
| FIRST_TAG=$(echo "$ALL_TAGS" | awk '{print $1}') | |
| TEMP_BRANCH="temp-release-${FIRST_TAG}" | |
| echo "Temp branch: $TEMP_BRANCH" | |
| # Retry loop for handling concurrent bumps | |
| MAX_RETRIES=5 | |
| RETRY_COUNT=0 | |
| while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do | |
| RETRY_COUNT=$((RETRY_COUNT + 1)) | |
| echo "" | |
| echo "=== Attempt $RETRY_COUNT/$MAX_RETRIES ===" | |
| # Fetch latest main to check if we need to rebase | |
| git fetch origin main | |
| # Check if our HEAD is a descendant of origin/main (fast-forward possible) | |
| if ! git merge-base --is-ancestor origin/main HEAD; then | |
| echo "Main branch has moved forward, rebasing..." | |
| # Rebase our commit(s) onto the latest main | |
| git rebase origin/main | |
| # Update ALL local tags to point to their rebased commits | |
| # Each tag should point to the commit with its corresponding message | |
| for tag in $ALL_TAGS; do | |
| # Find the commit message pattern for this tag | |
| TAG_MSG=$(echo "$tag" | sed 's/-v/ to v/') | |
| TAG_MSG="Bump $TAG_MSG" | |
| # Find the commit with this message and update the tag | |
| COMMIT_FOR_TAG=$(git log --oneline --grep="$TAG_MSG" -1 --format="%H" 2>/dev/null || true) | |
| if [ -n "$COMMIT_FOR_TAG" ]; then | |
| git tag -f "$tag" "$COMMIT_FOR_TAG" | |
| echo "Updated tag $tag to commit $COMMIT_FOR_TAG" | |
| else | |
| # Fallback: point to HEAD | |
| git tag -f "$tag" HEAD | |
| echo "Updated tag $tag to HEAD (fallback)" | |
| fi | |
| done | |
| fi | |
| # Get the current commit SHA (may have changed after rebase) | |
| COMMIT_SHA=$(git rev-parse HEAD) | |
| echo "Commit SHA: $COMMIT_SHA" | |
| # Push to temp branch | |
| echo "Pushing to temp branch..." | |
| git push origin "HEAD:refs/heads/${TEMP_BRANCH}" --force | |
| # Try to update main branch via API | |
| echo "Updating main branch via API..." | |
| if gh api -X PATCH /repos/${{ github.repository }}/git/refs/heads/main \ | |
| -f sha="${COMMIT_SHA}" \ | |
| -F force=false 2>&1; then | |
| echo "Successfully updated main branch!" | |
| # Now create ALL tags via API (only after main is successfully updated) | |
| echo "Creating tags via API..." | |
| for tag in $ALL_TAGS; do | |
| echo "Creating tag: $tag" | |
| # Get the SHA for this specific tag | |
| TAG_SHA=$(git rev-parse "$tag^{commit}" 2>/dev/null || echo "$COMMIT_SHA") | |
| # Delete existing remote tag if present (in case of retry with different SHA) | |
| gh api -X DELETE /repos/${{ github.repository }}/git/refs/tags/${tag} 2>/dev/null || true | |
| gh api /repos/${{ github.repository }}/git/refs \ | |
| -f ref="refs/tags/${tag}" \ | |
| -f sha="${TAG_SHA}" | |
| echo "Tag $tag created successfully!" | |
| done | |
| # Clean up temp branch | |
| echo "Cleaning up temp branch..." | |
| git push origin --delete "${TEMP_BRANCH}" || true | |
| exit 0 | |
| fi | |
| echo "Failed to update main (not a fast-forward), will retry..." | |
| sleep 2 | |
| done | |
| echo "ERROR: Failed to update main branch after $MAX_RETRIES attempts" | |
| exit 1 | |
| # NOTE: Publishing is handled by tag-triggered workflows (cd-py-*.yml, cd-ts-*.yml, cd-swift-lume.yml) | |
| # When bump-version pushes a tag (e.g., agent-v0.7.10), the corresponding CD workflow | |
| # is automatically triggered by the tag push event. No need to call them here. |