Skip to content

CD: Bump Version

CD: Bump Version #133

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.