diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bdbf22214..af5309aed 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -309,47 +309,136 @@ jobs:
contents: write
pull-requests: write
steps:
- - name: Checkout main
+ - name: Checkout beta
uses: actions/checkout@v6
with:
- ref: main
+ ref: beta
fetch-depth: 0
- - name: Create PR to merge main into beta
- id: sync_pr
+ - name: Prepare sync branch from beta with merged main
+ id: sync_branch
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
+ 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 PR."
+ 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 main \
+ --head "$SYNC_BRANCH" \
--title "chore: sync main (v${NEW_VERSION}) into beta" \
- --body "Automated sync of version bump from main 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"
- echo "skipped=false" >> "$GITHUB_OUTPUT"
- name: Merge sync PR
- if: steps.sync_pr.outputs.skipped != 'true'
+ 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
- gh pr merge "$PR_NUMBER" --merge --no-delete-branch
+
+ # 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
diff --git a/MCPForUnity/Editor/Services/PackageUpdateService.cs b/MCPForUnity/Editor/Services/PackageUpdateService.cs
index 81c0161f7..5f594e9ca 100644
--- a/MCPForUnity/Editor/Services/PackageUpdateService.cs
+++ b/MCPForUnity/Editor/Services/PackageUpdateService.cs
@@ -1,9 +1,11 @@
using System;
using System.Net;
+using System.Text.RegularExpressions;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
+using PackageInfo = UnityEditor.PackageManager.PackageInfo;
namespace MCPForUnity.Editor.Services
{
@@ -14,17 +16,30 @@ public class PackageUpdateService : IPackageUpdateService
{
private const string LastCheckDateKey = EditorPrefKeys.LastUpdateCheck;
private const string CachedVersionKey = EditorPrefKeys.LatestKnownVersion;
+ private const string LastBetaCheckDateKey = EditorPrefKeys.LastUpdateCheck + ".beta";
+ private const string CachedBetaVersionKey = EditorPrefKeys.LatestKnownVersion + ".beta";
private const string LastAssetStoreCheckDateKey = EditorPrefKeys.LastAssetStoreUpdateCheck;
private const string CachedAssetStoreVersionKey = EditorPrefKeys.LatestKnownAssetStoreVersion;
- private const string PackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json";
+ private const string MainPackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json";
+ private const string BetaPackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/beta/MCPForUnity/package.json";
private const string AssetStoreVersionUrl = "https://gqoqjkkptwfbkwyssmnj.supabase.co/storage/v1/object/public/coplay-images/assetstoreversion.json";
///
public UpdateCheckResult CheckForUpdate(string currentVersion)
{
bool isGitInstallation = IsGitInstallation();
- string lastCheckDate = EditorPrefs.GetString(isGitInstallation ? LastCheckDateKey : LastAssetStoreCheckDateKey, "");
- string cachedLatestVersion = EditorPrefs.GetString(isGitInstallation ? CachedVersionKey : CachedAssetStoreVersionKey, "");
+ string gitBranch = isGitInstallation ? GetGitUpdateBranch(currentVersion) : "main";
+ bool useBetaChannel = isGitInstallation && string.Equals(gitBranch, "beta", StringComparison.OrdinalIgnoreCase);
+
+ string lastCheckKey = isGitInstallation
+ ? (useBetaChannel ? LastBetaCheckDateKey : LastCheckDateKey)
+ : LastAssetStoreCheckDateKey;
+ string cachedVersionKey = isGitInstallation
+ ? (useBetaChannel ? CachedBetaVersionKey : CachedVersionKey)
+ : CachedAssetStoreVersionKey;
+
+ string lastCheckDate = EditorPrefs.GetString(lastCheckKey, "");
+ string cachedLatestVersion = EditorPrefs.GetString(cachedVersionKey, "");
if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion))
{
@@ -38,14 +53,14 @@ public UpdateCheckResult CheckForUpdate(string currentVersion)
}
string latestVersion = isGitInstallation
- ? FetchLatestVersionFromGitHub()
+ ? FetchLatestVersionFromGitHub(gitBranch)
: FetchLatestVersionFromAssetStoreJson();
if (!string.IsNullOrEmpty(latestVersion))
{
// Cache the result
- EditorPrefs.SetString(isGitInstallation ? LastCheckDateKey : LastAssetStoreCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
- EditorPrefs.SetString(isGitInstallation ? CachedVersionKey : CachedAssetStoreVersionKey, latestVersion);
+ EditorPrefs.SetString(lastCheckKey, DateTime.Now.ToString("yyyy-MM-dd"));
+ EditorPrefs.SetString(cachedVersionKey, latestVersion);
return new UpdateCheckResult
{
@@ -68,31 +83,142 @@ public UpdateCheckResult CheckForUpdate(string currentVersion)
///
public bool IsNewerVersion(string version1, string version2)
+ {
+ if (!TryParseVersion(version1, out var left) || !TryParseVersion(version2, out var right))
+ {
+ return false;
+ }
+
+ return CompareVersions(left, right) > 0;
+ }
+
+ private static int CompareVersions(ParsedVersion left, ParsedVersion right)
+ {
+ int cmp = left.Major.CompareTo(right.Major);
+ if (cmp != 0) return cmp;
+
+ cmp = left.Minor.CompareTo(right.Minor);
+ if (cmp != 0) return cmp;
+
+ cmp = left.Patch.CompareTo(right.Patch);
+ if (cmp != 0) return cmp;
+
+ // Stable is newer than prerelease when core version matches.
+ if (!left.IsPrerelease && right.IsPrerelease) return 1;
+ if (left.IsPrerelease && !right.IsPrerelease) return -1;
+ if (!left.IsPrerelease && !right.IsPrerelease) return 0;
+
+ cmp = GetPrereleaseRank(left.PrereleaseLabel).CompareTo(GetPrereleaseRank(right.PrereleaseLabel));
+ if (cmp != 0) return cmp;
+
+ cmp = left.PrereleaseNumber.CompareTo(right.PrereleaseNumber);
+ if (cmp != 0) return cmp;
+
+ return string.Compare(left.PrereleaseLabel, right.PrereleaseLabel, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static int GetPrereleaseRank(string label)
+ {
+ if (string.IsNullOrEmpty(label))
+ {
+ return 0;
+ }
+
+ switch (label.ToLowerInvariant())
+ {
+ case "a":
+ case "alpha":
+ return 1;
+ case "b":
+ case "beta":
+ return 2;
+ case "rc":
+ return 3;
+ case "preview":
+ case "pre":
+ return 4;
+ default:
+ return 5;
+ }
+ }
+
+ private static bool TryParseVersion(string version, out ParsedVersion parsed)
+ {
+ parsed = default;
+ if (string.IsNullOrWhiteSpace(version))
+ {
+ return false;
+ }
+
+ string normalized = version.Trim().TrimStart('v', 'V');
+ var match = Regex.Match(
+ normalized,
+ @"^(?\d+)\.(?\d+)\.(?\d+)(?:-(?