name: Release concurrency: group: release-main cancel-in-progress: false on: workflow_dispatch: inputs: version_bump: description: "Version bump type (none = release beta version as-is)" type: choice options: - patch - minor - major - none default: patch required: true jobs: bump: name: Bump version, tag, and create release runs-on: ubuntu-latest permissions: contents: write pull-requests: write outputs: new_version: ${{ steps.compute.outputs.new_version }} tag: ${{ steps.tag.outputs.tag }} bump_branch: ${{ steps.bump_branch.outputs.name }} steps: - name: Ensure workflow is running on main shell: bash run: | set -euo pipefail if [[ "${GITHUB_REF_NAME}" != "main" ]]; then echo "This workflow must be run on the main branch. Current ref: ${GITHUB_REF_NAME}" >&2 exit 1 fi - name: Checkout repository uses: actions/checkout@v6 with: ref: main fetch-depth: 0 - name: Show current versions id: preview shell: bash run: | set -euo pipefail echo "============================================" echo "CURRENT VERSION STATUS" echo "============================================" # Get main version MAIN_VERSION=$(jq -r '.version' "MCPForUnity/package.json") MAIN_PYPI=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml) echo "Main branch:" echo " Unity package: $MAIN_VERSION" echo " PyPI server: $MAIN_PYPI" echo "" # Get beta version git fetch origin beta BETA_VERSION=$(git show origin/beta:MCPForUnity/package.json | jq -r '.version') BETA_PYPI=$(git show origin/beta:Server/pyproject.toml | grep -oP '(?<=version = ")[^"]+') echo "Beta branch:" echo " Unity package: $BETA_VERSION" echo " PyPI server: $BETA_PYPI" echo "" # Compute stripped version (used for "none" bump option) STRIPPED=$(echo "$BETA_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//') echo "stripped_version=$STRIPPED" >> "$GITHUB_OUTPUT" # Show what will happen BUMP="${{ inputs.version_bump }}" echo "Selected bump type: $BUMP" echo "After stripping beta suffix: $STRIPPED" if [[ "$BUMP" == "none" ]]; then echo "Release version will be: $STRIPPED" else IFS='.' read -r MA MI PA <<< "$STRIPPED" case "$BUMP" in major) ((MA+=1)); MI=0; PA=0 ;; minor) ((MI+=1)); PA=0 ;; patch) ((PA+=1)) ;; esac echo "Release version will be: $MA.$MI.$PA" fi echo "============================================" - name: Merge beta into main shell: bash run: | set -euo pipefail git config user.name "GitHub Actions" git config user.email "actions@github.com" # Fetch beta branch git fetch origin beta # Check if beta has changes not in main if git merge-base --is-ancestor origin/beta HEAD; then echo "beta is already merged into main. Nothing to merge." else echo "Merging beta into main..." git merge origin/beta --no-edit -m "chore: merge beta into main for release" echo "Beta merged successfully." fi - name: Strip beta suffix from version if present shell: bash run: | set -euo pipefail CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json") echo "Current version: $CURRENT_VERSION" # Strip beta/alpha/rc suffix if present (e.g., "9.4.0-beta.1" -> "9.4.0") if [[ "$CURRENT_VERSION" == *"-"* ]]; then STABLE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//') # Validate we have a proper X.Y.Z format after stripping if ! [[ "$STABLE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Error: Could not parse version '$CURRENT_VERSION' -> '$STABLE_VERSION'" >&2 exit 1 fi echo "Stripping prerelease suffix: $CURRENT_VERSION -> $STABLE_VERSION" jq --arg v "$STABLE_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json mv tmp.json MCPForUnity/package.json # Also update pyproject.toml sed -i "s/^version = .*/version = \"${STABLE_VERSION}\"/" Server/pyproject.toml else echo "Version is already stable: $CURRENT_VERSION" fi - name: Compute new version id: compute env: PREVIEWED_STRIPPED: ${{ steps.preview.outputs.stripped_version }} shell: bash run: | set -euo pipefail BUMP="${{ inputs.version_bump }}" CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json") echo "Current version: $CURRENT_VERSION" # Sanity check: ensure current version matches what was previewed if [[ "$CURRENT_VERSION" != "$PREVIEWED_STRIPPED" ]]; then echo "Warning: Current version ($CURRENT_VERSION) differs from previewed ($PREVIEWED_STRIPPED)" echo "This may indicate an unexpected merge result. Proceeding with current version." fi if [[ "$BUMP" == "none" ]]; then # Use the previewed stripped version to ensure consistency with what user saw NEW_VERSION="$PREVIEWED_STRIPPED" else IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION" case "$BUMP" in major) ((MA+=1)); MI=0; PA=0 ;; minor) ((MI+=1)); PA=0 ;; patch) ((PA+=1)) ;; *) echo "Unknown version_bump: $BUMP" >&2 exit 1 ;; esac NEW_VERSION="$MA.$MI.$PA" fi echo "New version: $NEW_VERSION" echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" - name: Compute tag id: tag env: NEW_VERSION: ${{ steps.compute.outputs.new_version }} shell: bash run: | set -euo pipefail echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Update files to new version env: NEW_VERSION: ${{ steps.compute.outputs.new_version }} shell: bash run: | set -euo pipefail echo "Updating all version references to $NEW_VERSION" python3 tools/update_versions.py --version "$NEW_VERSION" - name: Commit version bump to a temporary branch id: bump_branch env: NEW_VERSION: ${{ steps.compute.outputs.new_version }} shell: bash run: | set -euo pipefail BRANCH="release/v${NEW_VERSION}" echo "name=$BRANCH" >> "$GITHUB_OUTPUT" git config user.name "GitHub Actions" git config user.email "actions@github.com" git checkout -b "$BRANCH" git add MCPForUnity/package.json manifest.json "Server/pyproject.toml" Server/README.md if git diff --cached --quiet; then echo "No version changes to commit." else git commit -m "chore: bump version to ${NEW_VERSION}" fi echo "Pushing bump branch $BRANCH" git push origin "$BRANCH" - name: Create PR for version bump into main id: bump_pr env: GH_TOKEN: ${{ github.token }} NEW_VERSION: ${{ steps.compute.outputs.new_version }} BRANCH: ${{ steps.bump_branch.outputs.name }} shell: bash run: | set -euo pipefail PR_URL=$(gh pr create \ --base main \ --head "$BRANCH" \ --title "chore: bump version to ${NEW_VERSION}" \ --body "Automated version bump to ${NEW_VERSION}.") echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" - name: Enable auto-merge and merge PR env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ steps.bump_pr.outputs.pr_number }} shell: bash run: | set -euo pipefail # Enable auto-merge (requires repo setting "Allow auto-merge") gh pr merge "$PR_NUMBER" --merge --auto || true # Wait for PR to be merged (poll up to 2 minutes) for i in {1..24}; do STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state') if [[ "$STATE" == "MERGED" ]]; then echo "PR merged successfully." exit 0 fi echo "Waiting for PR to merge... (state: $STATE)" sleep 5 done echo "PR did not merge in time. Attempting direct merge..." gh pr merge "$PR_NUMBER" --merge - name: Fetch merged main and create tag env: TAG: ${{ steps.tag.outputs.tag }} shell: bash run: | set -euo pipefail git fetch origin main git checkout main git pull origin main echo "Preparing to create tag $TAG" if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then echo "Tag $TAG already exists on remote. Refusing to release." >&2 exit 1 fi git tag -a "$TAG" -m "Version ${TAG#v}" git push origin "$TAG" - name: Clean up release branch if: always() env: GH_TOKEN: ${{ github.token }} BRANCH: ${{ steps.bump_branch.outputs.name }} shell: bash run: | set -euo pipefail git push origin --delete "$BRANCH" || true - name: Create GitHub release uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.tag.outputs.tag }} name: ${{ steps.tag.outputs.tag }} generate_release_notes: true sync_beta: name: Merge main back into beta via PR needs: - bump runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Checkout main uses: actions/checkout@v6 with: ref: main fetch-depth: 0 - name: Create PR to merge main into beta id: sync_pr env: GH_TOKEN: ${{ github.token }} NEW_VERSION: ${{ needs.bump.outputs.new_version }} shell: bash run: | set -euo pipefail # Check if beta is behind main git fetch origin beta if git merge-base --is-ancestor origin/main origin/beta; then echo "beta is already up to date with main. Skipping PR." echo "skipped=true" >> "$GITHUB_OUTPUT" exit 0 fi PR_URL=$(gh pr create \ --base beta \ --head main \ --title "chore: sync main (v${NEW_VERSION}) into beta" \ --body "Automated sync of version bump from main into beta.") echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" echo "skipped=false" >> "$GITHUB_OUTPUT" - name: Enable auto-merge and merge sync PR if: steps.sync_pr.outputs.skipped != 'true' env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ steps.sync_pr.outputs.pr_number }} shell: bash run: | set -euo pipefail # Enable auto-merge (requires repo setting "Allow auto-merge") # Use --no-delete-branch to prevent deleting main (the head branch) gh pr merge "$PR_NUMBER" --merge --auto --no-delete-branch || true # Wait for PR to be merged (poll up to 2 minutes) for i in {1..24}; do STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state') if [[ "$STATE" == "MERGED" ]]; then echo "Sync PR merged successfully." exit 0 fi echo "Waiting for sync PR to merge... (state: $STATE)" sleep 5 done echo "Sync PR did not merge in time. Attempting direct merge..." gh pr merge "$PR_NUMBER" --merge --no-delete-branch publish_docker: name: Publish Docker image needs: - bump runs-on: ubuntu-latest permissions: contents: read steps: - name: Check out the repo uses: actions/checkout@v6 with: ref: ${{ needs.bump.outputs.tag }} fetch-depth: 0 - name: Build and push Docker image uses: ./.github/actions/publish-docker with: docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} image: ${{ secrets.DOCKER_USERNAME }}/mcp-for-unity-server version: ${{ needs.bump.outputs.new_version }} include_branch_tags: "false" context: . dockerfile: Server/Dockerfile platforms: linux/amd64 publish_pypi: name: Publish Python distribution to PyPI needs: - bump runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/mcpforunityserver permissions: contents: read id-token: write steps: - uses: actions/checkout@v6 with: ref: ${{ needs.bump.outputs.tag }} fetch-depth: 0 # Inlined from .github/actions/publish-pypi to avoid nested composite action issue # with pypa/gh-action-pypi-publish (see https://github.com/pypa/gh-action-pypi-publish/issues/338) - name: Install uv uses: astral-sh/setup-uv@v7 with: version: "latest" enable-cache: true cache-dependency-glob: "Server/uv.lock" - name: Build a binary wheel and a source tarball shell: bash run: uv build working-directory: ./Server - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: Server/dist/ publish_mcpb: name: Generate and publish MCPB bundle needs: - bump runs-on: ubuntu-latest permissions: contents: write steps: - name: Check out the repo uses: actions/checkout@v6 with: ref: ${{ needs.bump.outputs.tag }} fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "20" - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Generate MCPB bundle env: NEW_VERSION: ${{ needs.bump.outputs.new_version }} shell: bash run: | set -euo pipefail python3 tools/generate_mcpb.py "$NEW_VERSION" \ --output "unity-mcp-${NEW_VERSION}.mcpb" \ --icon docs/images/coplay-logo.png - name: Upload MCPB to release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.bump.outputs.tag }} files: unity-mcp-${{ needs.bump.outputs.new_version }}.mcpb