Release #36
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: 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 beta | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: beta | |
| fetch-depth: 0 | |
| - name: Prepare sync branch from beta with merged main | |
| id: sync_branch | |
| env: | |
| NEW_VERSION: ${{ needs.bump.outputs.new_version }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git config user.name "GitHub Actions" | |
| git config user.email "actions@github.com" | |
| # Fetch both branches so we can build a merge commit in CI. | |
| git fetch origin main beta | |
| if git merge-base --is-ancestor origin/main origin/beta; then | |
| echo "beta is already up to date with main. Skipping sync." | |
| echo "skipped=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| SYNC_BRANCH="sync/main-v${NEW_VERSION}-into-beta-${GITHUB_RUN_ID}" | |
| echo "name=$SYNC_BRANCH" >> "$GITHUB_OUTPUT" | |
| echo "skipped=false" >> "$GITHUB_OUTPUT" | |
| git checkout -b "$SYNC_BRANCH" origin/beta | |
| if git merge origin/main --no-ff --no-commit; then | |
| echo "main merged cleanly into sync branch." | |
| else | |
| echo "Merge conflicts detected. Attempting expected conflict resolution for beta version files." | |
| CONFLICTS=$(git diff --name-only --diff-filter=U || true) | |
| if [[ -n "$CONFLICTS" ]]; then | |
| echo "$CONFLICTS" | |
| fi | |
| # Keep beta-side prerelease versions if these files conflict. | |
| for file in MCPForUnity/package.json Server/pyproject.toml; do | |
| if git ls-files -u -- "$file" | grep -q .; then | |
| echo "Keeping beta version for $file" | |
| git checkout --ours -- "$file" | |
| git add "$file" | |
| fi | |
| done | |
| REMAINING=$(git diff --name-only --diff-filter=U || true) | |
| if [[ -n "$REMAINING" ]]; then | |
| echo "Unexpected unresolved conflicts remain:" | |
| echo "$REMAINING" | |
| exit 1 | |
| fi | |
| fi | |
| git commit -m "chore: sync main (v${NEW_VERSION}) into beta" | |
| # After releasing X.Y.Z on main, beta should move to X.Y.(Z+1)-beta.1. | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$NEW_VERSION" | |
| NEXT_PATCH=$((PATCH + 1)) | |
| NEXT_BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.1" | |
| echo "beta_version=$NEXT_BETA_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Setting beta version to $NEXT_BETA_VERSION" | |
| CURRENT_BETA_VERSION=$(jq -r '.version' MCPForUnity/package.json) | |
| if [[ "$CURRENT_BETA_VERSION" != "$NEXT_BETA_VERSION" ]]; then | |
| jq --arg v "$NEXT_BETA_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json | |
| mv tmp.json MCPForUnity/package.json | |
| git add MCPForUnity/package.json | |
| git commit -m "chore: set beta version to ${NEXT_BETA_VERSION} after release v${NEW_VERSION}" | |
| else | |
| echo "Beta version already at target: $NEXT_BETA_VERSION" | |
| fi | |
| echo "Pushing sync branch $SYNC_BRANCH" | |
| git push origin "$SYNC_BRANCH" | |
| - name: Create PR to merge sync branch into beta | |
| if: steps.sync_branch.outputs.skipped != 'true' | |
| id: sync_pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| NEW_VERSION: ${{ needs.bump.outputs.new_version }} | |
| NEXT_BETA_VERSION: ${{ steps.sync_branch.outputs.beta_version }} | |
| SYNC_BRANCH: ${{ steps.sync_branch.outputs.name }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| PR_URL=$(gh pr create \ | |
| --base beta \ | |
| --head "$SYNC_BRANCH" \ | |
| --title "chore: sync main (v${NEW_VERSION}) into beta" \ | |
| --body "Automated sync of main back into beta after release v${NEW_VERSION}, including beta version set to ${NEXT_BETA_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: Merge sync PR | |
| if: steps.sync_branch.outputs.skipped != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.sync_pr.outputs.pr_number }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Best effort: auto-merge if repository settings allow it. | |
| gh pr merge "$PR_NUMBER" --merge --auto --delete-branch || true | |
| # Retry direct merge for up to 2 minutes while checks settle. | |
| 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 | |
| if gh pr merge "$PR_NUMBER" --merge --delete-branch >/dev/null 2>&1; then | |
| echo "Sync PR merged successfully." | |
| exit 0 | |
| fi | |
| echo "Waiting for sync PR to become mergeable... (state: $STATE)" | |
| sleep 5 | |
| done | |
| echo "Sync PR did not merge in time." | |
| gh pr view "$PR_NUMBER" --json state,mergeStateStatus,isDraft -q '{state: .state, mergeStateStatus: .mergeStateStatus, isDraft: .isDraft}' | |
| exit 1 | |
| 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 |