From 6ecac0c557cfb21e7121e07cde6b05ab72e98495 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 28 Jun 2026 23:39:01 +0900 Subject: [PATCH 01/16] Rename project runner release identity Rename the project-local runner binary, release tag, assets, and pin contract to uloop-project-runner so dispatcher-owned and project-local CLI surfaces are no longer conflated. Update release automation, dispatcher resolution, Unity pin synchronization, and documentation to use the new project runner naming while keeping the global uloop dispatcher command unchanged. --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/native-cli-publish.yml | 2 +- .../{cli-pin.json => project-runner-pin.json} | 2 +- Packages/src/Editor/Domain/CliConstants.cs | 10 +- .../Infrastructure/Api/JsonRpcProcessor.cs | 10 +- .../Infrastructure/Api/JsonRpcRequest.cs | 2 +- .../CLI/CliInstallationDetector.cs | 4 +- .../Infrastructure/CLI/CliPinSynchronizer.cs | 8 +- .../Infrastructure/CLI/NativeCliInstaller.cs | 4 +- .../Presentation/Setup/SetupWizardWindow.cs | 2 +- .../UnityCliLoopSettingsWindow.cs | 2 +- .../ToolContracts/UnityCliLoopConstants.cs | 2 +- .../{cli-pin.json => project-runner-pin.json} | 2 +- ...json.meta => project-runner-pin.json.meta} | 0 README.md | 6 +- cli/cmd/{uloop => dispatcher}/main.go | 0 cli/cmd/project-runner/main.go | 12 ++ cli/cmd/uloop-cli/main.go | 12 -- cli/contract.go | 6 +- cli/contract.json | 2 +- cli/contract_test.go | 6 +- cli/dispatcher-contract.json | 2 +- .../architecture/architecture_test.go | 8 +- .../dispatcher_minimum_version_guard.go | 44 +++---- .../dispatcher_minimum_version_guard_test.go | 34 ++--- .../dispatcher_version_bump_guard.go | 2 +- .../protocol_minimum_version_guard.go | 116 ++++++++-------- .../protocol_minimum_version_guard_test.go | 120 ++++++++--------- .../automation/release_pr_checks_test.go | 2 +- cli/internal/cli/dispatcher.go | 20 +-- cli/internal/cli/dispatcher_download.go | 30 ++--- cli/internal/cli/dispatcher_pin.go | 44 +++---- cli/internal/cli/dispatcher_test.go | 124 +++++++----------- cli/internal/cli/dispatcher_update_version.go | 2 +- cli/internal/cli/help_test.go | 4 +- cli/internal/cli/run.go | 4 +- cli/internal/cli/tools.go | 2 +- cli/internal/cli/update_test.go | 16 +-- .../projectrunner.go} | 2 +- cli/internal/unityipc/client.go | 16 +-- cli/internal/unityipc/client_test.go | 8 +- cli/internal/update/command.go | 4 +- cli/internal/update/installer.go | 14 +- cli/internal/update/installer_test.go | 16 +-- cli/protocol_version_consistency_test.go | 12 +- release-please-config.json | 16 +-- scripts/build-go-cli.sh | 12 +- scripts/package-go-cli.sh | 43 +----- scripts/resolve-native-cli-release-target.sh | 14 +- .../sync-release-please-package-releases.sh | 8 +- scripts/test-release-please-config.sh | 14 +- .../test-resolve-native-cli-release-target.sh | 48 +++---- ...st-sync-release-please-package-releases.sh | 56 ++++---- .../test-verify-native-cli-release-assets.sh | 24 ++-- scripts/verify-go-cli-dist.sh | 6 +- scripts/verify-native-cli-release-assets.sh | 41 ++---- 56 files changed, 461 insertions(+), 563 deletions(-) rename .uloop/{cli-pin.json => project-runner-pin.json} (81%) rename Packages/src/{cli-pin.json => project-runner-pin.json} (81%) rename Packages/src/{cli-pin.json.meta => project-runner-pin.json.meta} (100%) rename cli/cmd/{uloop => dispatcher}/main.go (100%) create mode 100644 cli/cmd/project-runner/main.go delete mode 100644 cli/cmd/uloop-cli/main.go rename cli/internal/{projectcli/projectcli.go => projectrunner/projectrunner.go} (91%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 016fc96e8..aad6ada98 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,7 +2,7 @@ name: Build and Test Unity CLI Loop on: push: - tags: ["v*", "cli-v*", "dispatcher-v*"] + tags: ["v*", "uloop-project-runner-v*", "dispatcher-v*"] pull_request: branches: [main, v3-beta] workflow_dispatch: diff --git a/.github/workflows/native-cli-publish.yml b/.github/workflows/native-cli-publish.yml index 8c4f39be5..cc9e8f393 100644 --- a/.github/workflows/native-cli-publish.yml +++ b/.github/workflows/native-cli-publish.yml @@ -12,7 +12,7 @@ on: required: false default: "" release-tag: - description: "CLI release tag to upload assets to. Defaults to cli-v{CLI version}." + description: "Project runner release tag to upload assets to. Defaults to uloop-project-runner-v{version}." required: false default: "" dry-run: diff --git a/.uloop/cli-pin.json b/.uloop/project-runner-pin.json similarity index 81% rename from .uloop/cli-pin.json rename to .uloop/project-runner-pin.json index f0aaacd0c..c40eba2f6 100644 --- a/.uloop/cli-pin.json +++ b/.uloop/project-runner-pin.json @@ -1,5 +1,5 @@ { - "cliVersion": "3.0.0-beta.43", + "projectRunnerVersion": "3.0.0-beta.43", "minimumDispatcherVersion": "3.0.1-beta.2", "packageName": "io.github.hatayama.uloopmcp", "packageVersion": "3.0.0-beta.44", diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index b6bd11bbc..46b52bebe 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -10,10 +10,10 @@ public static class CliConstants // Bump it together with cli/contract.json protocolVersion only when this package can // no longer interoperate with a different CLI protocol generation. public const int REQUIRED_CLI_PROTOCOL_VERSION = 2; - // Why: setup installs this pinned release; protocol bump PRs can advance it only after - // the matching CLI tag is published. - public const string MINIMUM_REQUIRED_CLI_VERSION = "3.0.0-beta.40"; - public const string MINIMUM_REQUIRED_CLI_RELEASE_TAG = CLI_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_CLI_VERSION; + // Why: setup installs this pinned project runner release; protocol bump PRs can advance it only after + // the matching project runner tag is published. + public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "3.0.0-beta.40"; + public const string MINIMUM_REQUIRED_PROJECT_RUNNER_RELEASE_TAG = PROJECT_RUNNER_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION; // Why: global uloop is a dispatcher; project-local CLI versions are downloaded separately. public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "3.0.1-beta.2"; public const string MINIMUM_REQUIRED_DISPATCHER_RELEASE_TAG = DISPATCHER_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_DISPATCHER_VERSION; @@ -40,7 +40,7 @@ public static class CliConstants public const string POSIX_PATH_SEPARATOR = ":"; public const string WINDOWS_PATH_SEPARATOR = ";"; public const string RELEASE_TAG_PREFIX = "v"; - public const string CLI_RELEASE_TAG_PREFIX = "cli-v"; + public const string PROJECT_RUNNER_RELEASE_TAG_PREFIX = "uloop-project-runner-v"; public const string DISPATCHER_RELEASE_TAG_PREFIX = "dispatcher-v"; public const string BETA_VERSION_MARKER = "-beta."; public const string SKILL_DIR_PREFIX = "uloop-"; diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs index 4dac6a20e..e768fb693 100644 --- a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs @@ -89,7 +89,7 @@ private static JsonRpcRequest ParseRequest(string jsonRequest) { Method = request["method"]?.ToString(), Params = request["params"], - ClientCliVersion = ReadClientCliVersion(request), + ClientProjectRunnerVersion = ReadClientProjectRunnerVersion(request), ClientProtocolVersion = ReadClientProtocolVersion(request), AcceptsDispatchAck = ReadAcceptsDispatchAck(request), AcceptsHeartbeat = ReadAcceptsHeartbeat(request), @@ -97,7 +97,7 @@ private static JsonRpcRequest ParseRequest(string jsonRequest) }; } - private static string ReadClientCliVersion(JObject request) + private static string ReadClientProjectRunnerVersion(JObject request) { JObject metadata = request["uloop"] as JObject; if (metadata == null) @@ -105,8 +105,8 @@ private static string ReadClientCliVersion(JObject request) return null; } - string cliVersion = metadata["cliVersion"]?.ToString(); - return string.IsNullOrWhiteSpace(cliVersion) ? null : cliVersion; + string projectRunnerVersion = metadata["projectRunnerVersion"]?.ToString(); + return string.IsNullOrWhiteSpace(projectRunnerVersion) ? null : projectRunnerVersion; } private static int? ReadClientProtocolVersion(JObject request) @@ -216,7 +216,7 @@ private static async Task ProcessRpcRequest( { return CreateCliProtocolMismatchResponse( request.Id, - request.ClientCliVersion, + request.ClientProjectRunnerVersion, request.ClientProtocolVersion); } diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcRequest.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcRequest.cs index 63d33204e..05368226e 100644 --- a/Packages/src/Editor/Infrastructure/Api/JsonRpcRequest.cs +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcRequest.cs @@ -16,7 +16,7 @@ internal class JsonRpcRequest /// public JToken Params { get; set; } - public string ClientCliVersion { get; set; } + public string ClientProjectRunnerVersion { get; set; } /// /// IPC protocol generation the client speaks. Null when the client predates the diff --git a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs index fd5fe6421..99e0c0d14 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs @@ -45,7 +45,7 @@ public sealed class CliInstallationDetector : ICliInstallationDetector private const string SHELL_CONTRACT_STATUS_START_MARKER = "__ULOOP_CONTRACT_STATUS_START__"; private const string SHELL_CONTRACT_STATUS_END_MARKER = "__ULOOP_CONTRACT_STATUS_END__"; private const string SHELL_SUCCESS_EXIT_CODE = "0"; - private const string VERSION_JSON_CLI_VERSION_PROPERTY = "CliVersion"; + private const string VERSION_JSON_PROJECT_RUNNER_VERSION_PROPERTY = "ProjectRunnerVersion"; private const string VERSION_JSON_DISPATCHER_VERSION_PROPERTY = "DispatcherVersion"; private const string VERSION_JSON_DISPATCHER_CONTRACT_VERSION_PROPERTY = "DispatcherContractVersion"; @@ -376,7 +376,7 @@ private static CliInstallationDetection ParseCliContractOutput(string output, st return dispatcherDetection; } - string version = parsed[VERSION_JSON_CLI_VERSION_PROPERTY]?.ToString(); + string version = parsed[VERSION_JSON_PROJECT_RUNNER_VERSION_PROPERTY]?.ToString(); return new CliInstallationDetection(version, executablePath); } catch (JsonException) diff --git a/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs b/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs index 9cb709999..5ac801ac0 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs @@ -37,22 +37,22 @@ internal static bool SyncProjectPinFile(string packageRoot, string projectRoot) if (string.IsNullOrWhiteSpace(packageRoot)) { Debug.LogWarning( - $"Unity CLI Loop skipped {UnityCliLoopConstants.ULOOP_CLI_PIN_FILE_NAME} synchronization because the package root is empty."); + $"Unity CLI Loop skipped {UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME} synchronization because the package root is empty."); return false; } if (string.IsNullOrWhiteSpace(projectRoot)) { Debug.LogWarning( - $"Unity CLI Loop skipped {UnityCliLoopConstants.ULOOP_CLI_PIN_FILE_NAME} synchronization because the project root is empty."); + $"Unity CLI Loop skipped {UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME} synchronization because the project root is empty."); return false; } - string sourcePath = Path.Combine(packageRoot, UnityCliLoopConstants.ULOOP_CLI_PIN_FILE_NAME); + string sourcePath = Path.Combine(packageRoot, UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); string destinationPath = Path.Combine( projectRoot, UnityCliLoopConstants.ULOOP_DIR, - UnityCliLoopConstants.ULOOP_CLI_PIN_FILE_NAME); + UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); if (!File.Exists(sourcePath)) { diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs index f7fb9aa0d..6698b31f6 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs @@ -958,9 +958,9 @@ private static string BuildReleaseTag(string cliReleaseTag) { return cliReleaseTag; } - if (cliReleaseTag.StartsWith(CliConstants.CLI_RELEASE_TAG_PREFIX, StringComparison.Ordinal)) + if (cliReleaseTag.StartsWith(CliConstants.PROJECT_RUNNER_RELEASE_TAG_PREFIX, StringComparison.Ordinal)) { - return $"{CliConstants.DISPATCHER_RELEASE_TAG_PREFIX}{cliReleaseTag.Substring(CliConstants.CLI_RELEASE_TAG_PREFIX.Length)}"; + return $"{CliConstants.DISPATCHER_RELEASE_TAG_PREFIX}{cliReleaseTag.Substring(CliConstants.PROJECT_RUNNER_RELEASE_TAG_PREFIX.Length)}"; } if (cliReleaseTag.StartsWith(CliConstants.RELEASE_TAG_PREFIX, StringComparison.Ordinal)) { diff --git a/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs index 2a81a2baf..9e8600c8e 100644 --- a/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs @@ -1232,7 +1232,7 @@ private async void HandleInstallCli() true); EditorUtility.DisplayDialog( "Installation Failed", - $"Failed to install uloop-cli.\n\n{result.ErrorOutput}\n\n" + $"Failed to install uloop CLI.\n\n{result.ErrorOutput}\n\n" + $"You can install manually:\n {command.ManualCommand}", "OK"); return; diff --git a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs index fbfb17a8f..275130f7a 100644 --- a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs +++ b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs @@ -1014,7 +1014,7 @@ private async void HandleInstallSkills() { EditorUtility.DisplayDialog( "CLI Not Found", - "uloop-cli is not installed. Please install the CLI first.", + "uloop CLI is not installed. Please install the CLI first.", "OK"); return; } diff --git a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs index d95d496ae..ff0eaff2a 100644 --- a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs @@ -56,7 +56,7 @@ public static UnityEditor.PackageManager.PackageInfo PackageInfo // .uloop directory public const string ULOOP_DIR = ".uloop"; public const string ULOOP_TOOL_SETTINGS_FILE_NAME = "settings.tools.json"; - public const string ULOOP_CLI_PIN_FILE_NAME = "cli-pin.json"; + public const string ULOOP_PROJECT_RUNNER_PIN_FILE_NAME = "project-runner-pin.json"; // Command name constants public const string TOOL_NAME_COMPILE = "compile"; diff --git a/Packages/src/cli-pin.json b/Packages/src/project-runner-pin.json similarity index 81% rename from Packages/src/cli-pin.json rename to Packages/src/project-runner-pin.json index f0aaacd0c..c40eba2f6 100644 --- a/Packages/src/cli-pin.json +++ b/Packages/src/project-runner-pin.json @@ -1,5 +1,5 @@ { - "cliVersion": "3.0.0-beta.43", + "projectRunnerVersion": "3.0.0-beta.43", "minimumDispatcherVersion": "3.0.1-beta.2", "packageName": "io.github.hatayama.uloopmcp", "packageVersion": "3.0.0-beta.44", diff --git a/Packages/src/cli-pin.json.meta b/Packages/src/project-runner-pin.json.meta similarity index 100% rename from Packages/src/cli-pin.json.meta rename to Packages/src/project-runner-pin.json.meta diff --git a/README.md b/README.md index 940b551e4..f52f3f8ad 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Scope(s): io.github.hatayama.uloopmcp Select Window > Unity CLI Loop > Settings. A dedicated window will open. If the **CLI** button is not highlighted in blue, press **Install CLI**. -The installer places the global `uloop` dispatcher on PATH. Project-specific `uloop-cli` binaries are downloaded into the user cache automatically from each project's `.uloop/cli-pin.json`. +The installer places the global `uloop` dispatcher on PATH. Project-specific `uloop-project-runner` binaries are downloaded into the user cache automatically from each project's `.uloop/project-runner-pin.json`. To return to the v2 line, press **Uninstall CLI** in Settings, downgrade the U-LOOP package to a v2 version such as `2.1.1`, then press **Install CLI** again from Settings. @@ -612,7 +612,7 @@ The `.uloop/` directory at the project root stores CLI cache, tool registry, and | File | Purpose | Git-track? | |------|---------|------------| -| `cli-pin.json` | Project CLI version contract used by the global dispatcher | Yes | +| `project-runner-pin.json` | Project runner version contract used by the global dispatcher | Yes | | `settings.tools.json` | Per-tool enable/disable preferences | Optional | | `tools.json` | Auto-generated CLI tool registry | No | | `outputs/` | Runtime outputs (test results, screenshots, hierarchy dumps) | No | @@ -622,7 +622,7 @@ The `.uloop/` directory at the project root stores CLI cache, tool registry, and > > ```gitignore > **/.uloop/* -> !**/.uloop/cli-pin.json +> !**/.uloop/project-runner-pin.json > !**/.uloop/settings.tools.json > ``` > diff --git a/cli/cmd/uloop/main.go b/cli/cmd/dispatcher/main.go similarity index 100% rename from cli/cmd/uloop/main.go rename to cli/cmd/dispatcher/main.go diff --git a/cli/cmd/project-runner/main.go b/cli/cmd/project-runner/main.go new file mode 100644 index 000000000..3ebca6633 --- /dev/null +++ b/cli/cmd/project-runner/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "context" + "os" + + "github.com/hatayama/unity-cli-loop/cli/internal/projectrunner" +) + +func main() { + os.Exit(projectrunner.Run(context.Background(), os.Args[1:], os.Stdout, os.Stderr)) +} diff --git a/cli/cmd/uloop-cli/main.go b/cli/cmd/uloop-cli/main.go deleted file mode 100644 index 3785cdb16..000000000 --- a/cli/cmd/uloop-cli/main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "context" - "os" - - "github.com/hatayama/unity-cli-loop/cli/internal/projectcli" -) - -func main() { - os.Exit(projectcli.Run(context.Background(), os.Args[1:], os.Stdout, os.Stderr)) -} diff --git a/cli/contract.go b/cli/contract.go index 5399149cb..ace313b6b 100644 --- a/cli/contract.go +++ b/cli/contract.go @@ -24,8 +24,8 @@ type Contract struct { SchemaVersion int `json:"schemaVersion"` // ProtocolVersion is the C# IPC contract generation this binary speaks. It moves only // when the Unity package and the CLI can no longer interoperate, never per release. - ProtocolVersion int `json:"protocolVersion"` - CliVersion string `json:"cliVersion"` + ProtocolVersion int `json:"protocolVersion"` + ProjectRunnerVersion string `json:"projectRunnerVersion"` } type DispatcherContract struct { @@ -51,7 +51,7 @@ func mustLoadContract() Contract { if contract.SchemaVersion != schemaVersion { panic(fmt.Sprintf("CLI contract schema version mismatch: %d", contract.SchemaVersion)) } - requireString(contract.CliVersion, "cliVersion") + requireString(contract.ProjectRunnerVersion, "projectRunnerVersion") if contract.ProtocolVersion < 1 { panic(fmt.Sprintf("CLI contract protocolVersion must be at least 1, got %d", contract.ProtocolVersion)) } diff --git a/cli/contract.json b/cli/contract.json index 3e84473ba..6a73ac7ad 100644 --- a/cli/contract.json +++ b/cli/contract.json @@ -1,5 +1,5 @@ { "schemaVersion": 1, "protocolVersion": 2, - "cliVersion": "3.0.0-beta.43" + "projectRunnerVersion": "3.0.0-beta.43" } diff --git a/cli/contract_test.go b/cli/contract_test.go index 29583b8eb..bc3383f23 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -8,8 +8,8 @@ import ( ) func TestCliContractProvidesRuntimeVersion(t *testing.T) { - // Verifies that the native CLI owns its runtime version from the single CLI module. - requireValidContractVersion(t, "cliVersion", Current.CliVersion) + // Verifies that the project runner owns its runtime version from the single CLI module. + requireValidContractVersion(t, "projectRunnerVersion", Current.ProjectRunnerVersion) } func TestCliContractProvidesProtocolVersion(t *testing.T) { @@ -41,7 +41,7 @@ func TestCliContractDoesNotDeclareDispatcherReleaseFields(t *testing.T) { func TestDispatcherContractDoesNotDeclareCliReleaseFields(t *testing.T) { // Verifies dispatcher releases stay independent from project-local CLI release metadata. fields := requireContractFieldMap(t, dispatcherContractFileName) - requireContractFieldMissing(t, fields, "cliVersion") + requireContractFieldMissing(t, fields, "projectRunnerVersion") requireContractFieldMissing(t, fields, "protocolVersion") } diff --git a/cli/dispatcher-contract.json b/cli/dispatcher-contract.json index 58d0c40ee..f1a164c23 100644 --- a/cli/dispatcher-contract.json +++ b/cli/dispatcher-contract.json @@ -1,5 +1,5 @@ { "schemaVersion": 1, - "dispatcherVersion": "3.0.1-beta.5", + "dispatcherVersion": "3.0.1-beta.6", "dispatcherContractVersion": 1 } diff --git a/cli/internal/architecture/architecture_test.go b/cli/internal/architecture/architecture_test.go index 0f19adf6f..5484ced9f 100644 --- a/cli/internal/architecture/architecture_test.go +++ b/cli/internal/architecture/architecture_test.go @@ -48,7 +48,7 @@ func TestCliFeaturePackagesDoNotImportCli(t *testing.T) { for _, goPackage := range packages { if goPackage.ImportPath == cliModulePath+"/internal/cli" || goPackage.ImportPath == cliModulePath+"/internal/dispatcher" || - goPackage.ImportPath == cliModulePath+"/internal/projectcli" || + goPackage.ImportPath == cliModulePath+"/internal/projectrunner" || strings.HasPrefix(goPackage.ImportPath, cliModulePath+"/cmd/") { continue } @@ -71,7 +71,7 @@ func TestCliInternalPackagesStayInsideExplicitBoundaries(t *testing.T) { if goPackage.ImportPath == cliModulePath+"/internal/architecture" { continue } - for _, boundary := range []string{"/internal/automation", "/internal/cli", "/internal/dispatcher", "/internal/install", "/internal/project", "/internal/projectcli", "/internal/skills", "/internal/tools", "/internal/uninstall", "/internal/unityipc", "/internal/update", "/internal/version"} { + for _, boundary := range []string{"/internal/automation", "/internal/cli", "/internal/dispatcher", "/internal/install", "/internal/project", "/internal/projectrunner", "/internal/skills", "/internal/tools", "/internal/uninstall", "/internal/unityipc", "/internal/update", "/internal/version"} { if strings.Contains(goPackage.ImportPath, boundary) { goto nextPackage } @@ -83,12 +83,12 @@ func TestCliInternalPackagesStayInsideExplicitBoundaries(t *testing.T) { // Tests that the dispatcher command only enters the dispatcher package. func TestDispatcherCommandOnlyDependsOnDispatcherEntrypoint(t *testing.T) { - assertCommandOnlyDependsOnInternalEntrypoint(t, "./cmd/uloop", cliModulePath+"/internal/dispatcher") + assertCommandOnlyDependsOnInternalEntrypoint(t, "./cmd/dispatcher", cliModulePath+"/internal/dispatcher") } // Tests that the project-local CLI command only enters the project CLI package. func TestProjectCliCommandOnlyDependsOnProjectCliEntrypoint(t *testing.T) { - assertCommandOnlyDependsOnInternalEntrypoint(t, "./cmd/uloop-cli", cliModulePath+"/internal/projectcli") + assertCommandOnlyDependsOnInternalEntrypoint(t, "./cmd/project-runner", cliModulePath+"/internal/projectrunner") } func assertCommandOnlyDependsOnInternalEntrypoint(t *testing.T, commandPath string, expectedEntrypoint string) { diff --git a/cli/internal/automation/dispatcher_minimum_version_guard.go b/cli/internal/automation/dispatcher_minimum_version_guard.go index ce28c4b2e..d3afe6404 100644 --- a/cli/internal/automation/dispatcher_minimum_version_guard.go +++ b/cli/internal/automation/dispatcher_minimum_version_guard.go @@ -16,8 +16,8 @@ const ( cliContractFile = "cli/contract.json" dispatcherContractFile = "cli/dispatcher-contract.json" dispatcherReleaseTagPrefix = "dispatcher-v" - unityPackageCliPinFile = "Packages/src/cli-pin.json" - unityProjectCliPinFile = ".uloop/cli-pin.json" + unityPackageCliPinFile = "Packages/src/project-runner-pin.json" + unityProjectCliPinFile = ".uloop/project-runner-pin.json" minimumDispatcherContractVersion = 1 minimumDispatcherVersionDescription = "minimumDispatcherVersion" ) @@ -25,18 +25,18 @@ const ( var minimumDispatcherVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_DISPATCHER_VERSION\s*=\s*"([^"]+)"`) type dispatcherMinimumVersionValues struct { - CurrentCliVersion string + CurrentProjectRunnerVersion string CurrentDispatcherVersion string CurrentDispatcherContractVersion int MinimumDispatcherVersion string - PackagePinCliVersion string + PackagePinProjectRunnerVersion string PackagePinMinimumDispatcherVersion string - ProjectPinCliVersion string + ProjectPinProjectRunnerVersion string ProjectPinMinimumDispatcherVersion string } type dispatcherMinimumVersionCliContract struct { - CliVersion string `json:"cliVersion"` + ProjectRunnerVersion string `json:"projectRunnerVersion"` } type dispatcherMinimumVersionContract struct { @@ -53,7 +53,7 @@ type dispatcherMinimumVersionCliPin struct { SchemaVersion int `json:"schemaVersion"` PackageName string `json:"packageName"` PackageVersion string `json:"packageVersion"` - CliVersion string `json:"cliVersion"` + ProjectRunnerVersion string `json:"projectRunnerVersion"` RequiredProtocolVersion int `json:"requiredProtocolVersion"` MinimumDispatcherVersion string `json:"minimumDispatcherVersion"` } @@ -159,13 +159,13 @@ func parseDispatcherMinimumVersionValues( } values := dispatcherMinimumVersionValues{ - CurrentCliVersion: cliContract.CliVersion, + CurrentProjectRunnerVersion: cliContract.ProjectRunnerVersion, CurrentDispatcherVersion: dispatcherContract.DispatcherVersion, CurrentDispatcherContractVersion: dispatcherContract.DispatcherContractVersion, MinimumDispatcherVersion: minimumDispatcherVersion, - PackagePinCliVersion: packagePin.CliVersion, + PackagePinProjectRunnerVersion: packagePin.ProjectRunnerVersion, PackagePinMinimumDispatcherVersion: packagePin.MinimumDispatcherVersion, - ProjectPinCliVersion: projectPin.CliVersion, + ProjectPinProjectRunnerVersion: projectPin.ProjectRunnerVersion, ProjectPinMinimumDispatcherVersion: projectPin.MinimumDispatcherVersion, } return values, validateDispatcherMinimumVersionValues(values) @@ -176,8 +176,8 @@ func parseDispatcherMinimumVersionCliContract(content []byte) (dispatcherMinimum if err := json.Unmarshal(content, &contract); err != nil { return dispatcherMinimumVersionCliContract{}, fmt.Errorf("%s is invalid JSON: %w", cliContractFile, err) } - if contract.CliVersion == "" { - return dispatcherMinimumVersionCliContract{}, fmt.Errorf("%s does not define cliVersion", cliContractFile) + if contract.ProjectRunnerVersion == "" { + return dispatcherMinimumVersionCliContract{}, fmt.Errorf("%s does not define projectRunnerVersion", cliContractFile) } return contract, nil } @@ -211,8 +211,8 @@ func parseDispatcherMinimumVersionPin(path string, content []byte) (dispatcherMi if err := json.Unmarshal(content, &pin); err != nil { return dispatcherMinimumVersionCliPin{}, fmt.Errorf("%s is invalid JSON: %w", path, err) } - if pin.CliVersion == "" { - return dispatcherMinimumVersionCliPin{}, fmt.Errorf("%s does not define cliVersion", path) + if pin.ProjectRunnerVersion == "" { + return dispatcherMinimumVersionCliPin{}, fmt.Errorf("%s does not define projectRunnerVersion", path) } if pin.MinimumDispatcherVersion == "" { return dispatcherMinimumVersionCliPin{}, fmt.Errorf("%s does not define %s", path, minimumDispatcherVersionDescription) @@ -221,19 +221,19 @@ func parseDispatcherMinimumVersionPin(path string, content []byte) (dispatcherMi } func validateDispatcherMinimumVersionValues(values dispatcherMinimumVersionValues) error { - if values.PackagePinCliVersion != values.CurrentCliVersion { - return fmt.Errorf("%s cliVersion %q does not match %s cliVersion %q", + if values.PackagePinProjectRunnerVersion != values.CurrentProjectRunnerVersion { + return fmt.Errorf("%s projectRunnerVersion %q does not match %s projectRunnerVersion %q", unityPackageCliPinFile, - values.PackagePinCliVersion, + values.PackagePinProjectRunnerVersion, cliContractFile, - values.CurrentCliVersion) + values.CurrentProjectRunnerVersion) } - if values.ProjectPinCliVersion != values.PackagePinCliVersion { - return fmt.Errorf("%s cliVersion %q does not match %s cliVersion %q", + if values.ProjectPinProjectRunnerVersion != values.PackagePinProjectRunnerVersion { + return fmt.Errorf("%s projectRunnerVersion %q does not match %s projectRunnerVersion %q", unityProjectCliPinFile, - values.ProjectPinCliVersion, + values.ProjectPinProjectRunnerVersion, unityPackageCliPinFile, - values.PackagePinCliVersion) + values.PackagePinProjectRunnerVersion) } if values.PackagePinMinimumDispatcherVersion != values.MinimumDispatcherVersion { return fmt.Errorf("%s %s %q does not match %s MINIMUM_REQUIRED_DISPATCHER_VERSION %q", diff --git a/cli/internal/automation/dispatcher_minimum_version_guard_test.go b/cli/internal/automation/dispatcher_minimum_version_guard_test.go index e5b335a92..47b11bc87 100644 --- a/cli/internal/automation/dispatcher_minimum_version_guard_test.go +++ b/cli/internal/automation/dispatcher_minimum_version_guard_test.go @@ -13,7 +13,7 @@ import ( // Verifies release PRs cannot point minimumDispatcherVersion at a dispatcher tag without contract metadata. func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleaseLacksDispatcherContract_Fails(t *testing.T) { result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ - currentCliVersion: "3.0.0-beta.40", + currentProjectRunnerVersion: "3.0.0-beta.40", currentDispatcherVersion: "1.0.1", currentDispatcherContractVersion: 1, minimumDispatcherVersion: "1.0.0", @@ -30,7 +30,7 @@ func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleaseLacksDispatcherContr // Verifies release PRs pass when the current dispatcher release itself is the minimum dispatcher version. func TestRunDispatcherMinimumVersionCheck_WhenMinimumIsCurrentRelease_Passes(t *testing.T) { result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ - currentCliVersion: "3.0.0-beta.40", + currentProjectRunnerVersion: "3.0.0-beta.40", currentDispatcherVersion: "1.0.0", currentDispatcherContractVersion: 1, minimumDispatcherVersion: "1.0.0", @@ -46,7 +46,7 @@ func TestRunDispatcherMinimumVersionCheck_WhenMinimumIsCurrentRelease_Passes(t * // Verifies committed pin files cannot drift from the C# minimum dispatcher version. func TestRunDispatcherMinimumVersionCheck_WhenProjectPinDiffersFromPackagePin_Fails(t *testing.T) { result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ - currentCliVersion: "3.0.0-beta.40", + currentProjectRunnerVersion: "3.0.0-beta.40", currentDispatcherVersion: "1.0.0", currentDispatcherContractVersion: 1, minimumDispatcherVersion: "1.0.0", @@ -56,13 +56,13 @@ func TestRunDispatcherMinimumVersionCheck_WhenProjectPinDiffersFromPackagePin_Fa if result.exitCode != 1 { t.Fatalf("expected exit code 1, got %d\nstdout: %s", result.exitCode, result.stdout) } - assertDispatcherMinimumVersionLogContains(t, result.stderr, ".uloop/cli-pin.json minimumDispatcherVersion") + assertDispatcherMinimumVersionLogContains(t, result.stderr, ".uloop/project-runner-pin.json minimumDispatcherVersion") } // Verifies invalid dispatcher contract metadata reports the actual bad value. func TestRunDispatcherMinimumVersionCheck_WhenCurrentDispatcherContractIsInvalid_FailsWithValue(t *testing.T) { result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ - currentCliVersion: "3.0.0-beta.40", + currentProjectRunnerVersion: "3.0.0-beta.40", currentDispatcherVersion: "1.0.0", currentDispatcherContractVersion: 0, minimumDispatcherVersion: "1.0.0", @@ -75,7 +75,7 @@ func TestRunDispatcherMinimumVersionCheck_WhenCurrentDispatcherContractIsInvalid } type dispatcherMinimumVersionCase struct { - currentCliVersion string + currentProjectRunnerVersion string currentDispatcherVersion string currentDispatcherContractVersion int minimumDispatcherVersion string @@ -138,19 +138,19 @@ func prepareDispatcherMinimumVersionFiles(t *testing.T, workDir string, testCase } writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, cliContractFile), buildDispatcherMinimumVersionCliContract( - testCase.currentCliVersion)) + testCase.currentProjectRunnerVersion)) writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, dispatcherContractFile), buildDispatcherMinimumVersionContract( currentDispatcherVersion, testCase.currentDispatcherContractVersion)) writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, protocolMinimumVersionFile), buildDispatcherMinimumVersionConstants( 2, - testCase.currentCliVersion, + testCase.currentProjectRunnerVersion, testCase.minimumDispatcherVersion)) writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, unityPackageCliPinFile), buildDispatcherMinimumVersionPin( - testCase.currentCliVersion, + testCase.currentProjectRunnerVersion, testCase.minimumDispatcherVersion)) writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, unityProjectCliPinFile), buildDispatcherMinimumVersionPin( - testCase.currentCliVersion, + testCase.currentProjectRunnerVersion, projectMinimumDispatcherVersion)) } @@ -163,8 +163,8 @@ func writeDispatcherMinimumVersionFile(t *testing.T, path string, content string writeFile(t, path, content) } -func buildDispatcherMinimumVersionCliContract(cliVersion string) string { - return `{"schemaVersion":1,"protocolVersion":2,"cliVersion":"` + cliVersion + `"}` +func buildDispatcherMinimumVersionCliContract(projectRunnerVersion string) string { + return `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"` + projectRunnerVersion + `"}` } func buildDispatcherMinimumVersionContract(dispatcherVersion string, dispatcherContractVersion int) string { @@ -175,7 +175,7 @@ func buildDispatcherMinimumVersionContract(dispatcherVersion string, dispatcherC func buildDispatcherMinimumVersionConstants( requiredProtocolVersion int, - minimumCliVersion string, + minimumProjectRunnerVersion string, minimumDispatcherVersion string, ) string { return `namespace Tests { @@ -183,15 +183,15 @@ public static class CliConstants { public const int REQUIRED_CLI_PROTOCOL_VERSION = ` + strconv.Itoa(requiredProtocolVersion) + `; -public const string MINIMUM_REQUIRED_CLI_VERSION = "` + minimumCliVersion + `"; +public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "` + minimumProjectRunnerVersion + `"; public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "` + minimumDispatcherVersion + `"; } }` } -func buildDispatcherMinimumVersionPin(cliVersion string, minimumDispatcherVersion string) string { - return `{"schemaVersion":1,"packageName":"test.package","packageVersion":"3.0.0-beta.40","cliVersion":"` + - cliVersion + +func buildDispatcherMinimumVersionPin(projectRunnerVersion string, minimumDispatcherVersion string) string { + return `{"schemaVersion":1,"packageName":"test.package","packageVersion":"3.0.0-beta.40","projectRunnerVersion":"` + + projectRunnerVersion + `","requiredProtocolVersion":2,"minimumDispatcherVersion":"` + minimumDispatcherVersion + `"}` diff --git a/cli/internal/automation/dispatcher_version_bump_guard.go b/cli/internal/automation/dispatcher_version_bump_guard.go index f8640bc8c..9634bf37b 100644 --- a/cli/internal/automation/dispatcher_version_bump_guard.go +++ b/cli/internal/automation/dispatcher_version_bump_guard.go @@ -14,7 +14,7 @@ import ( ) var dispatcherReleaseInputPatterns = []string{ - "cli/cmd/uloop/main.go", + "cli/cmd/dispatcher/main.go", "cli/contract.go", dispatcherContractFile, "cli/internal/cli/completion*.go", diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/cli/internal/automation/protocol_minimum_version_guard.go index 537bdf5e1..6b1087eec 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/cli/internal/automation/protocol_minimum_version_guard.go @@ -15,27 +15,21 @@ import ( ) const ( - protocolMinimumVersionFile = "Packages/src/Editor/Domain/CliConstants.cs" - protocolMinimumVersionMarker = "" - cliReleaseTagPrefix = "cli-v" + protocolMinimumVersionFile = "Packages/src/Editor/Domain/CliConstants.cs" + protocolMinimumVersionMarker = "" + projectRunnerReleaseTagPrefix = "uloop-project-runner-v" ) var ( - requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) - minimumCliVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) - requiredMinimumCliReleaseAssets = []string{ - "uloop-cli-darwin-amd64.tar.gz", - "uloop-cli-darwin-amd64.tar.gz.sha256", - "uloop-cli-darwin-arm64.tar.gz", - "uloop-cli-darwin-arm64.tar.gz.sha256", - "uloop-cli-windows-amd64.zip", - "uloop-cli-windows-amd64.zip.sha256", - "uloop-darwin-amd64.tar.gz", - "uloop-darwin-amd64.tar.gz.sha256", - "uloop-darwin-arm64.tar.gz", - "uloop-darwin-arm64.tar.gz.sha256", - "uloop-windows-amd64.zip", - "uloop-windows-amd64.zip.sha256", + requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) + minimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) + requiredMinimumCliReleaseAssets = []string{ + "uloop-project-runner-darwin-amd64.tar.gz", + "uloop-project-runner-darwin-amd64.tar.gz.sha256", + "uloop-project-runner-darwin-arm64.tar.gz", + "uloop-project-runner-darwin-arm64.tar.gz.sha256", + "uloop-project-runner-windows-amd64.zip", + "uloop-project-runner-windows-amd64.zip.sha256", } ) @@ -45,23 +39,23 @@ type ProtocolMinimumVersionGuardConfig struct { } type ProtocolMinimumVersionValues struct { - RequiredProtocolVersion int - HasRequiredProtocol bool - MinimumCliVersion string + RequiredProtocolVersion int + HasRequiredProtocol bool + MinimumProjectRunnerVersion string } type ProtocolMinimumVersionGuardResult struct { - Base ProtocolMinimumVersionValues - Head ProtocolMinimumVersionValues - RequiredProtocolChanged bool - MinimumCliVersionChanged bool - NeedsMinimumVersionUpdate bool - MinimumCliReleaseProtocolError string + Base ProtocolMinimumVersionValues + Head ProtocolMinimumVersionValues + RequiredProtocolChanged bool + MinimumProjectRunnerVersionChanged bool + NeedsMinimumVersionUpdate bool + MinimumCliReleaseProtocolError string } type minimumCliReleaseContract struct { - ProtocolVersion *json.RawMessage `json:"protocolVersion"` - CliVersion string `json:"cliVersion"` + ProtocolVersion *json.RawMessage `json:"protocolVersion"` + ProjectRunnerVersion string `json:"projectRunnerVersion"` } type minimumCliReleaseView struct { @@ -124,9 +118,9 @@ func RunMinimumCliReleaseProtocolCheck(ctx context.Context, stdout io.Writer, st writeProtocolMinimumVersionLine( stdout, fmt.Sprintf( - "Minimum CLI release %s%s advertises protocol %d.", - cliReleaseTagPrefix, - values.MinimumCliVersion, + "Minimum project runner release %s%s advertises protocol %d.", + projectRunnerReleaseTagPrefix, + values.MinimumProjectRunnerVersion, values.RequiredProtocolVersion)) return 0 } @@ -172,14 +166,14 @@ func AnalyzeProtocolMinimumVersionGuard( ) ProtocolMinimumVersionGuardResult { requiredProtocolChanged := base.HasRequiredProtocol != head.HasRequiredProtocol || base.RequiredProtocolVersion != head.RequiredProtocolVersion - minimumCliVersionChanged := base.MinimumCliVersion != head.MinimumCliVersion + minimumProjectRunnerVersionChanged := base.MinimumProjectRunnerVersion != head.MinimumProjectRunnerVersion return ProtocolMinimumVersionGuardResult{ - Base: base, - Head: head, - RequiredProtocolChanged: requiredProtocolChanged, - MinimumCliVersionChanged: minimumCliVersionChanged, - NeedsMinimumVersionUpdate: requiredProtocolChanged && !minimumCliVersionChanged, + Base: base, + Head: head, + RequiredProtocolChanged: requiredProtocolChanged, + MinimumProjectRunnerVersionChanged: minimumProjectRunnerVersionChanged, + NeedsMinimumVersionUpdate: requiredProtocolChanged && !minimumProjectRunnerVersionChanged, } } @@ -197,11 +191,11 @@ func ParseProtocolMinimumVersionValues(content []byte) (ProtocolMinimumVersionVa values.HasRequiredProtocol = true } - minimumMatches := minimumCliVersionPattern.FindStringSubmatch(text) + minimumMatches := minimumProjectRunnerVersionPattern.FindStringSubmatch(text) if len(minimumMatches) != 2 { - return ProtocolMinimumVersionValues{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_CLI_VERSION", protocolMinimumVersionFile) + return ProtocolMinimumVersionValues{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION", protocolMinimumVersionFile) } - values.MinimumCliVersion = minimumMatches[1] + values.MinimumProjectRunnerVersion = minimumMatches[1] return values, nil } @@ -213,21 +207,21 @@ func VerifyMinimumCliReleaseProtocol(values ProtocolMinimumVersionValues, contra contract := minimumCliReleaseContract{} err := json.Unmarshal(contractContent, &contract) if err != nil { - return fmt.Errorf("CLI release contract is invalid JSON: %w", err) + return fmt.Errorf("project runner release contract is invalid JSON: %w", err) } protocolVersion, hasProtocolVersion := minimumCliReleaseProtocolVersion(contract.ProtocolVersion) if !hasProtocolVersion { return fmt.Errorf( - "CLI release %s%s does not define protocolVersion", - cliReleaseTagPrefix, - values.MinimumCliVersion) + "project runner release %s%s does not define protocolVersion", + projectRunnerReleaseTagPrefix, + values.MinimumProjectRunnerVersion) } if protocolVersion != values.RequiredProtocolVersion { return fmt.Errorf( - "unity package requires protocol %d, but CLI release %s%s advertises protocol %d", + "unity package requires protocol %d, but project runner release %s%s advertises protocol %d", values.RequiredProtocolVersion, - cliReleaseTagPrefix, - values.MinimumCliVersion, + projectRunnerReleaseTagPrefix, + values.MinimumProjectRunnerVersion, protocolVersion) } return nil @@ -250,10 +244,10 @@ func verifyMinimumCliReleaseProtocolAtRef( repoRoot string, values ProtocolMinimumVersionValues, ) error { - releaseTag := cliReleaseTagPrefix + values.MinimumCliVersion + releaseTag := projectRunnerReleaseTagPrefix + values.MinimumProjectRunnerVersion contractContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, releaseTag, "cli/contract.json") if err != nil { - return fmt.Errorf("CLI release %s does not provide cli/contract.json", releaseTag) + return fmt.Errorf("project runner release %s does not provide cli/contract.json", releaseTag) } if err := VerifyMinimumCliReleaseProtocol(values, []byte(contractContent)); err != nil { return err @@ -284,18 +278,18 @@ func verifyMinimumCliReleaseIsPublished(ctx context.Context, repoRoot string, re "--json", "isDraft,assets") if err != nil { - return fmt.Errorf("CLI release %s is not published with complete native assets: %w", releaseTag, err) + return fmt.Errorf("project runner release %s is not published with complete native assets: %w", releaseTag, err) } releaseView := minimumCliReleaseView{} if err := json.Unmarshal([]byte(output), &releaseView); err != nil { - return fmt.Errorf("CLI release %s metadata is invalid JSON: %w", releaseTag, err) + return fmt.Errorf("project runner release %s metadata is invalid JSON: %w", releaseTag, err) } if releaseView.IsDraft { - return fmt.Errorf("CLI release %s is still draft", releaseTag) + return fmt.Errorf("project runner release %s is still draft", releaseTag) } if missingAsset := missingMinimumCliReleaseAsset(releaseView.Assets); missingAsset != "" { - return fmt.Errorf("CLI release %s is missing release asset %s", releaseTag, missingAsset) + return fmt.Errorf("project runner release %s is missing release asset %s", releaseTag, missingAsset) } return nil } @@ -321,11 +315,11 @@ func FormatProtocolMinimumVersionWarning(result ProtocolMinimumVersionGuardResul builder.WriteString(protocolMinimumVersionMarker) builder.WriteString("\n") if result.NeedsMinimumVersionUpdate { - builder.WriteString("Protocol version changed, but `MINIMUM_REQUIRED_CLI_VERSION` did not.\n\n") + builder.WriteString("Protocol version changed, but `MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` did not.\n\n") } else if result.RequiredProtocolChanged { - builder.WriteString("Protocol version changed, but `MINIMUM_REQUIRED_CLI_VERSION` does not point to a published CLI release that advertises the required protocol.\n\n") + builder.WriteString("Protocol version changed, but `MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` does not point to a published project runner release that advertises the required protocol.\n\n") } else { - builder.WriteString("`MINIMUM_REQUIRED_CLI_VERSION` changed, but it does not point to a published CLI release that advertises the required protocol.\n\n") + builder.WriteString("`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` changed, but it does not point to a published project runner release that advertises the required protocol.\n\n") } builder.WriteString("- Base required protocol: ") builder.WriteString(protocolMinimumVersionValueLabel(result.Base)) @@ -333,8 +327,8 @@ func FormatProtocolMinimumVersionWarning(result ProtocolMinimumVersionGuardResul builder.WriteString("- Head required protocol: ") builder.WriteString(protocolMinimumVersionValueLabel(result.Head)) builder.WriteString("\n") - builder.WriteString("- Current minimum CLI: `") - builder.WriteString(result.Head.MinimumCliVersion) + builder.WriteString("- Current minimum project runner: `") + builder.WriteString(result.Head.MinimumProjectRunnerVersion) builder.WriteString("`\n") if result.MinimumCliReleaseProtocolError != "" { builder.WriteString("- Release check: ") @@ -342,7 +336,7 @@ func FormatProtocolMinimumVersionWarning(result ProtocolMinimumVersionGuardResul builder.WriteString("\n") } builder.WriteString("\n") - builder.WriteString("Update `MINIMUM_REQUIRED_CLI_VERSION` to a published CLI release that advertises the new protocol before releasing the Unity package.") + builder.WriteString("Update `MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` to a published project runner release that advertises the new protocol before releasing the Unity package.") return builder.String() } @@ -354,7 +348,7 @@ func protocolMinimumVersionGuardNeedsReleaseCheck(result ProtocolMinimumVersionG if result.NeedsMinimumVersionUpdate { return false } - return result.RequiredProtocolChanged || result.MinimumCliVersionChanged + return result.RequiredProtocolChanged || result.MinimumProjectRunnerVersionChanged } func protocolMinimumVersionValueLabel(values ProtocolMinimumVersionValues) string { diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/cli/internal/automation/protocol_minimum_version_guard_test.go index dd0b61392..a4b281a1b 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/internal/automation/protocol_minimum_version_guard_test.go @@ -11,17 +11,17 @@ import ( ) func TestAnalyzeProtocolMinimumVersionGuard_WhenProtocolChangesWithoutMinimumVersionChange_Warns(t *testing.T) { - // Verifies protocol bumps cannot leave the installer target on the old CLI release. + // Verifies protocol bumps cannot leave the installer target on the old project runner release. result := AnalyzeProtocolMinimumVersionGuard( ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 1, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.32", + RequiredProtocolVersion: 1, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.32", }, ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 2, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.32", + RequiredProtocolVersion: 2, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.32", }) if !result.NeedsMinimumVersionUpdate { @@ -33,14 +33,14 @@ func TestAnalyzeProtocolMinimumVersionGuard_WhenProtocolAndMinimumVersionChange_ // Verifies paired protocol and installer target updates clear the update-omission warning. result := AnalyzeProtocolMinimumVersionGuard( ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 1, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.32", + RequiredProtocolVersion: 1, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.32", }, ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 2, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.33", + RequiredProtocolVersion: 2, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.33", }) if result.NeedsMinimumVersionUpdate { @@ -52,14 +52,14 @@ func TestAnalyzeProtocolMinimumVersionGuard_WhenProtocolDoesNotChange_DoesNotWar // Verifies ordinary package edits do not force a CLI installer target bump. result := AnalyzeProtocolMinimumVersionGuard( ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 2, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.32", + RequiredProtocolVersion: 2, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.32", }, ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 2, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.32", + RequiredProtocolVersion: 2, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.32", }) if result.NeedsMinimumVersionUpdate { @@ -68,18 +68,18 @@ func TestAnalyzeProtocolMinimumVersionGuard_WhenProtocolDoesNotChange_DoesNotWar } func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseMatches_Passes(t *testing.T) { - // Verifies protocol bump PRs pass only after the selected CLI release advertises the new protocol. + // Verifies protocol bump PRs pass only after the selected project runner release advertises the new protocol. result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.33"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, }) if result.exitCode != 0 { t.Fatalf("expected exit code 0, got %d\nstderr: %s", result.exitCode, result.stderr) } assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") - assertProtocolMinimumVersionLogContains(t, result.gitLog, "cli-v3.0.0-beta.33:cli/contract.json") + assertProtocolMinimumVersionLogContains(t, result.gitLog, "uloop-project-runner-v3.0.0-beta.33:cli/contract.json") } func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseProtocolDiffers_Fails(t *testing.T) { @@ -87,45 +87,45 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseProtocolDiffers_Fails( result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":1,"cliVersion":"3.0.0-beta.33"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":1,"projectRunnerVersion":"3.0.0-beta.33"}`, }) if result.exitCode != 1 { t.Fatalf("expected exit code 1, got %d\nstdout: %s", result.exitCode, result.stdout) } - assertProtocolMinimumVersionLogContains(t, result.stderr, "does not point to a published CLI release") + assertProtocolMinimumVersionLogContains(t, result.stderr, "does not point to a published project runner release") assertProtocolMinimumVersionLogContains(t, result.stderr, "advertises protocol 1") } func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseIsDraft_Fails(t *testing.T) { - // Verifies protocol bump PRs wait for a published CLI release, not only a git tag. + // Verifies protocol bump PRs wait for a published project runner release, not only a git tag. result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.33"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, releaseView: `{"isDraft":true,"assets":[]}`, }) if result.exitCode != 1 { t.Fatalf("expected exit code 1, got %d\nstdout: %s", result.exitCode, result.stdout) } - assertProtocolMinimumVersionLogContains(t, result.stderr, "does not point to a published CLI release") + assertProtocolMinimumVersionLogContains(t, result.stderr, "does not point to a published project runner release") assertProtocolMinimumVersionLogContains(t, result.stderr, "is still draft") } func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseAssetsAreMissing_Fails(t *testing.T) { - // Verifies protocol bump PRs wait for installable native CLI release assets. + // Verifies protocol bump PRs wait for installable native project runner release assets. result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.33"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, releaseView: `{"isDraft":false,"assets":[]}`, }) if result.exitCode != 1 { t.Fatalf("expected exit code 1, got %d\nstdout: %s", result.exitCode, result.stdout) } - assertProtocolMinimumVersionLogContains(t, result.stderr, "does not point to a published CLI release") + assertProtocolMinimumVersionLogContains(t, result.stderr, "does not point to a published project runner release") assertProtocolMinimumVersionLogContains(t, result.stderr, "is missing release asset") } @@ -134,35 +134,35 @@ func TestRunProtocolMinimumVersionGuard_WhenOnlyMinimumReleaseProtocolDiffers_Fa result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ baseContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.32"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":1,"cliVersion":"3.0.0-beta.33"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":1,"projectRunnerVersion":"3.0.0-beta.33"}`, }) if result.exitCode != 1 { t.Fatalf("expected exit code 1, got %d\nstdout: %s", result.exitCode, result.stdout) } - assertProtocolMinimumVersionLogContains(t, result.stderr, "`MINIMUM_REQUIRED_CLI_VERSION` changed") + assertProtocolMinimumVersionLogContains(t, result.stderr, "`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` changed") assertProtocolMinimumVersionLogContains(t, result.stderr, "advertises protocol 1") } func TestVerifyMinimumCliReleaseProtocol_WhenTagProtocolMatches_Passes(t *testing.T) { - // Verifies release validation accepts a CLI tag that advertises the required protocol. + // Verifies release validation accepts a project runner tag that advertises the required protocol. err := VerifyMinimumCliReleaseProtocol(ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 2, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.33", - }, []byte(`{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.33"}`)) + RequiredProtocolVersion: 2, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.33", + }, []byte(`{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`)) if err != nil { t.Fatalf("expected matching release protocol, got %v", err) } } func TestVerifyMinimumCliReleaseProtocol_WhenTagProtocolIsMissing_Fails(t *testing.T) { - // Verifies release validation rejects CLI tags that predate protocol metadata. + // Verifies release validation rejects project runner tags that predate protocol metadata. err := VerifyMinimumCliReleaseProtocol(ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 2, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.32", - }, []byte(`{"schemaVersion":1,"cliVersion":"3.0.0-beta.32"}`)) + RequiredProtocolVersion: 2, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.32", + }, []byte(`{"schemaVersion":1,"projectRunnerVersion":"3.0.0-beta.32"}`)) if err == nil { t.Fatal("expected missing protocolVersion to fail") @@ -175,10 +175,10 @@ func TestVerifyMinimumCliReleaseProtocol_WhenTagProtocolIsMissing_Fails(t *testi func TestVerifyMinimumCliReleaseProtocol_WhenProtocolVersionIsOutOfRange_FailsAsMissingMetadata(t *testing.T) { // Verifies malformed protocol metadata is reported consistently with missing metadata. err := VerifyMinimumCliReleaseProtocol(ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 2, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.33", - }, []byte(`{"schemaVersion":1,"protocolVersion":999999999999999999999999999999,"cliVersion":"3.0.0-beta.33"}`)) + RequiredProtocolVersion: 2, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.33", + }, []byte(`{"schemaVersion":1,"protocolVersion":999999999999999999999999999999,"projectRunnerVersion":"3.0.0-beta.33"}`)) if err == nil { t.Fatal("expected out-of-range protocolVersion to fail") @@ -192,12 +192,12 @@ func TestVerifyMinimumCliReleaseProtocol_WhenProtocolVersionIsOutOfRange_FailsAs } func TestVerifyMinimumCliReleaseProtocol_WhenTagProtocolDiffers_Fails(t *testing.T) { - // Verifies release validation rejects published CLIs from a different protocol generation. + // Verifies release validation rejects published project runners from a different protocol generation. err := VerifyMinimumCliReleaseProtocol(ProtocolMinimumVersionValues{ - RequiredProtocolVersion: 3, - HasRequiredProtocol: true, - MinimumCliVersion: "3.0.0-beta.33", - }, []byte(`{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.33"}`)) + RequiredProtocolVersion: 3, + HasRequiredProtocol: true, + MinimumProjectRunnerVersion: "3.0.0-beta.33", + }, []byte(`{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`)) if err == nil { t.Fatal("expected mismatched protocolVersion to fail") @@ -223,7 +223,7 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenRefIsProvided_ReadsValuesAtRef(t prepareProtocolMinimumVersionGitContents(t, workDir, protocolMinimumVersionRefCase{ baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.33"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, }) t.Setenv("PATH", mockBin+string(os.PathListSeparator)+os.Getenv("PATH")) @@ -242,9 +242,9 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenRefIsProvided_ReadsValuesAtRef(t if exitCode != 0 { t.Fatalf("expected exit code 0, got %d\nstderr: %s", exitCode, stderr.String()) } - assertProtocolMinimumVersionLogContains(t, stdout.String(), "Minimum CLI release cli-v3.0.0-beta.33 advertises protocol 2.") + assertProtocolMinimumVersionLogContains(t, stdout.String(), "Minimum project runner release uloop-project-runner-v3.0.0-beta.33 advertises protocol 2.") assertProtocolMinimumVersionLogContains(t, readFile(t, gitLogPath), "protocol-release:"+protocolMinimumVersionFile) - assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view cli-v3.0.0-beta.33") + assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view uloop-project-runner-v3.0.0-beta.33") } func TestRunProtocolMinimumVersionComment_WhenWarningExists_UpsertsComment(t *testing.T) { @@ -268,7 +268,7 @@ func TestRunProtocolMinimumVersionComment_WhenWarningIsResolved_DeletesComment(t result := runProtocolMinimumVersionCommentCase(t, protocolMinimumVersionCommentCase{ baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.33"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, commentIDs: "123", }) @@ -284,7 +284,7 @@ func TestRunProtocolMinimumVersionComment_WhenMinimumReleaseProtocolDiffers_Upse result := runProtocolMinimumVersionCommentCase(t, protocolMinimumVersionCommentCase{ baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":1,"cliVersion":"3.0.0-beta.33"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":1,"projectRunnerVersion":"3.0.0-beta.33"}`, commentIDs: "123", }) @@ -425,13 +425,13 @@ func prepareProtocolMinimumVersionGitContents(t *testing.T, workDir string, test t.Setenv("GIT_HEAD_CONTENT", headContentPath) } -func buildProtocolMinimumVersionConstants(requiredProtocolVersion int, minimumCliVersion string) string { +func buildProtocolMinimumVersionConstants(requiredProtocolVersion int, minimumProjectRunnerVersion string) string { return `namespace Tests { public static class CliConstants { public const int REQUIRED_CLI_PROTOCOL_VERSION = ` + strconv.Itoa(requiredProtocolVersion) + `; -public const string MINIMUM_REQUIRED_CLI_VERSION = "` + minimumCliVersion + `"; +public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "` + minimumProjectRunnerVersion + `"; } }` } @@ -458,7 +458,7 @@ if [ "$1" = "show" ]; then origin/v3-beta:*) cat "$GIT_BASE_CONTENT" ;; protocol-pr-head:*) cat "$GIT_HEAD_CONTENT" ;; protocol-release:*) cat "$GIT_HEAD_CONTENT" ;; - cli-v*:cli/contract.json) + uloop-project-runner-v*:cli/contract.json) if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then cat "$GIT_RELEASE_CONTENT" else @@ -493,7 +493,7 @@ if [ "$1" = "release" ] && [ "$2" = "view" ]; then if [ -n "${GH_RELEASE_VIEW:-}" ]; then printf '%s\n' "$GH_RELEASE_VIEW" else - printf '%s\n' '{"isDraft":false,"assets":[{"name":"uloop-cli-darwin-amd64.tar.gz","size":1},{"name":"uloop-cli-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-cli-windows-amd64.zip","size":1},{"name":"uloop-cli-windows-amd64.zip.sha256","size":1},{"name":"uloop-darwin-amd64.tar.gz","size":1},{"name":"uloop-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-darwin-arm64.tar.gz","size":1},{"name":"uloop-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-windows-amd64.zip","size":1},{"name":"uloop-windows-amd64.zip.sha256","size":1}]}' + printf '%s\n' '{"isDraft":false,"assets":[{"name":"uloop-project-runner-darwin-amd64.tar.gz","size":1},{"name":"uloop-project-runner-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-project-runner-darwin-arm64.tar.gz","size":1},{"name":"uloop-project-runner-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-project-runner-windows-amd64.zip","size":1},{"name":"uloop-project-runner-windows-amd64.zip.sha256","size":1}]}' fi exit 0 fi diff --git a/cli/internal/automation/release_pr_checks_test.go b/cli/internal/automation/release_pr_checks_test.go index f4f7eed8d..939f4e494 100644 --- a/cli/internal/automation/release_pr_checks_test.go +++ b/cli/internal/automation/release_pr_checks_test.go @@ -312,7 +312,7 @@ fi if [ "$1" = "show" ]; then case "$2" in - cli-v*:cli/contract.json) + uloop-project-runner-v*:cli/contract.json) if [ -n "${GIT_RELEASE_CONTRACT:-}" ]; then cat "$GIT_RELEASE_CONTRACT" else diff --git a/cli/internal/cli/dispatcher.go b/cli/internal/cli/dispatcher.go index 50ee42b47..d92a507a9 100644 --- a/cli/internal/cli/dispatcher.go +++ b/cli/internal/cli/dispatcher.go @@ -23,11 +23,11 @@ const ( dispatcherCacheDirectoryName = "uloop" dispatcherVersionsDirectoryName = "versions" dispatcherUpdateStateFileName = "dispatcher-update.json" - dispatcherProjectPinRelativePath = ".uloop/cli-pin.json" - dispatcherPackagePinFileName = "cli-pin.json" + dispatcherProjectPinRelativePath = ".uloop/project-runner-pin.json" + dispatcherPackagePinFileName = "project-runner-pin.json" dispatcherUnityPackageName = "io.github.hatayama.uloopmcp" - dispatcherRealCLIUnixFileName = "uloop-cli" - dispatcherRealCLIWindowsFileName = "uloop-cli.exe" + dispatcherRealCLIUnixFileName = "uloop-project-runner" + dispatcherRealCLIWindowsFileName = "uloop-project-runner.exe" dispatcherReleaseRepository = "hatayama/unity-cli-loop" dispatcherReleaseBaseURL = "https://github.com/" + dispatcherReleaseRepository + "/releases/download" dispatcherSelfUpdateInterval = 24 * time.Hour @@ -285,12 +285,12 @@ func dispatcherPinResolutionError(projectRoot string, cause error) cliError { return cliError{ ErrorCode: errorCodeInternalError, Phase: errorPhaseProjectResolve, - Message: "Could not resolve the required uloop CLI for this Unity project.", + Message: "Could not resolve the required uloop project runner for this Unity project.", Retryable: true, SafeToRetry: true, ProjectRoot: projectRoot, NextActions: []string{ - "Open the Unity project once so Unity CLI Loop can write `.uloop/cli-pin.json`.", + "Open the Unity project once so Unity CLI Loop can write `.uloop/project-runner-pin.json`.", "Run the CLI setup from Unity CLI Loop Settings if the pin file is still missing.", }, Details: map[string]any{ @@ -303,15 +303,15 @@ func dispatcherRealCLIResolutionError(projectRoot string, pin dispatcherPin, cau return cliError{ ErrorCode: errorCodeInternalError, Phase: errorPhaseExecution, - Message: "Could not prepare the pinned uloop CLI version.", + Message: "Could not prepare the pinned uloop project runner version.", Retryable: true, SafeToRetry: true, ProjectRoot: projectRoot, NextActions: []string{"Check network access to GitHub releases, then retry the command."}, Details: map[string]any{ - "Cause": cause.Error(), - "CliVersion": pin.CLIVersion, - "PinSource": pin.SourcePath, + "Cause": cause.Error(), + "ProjectRunnerVersion": pin.ProjectRunnerVersion, + "PinSource": pin.SourcePath, }, } } diff --git a/cli/internal/cli/dispatcher_download.go b/cli/internal/cli/dispatcher_download.go index de7da8126..09d4f55a7 100644 --- a/cli/internal/cli/dispatcher_download.go +++ b/cli/internal/cli/dispatcher_download.go @@ -24,8 +24,8 @@ import ( var dispatcherHTTPClient = &http.Client{Timeout: 2 * time.Minute} func resolveDispatcherRealCLI(ctx context.Context, pin dispatcherPin, stderr io.Writer) (string, error) { - pin.CLIVersion = strings.TrimSpace(pin.CLIVersion) - if err := validateDispatcherCLIVersion(pin.CLIVersion); err != nil { + pin.ProjectRunnerVersion = strings.TrimSpace(pin.ProjectRunnerVersion) + if err := validateDispatcherProjectRunnerVersion(pin.ProjectRunnerVersion); err != nil { return "", err } if siblingPath, ok := dispatcherSiblingRealCLIPath(pin); ok { @@ -36,16 +36,16 @@ func resolveDispatcherRealCLI(ctx context.Context, pin dispatcherPin, stderr io. if err != nil { return "", err } - realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, pin.CLIVersion, runtime.GOOS, runtime.GOARCH) + realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, pin.ProjectRunnerVersion, runtime.GOOS, runtime.GOARCH) if isExecutableFile(realCLIPath) { return realCLIPath, nil } - return downloadDispatcherRealCLI(ctx, cacheRoot, pin.CLIVersion, runtime.GOOS, runtime.GOARCH, stderr) + return downloadDispatcherRealCLI(ctx, cacheRoot, pin.ProjectRunnerVersion, runtime.GOOS, runtime.GOARCH, stderr) } func dispatcherSiblingRealCLIPath(pin dispatcherPin) (string, bool) { - if pin.CLIVersion != version { + if pin.ProjectRunnerVersion != version { return "", false } executablePath, err := os.Executable() @@ -91,11 +91,11 @@ func dispatcherCacheRoot(goos string) (string, error) { } } -func dispatcherCachedRealCLIPath(cacheRoot string, cliVersion string, goos string, goarch string) string { +func dispatcherCachedRealCLIPath(cacheRoot string, projectRunnerVersion string, goos string, goarch string) string { return filepath.Join( cacheRoot, dispatcherVersionsDirectoryName, - cliVersion, + projectRunnerVersion, dispatcherPlatformName(goos, goarch), dispatcherRealCLIFileName(goos)) } @@ -122,12 +122,12 @@ func isExecutableFile(filePath string) bool { return info.Mode()&0o111 != 0 } -func downloadDispatcherRealCLI(ctx context.Context, cacheRoot string, cliVersion string, goos string, goarch string, stderr io.Writer) (string, error) { +func downloadDispatcherRealCLI(ctx context.Context, cacheRoot string, projectRunnerVersion string, goos string, goarch string, stderr io.Writer) (string, error) { assetName, err := dispatcherReleaseAssetName(goos, goarch) if err != nil { return "", err } - realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, cliVersion, goos, goarch) + realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, projectRunnerVersion, goos, goarch) if err := os.MkdirAll(filepath.Dir(realCLIPath), 0o755); err != nil { return "", err } @@ -142,8 +142,8 @@ func downloadDispatcherRealCLI(ctx context.Context, cacheRoot string, cliVersion archivePath := filepath.Join(tempDir, assetName) checksumPath := archivePath + ".sha256" - assetURL := dispatcherReleaseAssetURL(cliVersion, assetName) - writeFormat(stderr, "uloop: downloading pinned CLI %s for %s...\n", cliVersion, dispatcherPlatformName(goos, goarch)) + assetURL := dispatcherReleaseAssetURL(projectRunnerVersion, assetName) + writeFormat(stderr, "uloop: downloading pinned project runner %s for %s...\n", projectRunnerVersion, dispatcherPlatformName(goos, goarch)) if err := downloadDispatcherFile(ctx, assetURL, archivePath); err != nil { return "", err } @@ -189,19 +189,19 @@ func dispatcherReleaseAssetName(goos string, goarch string) (string, error) { if goarch != "arm64" && goarch != "amd64" { return "", fmt.Errorf("unsupported darwin architecture: %s", goarch) } - return "uloop-cli-darwin-" + goarch + ".tar.gz", nil + return "uloop-project-runner-darwin-" + goarch + ".tar.gz", nil case "windows": if goarch != "amd64" { return "", fmt.Errorf("unsupported windows architecture: %s", goarch) } - return "uloop-cli-windows-amd64.zip", nil + return "uloop-project-runner-windows-amd64.zip", nil default: return "", fmt.Errorf("unsupported platform: %s-%s", goos, goarch) } } -func dispatcherReleaseAssetURL(cliVersion string, assetName string) string { - return dispatcherReleaseBaseURL + "/" + sharedupdate.CLIReleaseTag(cliVersion) + "/" + assetName +func dispatcherReleaseAssetURL(projectRunnerVersion string, assetName string) string { + return dispatcherReleaseBaseURL + "/" + sharedupdate.ProjectRunnerReleaseTag(projectRunnerVersion) + "/" + assetName } func downloadDispatcherFile(ctx context.Context, url string, destinationPath string) error { diff --git a/cli/internal/cli/dispatcher_pin.go b/cli/internal/cli/dispatcher_pin.go index 9c00a5aad..e0a894a93 100644 --- a/cli/internal/cli/dispatcher_pin.go +++ b/cli/internal/cli/dispatcher_pin.go @@ -11,17 +11,17 @@ import ( ) var ( - dispatcherMinimumCliVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) - dispatcherMinimumVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_DISPATCHER_VERSION\s*=\s*"([^"]+)"`) - dispatcherRequiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) - dispatcherCLIVersionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$`) + dispatcherMinimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) + dispatcherMinimumVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_DISPATCHER_VERSION\s*=\s*"([^"]+)"`) + dispatcherRequiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) + dispatcherProjectRunnerVersionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$`) ) type dispatcherPin struct { SchemaVersion int `json:"schemaVersion"` PackageName string `json:"packageName"` PackageVersion string `json:"packageVersion"` - CLIVersion string `json:"cliVersion"` + ProjectRunnerVersion string `json:"projectRunnerVersion"` RequiredProtocolVersion int `json:"requiredProtocolVersion"` MinimumDispatcherVersion string `json:"minimumDispatcherVersion"` SourcePath string `json:"-"` @@ -64,7 +64,7 @@ func loadDispatcherPin(projectRoot string) (dispatcherPin, error) { if invalidPackagePinError != nil { return dispatcherPin{}, invalidPackagePinError } - return dispatcherPin{}, fmt.Errorf("cli pin not found under %s", projectRoot) + return dispatcherPin{}, fmt.Errorf("project runner pin not found under %s", projectRoot) } func dispatcherPinCandidatePaths(projectRoot string) []dispatcherPinCandidatePath { @@ -118,16 +118,16 @@ func readDispatcherPin(pinPath string) (dispatcherPin, error) { if err := json.Unmarshal(content, &pin); err != nil { return dispatcherPin{}, fmt.Errorf("failed to parse %s: %w", pinPath, err) } - pin.CLIVersion = normalizeDispatcherVersion(pin.CLIVersion) - if pin.CLIVersion == "" { - return dispatcherPin{}, fmt.Errorf("%s does not define cliVersion", pinPath) + pin.ProjectRunnerVersion = normalizeDispatcherVersion(pin.ProjectRunnerVersion) + if pin.ProjectRunnerVersion == "" { + return dispatcherPin{}, fmt.Errorf("%s does not define projectRunnerVersion", pinPath) } - if err := validateDispatcherCLIVersion(pin.CLIVersion); err != nil { - return dispatcherPin{}, fmt.Errorf("%s defines invalid cliVersion: %w", pinPath, err) + if err := validateDispatcherProjectRunnerVersion(pin.ProjectRunnerVersion); err != nil { + return dispatcherPin{}, fmt.Errorf("%s defines invalid projectRunnerVersion: %w", pinPath, err) } pin.MinimumDispatcherVersion = normalizeDispatcherVersion(pin.MinimumDispatcherVersion) if pin.MinimumDispatcherVersion != "" { - if err := validateDispatcherCLIVersion(pin.MinimumDispatcherVersion); err != nil { + if err := validateDispatcherProjectRunnerVersion(pin.MinimumDispatcherVersion); err != nil { return dispatcherPin{}, fmt.Errorf("%s defines invalid minimumDispatcherVersion: %w", pinPath, err) } } @@ -144,20 +144,20 @@ func readDispatcherPinFromCliConstants(constantsPath string) (dispatcherPin, err return dispatcherPin{}, err } text := string(content) - versionMatch := dispatcherMinimumCliVersionPattern.FindStringSubmatch(text) + versionMatch := dispatcherMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) if len(versionMatch) != 2 { - return dispatcherPin{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_CLI_VERSION", constantsPath) + return dispatcherPin{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION", constantsPath) } - cliVersion := normalizeDispatcherVersion(versionMatch[1]) - if err := validateDispatcherCLIVersion(cliVersion); err != nil { - return dispatcherPin{}, fmt.Errorf("%s defines invalid MINIMUM_REQUIRED_CLI_VERSION: %w", constantsPath, err) + projectRunnerVersion := normalizeDispatcherVersion(versionMatch[1]) + if err := validateDispatcherProjectRunnerVersion(projectRunnerVersion); err != nil { + return dispatcherPin{}, fmt.Errorf("%s defines invalid MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION: %w", constantsPath, err) } dispatcherVersionMatch := dispatcherMinimumVersionPattern.FindStringSubmatch(text) if len(dispatcherVersionMatch) != 2 { return dispatcherPin{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_DISPATCHER_VERSION", constantsPath) } minimumDispatcherVersion := normalizeDispatcherVersion(dispatcherVersionMatch[1]) - if err := validateDispatcherCLIVersion(minimumDispatcherVersion); err != nil { + if err := validateDispatcherProjectRunnerVersion(minimumDispatcherVersion); err != nil { return dispatcherPin{}, fmt.Errorf("%s defines invalid MINIMUM_REQUIRED_DISPATCHER_VERSION: %w", constantsPath, err) } protocolVersion := 0 @@ -169,7 +169,7 @@ func readDispatcherPinFromCliConstants(constantsPath string) (dispatcherPin, err return dispatcherPin{ SchemaVersion: 1, PackageName: dispatcherUnityPackageName, - CLIVersion: cliVersion, + ProjectRunnerVersion: projectRunnerVersion, RequiredProtocolVersion: protocolVersion, MinimumDispatcherVersion: minimumDispatcherVersion, SourcePath: constantsPath, @@ -184,9 +184,9 @@ func normalizeDispatcherVersion(value string) string { return trimmed } -func validateDispatcherCLIVersion(cliVersion string) error { - if !dispatcherCLIVersionPattern.MatchString(cliVersion) { - return fmt.Errorf("expected semantic version, got %q", cliVersion) +func validateDispatcherProjectRunnerVersion(projectRunnerVersion string) error { + if !dispatcherProjectRunnerVersionPattern.MatchString(projectRunnerVersion) { + return fmt.Errorf("expected semantic version, got %q", projectRunnerVersion) } return nil } diff --git a/cli/internal/cli/dispatcher_test.go b/cli/internal/cli/dispatcher_test.go index 9407a1513..ec96b817b 100644 --- a/cli/internal/cli/dispatcher_test.go +++ b/cli/internal/cli/dispatcher_test.go @@ -200,14 +200,14 @@ func TestRunDispatcherVersionUsesDispatcherVersion(t *testing.T) { } } -func TestResolveDispatcherRealCLIRejectsInvalidCLIVersion(t *testing.T) { - // Verifies project pins cannot escape the dispatcher cache through cliVersion path segments. +func TestResolveDispatcherRealCLIRejectsInvalidProjectRunnerVersion(t *testing.T) { + // Verifies project pins cannot escape the dispatcher cache through projectRunnerVersion path segments. t.Setenv(dispatcherCacheDirEnvName, t.TempDir()) - _, err := resolveDispatcherRealCLI(context.Background(), dispatcherPin{CLIVersion: "../../../../payload"}, io.Discard) + _, err := resolveDispatcherRealCLI(context.Background(), dispatcherPin{ProjectRunnerVersion: "../../../../payload"}, io.Discard) if err == nil { - t.Fatal("expected invalid cliVersion error") + t.Fatal("expected invalid projectRunnerVersion error") } } @@ -341,15 +341,14 @@ func stubDispatcherUpdateHooks(t *testing.T, updatedVersion string) func() { } } -func TestExtractDispatcherRealCLIFromTarPrefersRealCLI(t *testing.T) { - // Verifies legacy bridge archives that contain dispatcher first still extract the real CLI binary. +func TestExtractDispatcherRealCLIFromTarRequiresProjectRunnerAsset(t *testing.T) { + // Verifies project runner release archives extract the project runner binary. tempDir := t.TempDir() - archivePath := filepath.Join(tempDir, "uloop-darwin-arm64.tar.gz") + archivePath := filepath.Join(tempDir, "uloop-project-runner-darwin-arm64.tar.gz") writeDispatcherTarGzArchive(t, archivePath, []dispatcherArchiveTestEntry{ - {Name: "uloop", Content: "dispatcher"}, - {Name: "uloop-cli", Content: "real"}, + {Name: "uloop-project-runner", Content: "real"}, }) - destinationPath := filepath.Join(tempDir, "uloop-cli") + destinationPath := filepath.Join(tempDir, "uloop-project-runner") err := extractDispatcherRealCLI(archivePath, filepath.Base(archivePath), destinationPath, "darwin") if err != nil { @@ -358,47 +357,14 @@ func TestExtractDispatcherRealCLIFromTarPrefersRealCLI(t *testing.T) { assertFileContent(t, destinationPath, "real") } -func TestExtractDispatcherRealCLIFromZipPrefersRealCLI(t *testing.T) { - // Verifies Windows legacy bridge archives that contain dispatcher first still extract the real CLI binary. +func TestExtractDispatcherRealCLIFromZipRequiresProjectRunnerAsset(t *testing.T) { + // Verifies Windows project runner release archives extract the project runner binary. tempDir := t.TempDir() - archivePath := filepath.Join(tempDir, "uloop-windows-amd64.zip") + archivePath := filepath.Join(tempDir, "uloop-project-runner-windows-amd64.zip") writeDispatcherZipArchive(t, archivePath, []dispatcherArchiveTestEntry{ - {Name: "uloop.exe", Content: "dispatcher"}, - {Name: "uloop-cli.exe", Content: "real"}, + {Name: "uloop-project-runner.exe", Content: "real"}, }) - destinationPath := filepath.Join(tempDir, "uloop-cli.exe") - - err := extractDispatcherRealCLI(archivePath, filepath.Base(archivePath), destinationPath, "windows") - if err != nil { - t.Fatalf("extractDispatcherRealCLI failed: %v", err) - } - assertFileContent(t, destinationPath, "real") -} - -func TestExtractDispatcherRealCLIFromTarRequiresRealCLIAsset(t *testing.T) { - // Verifies CLI release archives extract the real CLI binary without dispatcher payloads. - tempDir := t.TempDir() - archivePath := filepath.Join(tempDir, "uloop-cli-darwin-arm64.tar.gz") - writeDispatcherTarGzArchive(t, archivePath, []dispatcherArchiveTestEntry{ - {Name: "uloop-cli", Content: "real"}, - }) - destinationPath := filepath.Join(tempDir, "uloop-cli") - - err := extractDispatcherRealCLI(archivePath, filepath.Base(archivePath), destinationPath, "darwin") - if err != nil { - t.Fatalf("extractDispatcherRealCLI failed: %v", err) - } - assertFileContent(t, destinationPath, "real") -} - -func TestExtractDispatcherRealCLIFromZipRequiresRealCLIAsset(t *testing.T) { - // Verifies Windows CLI release archives extract the real CLI binary without dispatcher payloads. - tempDir := t.TempDir() - archivePath := filepath.Join(tempDir, "uloop-cli-windows-amd64.zip") - writeDispatcherZipArchive(t, archivePath, []dispatcherArchiveTestEntry{ - {Name: "uloop-cli.exe", Content: "real"}, - }) - destinationPath := filepath.Join(tempDir, "uloop-cli.exe") + destinationPath := filepath.Join(tempDir, "uloop-project-runner.exe") err := extractDispatcherRealCLI(archivePath, filepath.Base(archivePath), destinationPath, "windows") if err != nil { @@ -415,11 +381,11 @@ func TestDispatcherHTTPClientHasDownloadTimeout(t *testing.T) { } func TestDownloadDispatcherRealCLIWritesDownloadStatus(t *testing.T) { - // Verifies cache misses tell callers that dispatcher is downloading the pinned CLI. + // Verifies cache misses tell callers that dispatcher is downloading the pinned project runner. tempDir := t.TempDir() - archivePath := filepath.Join(tempDir, "uloop-cli-darwin-arm64.tar.gz") + archivePath := filepath.Join(tempDir, "uloop-project-runner-darwin-arm64.tar.gz") writeDispatcherTarGzArchive(t, archivePath, []dispatcherArchiveTestEntry{ - {Name: "uloop-cli", Content: "real"}, + {Name: "uloop-project-runner", Content: "real"}, }) archiveContent, err := os.ReadFile(archivePath) if err != nil { @@ -436,11 +402,11 @@ func TestDownloadDispatcherRealCLIWritesDownloadStatus(t *testing.T) { Transport: dispatcherRoundTripFunc(func(request *http.Request) (*http.Response, error) { content := []byte{} statusCode := http.StatusNotFound - if strings.HasSuffix(request.URL.Path, "/uloop-cli-darwin-arm64.tar.gz") { + if strings.HasSuffix(request.URL.Path, "/uloop-project-runner-darwin-arm64.tar.gz") { content = archiveContent statusCode = http.StatusOK } - if strings.HasSuffix(request.URL.Path, "/uloop-cli-darwin-arm64.tar.gz.sha256") { + if strings.HasSuffix(request.URL.Path, "/uloop-project-runner-darwin-arm64.tar.gz.sha256") { content = checksumContent statusCode = http.StatusOK } @@ -463,7 +429,7 @@ func TestDownloadDispatcherRealCLIWritesDownloadStatus(t *testing.T) { if err != nil { t.Fatalf("downloadDispatcherRealCLI failed: %v", err) } - expectedStatus := "uloop: downloading pinned CLI 3.0.0-beta.88 for darwin-arm64...\n" + expectedStatus := "uloop: downloading pinned project runner 3.0.0-beta.88 for darwin-arm64...\n" if stderr.String() != expectedStatus { t.Fatalf("download status mismatch: %q", stderr.String()) } @@ -505,8 +471,8 @@ func TestLoadDispatcherPinFallsBackToPackagePin(t *testing.T) { if err != nil { t.Fatalf("loadDispatcherPin failed: %v", err) } - if pin.CLIVersion != "3.0.0-beta.55" { - t.Fatalf("cliVersion mismatch: %s", pin.CLIVersion) + if pin.ProjectRunnerVersion != "3.0.0-beta.55" { + t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) } } @@ -527,8 +493,8 @@ func TestLoadDispatcherPinSkipsInvalidPackageCandidate(t *testing.T) { if err != nil { t.Fatalf("loadDispatcherPin failed: %v", err) } - if pin.CLIVersion != "3.0.0-beta.57" { - t.Fatalf("cliVersion mismatch: %s", pin.CLIVersion) + if pin.ProjectRunnerVersion != "3.0.0-beta.57" { + t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) } } @@ -542,22 +508,22 @@ func TestLoadDispatcherPinNormalizesVersionPrefixes(t *testing.T) { if err != nil { t.Fatalf("loadDispatcherPin failed: %v", err) } - if pin.CLIVersion != "3.0.0-beta.58" { - t.Fatalf("cliVersion mismatch: %s", pin.CLIVersion) + if pin.ProjectRunnerVersion != "3.0.0-beta.58" { + t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) } if pin.MinimumDispatcherVersion != "3.0.0-beta.39" { t.Fatalf("minimumDispatcherVersion mismatch: %s", pin.MinimumDispatcherVersion) } } -func TestLoadDispatcherPinRejectsInvalidCLIVersion(t *testing.T) { - // Verifies project pin cliVersion must be a release version, not a filesystem path. +func TestLoadDispatcherPinRejectsInvalidProjectRunnerVersion(t *testing.T) { + // Verifies project pin projectRunnerVersion must be a release version, not a filesystem path. projectRoot := createDispatcherUnityProject(t) pinPath := filepath.Join(projectRoot, dispatcherProjectPinRelativePath) if err := os.MkdirAll(filepath.Dir(pinPath), 0o755); err != nil { t.Fatalf("failed to create pin directory: %v", err) } - content := `{"schemaVersion":1,"packageName":"io.github.hatayama.uloopmcp","packageVersion":"3.0.0-beta.1","cliVersion":"../../payload","requiredProtocolVersion":2,"minimumDispatcherVersion":"3.0.0-beta.39"}` + content := `{"schemaVersion":1,"packageName":"io.github.hatayama.uloopmcp","packageVersion":"3.0.0-beta.1","projectRunnerVersion":"../../payload","requiredProtocolVersion":2,"minimumDispatcherVersion":"3.0.0-beta.39"}` if err := os.WriteFile(pinPath, []byte(content), 0o644); err != nil { t.Fatalf("failed to write pin: %v", err) } @@ -565,7 +531,7 @@ func TestLoadDispatcherPinRejectsInvalidCLIVersion(t *testing.T) { _, err := loadDispatcherPin(projectRoot) if err == nil { - t.Fatal("expected invalid cliVersion error") + t.Fatal("expected invalid projectRunnerVersion error") } } @@ -590,7 +556,7 @@ func TestLoadDispatcherPinFallsBackToCliConstants(t *testing.T) { t.Fatalf("failed to create constants directory: %v", err) } content := `public const int REQUIRED_CLI_PROTOCOL_VERSION = 3; -public const string MINIMUM_REQUIRED_CLI_VERSION = "3.0.0-beta.56"; +public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "3.0.0-beta.56"; public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "1.0.0";` if err := os.WriteFile(constantsPath, []byte(content), 0o644); err != nil { t.Fatalf("failed to write constants: %v", err) @@ -600,8 +566,8 @@ public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "1.0.0";` if err != nil { t.Fatalf("loadDispatcherPin failed: %v", err) } - if pin.CLIVersion != "3.0.0-beta.56" { - t.Fatalf("cliVersion mismatch: %s", pin.CLIVersion) + if pin.ProjectRunnerVersion != "3.0.0-beta.56" { + t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) } if pin.RequiredProtocolVersion != 3 { t.Fatalf("protocol mismatch: %d", pin.RequiredProtocolVersion) @@ -619,7 +585,7 @@ func TestLoadDispatcherPinFromCliConstantsNormalizesVersionPrefix(t *testing.T) t.Fatalf("failed to create constants directory: %v", err) } content := `public const int REQUIRED_CLI_PROTOCOL_VERSION = 3; -public const string MINIMUM_REQUIRED_CLI_VERSION = "v3.0.0-beta.59"; +public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "v3.0.0-beta.59"; public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "v1.0.0";` if err := os.WriteFile(constantsPath, []byte(content), 0o644); err != nil { t.Fatalf("failed to write constants: %v", err) @@ -629,8 +595,8 @@ public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "v1.0.0";` if err != nil { t.Fatalf("loadDispatcherPin failed: %v", err) } - if pin.CLIVersion != "3.0.0-beta.59" { - t.Fatalf("cliVersion mismatch: %s", pin.CLIVersion) + if pin.ProjectRunnerVersion != "3.0.0-beta.59" { + t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) } if pin.MinimumDispatcherVersion != "1.0.0" { t.Fatalf("minimumDispatcherVersion mismatch: %s", pin.MinimumDispatcherVersion) @@ -657,24 +623,24 @@ func createDispatcherUnityProject(t *testing.T) string { return projectRoot } -func writeDispatcherProjectPin(t *testing.T, projectRoot string, cliVersion string) { +func writeDispatcherProjectPin(t *testing.T, projectRoot string, projectRunnerVersion string) { t.Helper() pinPath := filepath.Join(projectRoot, dispatcherProjectPinRelativePath) - writeDispatcherPinFile(t, pinPath, cliVersion) + writeDispatcherPinFile(t, pinPath, projectRunnerVersion) } -func writeDispatcherPinFile(t *testing.T, pinPath string, cliVersion string) { +func writeDispatcherPinFile(t *testing.T, pinPath string, projectRunnerVersion string) { t.Helper() - writeDispatcherPinFileWithMinimum(t, pinPath, cliVersion, dispatcherVersion) + writeDispatcherPinFileWithMinimum(t, pinPath, projectRunnerVersion, dispatcherVersion) } -func writeDispatcherPinFileWithMinimum(t *testing.T, pinPath string, cliVersion string, minimumDispatcherVersion string) { +func writeDispatcherPinFileWithMinimum(t *testing.T, pinPath string, projectRunnerVersion string, minimumDispatcherVersion string) { t.Helper() if err := os.MkdirAll(filepath.Dir(pinPath), 0o755); err != nil { t.Fatalf("failed to create pin directory: %v", err) } - content := `{"schemaVersion":1,"packageName":"io.github.hatayama.uloopmcp","packageVersion":"3.0.0-beta.1","cliVersion":"` + - cliVersion + + content := `{"schemaVersion":1,"packageName":"io.github.hatayama.uloopmcp","packageVersion":"3.0.0-beta.1","projectRunnerVersion":"` + + projectRunnerVersion + `","requiredProtocolVersion":2,"minimumDispatcherVersion":"` + minimumDispatcherVersion + `"}` @@ -683,9 +649,9 @@ func writeDispatcherPinFileWithMinimum(t *testing.T, pinPath string, cliVersion } } -func writeCachedDispatcherRealCLI(t *testing.T, cacheRoot string, cliVersion string) string { +func writeCachedDispatcherRealCLI(t *testing.T, cacheRoot string, projectRunnerVersion string) string { t.Helper() - realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, cliVersion, runtime.GOOS, runtime.GOARCH) + realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, projectRunnerVersion, runtime.GOOS, runtime.GOARCH) if err := os.MkdirAll(filepath.Dir(realCLIPath), 0o755); err != nil { t.Fatalf("failed to create cached CLI directory: %v", err) } diff --git a/cli/internal/cli/dispatcher_update_version.go b/cli/internal/cli/dispatcher_update_version.go index d899b277f..81ad7c53d 100644 --- a/cli/internal/cli/dispatcher_update_version.go +++ b/cli/internal/cli/dispatcher_update_version.go @@ -36,7 +36,7 @@ func readInstalledDispatcherVersion(ctx context.Context) (string, error) { if updatedVersion == "" { return "", errors.New("updated dispatcher version is empty") } - if err := validateDispatcherCLIVersion(updatedVersion); err != nil { + if err := validateDispatcherProjectRunnerVersion(updatedVersion); err != nil { return "", err } return updatedVersion, nil diff --git a/cli/internal/cli/help_test.go b/cli/internal/cli/help_test.go index eb3ef7d6c..b81f2d7a6 100644 --- a/cli/internal/cli/help_test.go +++ b/cli/internal/cli/help_test.go @@ -62,8 +62,8 @@ func TestRunProjectLocalVersionJSONIncludesProtocolVersion(t *testing.T) { if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { t.Fatalf("version json output is not JSON: %v\n%s", err, stdout.String()) } - if payload["CliVersion"] != version { - t.Fatalf("cliVersion mismatch: %#v", payload) + if payload["ProjectRunnerVersion"] != version { + t.Fatalf("projectRunnerVersion mismatch: %#v", payload) } if payload["ProtocolVersion"] != float64(protocolVersion) { t.Fatalf("protocolVersion mismatch: %#v", payload) diff --git a/cli/internal/cli/run.go b/cli/internal/cli/run.go index d800f8685..3b4cedcf0 100644 --- a/cli/internal/cli/run.go +++ b/cli/internal/cli/run.go @@ -56,8 +56,8 @@ func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stder func writeVersionJSON(stdout io.Writer) { content, err := json.Marshal(map[string]any{ - "CliVersion": version, - "ProtocolVersion": protocolVersion, + "ProjectRunnerVersion": version, + "ProtocolVersion": protocolVersion, }) if err != nil { panic(err) diff --git a/cli/internal/cli/tools.go b/cli/internal/cli/tools.go index 53bc59a50..70ac05e6e 100644 --- a/cli/internal/cli/tools.go +++ b/cli/internal/cli/tools.go @@ -11,7 +11,7 @@ import ( ) var ( - version = clicontract.Current.CliVersion + version = clicontract.Current.ProjectRunnerVersion protocolVersion = clicontract.Current.ProtocolVersion dispatcherVersion = clicontract.DispatcherCurrent.DispatcherVersion dispatcherContractVersion = clicontract.DispatcherCurrent.DispatcherContractVersion diff --git a/cli/internal/cli/update_test.go b/cli/internal/cli/update_test.go index bbacb7414..a6ac48177 100644 --- a/cli/internal/cli/update_test.go +++ b/cli/internal/cli/update_test.go @@ -105,10 +105,10 @@ func TestUpdateCommandForDarwinNormalizesRequestedVersionPrefix(t *testing.T) { } } -func TestUpdateCommandForDarwinNormalizesLegacyCliReleaseTag(t *testing.T) { - // Verifies migrated CLI release tags resolve to the matching dispatcher release. +func TestUpdateCommandForDarwinNormalizesProjectRunnerReleaseTag(t *testing.T) { + // Verifies project runner release tags resolve to the matching dispatcher release. commandName, args, err := updateCommandForOSWithOptions("darwin", updateOptions{ - targetVersion: "cli-v3.0.0-beta.6", + targetVersion: "uloop-project-runner-v3.0.0-beta.6", }) if err != nil { t.Fatalf("updateCommandForOSWithOptions failed: %v", err) @@ -121,8 +121,8 @@ func TestUpdateCommandForDarwinNormalizesLegacyCliReleaseTag(t *testing.T) { if !strings.Contains(joinedArgs, "dispatcher-v3.0.0-beta.6/scripts/install.sh") { t.Fatalf("installer URL mismatch: %s", joinedArgs) } - if strings.Contains(joinedArgs, "dispatcher-vcli-v3.0.0-beta.6") { - t.Fatalf("installer version contains legacy CLI prefix: %s", joinedArgs) + if strings.Contains(joinedArgs, "dispatcher-vuloop-project-runner-v3.0.0-beta.6") { + t.Fatalf("installer version contains project runner prefix: %s", joinedArgs) } } @@ -159,9 +159,9 @@ func TestParseUpdateOptionsNormalizesVersionPrefix(t *testing.T) { } } -func TestParseUpdateOptionsNormalizesLegacyCliReleaseTag(t *testing.T) { - // Verifies parsed legacy CLI release tags are normalized before dispatcher tag selection. - options, err := parseUpdateOptions([]string{"--to-version", "cli-v3.0.0-beta.6"}) +func TestParseUpdateOptionsNormalizesProjectRunnerReleaseTag(t *testing.T) { + // Verifies parsed project runner release tags are normalized before dispatcher tag selection. + options, err := parseUpdateOptions([]string{"--to-version", "uloop-project-runner-v3.0.0-beta.6"}) if err != nil { t.Fatalf("parseUpdateOptions failed: %v", err) } diff --git a/cli/internal/projectcli/projectcli.go b/cli/internal/projectrunner/projectrunner.go similarity index 91% rename from cli/internal/projectcli/projectcli.go rename to cli/internal/projectrunner/projectrunner.go index 9b25a6338..8a47b72f2 100644 --- a/cli/internal/projectcli/projectcli.go +++ b/cli/internal/projectrunner/projectrunner.go @@ -1,4 +1,4 @@ -package projectcli +package projectrunner import ( "context" diff --git a/cli/internal/unityipc/client.go b/cli/internal/unityipc/client.go index 1985e993e..d17c51ef1 100644 --- a/cli/internal/unityipc/client.go +++ b/cli/internal/unityipc/client.go @@ -66,10 +66,10 @@ type rpcRequest struct { } type rpcClientMetadata struct { - CLIVersion string `json:"cliVersion"` - ProtocolVersion int `json:"protocolVersion"` - AcceptsDispatchAck bool `json:"acceptsDispatchAck"` - AcceptsHeartbeat bool `json:"acceptsHeartbeat"` + ProjectRunnerVersion string `json:"projectRunnerVersion"` + ProtocolVersion int `json:"protocolVersion"` + AcceptsDispatchAck bool `json:"acceptsDispatchAck"` + AcceptsHeartbeat bool `json:"acceptsHeartbeat"` } type rpcResponse struct { @@ -189,10 +189,10 @@ func (client *Client) SendWithProgressOutcomeAcceptContext( Method: method, Params: params, ULoop: rpcClientMetadata{ - CLIVersion: client.clientVersion, - ProtocolVersion: clicontract.Current.ProtocolVersion, - AcceptsDispatchAck: true, - AcceptsHeartbeat: true, + ProjectRunnerVersion: client.clientVersion, + ProtocolVersion: clicontract.Current.ProtocolVersion, + AcceptsDispatchAck: true, + AcceptsHeartbeat: true, }, ID: client.requestID, } diff --git a/cli/internal/unityipc/client_test.go b/cli/internal/unityipc/client_test.go index e21de1349..7953b2109 100644 --- a/cli/internal/unityipc/client_test.go +++ b/cli/internal/unityipc/client_test.go @@ -40,8 +40,8 @@ func TestFormatConnectionAttemptErrorExplainsDialFailureWithoutDisconnectClaim(t } } -func TestSendIncludesCliVersionWithoutProjectIdentityMetadata(t *testing.T) { - // Verifies that requests carry CLI compatibility metadata without reviving legacy project identity metadata. +func TestSendIncludesProjectRunnerVersionWithoutProjectIdentityMetadata(t *testing.T) { + // Verifies that requests carry project runner compatibility metadata without reviving legacy project identity metadata. if runtime.GOOS == "windows" { t.Skip("TCP endpoint injection is only used by this non-Windows client test") } @@ -122,8 +122,8 @@ func assertClientMetadataRequest(t *testing.T, request map[string]any) { if !ok { t.Fatalf("request should include uloop metadata: %#v", request) } - if metadata["cliVersion"] != "3.0.0-beta.6" { - t.Fatalf("cli version metadata mismatch: %#v", metadata) + if metadata["projectRunnerVersion"] != "3.0.0-beta.6" { + t.Fatalf("project runner version metadata mismatch: %#v", metadata) } if metadata["protocolVersion"] != float64(clicontract.Current.ProtocolVersion) { t.Fatalf("protocol version metadata mismatch: %#v", metadata) diff --git a/cli/internal/update/command.go b/cli/internal/update/command.go index 83ab045b6..3be110930 100644 --- a/cli/internal/update/command.go +++ b/cli/internal/update/command.go @@ -55,8 +55,8 @@ func NormalizeTargetVersion(value string) string { if strings.HasPrefix(lower, dispatcherTagPrefix) { return trimmed[len(dispatcherTagPrefix):] } - if strings.HasPrefix(lower, cliReleaseTagPrefix) { - return trimmed[len(cliReleaseTagPrefix):] + if strings.HasPrefix(lower, projectRunnerReleaseTagPrefix) { + return trimmed[len(projectRunnerReleaseTagPrefix):] } if strings.HasPrefix(lower, "v") { return trimmed[1:] diff --git a/cli/internal/update/installer.go b/cli/internal/update/installer.go index d86acd477..212e6f9ad 100644 --- a/cli/internal/update/installer.go +++ b/cli/internal/update/installer.go @@ -8,22 +8,22 @@ const ( LatestStable = "latest" LatestBeta = "latest-beta" - repositoryRawBaseURL = "https://raw.githubusercontent.com/hatayama/unity-cli-loop" - cliReleaseTagPrefix = "cli-v" - dispatcherTagPrefix = "dispatcher-v" - betaVersionMarker = "-beta." + repositoryRawBaseURL = "https://raw.githubusercontent.com/hatayama/unity-cli-loop" + projectRunnerReleaseTagPrefix = "uloop-project-runner-v" + dispatcherTagPrefix = "dispatcher-v" + betaVersionMarker = "-beta." ) func ScriptURL(version string, scriptName string) string { return repositoryRawBaseURL + "/" + DispatcherReleaseTag(version) + "/scripts/" + scriptName } -func CLIReleaseTag(version string) string { - if strings.HasPrefix(version, cliReleaseTagPrefix) || strings.HasPrefix(version, strings.ToUpper(cliReleaseTagPrefix)) { +func ProjectRunnerReleaseTag(version string) string { + if strings.HasPrefix(version, projectRunnerReleaseTagPrefix) || strings.HasPrefix(version, strings.ToUpper(projectRunnerReleaseTagPrefix)) { return version } - return cliReleaseTagPrefix + version + return projectRunnerReleaseTagPrefix + version } func DispatcherReleaseTag(version string) string { diff --git a/cli/internal/update/installer_test.go b/cli/internal/update/installer_test.go index c5f0ddd29..f9219130c 100644 --- a/cli/internal/update/installer_test.go +++ b/cli/internal/update/installer_test.go @@ -22,20 +22,20 @@ func TestScriptURLForStableVersionUsesReleaseInstaller(t *testing.T) { } } -func TestCLIReleaseTagAddsMissingPrefix(t *testing.T) { - // Verifies CLI downloads use the GitHub release tag format. - tag := CLIReleaseTag("3.0.0-beta.3") +func TestProjectRunnerReleaseTagAddsMissingPrefix(t *testing.T) { + // Verifies project runner downloads use the GitHub release tag format. + tag := ProjectRunnerReleaseTag("3.0.0-beta.3") - if tag != "cli-v3.0.0-beta.3" { + if tag != "uloop-project-runner-v3.0.0-beta.3" { t.Fatalf("release tag mismatch: %s", tag) } } -func TestCLIReleaseTagKeepsCliPrefix(t *testing.T) { - // Verifies exact CLI release tags are not rewritten. - tag := CLIReleaseTag("cli-v3.0.0-beta.3") +func TestProjectRunnerReleaseTagKeepsProjectRunnerPrefix(t *testing.T) { + // Verifies exact project runner release tags are not rewritten. + tag := ProjectRunnerReleaseTag("uloop-project-runner-v3.0.0-beta.3") - if tag != "cli-v3.0.0-beta.3" { + if tag != "uloop-project-runner-v3.0.0-beta.3" { t.Fatalf("release tag mismatch: %s", tag) } } diff --git a/cli/protocol_version_consistency_test.go b/cli/protocol_version_consistency_test.go index 807be3998..b27672bb7 100644 --- a/cli/protocol_version_consistency_test.go +++ b/cli/protocol_version_consistency_test.go @@ -14,8 +14,8 @@ import ( const ( unityProtocolConstantPath = "../Packages/src/Editor/Domain/CliConstants.cs" unityPackageManifestPath = "../Packages/src/package.json" - unityPackageCliPinPath = "../Packages/src/cli-pin.json" - unityProjectCliPinPath = "../.uloop/cli-pin.json" + unityPackageCliPinPath = "../Packages/src/project-runner-pin.json" + unityProjectCliPinPath = "../.uloop/project-runner-pin.json" ) var ( @@ -32,7 +32,7 @@ type unityPackageCliPin struct { SchemaVersion int `json:"schemaVersion"` PackageName string `json:"packageName"` PackageVersion string `json:"packageVersion"` - CLIVersion string `json:"cliVersion"` + ProjectRunnerVersion string `json:"projectRunnerVersion"` RequiredProtocolVersion int `json:"requiredProtocolVersion"` MinimumDispatcherVersion string `json:"minimumDispatcherVersion"` } @@ -54,7 +54,7 @@ func TestProtocolVersionMatchesUnityPackage(t *testing.T) { } // TestUnityPackageCliPinMatchesReleaseContracts verifies the dispatcher pin copied into -// projects points at the package release, CLI release, and protocol generation from their +// projects points at the package release, project runner release, and protocol generation from their // canonical declarations. func TestUnityPackageCliPinMatchesReleaseContracts(t *testing.T) { manifest := readJSONFile[unityPackageManifest](t, unityPackageManifestPath) @@ -69,8 +69,8 @@ func TestUnityPackageCliPinMatchesReleaseContracts(t *testing.T) { if pin.PackageVersion != manifest.Version { t.Fatalf("expected %s packageVersion to match %s version: %q != %q", unityPackageCliPinPath, unityPackageManifestPath, pin.PackageVersion, manifest.Version) } - if pin.CLIVersion != Current.CliVersion { - t.Fatalf("expected %s cliVersion to match cli/contract.json cliVersion: %q != %q", unityPackageCliPinPath, pin.CLIVersion, Current.CliVersion) + if pin.ProjectRunnerVersion != Current.ProjectRunnerVersion { + t.Fatalf("expected %s projectRunnerVersion to match cli/contract.json projectRunnerVersion: %q != %q", unityPackageCliPinPath, pin.ProjectRunnerVersion, Current.ProjectRunnerVersion) } if pin.RequiredProtocolVersion != Current.ProtocolVersion { t.Fatalf("expected %s requiredProtocolVersion to match cli/contract.json protocolVersion: %d != %d", unityPackageCliPinPath, pin.RequiredProtocolVersion, Current.ProtocolVersion) diff --git a/release-please-config.json b/release-please-config.json index dad325ad5..c68ada0ec 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -19,18 +19,18 @@ }, { "type": "json", - "path": "Packages/src/cli-pin.json", + "path": "Packages/src/project-runner-pin.json", "jsonpath": "$.packageVersion" }, { "type": "json", - "path": ".uloop/cli-pin.json", + "path": ".uloop/project-runner-pin.json", "jsonpath": "$.packageVersion" } ] }, "cli": { - "component": "cli", + "component": "uloop-project-runner", "release-type": "go", "versioning": "prerelease", "prerelease": true, @@ -47,17 +47,17 @@ { "type": "json", "path": "contract.json", - "jsonpath": "$.cliVersion" + "jsonpath": "$.projectRunnerVersion" }, { "type": "json", - "path": "/Packages/src/cli-pin.json", - "jsonpath": "$.cliVersion" + "path": "/Packages/src/project-runner-pin.json", + "jsonpath": "$.projectRunnerVersion" }, { "type": "json", - "path": "/.uloop/cli-pin.json", - "jsonpath": "$.cliVersion" + "path": "/.uloop/project-runner-pin.json", + "jsonpath": "$.projectRunnerVersion" } ] } diff --git a/scripts/build-go-cli.sh b/scripts/build-go-cli.sh index 21248f4c3..b4530e929 100755 --- a/scripts/build-go-cli.sh +++ b/scripts/build-go-cli.sh @@ -28,9 +28,9 @@ build_binary() { ) } -build_binary darwin arm64 uloop "$CLI_DIR" ./cmd/uloop -build_binary darwin arm64 uloop-cli "$CLI_DIR" ./cmd/uloop-cli -build_binary darwin amd64 uloop "$CLI_DIR" ./cmd/uloop -build_binary darwin amd64 uloop-cli "$CLI_DIR" ./cmd/uloop-cli -build_binary windows amd64 uloop "$CLI_DIR" ./cmd/uloop -build_binary windows amd64 uloop-cli "$CLI_DIR" ./cmd/uloop-cli +build_binary darwin arm64 uloop "$CLI_DIR" ./cmd/dispatcher +build_binary darwin arm64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner +build_binary darwin amd64 uloop "$CLI_DIR" ./cmd/dispatcher +build_binary darwin amd64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner +build_binary windows amd64 uloop "$CLI_DIR" ./cmd/dispatcher +build_binary windows amd64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner diff --git a/scripts/package-go-cli.sh b/scripts/package-go-cli.sh index 12f8e8a09..d9af1d411 100755 --- a/scripts/package-go-cli.sh +++ b/scripts/package-go-cli.sh @@ -13,27 +13,11 @@ package_unix() { platform="$1" tmp_dir="$RELEASE_DIR/tmp-$platform" mkdir -p "$tmp_dir" - cp "$DIST_DIR/$platform/uloop-cli" "$tmp_dir/uloop-cli" - chmod +x "$tmp_dir/uloop-cli" + cp "$DIST_DIR/$platform/uloop-project-runner" "$tmp_dir/uloop-project-runner" + chmod +x "$tmp_dir/uloop-project-runner" ( cd "$tmp_dir" - tar -czf "$RELEASE_DIR/uloop-cli-$platform.tar.gz" uloop-cli - ) - rm -rf "$tmp_dir" -} - -# Legacy beta launchers still self-update from cli-v* releases and look for uloop-* assets. -package_legacy_dispatcher_unix() { - platform="$1" - tmp_dir="$RELEASE_DIR/tmp-legacy-$platform" - mkdir -p "$tmp_dir" - cp "$DIST_DIR/$platform/uloop" "$tmp_dir/uloop" - cp "$DIST_DIR/$platform/uloop-cli" "$tmp_dir/uloop-cli" - chmod +x "$tmp_dir/uloop" - chmod +x "$tmp_dir/uloop-cli" - ( - cd "$tmp_dir" - tar -czf "$RELEASE_DIR/uloop-$platform.tar.gz" uloop uloop-cli + tar -czf "$RELEASE_DIR/uloop-project-runner-$platform.tar.gz" uloop-project-runner ) rm -rf "$tmp_dir" } @@ -42,24 +26,10 @@ package_windows() { platform="windows-amd64" tmp_dir="$RELEASE_DIR/tmp-$platform" mkdir -p "$tmp_dir" - cp "$DIST_DIR/$platform/uloop-cli.exe" "$tmp_dir/uloop-cli.exe" - ( - cd "$tmp_dir" - zip -q "$RELEASE_DIR/uloop-cli-$platform.zip" uloop-cli.exe - ) - rm -rf "$tmp_dir" -} - -# Legacy beta launchers install uloop.exe globally, while their dispatcher cache reads uloop-cli.exe. -package_legacy_dispatcher_windows() { - platform="windows-amd64" - tmp_dir="$RELEASE_DIR/tmp-legacy-$platform" - mkdir -p "$tmp_dir" - cp "$DIST_DIR/$platform/uloop.exe" "$tmp_dir/uloop.exe" - cp "$DIST_DIR/$platform/uloop-cli.exe" "$tmp_dir/uloop-cli.exe" + cp "$DIST_DIR/$platform/uloop-project-runner.exe" "$tmp_dir/uloop-project-runner.exe" ( cd "$tmp_dir" - zip -q "$RELEASE_DIR/uloop-$platform.zip" uloop.exe uloop-cli.exe + zip -q "$RELEASE_DIR/uloop-project-runner-$platform.zip" uloop-project-runner.exe ) rm -rf "$tmp_dir" } @@ -83,9 +53,6 @@ create_checksum() { package_unix darwin-arm64 package_unix darwin-amd64 package_windows -package_legacy_dispatcher_unix darwin-arm64 -package_legacy_dispatcher_unix darwin-amd64 -package_legacy_dispatcher_windows for asset_path in "$RELEASE_DIR"/*.tar.gz "$RELEASE_DIR"/*.zip; do create_checksum "$asset_path" diff --git a/scripts/resolve-native-cli-release-target.sh b/scripts/resolve-native-cli-release-target.sh index a97a1e755..8284d4585 100755 --- a/scripts/resolve-native-cli-release-target.sh +++ b/scripts/resolve-native-cli-release-target.sh @@ -247,13 +247,13 @@ release_commit_sha_for_version() { VERSION=$(jq -r '.["cli"]' .release-please-manifest.json) if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then - echo "Could not resolve CLI release version from .release-please-manifest.json." >&2 + echo "Could not resolve project runner release version from .release-please-manifest.json." >&2 exit 1 fi -RELEASE_TAG="${INPUT_RELEASE_TAG:-cli-v$VERSION}" +RELEASE_TAG="${INPUT_RELEASE_TAG:-uloop-project-runner-v$VERSION}" case "$RELEASE_TAG" in - cli-v[0-9]*) + uloop-project-runner-v[0-9]*) ;; *) echo "Invalid release tag: $RELEASE_TAG" >&2 @@ -322,16 +322,16 @@ elif release_is_published_with_cli_assets "$RELEASE_TAG"; then else PREVIOUS_CLI_RELEASE_TAG=$(latest_cli_asset_release_tag "$RELEASE_TAG") if [ -z "$PREVIOUS_CLI_RELEASE_TAG" ]; then - echo "No previous CLI asset release found; publishing native CLI assets." >&2 + echo "No previous project runner asset release found; publishing native project runner assets." >&2 SHOULD_PUBLISH=true elif release_commit_updates_cli_version "$RELEASE_TARGET_SHA" "$VERSION"; then - echo "CLI release metadata changed in $RELEASE_TARGET_SHA; publishing native CLI assets." >&2 + echo "Project runner release metadata changed in $RELEASE_TARGET_SHA; publishing native project runner assets." >&2 SHOULD_PUBLISH=true elif cli_release_inputs_changed "$PREVIOUS_CLI_RELEASE_TAG" "$TARGET_SHA"; then - echo "CLI release inputs changed since $PREVIOUS_CLI_RELEASE_TAG; publishing native CLI assets." >&2 + echo "Project runner release inputs changed since $PREVIOUS_CLI_RELEASE_TAG; publishing native project runner assets." >&2 SHOULD_PUBLISH=true else - echo "CLI release inputs are unchanged since $PREVIOUS_CLI_RELEASE_TAG; skipping native CLI publish." >&2 + echo "Project runner release inputs are unchanged since $PREVIOUS_CLI_RELEASE_TAG; skipping native CLI publish." >&2 fi fi diff --git a/scripts/sync-release-please-package-releases.sh b/scripts/sync-release-please-package-releases.sh index a0635f76f..6692d2d90 100755 --- a/scripts/sync-release-please-package-releases.sh +++ b/scripts/sync-release-please-package-releases.sh @@ -5,7 +5,7 @@ ROOT_DIR=${ULOOP_REPO_ROOT:-$(CDPATH= cd "$(dirname "$0")/.." && pwd)} CONFIG="$ROOT_DIR/release-please-config.json" MANIFEST="$ROOT_DIR/.release-please-manifest.json" CLI_PACKAGE_PATH="cli" -UNITY_PACKAGE_CLI_PIN_FILE="Packages/src/cli-pin.json" +UNITY_PACKAGE_CLI_PIN_FILE="Packages/src/project-runner-pin.json" REPO_FULL_NAME=${GITHUB_REPOSITORY:-hatayama/unity-cli-loop} TMP_DIR=$(mktemp -d) @@ -368,7 +368,7 @@ wait_for_cli_release_ready() { while :; do if cli_release_is_ready "$release_tag"; then if [ "$elapsed_seconds" -gt 0 ]; then - echo "CLI release $release_tag is now published with complete assets." + echo "Project runner release $release_tag is now published with complete assets." fi return 0 fi @@ -389,7 +389,7 @@ wait_for_cli_release_ready() { sleep_seconds=$delay_seconds fi - echo "CLI release $release_tag is not published with complete assets yet; waiting ${delay_seconds}s before retry." + echo "Project runner release $release_tag is not published with complete assets yet; waiting ${delay_seconds}s before retry." if [ "$sleep_seconds" -gt 0 ]; then sleep "$sleep_seconds" fi @@ -476,7 +476,7 @@ if [ -n "$cli_version" ] && jq -e --arg package_path "$CLI_PACKAGE_PATH" '.packa cli_release_tag=$(release_tag_from_config "$CLI_PACKAGE_PATH" "$cli_version") if ! wait_for_cli_release_ready "$cli_release_tag"; then mark_package_release_sync_ready false - echo "CLI release $cli_release_tag is not published with complete assets; package release sync will wait." + echo "Project runner release $cli_release_tag is not published with complete assets; package release sync will wait." exit 0 fi fetch_cli_release_tag "$cli_release_tag" diff --git a/scripts/test-release-please-config.sh b/scripts/test-release-please-config.sh index 5cfd6548f..4b52b5fba 100755 --- a/scripts/test-release-please-config.sh +++ b/scripts/test-release-please-config.sh @@ -120,21 +120,21 @@ assert_json_value '.packages["."].["exclude-paths"][0]' 'cli' assert_json_value '.packages["."].["extra-files"] | length' '3' assert_json_value '.packages["."].["extra-files"][0].path' 'Packages/src/package.json' assert_json_value '.packages["."].["extra-files"][0].jsonpath' '$.version' -assert_json_value '.packages["."].["extra-files"][1].path' 'Packages/src/cli-pin.json' +assert_json_value '.packages["."].["extra-files"][1].path' 'Packages/src/project-runner-pin.json' assert_json_value '.packages["."].["extra-files"][1].jsonpath' '$.packageVersion' -assert_json_value '.packages["."].["extra-files"][2].path' '.uloop/cli-pin.json' +assert_json_value '.packages["."].["extra-files"][2].path' '.uloop/project-runner-pin.json' assert_json_value '.packages["."].["extra-files"][2].jsonpath' '$.packageVersion' -assert_json_value '.packages["cli"].component' 'cli' +assert_json_value '.packages["cli"].component' 'uloop-project-runner' assert_json_value '.packages["cli"].["include-component-in-tag"]' 'true' assert_json_value '.packages["cli"].["changelog-path"]' 'CHANGELOG.md' assert_json_value '.packages["cli"].["extra-files"] | length' '4' assert_json_value '.packages["cli"].["extra-files"][0].path' 'internal/tools/default-tools.json' assert_json_value '.packages["cli"].["extra-files"][1].path' 'contract.json' -assert_json_value '.packages["cli"].["extra-files"][2].path' '/Packages/src/cli-pin.json' -assert_json_value '.packages["cli"].["extra-files"][2].jsonpath' '$.cliVersion' -assert_json_value '.packages["cli"].["extra-files"][3].path' '/.uloop/cli-pin.json' -assert_json_value '.packages["cli"].["extra-files"][3].jsonpath' '$.cliVersion' +assert_json_value '.packages["cli"].["extra-files"][2].path' '/Packages/src/project-runner-pin.json' +assert_json_value '.packages["cli"].["extra-files"][2].jsonpath' '$.projectRunnerVersion' +assert_json_value '.packages["cli"].["extra-files"][3].path' '/.uloop/project-runner-pin.json' +assert_json_value '.packages["cli"].["extra-files"][3].jsonpath' '$.projectRunnerVersion' assert_file_contains "$RELEASE_WORKFLOW" 'id: package_release_sync' assert_file_contains "$RELEASE_WORKFLOW" "steps.package_release_sync.outputs.ready != 'false'" diff --git a/scripts/test-resolve-native-cli-release-target.sh b/scripts/test-resolve-native-cli-release-target.sh index bb56efba5..7262a1d48 100755 --- a/scripts/test-resolve-native-cli-release-target.sh +++ b/scripts/test-resolve-native-cli-release-target.sh @@ -72,7 +72,7 @@ set -eu asset_json() { has_assets=$1 if [ "$has_assets" = "true" ]; then - printf '[{"name":"uloop-cli-darwin-amd64.tar.gz","size":1},{"name":"uloop-cli-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-cli-windows-amd64.zip","size":1},{"name":"uloop-cli-windows-amd64.zip.sha256","size":1},{"name":"uloop-darwin-amd64.tar.gz","size":1},{"name":"uloop-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-darwin-arm64.tar.gz","size":1},{"name":"uloop-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-windows-amd64.zip","size":1},{"name":"uloop-windows-amd64.zip.sha256","size":1}]' + printf '[{"name":"uloop-project-runner-darwin-amd64.tar.gz","size":1},{"name":"uloop-project-runner-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-project-runner-darwin-arm64.tar.gz","size":1},{"name":"uloop-project-runner-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-project-runner-windows-amd64.zip","size":1},{"name":"uloop-project-runner-windows-amd64.zip.sha256","size":1}]' return fi printf '[]' @@ -103,7 +103,7 @@ release_json() { if [ "$1" = "release" ] && [ "$2" = "view" ]; then tag=$3 - if [ "$tag" = "cli-v$CURRENT_VERSION" ]; then + if [ "$tag" = "uloop-project-runner-v$CURRENT_VERSION" ]; then release_json "$CURRENT_RELEASE_STATE" "$CURRENT_RELEASE_HAS_ASSETS" exit 0 fi @@ -223,7 +223,7 @@ run_success_case() { assert_contains output.txt "publish=$expected_publish" assert_contains output.txt "release=$expected_release" - assert_contains output.txt "tag=cli-v$current_version" + assert_contains output.txt "tag=uloop-project-runner-v$current_version" assert_contains output.txt "version=$current_version" assert_contains output.txt "sha=$expected_sha" assert_contains output.txt "build_sha=$build_sha_value" @@ -256,7 +256,7 @@ run_failure_case() { BUILD_COMMIT_SUBJECT="chore(v3-beta): release $current_version" \ RELEASE_COMMIT_SHA=target-sha \ RELEASE_COMMIT_SUBJECT="chore(v3-beta): release $current_version" \ - PREVIOUS_RELEASE_TAG=cli-v3.0.0-beta.1 \ + PREVIOUS_RELEASE_TAG=uloop-project-runner-v3.0.0-beta.1 \ PREVIOUS_RELEASE_HAS_ASSETS=true \ CLI_SOURCE_CHANGED=false \ CONTRACT_CHANGED=false \ @@ -279,79 +279,79 @@ run_failure_case() { ) } -# Verifies already published complete CLI assets are not rebuilt. +# Verifies already published complete Project runner assets are not rebuilt. test_complete_current_release_skips() { - run_success_case current-complete 3.0.0-beta.2 push v3-beta published true cli-v3.0.0-beta.1 true true false false false false + run_success_case current-complete 3.0.0-beta.2 push v3-beta published true uloop-project-runner-v3.0.0-beta.1 true true false false false false } -# Verifies package-only version changes do not publish CLI assets. +# Verifies package-only version changes do not publish Project runner assets. test_package_version_change_without_cli_change_skips() { - run_success_case package-only 3.0.0-beta.3 push v3-beta missing false cli-v3.0.0-beta.1 true false false false false true + run_success_case package-only 3.0.0-beta.3 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true false false false false true } # Verifies CLI source changes publish assets on the current release tag. test_cli_change_publishes() { - run_success_case cli-change 3.0.0-beta.3 push v3-beta missing false cli-v3.0.0-beta.1 true true false false true true + run_success_case cli-change 3.0.0-beta.3 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true true false false true true } -# Verifies missing CLI assets can be uploaded to an already published package release. +# Verifies missing Project runner assets can be uploaded to an already published package release. test_published_current_release_can_receive_cli_assets() { - run_success_case published-missing-assets 3.0.0-beta.3 push v3-beta published false cli-v3.0.0-beta.1 true true false false true false + run_success_case published-missing-assets 3.0.0-beta.3 push v3-beta published false uloop-project-runner-v3.0.0-beta.1 true true false false true false } # Verifies non-version CLI contract changes publish assets. test_cli_contract_change_publishes() { - run_success_case contract-change 3.0.0-beta.3 push v3-beta missing false cli-v3.0.0-beta.1 true false true false true true + run_success_case contract-change 3.0.0-beta.3 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true false true false true true } # Verifies CLI requirement version bumps publish assets for that required tag. test_cli_requirement_change_publishes() { - run_success_case cli-requirement-change 3.0.0-beta.3 push v3-beta missing false cli-v3.0.0-beta.1 true false false true true true + run_success_case cli-requirement-change 3.0.0-beta.3 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true false false true true true } -# Verifies CLI release metadata-only release commits still publish native CLI assets. +# Verifies Project runner release metadata-only release commits still publish native Project runner assets. test_cli_release_metadata_change_publishes() { - run_success_case cli-release-metadata-change 3.0.0-beta.3 push v3-beta missing false cli-v3.0.0-beta.1 true false false false true true target-sha target-sha target-sha "chore: release v3-beta" "chore: release v3-beta" true false + run_success_case cli-release-metadata-change 3.0.0-beta.3 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true false false false true true target-sha target-sha target-sha "chore: release v3-beta" "chore: release v3-beta" true false } -# Verifies the first CLI asset release is published when no previous asset tag exists. +# Verifies the first Project runner asset release is published when no previous asset tag exists. test_missing_previous_cli_release_publishes() { run_success_case bootstrap 3.0.0-beta.0 push v3-beta missing false "" false false false false true true } # Verifies a recovered release still tags the original release PR merge commit. test_recovery_targets_release_commit() { - run_success_case recovery-target 3.0.0-beta.2 push v3-beta missing false cli-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "fix: follow-up change" + run_success_case recovery-target 3.0.0-beta.2 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "fix: follow-up change" } # Verifies grouped manifest release commits remain the recovery target. test_recovery_targets_grouped_release_commit() { - run_success_case recovery-grouped-target 3.0.0-beta.2 push v3-beta missing false cli-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore: release v3-beta" "fix: follow-up change" false true + run_success_case recovery-grouped-target 3.0.0-beta.2 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore: release v3-beta" "fix: follow-up change" false true } -# Verifies package-only grouped release commits do not steal the CLI release target. +# Verifies package-only grouped release commits do not steal the Project runner release target. test_recovery_ignores_grouped_package_only_release_commit() { - run_success_case recovery-grouped-package-only-target 3.0.0-beta.2 push v3-beta missing false cli-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "chore: release v3-beta" false false + run_success_case recovery-grouped-package-only-target 3.0.0-beta.2 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "chore: release v3-beta" false false } # Verifies recovery ignores follow-up commits that only mention the release version. test_recovery_ignores_non_release_subject_mentions() { - run_success_case recovery-non-release-subject 3.0.0-beta.2 push v3-beta missing false cli-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "fix: keep release 3.0.0-beta.2 on the release commit" + run_success_case recovery-non-release-subject 3.0.0-beta.2 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "fix: keep release 3.0.0-beta.2 on the release commit" } # Verifies recovery ignores metadata-touching commits unless their subject is a release commit. test_recovery_ignores_non_release_metadata_commit() { - run_success_case recovery-non-release-metadata 3.0.0-beta.2 push v3-beta missing false cli-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "fix: repair release metadata" true false + run_success_case recovery-non-release-metadata 3.0.0-beta.2 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "fix: repair release metadata" true false } # Verifies recovery ignores non-release subjects with a later release marker. test_recovery_requires_release_marker_after_scope() { - run_success_case recovery-scoped-marker 3.0.0-beta.2 push v3-beta missing false cli-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "chore(v3-beta) follow-up): release 3.0.0-beta.2" + run_success_case recovery-scoped-marker 3.0.0-beta.2 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "chore(v3-beta) follow-up): release 3.0.0-beta.2" } # Verifies version matching does not confuse beta.2 with beta.20. test_recovery_target_uses_exact_version_boundary() { - run_success_case recovery-boundary 3.0.0-beta.2 push v3-beta missing false cli-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "chore(v3-beta): release 3.0.0-beta.20" + run_success_case recovery-boundary 3.0.0-beta.2 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true true false false true true release-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.2" "chore(v3-beta): release 3.0.0-beta.20" } # Verifies main refuses prerelease versions. diff --git a/scripts/test-sync-release-please-package-releases.sh b/scripts/test-sync-release-please-package-releases.sh index b9a3ffe0c..967d32c07 100755 --- a/scripts/test-sync-release-please-package-releases.sh +++ b/scripts/test-sync-release-please-package-releases.sh @@ -26,13 +26,13 @@ printf '%s\n' "$*" >> "$GH_LOG" asset_json() { case "${CLI_RELEASE_ASSETS:-complete}" in complete) - printf '[{"name":"uloop-cli-darwin-amd64.tar.gz","size":1},{"name":"uloop-cli-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-cli-windows-amd64.zip","size":1},{"name":"uloop-cli-windows-amd64.zip.sha256","size":1},{"name":"uloop-darwin-amd64.tar.gz","size":1},{"name":"uloop-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-darwin-arm64.tar.gz","size":1},{"name":"uloop-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-windows-amd64.zip","size":1},{"name":"uloop-windows-amd64.zip.sha256","size":1}]' + printf '[{"name":"uloop-project-runner-darwin-amd64.tar.gz","size":1},{"name":"uloop-project-runner-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-project-runner-darwin-arm64.tar.gz","size":1},{"name":"uloop-project-runner-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-project-runner-windows-amd64.zip","size":1},{"name":"uloop-project-runner-windows-amd64.zip.sha256","size":1}]' ;; missing) printf '[]' ;; empty) - printf '[{"name":"uloop-cli-darwin-amd64.tar.gz","size":0},{"name":"uloop-cli-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-cli-windows-amd64.zip","size":1},{"name":"uloop-cli-windows-amd64.zip.sha256","size":1},{"name":"uloop-darwin-amd64.tar.gz","size":1},{"name":"uloop-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-darwin-arm64.tar.gz","size":1},{"name":"uloop-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-windows-amd64.zip","size":1},{"name":"uloop-windows-amd64.zip.sha256","size":1}]' + printf '[{"name":"uloop-project-runner-darwin-amd64.tar.gz","size":0},{"name":"uloop-project-runner-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-project-runner-darwin-arm64.tar.gz","size":1},{"name":"uloop-project-runner-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-project-runner-windows-amd64.zip","size":1},{"name":"uloop-project-runner-windows-amd64.zip.sha256","size":1}]' ;; esac } @@ -50,7 +50,7 @@ dispatcher_asset_json() { if [ "$1" = "release" ] && [ "$2" = "view" ]; then tag=$3 - if [ "$tag" = "${CLI_RELEASE_TAG:-cli-v3.0.0-beta.6}" ]; then + if [ "$tag" = "${CLI_RELEASE_TAG:-uloop-project-runner-v3.0.0-beta.6}" ]; then if [ -n "${CLI_RELEASE_READY_AFTER_ATTEMPTS:-}" ]; then attempt_file="$GH_LOG.cli-release-attempts" attempt=1 @@ -177,7 +177,7 @@ write_release_files() { "changelog-path": "Packages/src/CHANGELOG.md" }, "cli": { - "component": "cli", + "component": "uloop-project-runner", "release-type": "go", "include-v-in-tag": true, "include-component-in-tag": true, @@ -194,12 +194,12 @@ EOF_CONFIG } EOF_MANIFEST - cat > Packages/src/cli-pin.json < Packages/src/project-runner-pin.json < scripts/verify-native-cli-release-assets.sh <<'EOF_VERIFY' @@ -231,18 +231,12 @@ set -eu if [ "${1:-}" = "--list" ]; then printf '%s\n' \ - uloop-cli-darwin-amd64.tar.gz \ - uloop-cli-darwin-amd64.tar.gz.sha256 \ - uloop-cli-darwin-arm64.tar.gz \ - uloop-cli-darwin-arm64.tar.gz.sha256 \ - uloop-cli-windows-amd64.zip \ - uloop-cli-windows-amd64.zip.sha256 \ - uloop-darwin-amd64.tar.gz \ - uloop-darwin-amd64.tar.gz.sha256 \ - uloop-darwin-arm64.tar.gz \ - uloop-darwin-arm64.tar.gz.sha256 \ - uloop-windows-amd64.zip \ - uloop-windows-amd64.zip.sha256 + uloop-project-runner-darwin-amd64.tar.gz \ + uloop-project-runner-darwin-amd64.tar.gz.sha256 \ + uloop-project-runner-darwin-arm64.tar.gz \ + uloop-project-runner-darwin-arm64.tar.gz.sha256 \ + uloop-project-runner-windows-amd64.zip \ + uloop-project-runner-windows-amd64.zip.sha256 exit 0 fi @@ -294,7 +288,7 @@ create_release_repo() { write_release_files 3.0.0-beta.6 git add . git commit -q -m "chore: release v3-beta" - git tag cli-v3.0.0-beta.6 + git tag uloop-project-runner-v3.0.0-beta.6 git rev-parse HEAD > "$work_dir/release-sha.txt" printf '%s\n' "follow-up" > follow-up.txt @@ -315,7 +309,7 @@ prepare_origin_branch() { ( cd "$work_dir" git remote add origin "$remote_dir" - git push -q origin "$commit_sha:refs/heads/$branch_name" refs/tags/cli-v3.0.0-beta.6 + git push -q origin "$commit_sha:refs/heads/$branch_name" refs/tags/uloop-project-runner-v3.0.0-beta.6 ) } @@ -370,7 +364,7 @@ run_sync() { EXISTING_RELEASE_TARGET="$existing_target" \ CLI_RELEASE_STATE="$cli_release_state" \ CLI_RELEASE_ASSETS="$cli_release_assets" \ - CLI_RELEASE_TAG="${CLI_RELEASE_TAG:-cli-v3.0.0-beta.6}" \ + CLI_RELEASE_TAG="${CLI_RELEASE_TAG:-uloop-project-runner-v3.0.0-beta.6}" \ CLI_RELEASE_WAIT_TIMEOUT_SECONDS="$cli_release_wait_timeout" \ CLI_RELEASE_WAIT_INTERVAL_SECONDS="$cli_release_wait_interval" \ CLI_RELEASE_READY_AFTER_ATTEMPTS="$cli_release_ready_after_attempts" \ @@ -395,7 +389,7 @@ test_creates_missing_root_release_from_release_commit() { assert_contains "$work_dir/gh.log" "release view v3.0.0-beta.6 --repo hatayama/unity-cli-loop --json isDraft,targetCommitish" assert_contains "$work_dir/gh.log" "release create v3.0.0-beta.6 --repo hatayama/unity-cli-loop --title v3.0.0-beta.6 --notes-file" assert_contains "$work_dir/gh.log" "--target $release_sha --prerelease" - assert_contains "$work_dir/gh.log" "release view cli-v3.0.0-beta.6 --repo hatayama/unity-cli-loop --json isDraft,targetCommitish,assets" + assert_contains "$work_dir/gh.log" "release view uloop-project-runner-v3.0.0-beta.6 --repo hatayama/unity-cli-loop --json isDraft,targetCommitish,assets" assert_contains "$work_dir/go.log" "run ./cmd/check-protocol-minimum-version --verify-release --ref $release_sha" assert_contains "$work_dir/github-output.txt" "ready=true" } @@ -442,7 +436,7 @@ test_existing_draft_root_release_without_release_commit_fails() { write_release_files 3.0.0-beta.7 ) - if CLI_RELEASE_TAG=cli-v3.0.0-beta.7 run_sync "$work_dir" v3.0.0-beta.7 true "manual-release-target"; then + if CLI_RELEASE_TAG=uloop-project-runner-v3.0.0-beta.7 run_sync "$work_dir" v3.0.0-beta.7 true "manual-release-target"; then echo "Expected draft release without a release commit to fail." >&2 exit 1 fi @@ -451,24 +445,24 @@ test_existing_draft_root_release_without_release_commit_fails() { assert_not_contains "$work_dir/gh.log" "release edit v3.0.0-beta.7" } -# Verifies package releases wait until the matching CLI release is public. +# Verifies package releases wait until the matching Project runner release is public. test_waits_for_cli_release_before_creating_root_release() { work_dir=$(create_release_repo waits-for-cli) run_sync "$work_dir" "" false "" missing - assert_contains "$work_dir/output.txt" "CLI release cli-v3.0.0-beta.6 is not published with complete assets; package release sync will wait." + assert_contains "$work_dir/output.txt" "Project runner release uloop-project-runner-v3.0.0-beta.6 is not published with complete assets; package release sync will wait." assert_not_contains "$work_dir/gh.log" "release create v3.0.0-beta.6" assert_contains "$work_dir/github-output.txt" "ready=false" } -# Verifies package releases wait until the matching CLI release has all native assets. +# Verifies package releases wait until the matching Project runner release has all native assets. test_waits_for_cli_assets_before_creating_root_release() { work_dir=$(create_release_repo waits-for-cli-assets) run_sync "$work_dir" "" false "" published missing - assert_contains "$work_dir/output.txt" "CLI release cli-v3.0.0-beta.6 is not published with complete assets; package release sync will wait." + assert_contains "$work_dir/output.txt" "Project runner release uloop-project-runner-v3.0.0-beta.6 is not published with complete assets; package release sync will wait." assert_not_contains "$work_dir/gh.log" "release create v3.0.0-beta.6" assert_contains "$work_dir/github-output.txt" "ready=false" } @@ -495,16 +489,16 @@ test_waits_when_dispatcher_asset_list_fails() { assert_contains "$work_dir/github-output.txt" "ready=false" } -# Verifies the release sync waits for a concurrently publishing CLI release before creating package releases. +# Verifies the release sync waits for a concurrently publishing Project runner release before creating package releases. test_retries_until_cli_assets_are_ready() { work_dir=$(create_release_repo retries-until-cli-ready) release_sha=$(cat "$work_dir/release-sha.txt") run_sync "$work_dir" "" false "" published complete 3 0 3 - assert_contains "$work_dir/output.txt" "CLI release cli-v3.0.0-beta.6 is not published with complete assets yet; waiting 1s before retry." + assert_contains "$work_dir/output.txt" "Project runner release uloop-project-runner-v3.0.0-beta.6 is not published with complete assets yet; waiting 1s before retry." assert_contains "$work_dir/sleep.log" "1" - assert_contains "$work_dir/output.txt" "CLI release cli-v3.0.0-beta.6 is now published with complete assets." + assert_contains "$work_dir/output.txt" "Project runner release uloop-project-runner-v3.0.0-beta.6 is now published with complete assets." assert_contains "$work_dir/gh.log" "release create v3.0.0-beta.6 --repo hatayama/unity-cli-loop --title v3.0.0-beta.6 --notes-file" assert_contains "$work_dir/gh.log" "--target $release_sha --prerelease" assert_contains "$work_dir/github-output.txt" "ready=true" diff --git a/scripts/test-verify-native-cli-release-assets.sh b/scripts/test-verify-native-cli-release-assets.sh index 912294f28..d26606322 100755 --- a/scripts/test-verify-native-cli-release-assets.sh +++ b/scripts/test-verify-native-cli-release-assets.sh @@ -36,31 +36,23 @@ write_checksum() { ) } -write_executable "$PAYLOAD_DIR/uloop-cli" "real" -write_executable "$PAYLOAD_DIR/uloop" "dispatcher" +write_executable "$PAYLOAD_DIR/uloop-project-runner" "real" -tar -czf "$RELEASE_DIR/uloop-cli-darwin-amd64.tar.gz" -C "$PAYLOAD_DIR" ./uloop-cli -tar -czf "$RELEASE_DIR/uloop-cli-darwin-arm64.tar.gz" -C "$PAYLOAD_DIR" ./uloop-cli -tar -czf "$RELEASE_DIR/uloop-darwin-amd64.tar.gz" -C "$PAYLOAD_DIR" ./uloop ./uloop-cli -tar -czf "$RELEASE_DIR/uloop-darwin-arm64.tar.gz" -C "$PAYLOAD_DIR" ./uloop ./uloop-cli -write_checksum "uloop-cli-darwin-amd64.tar.gz" -write_checksum "uloop-cli-darwin-arm64.tar.gz" -write_checksum "uloop-darwin-amd64.tar.gz" -write_checksum "uloop-darwin-arm64.tar.gz" +tar -czf "$RELEASE_DIR/uloop-project-runner-darwin-amd64.tar.gz" -C "$PAYLOAD_DIR" ./uloop-project-runner +tar -czf "$RELEASE_DIR/uloop-project-runner-darwin-arm64.tar.gz" -C "$PAYLOAD_DIR" ./uloop-project-runner +write_checksum "uloop-project-runner-darwin-amd64.tar.gz" +write_checksum "uloop-project-runner-darwin-arm64.tar.gz" if ! command -v zip >/dev/null 2>&1; then echo "zip is required to test native CLI release asset verification" >&2 exit 1 fi -write_executable "$PAYLOAD_DIR/uloop-cli.exe" "real" -write_executable "$PAYLOAD_DIR/uloop.exe" "dispatcher" +write_executable "$PAYLOAD_DIR/uloop-project-runner.exe" "real" ( cd "$PAYLOAD_DIR" - zip -q "$RELEASE_DIR/uloop-cli-windows-amd64.zip" uloop-cli.exe - zip -q "$RELEASE_DIR/uloop-windows-amd64.zip" uloop.exe uloop-cli.exe + zip -q "$RELEASE_DIR/uloop-project-runner-windows-amd64.zip" uloop-project-runner.exe ) -write_checksum "uloop-cli-windows-amd64.zip" -write_checksum "uloop-windows-amd64.zip" +write_checksum "uloop-project-runner-windows-amd64.zip" "$ROOT_DIR/scripts/verify-native-cli-release-assets.sh" "$RELEASE_DIR" diff --git a/scripts/verify-go-cli-dist.sh b/scripts/verify-go-cli-dist.sh index 55262c6d1..431389d4d 100755 --- a/scripts/verify-go-cli-dist.sh +++ b/scripts/verify-go-cli-dist.sh @@ -5,11 +5,11 @@ ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) DIST_FILES=" cli/dist/darwin-arm64/uloop -cli/dist/darwin-arm64/uloop-cli +cli/dist/darwin-arm64/uloop-project-runner cli/dist/darwin-amd64/uloop -cli/dist/darwin-amd64/uloop-cli +cli/dist/darwin-amd64/uloop-project-runner cli/dist/windows-amd64/uloop.exe -cli/dist/windows-amd64/uloop-cli.exe +cli/dist/windows-amd64/uloop-project-runner.exe " "$ROOT_DIR/scripts/build-go-cli.sh" diff --git a/scripts/verify-native-cli-release-assets.sh b/scripts/verify-native-cli-release-assets.sh index 80ee78845..d7f118d88 100755 --- a/scripts/verify-native-cli-release-assets.sh +++ b/scripts/verify-native-cli-release-assets.sh @@ -3,18 +3,12 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) EXPECTED_ASSETS=" -uloop-cli-darwin-amd64.tar.gz -uloop-cli-darwin-amd64.tar.gz.sha256 -uloop-cli-darwin-arm64.tar.gz -uloop-cli-darwin-arm64.tar.gz.sha256 -uloop-cli-windows-amd64.zip -uloop-cli-windows-amd64.zip.sha256 -uloop-darwin-amd64.tar.gz -uloop-darwin-amd64.tar.gz.sha256 -uloop-darwin-arm64.tar.gz -uloop-darwin-arm64.tar.gz.sha256 -uloop-windows-amd64.zip -uloop-windows-amd64.zip.sha256 +uloop-project-runner-darwin-amd64.tar.gz +uloop-project-runner-darwin-amd64.tar.gz.sha256 +uloop-project-runner-darwin-arm64.tar.gz +uloop-project-runner-darwin-arm64.tar.gz.sha256 +uloop-project-runner-windows-amd64.zip +uloop-project-runner-windows-amd64.zip.sha256 " if [ "${1:-}" = "--list" ]; then @@ -104,21 +98,12 @@ for asset_name in $EXPECTED_ASSETS; do require_file "$asset_name" done -verify_checksum "uloop-cli-darwin-amd64.tar.gz" -verify_checksum "uloop-cli-darwin-arm64.tar.gz" -verify_checksum "uloop-cli-windows-amd64.zip" -verify_checksum "uloop-darwin-amd64.tar.gz" -verify_checksum "uloop-darwin-arm64.tar.gz" -verify_checksum "uloop-windows-amd64.zip" - -require_tar_entry "uloop-cli-darwin-amd64.tar.gz" "uloop-cli" -require_tar_entry "uloop-cli-darwin-arm64.tar.gz" "uloop-cli" -require_zip_entry "uloop-cli-windows-amd64.zip" "uloop-cli.exe" -require_tar_entry "uloop-darwin-amd64.tar.gz" "uloop" -require_tar_entry "uloop-darwin-amd64.tar.gz" "uloop-cli" -require_tar_entry "uloop-darwin-arm64.tar.gz" "uloop" -require_tar_entry "uloop-darwin-arm64.tar.gz" "uloop-cli" -require_zip_entry "uloop-windows-amd64.zip" "uloop.exe" -require_zip_entry "uloop-windows-amd64.zip" "uloop-cli.exe" +verify_checksum "uloop-project-runner-darwin-amd64.tar.gz" +verify_checksum "uloop-project-runner-darwin-arm64.tar.gz" +verify_checksum "uloop-project-runner-windows-amd64.zip" + +require_tar_entry "uloop-project-runner-darwin-amd64.tar.gz" "uloop-project-runner" +require_tar_entry "uloop-project-runner-darwin-arm64.tar.gz" "uloop-project-runner" +require_zip_entry "uloop-project-runner-windows-amd64.zip" "uloop-project-runner.exe" echo "Native CLI release assets are complete." From cc292055e9339c1d8b938ed48ff3b914d5170711 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 28 Jun 2026 23:59:51 +0900 Subject: [PATCH 02/16] Fix project runner rename release gates Allow release guards to compare against v3-beta refs that still declare the legacy CLI minimum constant and to validate existing minimum project runner releases from the old cli-v tag namespace. Raise the package minimum dispatcher version to the dispatcher build that understands the renamed project-runner pin contract. --- .uloop/project-runner-pin.json | 2 +- Packages/src/Editor/Domain/CliConstants.cs | 2 +- Packages/src/project-runner-pin.json | 2 +- .../protocol_minimum_version_guard.go | 134 +++++++++++++----- .../protocol_minimum_version_guard_test.go | 124 +++++++++++++--- 5 files changed, 204 insertions(+), 60 deletions(-) diff --git a/.uloop/project-runner-pin.json b/.uloop/project-runner-pin.json index c40eba2f6..40dfa3a99 100644 --- a/.uloop/project-runner-pin.json +++ b/.uloop/project-runner-pin.json @@ -1,6 +1,6 @@ { "projectRunnerVersion": "3.0.0-beta.43", - "minimumDispatcherVersion": "3.0.1-beta.2", + "minimumDispatcherVersion": "3.0.1-beta.6", "packageName": "io.github.hatayama.uloopmcp", "packageVersion": "3.0.0-beta.44", "requiredProtocolVersion": 2, diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index 46b52bebe..9bd8ade31 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -15,7 +15,7 @@ public static class CliConstants public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "3.0.0-beta.40"; public const string MINIMUM_REQUIRED_PROJECT_RUNNER_RELEASE_TAG = PROJECT_RUNNER_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION; // Why: global uloop is a dispatcher; project-local CLI versions are downloaded separately. - public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "3.0.1-beta.2"; + public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "3.0.1-beta.6"; public const string MINIMUM_REQUIRED_DISPATCHER_RELEASE_TAG = DISPATCHER_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_DISPATCHER_VERSION; // Why: dispatcher setup compatibility is a launcher contract generation, not the IPC protocol generation. public const int REQUIRED_DISPATCHER_CONTRACT_VERSION = 1; diff --git a/Packages/src/project-runner-pin.json b/Packages/src/project-runner-pin.json index c40eba2f6..40dfa3a99 100644 --- a/Packages/src/project-runner-pin.json +++ b/Packages/src/project-runner-pin.json @@ -1,6 +1,6 @@ { "projectRunnerVersion": "3.0.0-beta.43", - "minimumDispatcherVersion": "3.0.1-beta.2", + "minimumDispatcherVersion": "3.0.1-beta.6", "packageName": "io.github.hatayama.uloopmcp", "packageVersion": "3.0.0-beta.44", "requiredProtocolVersion": 2, diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/cli/internal/automation/protocol_minimum_version_guard.go index 6b1087eec..16d4a69f8 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/cli/internal/automation/protocol_minimum_version_guard.go @@ -15,15 +15,17 @@ import ( ) const ( - protocolMinimumVersionFile = "Packages/src/Editor/Domain/CliConstants.cs" - protocolMinimumVersionMarker = "" - projectRunnerReleaseTagPrefix = "uloop-project-runner-v" + protocolMinimumVersionFile = "Packages/src/Editor/Domain/CliConstants.cs" + protocolMinimumVersionMarker = "" + projectRunnerReleaseTagPrefix = "uloop-project-runner-v" + legacyProjectRunnerReleaseTagPrefix = "cli-v" ) var ( - requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) - minimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) - requiredMinimumCliReleaseAssets = []string{ + requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) + minimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) + legacyMinimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) + requiredMinimumProjectRunnerAssets = []string{ "uloop-project-runner-darwin-amd64.tar.gz", "uloop-project-runner-darwin-amd64.tar.gz.sha256", "uloop-project-runner-darwin-arm64.tar.gz", @@ -31,8 +33,21 @@ var ( "uloop-project-runner-windows-amd64.zip", "uloop-project-runner-windows-amd64.zip.sha256", } + requiredLegacyMinimumProjectRunnerAssets = []string{ + "uloop-cli-darwin-amd64.tar.gz", + "uloop-cli-darwin-amd64.tar.gz.sha256", + "uloop-cli-darwin-arm64.tar.gz", + "uloop-cli-darwin-arm64.tar.gz.sha256", + "uloop-cli-windows-amd64.zip", + "uloop-cli-windows-amd64.zip.sha256", + } ) +type minimumProjectRunnerRelease struct { + Tag string + RequiredAssets []string +} + type ProtocolMinimumVersionGuardConfig struct { BaseRef string HeadRef string @@ -110,7 +125,8 @@ func RunMinimumCliReleaseProtocolCheck(ctx context.Context, stdout io.Writer, st return 1 } - if err := verifyMinimumCliReleaseProtocolAtRef(ctx, repoRoot, values); err != nil { + releaseTag, err := verifyMinimumCliReleaseProtocolAtRef(ctx, repoRoot, values) + if err != nil { writeProtocolMinimumVersionLine(stderr, err) return 1 } @@ -118,9 +134,8 @@ func RunMinimumCliReleaseProtocolCheck(ctx context.Context, stdout io.Writer, st writeProtocolMinimumVersionLine( stdout, fmt.Sprintf( - "Minimum project runner release %s%s advertises protocol %d.", - projectRunnerReleaseTagPrefix, - values.MinimumProjectRunnerVersion, + "Minimum project runner release %s advertises protocol %d.", + releaseTag, values.RequiredProtocolVersion)) return 0 } @@ -152,7 +167,7 @@ func AnalyzeProtocolMinimumVersionGuardForRefs( result := AnalyzeProtocolMinimumVersionGuard(baseValues, headValues) if protocolMinimumVersionGuardNeedsReleaseCheck(result) { - err = verifyMinimumCliReleaseProtocolAtRef(ctx, repoRoot, result.Head) + _, err = verifyMinimumCliReleaseProtocolAtRef(ctx, repoRoot, result.Head) if err != nil { result.MinimumCliReleaseProtocolError = err.Error() } @@ -191,15 +206,39 @@ func ParseProtocolMinimumVersionValues(content []byte) (ProtocolMinimumVersionVa values.HasRequiredProtocol = true } - minimumMatches := minimumProjectRunnerVersionPattern.FindStringSubmatch(text) - if len(minimumMatches) != 2 { + minimumProjectRunnerVersion, ok := parseMinimumProjectRunnerVersion(text) + if !ok { return ProtocolMinimumVersionValues{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION", protocolMinimumVersionFile) } - values.MinimumProjectRunnerVersion = minimumMatches[1] + values.MinimumProjectRunnerVersion = minimumProjectRunnerVersion return values, nil } func VerifyMinimumCliReleaseProtocol(values ProtocolMinimumVersionValues, contractContent []byte) error { + return verifyMinimumProjectRunnerReleaseProtocol( + projectRunnerReleaseTagPrefix+values.MinimumProjectRunnerVersion, + values, + contractContent) +} + +func parseMinimumProjectRunnerVersion(text string) (string, bool) { + minimumMatches := minimumProjectRunnerVersionPattern.FindStringSubmatch(text) + if len(minimumMatches) == 2 { + return minimumMatches[1], true + } + + legacyMinimumMatches := legacyMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) + if len(legacyMinimumMatches) == 2 { + return legacyMinimumMatches[1], true + } + return "", false +} + +func verifyMinimumProjectRunnerReleaseProtocol( + releaseTag string, + values ProtocolMinimumVersionValues, + contractContent []byte, +) error { if !values.HasRequiredProtocol { return fmt.Errorf("%s does not define REQUIRED_CLI_PROTOCOL_VERSION", protocolMinimumVersionFile) } @@ -212,16 +251,14 @@ func VerifyMinimumCliReleaseProtocol(values ProtocolMinimumVersionValues, contra protocolVersion, hasProtocolVersion := minimumCliReleaseProtocolVersion(contract.ProtocolVersion) if !hasProtocolVersion { return fmt.Errorf( - "project runner release %s%s does not define protocolVersion", - projectRunnerReleaseTagPrefix, - values.MinimumProjectRunnerVersion) + "project runner release %s does not define protocolVersion", + releaseTag) } if protocolVersion != values.RequiredProtocolVersion { return fmt.Errorf( - "unity package requires protocol %d, but project runner release %s%s advertises protocol %d", + "unity package requires protocol %d, but project runner release %s advertises protocol %d", values.RequiredProtocolVersion, - projectRunnerReleaseTagPrefix, - values.MinimumProjectRunnerVersion, + releaseTag, protocolVersion) } return nil @@ -243,16 +280,37 @@ func verifyMinimumCliReleaseProtocolAtRef( ctx context.Context, repoRoot string, values ProtocolMinimumVersionValues, -) error { - releaseTag := projectRunnerReleaseTagPrefix + values.MinimumProjectRunnerVersion - contractContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, releaseTag, "cli/contract.json") - if err != nil { - return fmt.Errorf("project runner release %s does not provide cli/contract.json", releaseTag) +) (string, error) { + releases := minimumProjectRunnerReleases(values.MinimumProjectRunnerVersion) + unavailableReleases := []string{} + for _, release := range releases { + contractContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, release.Tag, "cli/contract.json") + if err != nil { + unavailableReleases = append(unavailableReleases, release.Tag) + continue + } + if err := verifyMinimumProjectRunnerReleaseProtocol(release.Tag, values, []byte(contractContent)); err != nil { + return "", err + } + return verifyMinimumCliReleaseIsPublished(ctx, repoRoot, release) } - if err := VerifyMinimumCliReleaseProtocol(values, []byte(contractContent)); err != nil { - return err + + return "", fmt.Errorf( + "project runner release %s does not provide cli/contract.json", + strings.Join(unavailableReleases, " or ")) +} + +func minimumProjectRunnerReleases(version string) []minimumProjectRunnerRelease { + return []minimumProjectRunnerRelease{ + { + Tag: projectRunnerReleaseTagPrefix + version, + RequiredAssets: requiredMinimumProjectRunnerAssets, + }, + { + Tag: legacyProjectRunnerReleaseTagPrefix + version, + RequiredAssets: requiredLegacyMinimumProjectRunnerAssets, + }, } - return verifyMinimumCliReleaseIsPublished(ctx, repoRoot, releaseTag) } func minimumCliReleaseProtocolFile(ctx context.Context, repoRoot string, ref string) ([]byte, error) { @@ -267,34 +325,34 @@ func minimumCliReleaseProtocolFile(ctx context.Context, repoRoot string, ref str return []byte(content), nil } -func verifyMinimumCliReleaseIsPublished(ctx context.Context, repoRoot string, releaseTag string) error { +func verifyMinimumCliReleaseIsPublished(ctx context.Context, repoRoot string, release minimumProjectRunnerRelease) (string, error) { output, err := runProtocolMinimumVersionOutput( ctx, repoRoot, "gh", "release", "view", - releaseTag, + release.Tag, "--json", "isDraft,assets") if err != nil { - return fmt.Errorf("project runner release %s is not published with complete native assets: %w", releaseTag, err) + return "", fmt.Errorf("project runner release %s is not published with complete native assets: %w", release.Tag, err) } releaseView := minimumCliReleaseView{} if err := json.Unmarshal([]byte(output), &releaseView); err != nil { - return fmt.Errorf("project runner release %s metadata is invalid JSON: %w", releaseTag, err) + return "", fmt.Errorf("project runner release %s metadata is invalid JSON: %w", release.Tag, err) } if releaseView.IsDraft { - return fmt.Errorf("project runner release %s is still draft", releaseTag) + return "", fmt.Errorf("project runner release %s is still draft", release.Tag) } - if missingAsset := missingMinimumCliReleaseAsset(releaseView.Assets); missingAsset != "" { - return fmt.Errorf("project runner release %s is missing release asset %s", releaseTag, missingAsset) + if missingAsset := missingMinimumCliReleaseAsset(releaseView.Assets, release.RequiredAssets); missingAsset != "" { + return "", fmt.Errorf("project runner release %s is missing release asset %s", release.Tag, missingAsset) } - return nil + return release.Tag, nil } -func missingMinimumCliReleaseAsset(assets []minimumCliReleaseAsset) string { +func missingMinimumCliReleaseAsset(assets []minimumCliReleaseAsset, requiredAssetNames []string) string { availableAssets := map[string]bool{} for _, asset := range assets { if asset.Size > 0 { @@ -302,7 +360,7 @@ func missingMinimumCliReleaseAsset(assets []minimumCliReleaseAsset) string { } } - for _, assetName := range requiredMinimumCliReleaseAssets { + for _, assetName := range requiredAssetNames { if !availableAssets[assetName] { return assetName } diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/cli/internal/automation/protocol_minimum_version_guard_test.go index a4b281a1b..41327b476 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/internal/automation/protocol_minimum_version_guard_test.go @@ -82,6 +82,19 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseMatches_Passes(t *test assertProtocolMinimumVersionLogContains(t, result.gitLog, "uloop-project-runner-v3.0.0-beta.33:cli/contract.json") } +func TestRunProtocolMinimumVersionGuard_WhenBaseUsesLegacyMinimumConstant_Passes(t *testing.T) { + // Verifies rename PRs can compare against base branches that still use the old CLI minimum constant name. + result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ + baseContent: buildLegacyProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), + headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), + }) + + if result.exitCode != 0 { + t.Fatalf("expected exit code 0, got %d\nstderr: %s", result.exitCode, result.stderr) + } + assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") +} + func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseProtocolDiffers_Fails(t *testing.T) { // Verifies changing the minimum version text is not enough when the release uses the old protocol. result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ @@ -247,6 +260,48 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenRefIsProvided_ReadsValuesAtRef(t assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view uloop-project-runner-v3.0.0-beta.33") } +func TestRunMinimumCliReleaseProtocolCheck_WhenOnlyLegacyReleaseExists_Passes(t *testing.T) { + // Verifies release backfill checks can validate minimum releases published before the project runner tag rename. + workDir := t.TempDir() + mockBin := filepath.Join(workDir, "bin") + err := os.MkdirAll(mockBin, 0o755) + if err != nil { + t.Fatalf("failed to create mock bin: %v", err) + } + + gitLogPath := filepath.Join(workDir, "git.log") + ghLogPath := filepath.Join(workDir, "gh.log") + writeProtocolMinimumVersionMockGit(t, filepath.Join(mockBin, "git")) + writeProtocolMinimumVersionMockGH(t, filepath.Join(mockBin, "gh")) + prepareProtocolMinimumVersionGitContents(t, workDir, protocolMinimumVersionRefCase{ + headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.40"}`, + releaseContentPrefix: "cli-v", + }) + + t.Setenv("PATH", mockBin+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("ULOOP_REPOSITORY_ROOT", workDir) + t.Setenv("GIT_LOG", gitLogPath) + t.Setenv("GH_LOG", ghLogPath) + t.Setenv("GH_RELEASE_VIEW", `{"isDraft":false,"assets":[{"name":"uloop-cli-darwin-amd64.tar.gz","size":1},{"name":"uloop-cli-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-cli-windows-amd64.zip","size":1},{"name":"uloop-cli-windows-amd64.zip.sha256","size":1}]}`) + + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + exitCode := RunMinimumCliReleaseProtocolCheck( + context.Background(), + &stdout, + &stderr, + "protocol-release") + + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d\nstderr: %s", exitCode, stderr.String()) + } + assertProtocolMinimumVersionLogContains(t, stdout.String(), "Minimum project runner release cli-v3.0.0-beta.40 advertises protocol 2.") + assertProtocolMinimumVersionLogContains(t, readFile(t, gitLogPath), "uloop-project-runner-v3.0.0-beta.40:cli/contract.json") + assertProtocolMinimumVersionLogContains(t, readFile(t, gitLogPath), "cli-v3.0.0-beta.40:cli/contract.json") + assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view cli-v3.0.0-beta.40") +} + func TestRunProtocolMinimumVersionComment_WhenWarningExists_UpsertsComment(t *testing.T) { // Verifies PR comments explain protocol bump installer target omissions. result := runProtocolMinimumVersionCommentCase(t, protocolMinimumVersionCommentCase{ @@ -296,10 +351,11 @@ func TestRunProtocolMinimumVersionComment_WhenMinimumReleaseProtocolDiffers_Upse } type protocolMinimumVersionRefCase struct { - baseContent string - headContent string - releaseContent string - releaseView string + baseContent string + headContent string + releaseContent string + releaseContentPrefix string + releaseView string } type protocolMinimumVersionGuardRunResult struct { @@ -421,6 +477,9 @@ func prepareProtocolMinimumVersionGitContents(t *testing.T, workDir string, test writeFile(t, releaseContentPath, testCase.releaseContent) t.Setenv("GIT_RELEASE_CONTENT", releaseContentPath) } + if testCase.releaseContentPrefix != "" { + t.Setenv("GIT_RELEASE_CONTENT_PREFIX", testCase.releaseContentPrefix) + } t.Setenv("GIT_BASE_CONTENT", baseContentPath) t.Setenv("GIT_HEAD_CONTENT", headContentPath) } @@ -432,6 +491,17 @@ public const int REQUIRED_CLI_PROTOCOL_VERSION = ` + strconv.Itoa(requiredProtocolVersion) + `; public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "` + minimumProjectRunnerVersion + `"; +} + }` +} + +func buildLegacyProtocolMinimumVersionConstants(requiredProtocolVersion int, minimumCliVersion string) string { + return `namespace Tests { +public static class CliConstants { +public const int REQUIRED_CLI_PROTOCOL_VERSION = ` + + strconv.Itoa(requiredProtocolVersion) + + `; +public const string MINIMUM_REQUIRED_CLI_VERSION = "` + minimumCliVersion + `"; } }` } @@ -453,21 +523,37 @@ if [ "$1" = "-C" ]; then shift 2 fi -if [ "$1" = "show" ]; then - case "$2" in - origin/v3-beta:*) cat "$GIT_BASE_CONTENT" ;; - protocol-pr-head:*) cat "$GIT_HEAD_CONTENT" ;; - protocol-release:*) cat "$GIT_HEAD_CONTENT" ;; - uloop-project-runner-v*:cli/contract.json) - if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then - cat "$GIT_RELEASE_CONTENT" - else - echo "release not found" >&2 - exit 1 - fi - ;; - *) echo "unexpected git show ref: $2" >&2; exit 1 ;; - esac + if [ "$1" = "show" ]; then + case "$2" in + origin/v3-beta:*) cat "$GIT_BASE_CONTENT" ;; + protocol-pr-head:*) cat "$GIT_HEAD_CONTENT" ;; + protocol-release:*) cat "$GIT_HEAD_CONTENT" ;; + uloop-project-runner-v*:cli/contract.json) + if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then + if [ "${GIT_RELEASE_CONTENT_PREFIX:-uloop-project-runner-v}" != "uloop-project-runner-v" ]; then + echo "release not found" >&2 + exit 1 + fi + cat "$GIT_RELEASE_CONTENT" + else + echo "release not found" >&2 + exit 1 + fi + ;; + cli-v*:cli/contract.json) + if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then + if [ "${GIT_RELEASE_CONTENT_PREFIX:-uloop-project-runner-v}" != "cli-v" ]; then + echo "release not found" >&2 + exit 1 + fi + cat "$GIT_RELEASE_CONTENT" + else + echo "release not found" >&2 + exit 1 + fi + ;; + *) echo "unexpected git show ref: $2" >&2; exit 1 ;; + esac exit 0 fi From 949ba4847430569f93a53669b249fa016c2ffa81 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 00:11:51 +0900 Subject: [PATCH 03/16] Fix rename gaps in Unity installer tests Keep legacy cli-v installer tags normalized to dispatcher releases during the project runner rename and update EditMode pin synchronization tests to assert the renamed project-runner pin file. --- .../Tests/Editor/CliPinSynchronizerTests.cs | 47 ++++++++++++++----- Packages/src/Editor/Domain/CliConstants.cs | 1 + .../Infrastructure/CLI/NativeCliInstaller.cs | 4 ++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/Assets/Tests/Editor/CliPinSynchronizerTests.cs b/Assets/Tests/Editor/CliPinSynchronizerTests.cs index b00472d2e..3bc7741cb 100644 --- a/Assets/Tests/Editor/CliPinSynchronizerTests.cs +++ b/Assets/Tests/Editor/CliPinSynchronizerTests.cs @@ -6,6 +6,7 @@ using UnityEngine.TestTools; using io.github.hatayama.UnityCliLoop.Infrastructure; +using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Tests.Editor { @@ -14,7 +15,7 @@ public sealed class CliPinSynchronizerTests [Test] public void SyncProjectPinFile_WhenDestinationMissing_ShouldCopyPackagePin() { - // Tests that the dispatcher pin contract is published into the project .uloop directory. + // Tests that the project runner pin contract is published into the project .uloop directory. string root = CreateTestRoot(); string packageRoot = Path.Combine(root, "package"); string projectRoot = Path.Combine(root, "project"); @@ -23,13 +24,19 @@ public void SyncProjectPinFile_WhenDestinationMissing_ShouldCopyPackagePin() { Directory.CreateDirectory(packageRoot); Directory.CreateDirectory(projectRoot); - File.WriteAllText(Path.Combine(packageRoot, "cli-pin.json"), "{\"schemaVersion\":1}"); + File.WriteAllText( + Path.Combine(packageRoot, UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME), + "{\"schemaVersion\":1}"); bool changed = CliPinSynchronizer.SyncProjectPinFile(packageRoot, projectRoot); Assert.That(changed, Is.True); Assert.That( - File.ReadAllText(Path.Combine(projectRoot, ".uloop", "cli-pin.json")), + File.ReadAllText( + Path.Combine( + projectRoot, + UnityCliLoopConstants.ULOOP_DIR, + UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME)), Is.EqualTo("{\"schemaVersion\":1}")); } finally @@ -45,14 +52,18 @@ public void SyncProjectPinFile_WhenDestinationMatches_ShouldSkipWrite() string root = CreateTestRoot(); string packageRoot = Path.Combine(root, "package"); string projectRoot = Path.Combine(root, "project"); - string projectUloopRoot = Path.Combine(projectRoot, ".uloop"); + string projectUloopRoot = Path.Combine(projectRoot, UnityCliLoopConstants.ULOOP_DIR); try { Directory.CreateDirectory(packageRoot); Directory.CreateDirectory(projectUloopRoot); - string sourcePath = Path.Combine(packageRoot, "cli-pin.json"); - string destinationPath = Path.Combine(projectUloopRoot, "cli-pin.json"); + string sourcePath = Path.Combine( + packageRoot, + UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); + string destinationPath = Path.Combine( + projectUloopRoot, + UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); File.WriteAllText(sourcePath, "{\"schemaVersion\":1}"); File.WriteAllText(destinationPath, "{\"schemaVersion\":1}"); DateTime previousWriteTime = File.GetLastWriteTimeUtc(destinationPath); @@ -71,18 +82,22 @@ public void SyncProjectPinFile_WhenDestinationMatches_ShouldSkipWrite() [Test] public void SyncProjectPinFile_WhenPackagePinChanges_ShouldUpdateProjectPin() { - // Tests that package upgrades update the project dispatcher pin contract. + // Tests that package upgrades update the project runner pin contract. string root = CreateTestRoot(); string packageRoot = Path.Combine(root, "package"); string projectRoot = Path.Combine(root, "project"); - string projectUloopRoot = Path.Combine(projectRoot, ".uloop"); + string projectUloopRoot = Path.Combine(projectRoot, UnityCliLoopConstants.ULOOP_DIR); try { Directory.CreateDirectory(packageRoot); Directory.CreateDirectory(projectUloopRoot); - File.WriteAllText(Path.Combine(packageRoot, "cli-pin.json"), "{\"schemaVersion\":2}"); - string destinationPath = Path.Combine(projectUloopRoot, "cli-pin.json"); + File.WriteAllText( + Path.Combine(packageRoot, UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME), + "{\"schemaVersion\":2}"); + string destinationPath = Path.Combine( + projectUloopRoot, + UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); File.WriteAllText(destinationPath, "{\"schemaVersion\":1}"); bool changed = CliPinSynchronizer.SyncProjectPinFile(packageRoot, projectRoot); @@ -106,14 +121,22 @@ public void SyncProjectPinFile_WhenPackageRootMissing_ShouldSkipWrite() try { Directory.CreateDirectory(projectRoot); + string expectedWarning = + $"Unity CLI Loop skipped {UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME} synchronization because the package root is empty."; LogAssert.Expect( LogType.Warning, - "Unity CLI Loop skipped cli-pin.json synchronization because the package root is empty."); + expectedWarning); bool changed = CliPinSynchronizer.SyncProjectPinFile(string.Empty, projectRoot); Assert.That(changed, Is.False); - Assert.That(File.Exists(Path.Combine(projectRoot, ".uloop", "cli-pin.json")), Is.False); + Assert.That( + File.Exists( + Path.Combine( + projectRoot, + UnityCliLoopConstants.ULOOP_DIR, + UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME)), + Is.False); } finally { diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index 9bd8ade31..be3b646bd 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -41,6 +41,7 @@ public static class CliConstants public const string WINDOWS_PATH_SEPARATOR = ";"; public const string RELEASE_TAG_PREFIX = "v"; public const string PROJECT_RUNNER_RELEASE_TAG_PREFIX = "uloop-project-runner-v"; + public const string LEGACY_CLI_RELEASE_TAG_PREFIX = "cli-v"; public const string DISPATCHER_RELEASE_TAG_PREFIX = "dispatcher-v"; public const string BETA_VERSION_MARKER = "-beta."; public const string SKILL_DIR_PREFIX = "uloop-"; diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs index 6698b31f6..f76ace1be 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs @@ -962,6 +962,10 @@ private static string BuildReleaseTag(string cliReleaseTag) { return $"{CliConstants.DISPATCHER_RELEASE_TAG_PREFIX}{cliReleaseTag.Substring(CliConstants.PROJECT_RUNNER_RELEASE_TAG_PREFIX.Length)}"; } + if (cliReleaseTag.StartsWith(CliConstants.LEGACY_CLI_RELEASE_TAG_PREFIX, StringComparison.Ordinal)) + { + return $"{CliConstants.DISPATCHER_RELEASE_TAG_PREFIX}{cliReleaseTag.Substring(CliConstants.LEGACY_CLI_RELEASE_TAG_PREFIX.Length)}"; + } if (cliReleaseTag.StartsWith(CliConstants.RELEASE_TAG_PREFIX, StringComparison.Ordinal)) { return $"{CliConstants.DISPATCHER_RELEASE_TAG_PREFIX}{cliReleaseTag.Substring(CliConstants.RELEASE_TAG_PREFIX.Length)}"; From 11f007c99225724f33a79e7b28c82f8a2815b19c Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 00:24:13 +0900 Subject: [PATCH 04/16] Fix legacy rename upgrade paths Ensure the first uloop-project-runner tag for an existing cli-v version targets the rename build commit, while keeping legacy cli-v JSON and update inputs readable during setup and self-update flows. --- .../CLI/CliInstallationDetector.cs | 5 ++ cli/internal/cli/update_test.go | 33 ++++++++ cli/internal/update/command.go | 3 + cli/internal/update/installer.go | 1 + scripts/resolve-native-cli-release-target.sh | 8 ++ .../test-resolve-native-cli-release-target.sh | 84 ++++++++++++------- 6 files changed, 105 insertions(+), 29 deletions(-) diff --git a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs index 99e0c0d14..e157dd75f 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs @@ -46,6 +46,7 @@ public sealed class CliInstallationDetector : ICliInstallationDetector private const string SHELL_CONTRACT_STATUS_END_MARKER = "__ULOOP_CONTRACT_STATUS_END__"; private const string SHELL_SUCCESS_EXIT_CODE = "0"; private const string VERSION_JSON_PROJECT_RUNNER_VERSION_PROPERTY = "ProjectRunnerVersion"; + private const string VERSION_JSON_LEGACY_CLI_VERSION_PROPERTY = "CliVersion"; private const string VERSION_JSON_DISPATCHER_VERSION_PROPERTY = "DispatcherVersion"; private const string VERSION_JSON_DISPATCHER_CONTRACT_VERSION_PROPERTY = "DispatcherContractVersion"; @@ -377,6 +378,10 @@ private static CliInstallationDetection ParseCliContractOutput(string output, st } string version = parsed[VERSION_JSON_PROJECT_RUNNER_VERSION_PROPERTY]?.ToString(); + if (string.IsNullOrEmpty(version)) + { + version = parsed[VERSION_JSON_LEGACY_CLI_VERSION_PROPERTY]?.ToString(); + } return new CliInstallationDetection(version, executablePath); } catch (JsonException) diff --git a/cli/internal/cli/update_test.go b/cli/internal/cli/update_test.go index a6ac48177..329ecc8ae 100644 --- a/cli/internal/cli/update_test.go +++ b/cli/internal/cli/update_test.go @@ -126,6 +126,27 @@ func TestUpdateCommandForDarwinNormalizesProjectRunnerReleaseTag(t *testing.T) { } } +func TestUpdateCommandForDarwinNormalizesLegacyCliReleaseTag(t *testing.T) { + // Verifies legacy CLI release tags resolve to the matching dispatcher release. + commandName, args, err := updateCommandForOSWithOptions("darwin", updateOptions{ + targetVersion: "cli-v3.0.0-beta.6", + }) + if err != nil { + t.Fatalf("updateCommandForOSWithOptions failed: %v", err) + } + + if commandName != "sh" { + t.Fatalf("command mismatch: %s", commandName) + } + joinedArgs := strings.Join(args, " ") + if !strings.Contains(joinedArgs, "dispatcher-v3.0.0-beta.6/scripts/install.sh") { + t.Fatalf("installer URL mismatch: %s", joinedArgs) + } + if strings.Contains(joinedArgs, "dispatcher-vcli-v3.0.0-beta.6") { + t.Fatalf("installer version contains legacy CLI prefix: %s", joinedArgs) + } +} + func TestUpdateCommandForWindowsUsesRequestedVersion(t *testing.T) { // Verifies Windows dispatcher update can target the minimum release version requested by Unity. commandName, args, err := updateCommandForOSWithOptions("windows", updateOptions{ @@ -171,6 +192,18 @@ func TestParseUpdateOptionsNormalizesProjectRunnerReleaseTag(t *testing.T) { } } +func TestParseUpdateOptionsNormalizesLegacyCliReleaseTag(t *testing.T) { + // Verifies parsed legacy CLI release tags are normalized before dispatcher tag selection. + options, err := parseUpdateOptions([]string{"--to-version", "cli-v3.0.0-beta.6"}) + if err != nil { + t.Fatalf("parseUpdateOptions failed: %v", err) + } + + if options.targetVersion != "3.0.0-beta.6" { + t.Fatalf("target version mismatch: %#v", options) + } +} + func TestParseUpdateOptionsAcceptsEqualsSyntax(t *testing.T) { // Verifies AI-readable update commands may use a single --to-version=value token. options, err := parseUpdateOptions([]string{"--to-version=3.0.0-beta.6"}) diff --git a/cli/internal/update/command.go b/cli/internal/update/command.go index 3be110930..b28846a67 100644 --- a/cli/internal/update/command.go +++ b/cli/internal/update/command.go @@ -58,6 +58,9 @@ func NormalizeTargetVersion(value string) string { if strings.HasPrefix(lower, projectRunnerReleaseTagPrefix) { return trimmed[len(projectRunnerReleaseTagPrefix):] } + if strings.HasPrefix(lower, legacyCliReleaseTagPrefix) { + return trimmed[len(legacyCliReleaseTagPrefix):] + } if strings.HasPrefix(lower, "v") { return trimmed[1:] } diff --git a/cli/internal/update/installer.go b/cli/internal/update/installer.go index 212e6f9ad..35d30567e 100644 --- a/cli/internal/update/installer.go +++ b/cli/internal/update/installer.go @@ -10,6 +10,7 @@ const ( repositoryRawBaseURL = "https://raw.githubusercontent.com/hatayama/unity-cli-loop" projectRunnerReleaseTagPrefix = "uloop-project-runner-v" + legacyCliReleaseTagPrefix = "cli-v" dispatcherTagPrefix = "dispatcher-v" betaVersionMarker = "-beta." ) diff --git a/scripts/resolve-native-cli-release-target.sh b/scripts/resolve-native-cli-release-target.sh index 8284d4585..4b79123b6 100755 --- a/scripts/resolve-native-cli-release-target.sh +++ b/scripts/resolve-native-cli-release-target.sh @@ -2,6 +2,7 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) +LEGACY_CLI_RELEASE_TAG_PREFIX="cli-v" : "${EVENT_NAME:?EVENT_NAME is required}" @@ -305,6 +306,13 @@ RELEASE_TARGET_SHA=$(release_commit_sha_for_version "$VERSION" "$BUILD_SHA") if [ -z "$RELEASE_TARGET_SHA" ]; then RELEASE_TARGET_SHA=$BUILD_SHA fi +LEGACY_CLI_RELEASE_TAG="$LEGACY_CLI_RELEASE_TAG_PREFIX$VERSION" +if [ "$CAN_EVALUATE_CLI_RELEASE" = "true" ] && + [ "$RELEASE_TARGET_SHA" != "$BUILD_SHA" ] && + release_is_published "$LEGACY_CLI_RELEASE_TAG"; then + echo "Project runner release tag namespace changed for $VERSION; targeting build commit $BUILD_SHA instead of legacy release commit $RELEASE_TARGET_SHA." >&2 + RELEASE_TARGET_SHA=$BUILD_SHA +fi if [ "$CAN_EVALUATE_CLI_RELEASE" != "true" ]; then SHOULD_PUBLISH=false diff --git a/scripts/test-resolve-native-cli-release-target.sh b/scripts/test-resolve-native-cli-release-target.sh index 7262a1d48..8bb4a9e92 100755 --- a/scripts/test-resolve-native-cli-release-target.sh +++ b/scripts/test-resolve-native-cli-release-target.sh @@ -103,14 +103,19 @@ release_json() { if [ "$1" = "release" ] && [ "$2" = "view" ]; then tag=$3 - if [ "$tag" = "uloop-project-runner-v$CURRENT_VERSION" ]; then - release_json "$CURRENT_RELEASE_STATE" "$CURRENT_RELEASE_HAS_ASSETS" - exit 0 - fi - - if [ -n "$PREVIOUS_RELEASE_TAG" ] && [ "$tag" = "$PREVIOUS_RELEASE_TAG" ]; then - release_json published "$PREVIOUS_RELEASE_HAS_ASSETS" - exit 0 + if [ "$tag" = "uloop-project-runner-v$CURRENT_VERSION" ]; then + release_json "$CURRENT_RELEASE_STATE" "$CURRENT_RELEASE_HAS_ASSETS" + exit 0 + fi + + if [ "$tag" = "cli-v$CURRENT_VERSION" ]; then + release_json "$LEGACY_CURRENT_RELEASE_STATE" true + exit 0 + fi + + if [ -n "$PREVIOUS_RELEASE_TAG" ] && [ "$tag" = "$PREVIOUS_RELEASE_TAG" ]; then + release_json published "$PREVIOUS_RELEASE_HAS_ASSETS" + exit 0 fi exit 1 @@ -154,6 +159,18 @@ assert_contains() { fi } +assert_line_equals() { + file=$1 + expected=$2 + + if ! grep -Fx "$expected" "$file" >/dev/null; then + echo "Expected $file to contain line: $expected" >&2 + echo "Actual content:" >&2 + cat "$file" >&2 + exit 1 + fi +} + assert_script_contains() { expected=$1 @@ -181,12 +198,13 @@ run_success_case() { build_sha_value=${15:-target-sha} release_commit_sha=${16:-target-sha} release_commit_subject=${17:-} - build_commit_subject=${18:-} - build_commit_updates_cli=${19:-false} - release_commit_updates_cli=${20:-false} - if [ -z "$release_commit_subject" ]; then - release_commit_subject="chore(v3-beta): release $current_version" - fi + build_commit_subject=${18:-} + build_commit_updates_cli=${19:-false} + release_commit_updates_cli=${20:-false} + legacy_current_release_state=${21:-missing} + if [ -z "$release_commit_subject" ]; then + release_commit_subject="chore(v3-beta): release $current_version" + fi if [ -z "$build_commit_subject" ]; then build_commit_subject=$release_commit_subject fi @@ -206,11 +224,12 @@ run_success_case() { BUILD_SHA_VALUE="$build_sha_value" \ BUILD_COMMIT_SUBJECT="$build_commit_subject" \ BUILD_COMMIT_UPDATES_CLI="$build_commit_updates_cli" \ - RELEASE_COMMIT_SHA="$release_commit_sha" \ - RELEASE_COMMIT_SUBJECT="$release_commit_subject" \ - RELEASE_COMMIT_UPDATES_CLI="$release_commit_updates_cli" \ - PREVIOUS_RELEASE_TAG="$previous_release_tag" \ - PREVIOUS_RELEASE_HAS_ASSETS="$previous_release_has_assets" \ + RELEASE_COMMIT_SHA="$release_commit_sha" \ + RELEASE_COMMIT_SUBJECT="$release_commit_subject" \ + RELEASE_COMMIT_UPDATES_CLI="$release_commit_updates_cli" \ + LEGACY_CURRENT_RELEASE_STATE="$legacy_current_release_state" \ + PREVIOUS_RELEASE_TAG="$previous_release_tag" \ + PREVIOUS_RELEASE_HAS_ASSETS="$previous_release_has_assets" \ CLI_SOURCE_CHANGED="$cli_source_changed" \ CONTRACT_CHANGED="$contract_changed" \ CLI_REQUIREMENT_CHANGED="$cli_requirement_changed" \ @@ -221,13 +240,13 @@ run_success_case() { INPUT_DRY_RUN=false \ "$SCRIPT" > output.txt 2> stderr.txt - assert_contains output.txt "publish=$expected_publish" - assert_contains output.txt "release=$expected_release" - assert_contains output.txt "tag=uloop-project-runner-v$current_version" - assert_contains output.txt "version=$current_version" - assert_contains output.txt "sha=$expected_sha" - assert_contains output.txt "build_sha=$build_sha_value" - assert_contains output.txt "dry_run=false" + assert_line_equals output.txt "publish=$expected_publish" + assert_line_equals output.txt "release=$expected_release" + assert_line_equals output.txt "tag=uloop-project-runner-v$current_version" + assert_line_equals output.txt "version=$current_version" + assert_line_equals output.txt "sha=$expected_sha" + assert_line_equals output.txt "build_sha=$build_sha_value" + assert_line_equals output.txt "dry_run=false" ) } @@ -254,9 +273,10 @@ run_failure_case() { CURRENT_RELEASE_HAS_ASSETS=false \ BUILD_SHA_VALUE=target-sha \ BUILD_COMMIT_SUBJECT="chore(v3-beta): release $current_version" \ - RELEASE_COMMIT_SHA=target-sha \ - RELEASE_COMMIT_SUBJECT="chore(v3-beta): release $current_version" \ - PREVIOUS_RELEASE_TAG=uloop-project-runner-v3.0.0-beta.1 \ + RELEASE_COMMIT_SHA=target-sha \ + RELEASE_COMMIT_SUBJECT="chore(v3-beta): release $current_version" \ + LEGACY_CURRENT_RELEASE_STATE=missing \ + PREVIOUS_RELEASE_TAG=uloop-project-runner-v3.0.0-beta.1 \ PREVIOUS_RELEASE_HAS_ASSETS=true \ CLI_SOURCE_CHANGED=false \ CONTRACT_CHANGED=false \ @@ -314,6 +334,11 @@ test_cli_release_metadata_change_publishes() { run_success_case cli-release-metadata-change 3.0.0-beta.3 push v3-beta missing false uloop-project-runner-v3.0.0-beta.1 true false false false true true target-sha target-sha target-sha "chore: release v3-beta" "chore: release v3-beta" true false } +# Verifies the first renamed project runner tag targets the commit that contains the rename. +test_renamed_tag_with_existing_legacy_release_targets_build_commit() { + run_success_case renamed-tag-targets-build 3.0.0-beta.43 push v3-beta missing false "" false true false false true true build-sha build-sha release-sha "chore(v3-beta): release 3.0.0-beta.43" "refactor: rename project runner" false true published +} + # Verifies the first Project runner asset release is published when no previous asset tag exists. test_missing_previous_cli_release_publishes() { run_success_case bootstrap 3.0.0-beta.0 push v3-beta missing false "" false false false false true true @@ -377,6 +402,7 @@ test_published_current_release_can_receive_cli_assets test_cli_contract_change_publishes test_cli_requirement_change_publishes test_cli_release_metadata_change_publishes +test_renamed_tag_with_existing_legacy_release_targets_build_commit test_missing_previous_cli_release_publishes test_recovery_targets_release_commit test_recovery_targets_grouped_release_commit From 10dbd199a1a9146c1f6cf19972d6c92c4b32e246 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 00:31:25 +0900 Subject: [PATCH 05/16] Preserve legacy CLI metadata diagnostics Keep reading legacy cliVersion metadata when projectRunnerVersion is absent so pre-rename clients still receive accurate update-required error details. --- .../src/Editor/Infrastructure/Api/JsonRpcProcessor.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs index e768fb693..a945d9462 100644 --- a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs @@ -106,7 +106,13 @@ private static string ReadClientProjectRunnerVersion(JObject request) } string projectRunnerVersion = metadata["projectRunnerVersion"]?.ToString(); - return string.IsNullOrWhiteSpace(projectRunnerVersion) ? null : projectRunnerVersion; + if (!string.IsNullOrWhiteSpace(projectRunnerVersion)) + { + return projectRunnerVersion; + } + + string legacyCliVersion = metadata["cliVersion"]?.ToString(); + return string.IsNullOrWhiteSpace(legacyCliVersion) ? null : legacyCliVersion; } private static int? ReadClientProtocolVersion(JObject request) From 7e37ebf924f6dc613c36d539df3ec4624a0c3f01 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 00:42:29 +0900 Subject: [PATCH 06/16] Preserve legacy dispatcher project pins Keep the global dispatcher able to read existing cli-pin.json contracts and download their cli-v runner assets while isolating legacy cache entries from renamed project-runner releases. --- cli/internal/cli/dispatcher.go | 31 ++++--- cli/internal/cli/dispatcher_download.go | 91 +++++++++++++++---- cli/internal/cli/dispatcher_pin.go | 45 +++++++--- cli/internal/cli/dispatcher_test.go | 111 ++++++++++++++++++++++++ 4 files changed, 235 insertions(+), 43 deletions(-) diff --git a/cli/internal/cli/dispatcher.go b/cli/internal/cli/dispatcher.go index d92a507a9..fe368d745 100644 --- a/cli/internal/cli/dispatcher.go +++ b/cli/internal/cli/dispatcher.go @@ -18,19 +18,24 @@ import ( ) const ( - dispatcherCacheDirEnvName = "ULOOP_CACHE_DIR" - dispatcherDisableSelfUpdateEnvName = "ULOOP_DISABLE_SELF_UPDATE" - dispatcherCacheDirectoryName = "uloop" - dispatcherVersionsDirectoryName = "versions" - dispatcherUpdateStateFileName = "dispatcher-update.json" - dispatcherProjectPinRelativePath = ".uloop/project-runner-pin.json" - dispatcherPackagePinFileName = "project-runner-pin.json" - dispatcherUnityPackageName = "io.github.hatayama.uloopmcp" - dispatcherRealCLIUnixFileName = "uloop-project-runner" - dispatcherRealCLIWindowsFileName = "uloop-project-runner.exe" - dispatcherReleaseRepository = "hatayama/unity-cli-loop" - dispatcherReleaseBaseURL = "https://github.com/" + dispatcherReleaseRepository + "/releases/download" - dispatcherSelfUpdateInterval = 24 * time.Hour + dispatcherCacheDirEnvName = "ULOOP_CACHE_DIR" + dispatcherDisableSelfUpdateEnvName = "ULOOP_DISABLE_SELF_UPDATE" + dispatcherCacheDirectoryName = "uloop" + dispatcherVersionsDirectoryName = "versions" + dispatcherUpdateStateFileName = "dispatcher-update.json" + dispatcherProjectPinRelativePath = ".uloop/project-runner-pin.json" + dispatcherLegacyProjectPinRelativePath = ".uloop/cli-pin.json" + dispatcherPackagePinFileName = "project-runner-pin.json" + dispatcherLegacyPackagePinFileName = "cli-pin.json" + dispatcherUnityPackageName = "io.github.hatayama.uloopmcp" + dispatcherRealCLIUnixFileName = "uloop-project-runner" + dispatcherRealCLIWindowsFileName = "uloop-project-runner.exe" + dispatcherLegacyCLIUnixFileName = "uloop-cli" + dispatcherLegacyCLIWindowsFileName = "uloop-cli.exe" + dispatcherLegacyReleaseTagPrefix = "cli-v" + dispatcherReleaseRepository = "hatayama/unity-cli-loop" + dispatcherReleaseBaseURL = "https://github.com/" + dispatcherReleaseRepository + "/releases/download" + dispatcherSelfUpdateInterval = 24 * time.Hour ) var ( diff --git a/cli/internal/cli/dispatcher_download.go b/cli/internal/cli/dispatcher_download.go index 09d4f55a7..1c3518064 100644 --- a/cli/internal/cli/dispatcher_download.go +++ b/cli/internal/cli/dispatcher_download.go @@ -36,15 +36,18 @@ func resolveDispatcherRealCLI(ctx context.Context, pin dispatcherPin, stderr io. if err != nil { return "", err } - realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, pin.ProjectRunnerVersion, runtime.GOOS, runtime.GOARCH) + realCLIPath := dispatcherCachedRealCLIPathForPin(cacheRoot, pin, runtime.GOOS, runtime.GOARCH) if isExecutableFile(realCLIPath) { return realCLIPath, nil } - return downloadDispatcherRealCLI(ctx, cacheRoot, pin.ProjectRunnerVersion, runtime.GOOS, runtime.GOARCH, stderr) + return downloadDispatcherRealCLIForPin(ctx, cacheRoot, pin, runtime.GOOS, runtime.GOARCH, stderr) } func dispatcherSiblingRealCLIPath(pin dispatcherPin) (string, bool) { + if pin.LegacyRelease { + return "", false + } if pin.ProjectRunnerVersion != version { return "", false } @@ -92,14 +95,25 @@ func dispatcherCacheRoot(goos string) (string, error) { } func dispatcherCachedRealCLIPath(cacheRoot string, projectRunnerVersion string, goos string, goarch string) string { + return dispatcherCachedRealCLIPathForPin(cacheRoot, dispatcherPin{ProjectRunnerVersion: projectRunnerVersion}, goos, goarch) +} + +func dispatcherCachedRealCLIPathForPin(cacheRoot string, pin dispatcherPin, goos string, goarch string) string { return filepath.Join( cacheRoot, dispatcherVersionsDirectoryName, - projectRunnerVersion, + dispatcherCacheVersionDirectoryName(pin), dispatcherPlatformName(goos, goarch), dispatcherRealCLIFileName(goos)) } +func dispatcherCacheVersionDirectoryName(pin dispatcherPin) string { + if pin.LegacyRelease { + return dispatcherLegacyReleaseTagPrefix + pin.ProjectRunnerVersion + } + return pin.ProjectRunnerVersion +} + func dispatcherPlatformName(goos string, goarch string) string { return goos + "-" + goarch } @@ -123,11 +137,21 @@ func isExecutableFile(filePath string) bool { } func downloadDispatcherRealCLI(ctx context.Context, cacheRoot string, projectRunnerVersion string, goos string, goarch string, stderr io.Writer) (string, error) { - assetName, err := dispatcherReleaseAssetName(goos, goarch) + return downloadDispatcherRealCLIForPin( + ctx, + cacheRoot, + dispatcherPin{ProjectRunnerVersion: projectRunnerVersion}, + goos, + goarch, + stderr) +} + +func downloadDispatcherRealCLIForPin(ctx context.Context, cacheRoot string, pin dispatcherPin, goos string, goarch string, stderr io.Writer) (string, error) { + assetName, err := dispatcherReleaseAssetNameForPin(goos, goarch, pin) if err != nil { return "", err } - realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, projectRunnerVersion, goos, goarch) + realCLIPath := dispatcherCachedRealCLIPathForPin(cacheRoot, pin, goos, goarch) if err := os.MkdirAll(filepath.Dir(realCLIPath), 0o755); err != nil { return "", err } @@ -142,8 +166,8 @@ func downloadDispatcherRealCLI(ctx context.Context, cacheRoot string, projectRun archivePath := filepath.Join(tempDir, assetName) checksumPath := archivePath + ".sha256" - assetURL := dispatcherReleaseAssetURL(projectRunnerVersion, assetName) - writeFormat(stderr, "uloop: downloading pinned project runner %s for %s...\n", projectRunnerVersion, dispatcherPlatformName(goos, goarch)) + assetURL := dispatcherReleaseAssetURLForPin(pin, assetName) + writeFormat(stderr, "uloop: downloading pinned project runner %s for %s...\n", pin.ProjectRunnerVersion, dispatcherPlatformName(goos, goarch)) if err := downloadDispatcherFile(ctx, assetURL, archivePath); err != nil { return "", err } @@ -155,7 +179,7 @@ func downloadDispatcherRealCLI(ctx context.Context, cacheRoot string, projectRun } tempRealCLIPath := filepath.Join(tempDir, dispatcherRealCLIFileName(goos)) - if err := extractDispatcherRealCLI(archivePath, assetName, tempRealCLIPath, goos); err != nil { + if err := extractDispatcherRealCLIForPin(archivePath, assetName, tempRealCLIPath, goos, pin); err != nil { return "", err } if err := os.Chmod(tempRealCLIPath, 0o755); err != nil { @@ -184,17 +208,25 @@ func installDownloadedDispatcherRealCLI(tempRealCLIPath string, realCLIPath stri } func dispatcherReleaseAssetName(goos string, goarch string) (string, error) { + return dispatcherReleaseAssetNameForPin(goos, goarch, dispatcherPin{}) +} + +func dispatcherReleaseAssetNameForPin(goos string, goarch string, pin dispatcherPin) (string, error) { + assetPrefix := "uloop-project-runner" + if pin.LegacyRelease { + assetPrefix = "uloop-cli" + } switch goos { case "darwin": if goarch != "arm64" && goarch != "amd64" { return "", fmt.Errorf("unsupported darwin architecture: %s", goarch) } - return "uloop-project-runner-darwin-" + goarch + ".tar.gz", nil + return assetPrefix + "-darwin-" + goarch + ".tar.gz", nil case "windows": if goarch != "amd64" { return "", fmt.Errorf("unsupported windows architecture: %s", goarch) } - return "uloop-project-runner-windows-amd64.zip", nil + return assetPrefix + "-windows-amd64.zip", nil default: return "", fmt.Errorf("unsupported platform: %s-%s", goos, goarch) } @@ -204,6 +236,13 @@ func dispatcherReleaseAssetURL(projectRunnerVersion string, assetName string) st return dispatcherReleaseBaseURL + "/" + sharedupdate.ProjectRunnerReleaseTag(projectRunnerVersion) + "/" + assetName } +func dispatcherReleaseAssetURLForPin(pin dispatcherPin, assetName string) string { + if pin.LegacyRelease { + return dispatcherReleaseBaseURL + "/" + dispatcherLegacyReleaseTagPrefix + pin.ProjectRunnerVersion + "/" + assetName + } + return dispatcherReleaseAssetURL(pin.ProjectRunnerVersion, assetName) +} + func downloadDispatcherFile(ctx context.Context, url string, destinationPath string) error { request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -262,18 +301,23 @@ func verifyDispatcherChecksum(assetPath string, checksumPath string) error { } func extractDispatcherRealCLI(archivePath string, assetName string, destinationPath string, goos string) error { + return extractDispatcherRealCLIForPin(archivePath, assetName, destinationPath, goos, dispatcherPin{}) +} + +func extractDispatcherRealCLIForPin(archivePath string, assetName string, destinationPath string, goos string, pin dispatcherPin) error { if strings.HasSuffix(assetName, ".zip") { - return extractDispatcherRealCLIFromZip(archivePath, destinationPath, goos) + return extractDispatcherRealCLIFromZipForPin(archivePath, destinationPath, goos, pin) } - return extractDispatcherRealCLIFromTarGz(archivePath, destinationPath, goos) + return extractDispatcherRealCLIFromTarGzForPin(archivePath, destinationPath, goos, pin) } -func extractDispatcherRealCLIFromTarGz(archivePath string, destinationPath string, goos string) error { - found, err := extractDispatcherCLIFromTarGzEntry(archivePath, destinationPath, dispatcherRealCLIFileName(goos)) +func extractDispatcherRealCLIFromTarGzForPin(archivePath string, destinationPath string, goos string, pin dispatcherPin) error { + entryFileName := dispatcherArchiveCLIFileName(goos, pin) + found, err := extractDispatcherCLIFromTarGzEntry(archivePath, destinationPath, entryFileName) if err != nil || found { return err } - return fmt.Errorf("archive does not contain %s", dispatcherRealCLIFileName(goos)) + return fmt.Errorf("archive does not contain %s", entryFileName) } func extractDispatcherCLIFromTarGzEntry(archivePath string, destinationPath string, entryFileName string) (bool, error) { @@ -311,7 +355,7 @@ func extractDispatcherCLIFromTarGzEntry(archivePath string, destinationPath stri return false, nil } -func extractDispatcherRealCLIFromZip(archivePath string, destinationPath string, goos string) error { +func extractDispatcherRealCLIFromZipForPin(archivePath string, destinationPath string, goos string, pin dispatcherPin) error { reader, err := zip.OpenReader(archivePath) if err != nil { return err @@ -319,11 +363,22 @@ func extractDispatcherRealCLIFromZip(archivePath string, destinationPath string, defer func() { _ = reader.Close() }() - found, err := extractDispatcherCLIFromZipEntry(reader, destinationPath, dispatcherRealCLIFileName(goos)) + entryFileName := dispatcherArchiveCLIFileName(goos, pin) + found, err := extractDispatcherCLIFromZipEntry(reader, destinationPath, entryFileName) if err != nil || found { return err } - return fmt.Errorf("archive does not contain %s", dispatcherRealCLIFileName(goos)) + return fmt.Errorf("archive does not contain %s", entryFileName) +} + +func dispatcherArchiveCLIFileName(goos string, pin dispatcherPin) string { + if pin.LegacyRelease { + if goos == "windows" { + return dispatcherLegacyCLIWindowsFileName + } + return dispatcherLegacyCLIUnixFileName + } + return dispatcherRealCLIFileName(goos) } func extractDispatcherCLIFromZipEntry(reader *zip.ReadCloser, destinationPath string, entryFileName string) (bool, error) { diff --git a/cli/internal/cli/dispatcher_pin.go b/cli/internal/cli/dispatcher_pin.go index e0a894a93..fb26839f9 100644 --- a/cli/internal/cli/dispatcher_pin.go +++ b/cli/internal/cli/dispatcher_pin.go @@ -12,6 +12,7 @@ import ( var ( dispatcherMinimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) + dispatcherLegacyMinimumCliVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) dispatcherMinimumVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_DISPATCHER_VERSION\s*=\s*"([^"]+)"`) dispatcherRequiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) dispatcherProjectRunnerVersionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$`) @@ -22,8 +23,10 @@ type dispatcherPin struct { PackageName string `json:"packageName"` PackageVersion string `json:"packageVersion"` ProjectRunnerVersion string `json:"projectRunnerVersion"` + LegacyCliVersion string `json:"cliVersion"` RequiredProtocolVersion int `json:"requiredProtocolVersion"` MinimumDispatcherVersion string `json:"minimumDispatcherVersion"` + LegacyRelease bool `json:"-"` SourcePath string `json:"-"` } @@ -70,19 +73,24 @@ func loadDispatcherPin(projectRoot string) (dispatcherPin, error) { func dispatcherPinCandidatePaths(projectRoot string) []dispatcherPinCandidatePath { paths := []dispatcherPinCandidatePath{ {Path: filepath.Join(projectRoot, dispatcherProjectPinRelativePath), Required: true}, + {Path: filepath.Join(projectRoot, dispatcherLegacyProjectPinRelativePath), Required: true}, {Path: filepath.Join(projectRoot, "Packages", "src", dispatcherPackagePinFileName)}, + {Path: filepath.Join(projectRoot, "Packages", "src", dispatcherLegacyPackagePinFileName)}, {Path: filepath.Join(projectRoot, "Packages", dispatcherUnityPackageName, dispatcherPackagePinFileName)}, - } - packageCachePattern := filepath.Join( - projectRoot, - "Library", - "PackageCache", - dispatcherUnityPackageName+"@*", - dispatcherPackagePinFileName) - matches, err := filepath.Glob(packageCachePattern) - if err == nil { - for _, match := range matches { - paths = append(paths, dispatcherPinCandidatePath{Path: match}) + {Path: filepath.Join(projectRoot, "Packages", dispatcherUnityPackageName, dispatcherLegacyPackagePinFileName)}, + } + for _, packagePinFileName := range []string{dispatcherPackagePinFileName, dispatcherLegacyPackagePinFileName} { + packageCachePattern := filepath.Join( + projectRoot, + "Library", + "PackageCache", + dispatcherUnityPackageName+"@*", + packagePinFileName) + matches, err := filepath.Glob(packageCachePattern) + if err == nil { + for _, match := range matches { + paths = append(paths, dispatcherPinCandidatePath{Path: match}) + } } } return paths @@ -118,6 +126,10 @@ func readDispatcherPin(pinPath string) (dispatcherPin, error) { if err := json.Unmarshal(content, &pin); err != nil { return dispatcherPin{}, fmt.Errorf("failed to parse %s: %w", pinPath, err) } + if strings.TrimSpace(pin.ProjectRunnerVersion) == "" && strings.TrimSpace(pin.LegacyCliVersion) != "" { + pin.ProjectRunnerVersion = pin.LegacyCliVersion + pin.LegacyRelease = true + } pin.ProjectRunnerVersion = normalizeDispatcherVersion(pin.ProjectRunnerVersion) if pin.ProjectRunnerVersion == "" { return dispatcherPin{}, fmt.Errorf("%s does not define projectRunnerVersion", pinPath) @@ -144,7 +156,7 @@ func readDispatcherPinFromCliConstants(constantsPath string) (dispatcherPin, err return dispatcherPin{}, err } text := string(content) - versionMatch := dispatcherMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) + versionMatch, legacyRelease := dispatcherMinimumProjectRunnerVersionMatch(text) if len(versionMatch) != 2 { return dispatcherPin{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION", constantsPath) } @@ -172,10 +184,19 @@ func readDispatcherPinFromCliConstants(constantsPath string) (dispatcherPin, err ProjectRunnerVersion: projectRunnerVersion, RequiredProtocolVersion: protocolVersion, MinimumDispatcherVersion: minimumDispatcherVersion, + LegacyRelease: legacyRelease, SourcePath: constantsPath, }, nil } +func dispatcherMinimumProjectRunnerVersionMatch(text string) ([]string, bool) { + versionMatch := dispatcherMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) + if len(versionMatch) == 2 { + return versionMatch, false + } + return dispatcherLegacyMinimumCliVersionPattern.FindStringSubmatch(text), true +} + func normalizeDispatcherVersion(value string) string { trimmed := strings.TrimSpace(value) if strings.HasPrefix(trimmed, "v") || strings.HasPrefix(trimmed, "V") { diff --git a/cli/internal/cli/dispatcher_test.go b/cli/internal/cli/dispatcher_test.go index ec96b817b..ec5112525 100644 --- a/cli/internal/cli/dispatcher_test.go +++ b/cli/internal/cli/dispatcher_test.go @@ -436,6 +436,67 @@ func TestDownloadDispatcherRealCLIWritesDownloadStatus(t *testing.T) { assertFileContent(t, realCLIPath, "real") } +func TestDownloadDispatcherRealCLIUsesLegacyReleaseAssets(t *testing.T) { + // Verifies legacy project pins download the old cli-v release assets and install them under the runner cache path. + tempDir := t.TempDir() + archivePath := filepath.Join(tempDir, "uloop-cli-darwin-arm64.tar.gz") + writeDispatcherTarGzArchive(t, archivePath, []dispatcherArchiveTestEntry{ + {Name: "uloop-cli", Content: "legacy real"}, + }) + archiveContent, err := os.ReadFile(archivePath) + if err != nil { + t.Fatalf("failed to read archive: %v", err) + } + checksum := sha256.Sum256(archiveContent) + checksumContent := []byte(hex.EncodeToString(checksum[:]) + " " + filepath.Base(archivePath) + "\n") + + previousHTTPClient := dispatcherHTTPClient + defer func() { + dispatcherHTTPClient = previousHTTPClient + }() + requestPaths := []string{} + dispatcherHTTPClient = &http.Client{ + Transport: dispatcherRoundTripFunc(func(request *http.Request) (*http.Response, error) { + requestPaths = append(requestPaths, request.URL.Path) + content := []byte{} + statusCode := http.StatusNotFound + if strings.HasSuffix(request.URL.Path, "/cli-v3.0.0-beta.88/uloop-cli-darwin-arm64.tar.gz") { + content = archiveContent + statusCode = http.StatusOK + } + if strings.HasSuffix(request.URL.Path, "/cli-v3.0.0-beta.88/uloop-cli-darwin-arm64.tar.gz.sha256") { + content = checksumContent + statusCode = http.StatusOK + } + return &http.Response{ + StatusCode: statusCode, + Status: http.StatusText(statusCode), + Body: io.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + var stderr bytes.Buffer + pin := dispatcherPin{ProjectRunnerVersion: "3.0.0-beta.88", LegacyRelease: true} + realCLIPath, err := downloadDispatcherRealCLIForPin( + context.Background(), + t.TempDir(), + pin, + "darwin", + "arm64", + &stderr) + if err != nil { + t.Fatalf("downloadDispatcherRealCLIForPin failed: %v", err) + } + if !strings.Contains(strings.Join(requestPaths, "\n"), "/cli-v3.0.0-beta.88/uloop-cli-darwin-arm64.tar.gz") { + t.Fatalf("legacy asset URL was not requested: %v", requestPaths) + } + if !strings.Contains(realCLIPath, filepath.Join(dispatcherVersionsDirectoryName, "cli-v3.0.0-beta.88")) { + t.Fatalf("legacy cache path should include tag namespace: %s", realCLIPath) + } + assertFileContent(t, realCLIPath, "legacy real") +} + func TestInstallDownloadedDispatcherRealCLIKeepsExistingExecutable(t *testing.T) { // Verifies concurrent downloads do not delete an executable another dispatcher already cached. tempDir := t.TempDir() @@ -476,6 +537,30 @@ func TestLoadDispatcherPinFallsBackToPackagePin(t *testing.T) { } } +func TestLoadDispatcherPinReadsLegacyProjectPin(t *testing.T) { + // Verifies existing v3-beta project pins remain readable after the project runner pin rename. + projectRoot := createDispatcherUnityProject(t) + pinPath := filepath.Join(projectRoot, dispatcherLegacyProjectPinRelativePath) + if err := os.MkdirAll(filepath.Dir(pinPath), 0o755); err != nil { + t.Fatalf("failed to create pin directory: %v", err) + } + content := `{"schemaVersion":1,"packageName":"io.github.hatayama.uloopmcp","packageVersion":"3.0.0-beta.1","cliVersion":"3.0.0-beta.55","requiredProtocolVersion":2,"minimumDispatcherVersion":"3.0.1-beta.2"}` + if err := os.WriteFile(pinPath, []byte(content), 0o644); err != nil { + t.Fatalf("failed to write legacy pin: %v", err) + } + + pin, err := loadDispatcherPin(projectRoot) + if err != nil { + t.Fatalf("loadDispatcherPin failed: %v", err) + } + if pin.ProjectRunnerVersion != "3.0.0-beta.55" { + t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) + } + if !pin.LegacyRelease { + t.Fatal("expected legacy release pin") + } +} + func TestLoadDispatcherPinSkipsInvalidPackageCandidate(t *testing.T) { // Verifies stale package pins do not block a valid PackageCache pin during first startup. projectRoot := createDispatcherUnityProject(t) @@ -577,6 +662,32 @@ public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "1.0.0";` } } +func TestLoadDispatcherPinFallsBackToLegacyCliConstants(t *testing.T) { + // Verifies package constants from before the project runner rename still resolve the pinned release. + projectRoot := createDispatcherUnityProject(t) + constantsPath := filepath.Join(projectRoot, "Packages", "src", "Editor", "Domain", "CliConstants.cs") + if err := os.MkdirAll(filepath.Dir(constantsPath), 0o755); err != nil { + t.Fatalf("failed to create constants directory: %v", err) + } + content := `public const int REQUIRED_CLI_PROTOCOL_VERSION = 3; +public const string MINIMUM_REQUIRED_CLI_VERSION = "3.0.0-beta.56"; +public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "1.0.0";` + if err := os.WriteFile(constantsPath, []byte(content), 0o644); err != nil { + t.Fatalf("failed to write constants: %v", err) + } + + pin, err := loadDispatcherPin(projectRoot) + if err != nil { + t.Fatalf("loadDispatcherPin failed: %v", err) + } + if pin.ProjectRunnerVersion != "3.0.0-beta.56" { + t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) + } + if !pin.LegacyRelease { + t.Fatal("expected legacy release constants") + } +} + func TestLoadDispatcherPinFromCliConstantsNormalizesVersionPrefix(t *testing.T) { // Verifies v-prefixed fallback constants are normalized before dispatcher resolution. projectRoot := createDispatcherUnityProject(t) From 6aed5f25b102b4d318a7a2a1505b127be91a8252 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 00:50:42 +0900 Subject: [PATCH 07/16] Keep legacy project pins during rename Continue publishing cli-pin.json alongside the renamed project-runner pin so existing dispatchers can read the project contract and self-update before switching to the new pin name. --- .uloop/cli-pin.json | 8 +++++ .../Tests/Editor/CliPinSynchronizerTests.cs | 33 +++++++++++++++++++ .../Infrastructure/CLI/CliPinSynchronizer.cs | 16 +++++++-- .../ToolContracts/UnityCliLoopConstants.cs | 1 + Packages/src/cli-pin.json | 8 +++++ Packages/src/cli-pin.json.meta | 7 ++++ 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 .uloop/cli-pin.json create mode 100644 Packages/src/cli-pin.json create mode 100644 Packages/src/cli-pin.json.meta diff --git a/.uloop/cli-pin.json b/.uloop/cli-pin.json new file mode 100644 index 000000000..cc8d48560 --- /dev/null +++ b/.uloop/cli-pin.json @@ -0,0 +1,8 @@ +{ + "cliVersion": "3.0.0-beta.43", + "minimumDispatcherVersion": "3.0.1-beta.6", + "packageName": "io.github.hatayama.uloopmcp", + "packageVersion": "3.0.0-beta.44", + "requiredProtocolVersion": 2, + "schemaVersion": 1 +} diff --git a/Assets/Tests/Editor/CliPinSynchronizerTests.cs b/Assets/Tests/Editor/CliPinSynchronizerTests.cs index 3bc7741cb..abb966593 100644 --- a/Assets/Tests/Editor/CliPinSynchronizerTests.cs +++ b/Assets/Tests/Editor/CliPinSynchronizerTests.cs @@ -79,6 +79,39 @@ public void SyncProjectPinFile_WhenDestinationMatches_ShouldSkipWrite() } } + [Test] + public void SyncProjectPinFile_WhenLegacyPinExists_ShouldCopyLegacyPin() + { + // Tests that existing dispatchers can still bootstrap from the legacy project CLI pin. + string root = CreateTestRoot(); + string packageRoot = Path.Combine(root, "package"); + string projectRoot = Path.Combine(root, "project"); + + try + { + Directory.CreateDirectory(packageRoot); + Directory.CreateDirectory(projectRoot); + File.WriteAllText( + Path.Combine(packageRoot, UnityCliLoopConstants.ULOOP_LEGACY_CLI_PIN_FILE_NAME), + "{\"schemaVersion\":1,\"cliVersion\":\"3.0.0-beta.43\"}"); + + bool changed = CliPinSynchronizer.SyncProjectPinFile(packageRoot, projectRoot); + + Assert.That(changed, Is.True); + Assert.That( + File.ReadAllText( + Path.Combine( + projectRoot, + UnityCliLoopConstants.ULOOP_DIR, + UnityCliLoopConstants.ULOOP_LEGACY_CLI_PIN_FILE_NAME)), + Is.EqualTo("{\"schemaVersion\":1,\"cliVersion\":\"3.0.0-beta.43\"}")); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public void SyncProjectPinFile_WhenPackagePinChanges_ShouldUpdateProjectPin() { diff --git a/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs b/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs index 5ac801ac0..89953091f 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs @@ -48,11 +48,21 @@ internal static bool SyncProjectPinFile(string packageRoot, string projectRoot) return false; } - string sourcePath = Path.Combine(packageRoot, UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); - string destinationPath = Path.Combine( + bool projectRunnerPinChanged = SyncProjectPinFileByName( + packageRoot, projectRoot, - UnityCliLoopConstants.ULOOP_DIR, UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); + bool legacyCliPinChanged = SyncProjectPinFileByName( + packageRoot, + projectRoot, + UnityCliLoopConstants.ULOOP_LEGACY_CLI_PIN_FILE_NAME); + return projectRunnerPinChanged || legacyCliPinChanged; + } + + private static bool SyncProjectPinFileByName(string packageRoot, string projectRoot, string pinFileName) + { + string sourcePath = Path.Combine(packageRoot, pinFileName); + string destinationPath = Path.Combine(projectRoot, UnityCliLoopConstants.ULOOP_DIR, pinFileName); if (!File.Exists(sourcePath)) { diff --git a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs index ff0eaff2a..c31a3e5bd 100644 --- a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs @@ -57,6 +57,7 @@ public static UnityEditor.PackageManager.PackageInfo PackageInfo public const string ULOOP_DIR = ".uloop"; public const string ULOOP_TOOL_SETTINGS_FILE_NAME = "settings.tools.json"; public const string ULOOP_PROJECT_RUNNER_PIN_FILE_NAME = "project-runner-pin.json"; + public const string ULOOP_LEGACY_CLI_PIN_FILE_NAME = "cli-pin.json"; // Command name constants public const string TOOL_NAME_COMPILE = "compile"; diff --git a/Packages/src/cli-pin.json b/Packages/src/cli-pin.json new file mode 100644 index 000000000..cc8d48560 --- /dev/null +++ b/Packages/src/cli-pin.json @@ -0,0 +1,8 @@ +{ + "cliVersion": "3.0.0-beta.43", + "minimumDispatcherVersion": "3.0.1-beta.6", + "packageName": "io.github.hatayama.uloopmcp", + "packageVersion": "3.0.0-beta.44", + "requiredProtocolVersion": 2, + "schemaVersion": 1 +} diff --git a/Packages/src/cli-pin.json.meta b/Packages/src/cli-pin.json.meta new file mode 100644 index 000000000..ba9cd3265 --- /dev/null +++ b/Packages/src/cli-pin.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dff12f569bc6496fbf20a6b550c80e1a +TextScriptImporter: + externalObjects: {} +userData: +assetBundleName: +assetBundleVariant: From 743dffc94ee2c14d7a7d5716f47043e2da735500 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 01:02:33 +0900 Subject: [PATCH 08/16] Document legacy pin tracking during rename Keep the recommended .uloop gitignore pattern aligned with the temporary legacy cli-pin bootstrap path required by existing dispatchers. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f52f3f8ad..89f0da7de 100644 --- a/README.md +++ b/README.md @@ -613,6 +613,7 @@ The `.uloop/` directory at the project root stores CLI cache, tool registry, and | File | Purpose | Git-track? | |------|---------|------------| | `project-runner-pin.json` | Project runner version contract used by the global dispatcher | Yes | +| `cli-pin.json` | Legacy dispatcher bootstrap contract during the project runner pin rename | Yes | | `settings.tools.json` | Per-tool enable/disable preferences | Optional | | `tools.json` | Auto-generated CLI tool registry | No | | `outputs/` | Runtime outputs (test results, screenshots, hierarchy dumps) | No | @@ -623,6 +624,7 @@ The `.uloop/` directory at the project root stores CLI cache, tool registry, and > ```gitignore > **/.uloop/* > !**/.uloop/project-runner-pin.json +> !**/.uloop/cli-pin.json > !**/.uloop/settings.tools.json > ``` > From b4a78d47e9c07f701160f53867f8b3b8e6288844 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 01:50:05 +0900 Subject: [PATCH 09/16] Remove legacy project runner compatibility paths Drop the transitional cli-pin, cliVersion, cli-v, and uloop-cli asset fallbacks so the renamed project runner contract is the only supported path. Keep only the pre-rename base parser needed for this rename PR's release guard comparison. --- .coderabbit.yaml | 2 +- .uloop/cli-pin.json | 8 -- .../Tests/Editor/CliPinSynchronizerTests.cs | 33 ------ .../JsonRpcProcessorCliVersionGateTests.cs | 16 --- .../Tests/Editor/NativeCliInstallerTests.cs | 14 --- Packages/src/Editor/Domain/CliConstants.cs | 1 - .../Infrastructure/Api/JsonRpcProcessor.cs | 8 +- .../Infrastructure/CLI/CliPinSynchronizer.cs | 7 +- .../Infrastructure/CLI/NativeCliInstaller.cs | 4 - .../ToolContracts/UnityCliLoopConstants.cs | 1 - Packages/src/cli-pin.json | 8 -- Packages/src/cli-pin.json.meta | 7 -- README.md | 2 - .../protocol_minimum_version_guard.go | 67 ++++------- .../protocol_minimum_version_guard_test.go | 78 ++---------- cli/internal/cli/dispatcher.go | 31 ++--- cli/internal/cli/dispatcher_download.go | 66 ++--------- cli/internal/cli/dispatcher_pin.go | 45 ++----- cli/internal/cli/dispatcher_test.go | 111 ------------------ cli/internal/cli/update_test.go | 33 ------ cli/internal/update/command.go | 3 - cli/internal/update/installer.go | 1 - 22 files changed, 68 insertions(+), 478 deletions(-) delete mode 100644 .uloop/cli-pin.json delete mode 100644 Packages/src/cli-pin.json delete mode 100644 Packages/src/cli-pin.json.meta diff --git a/.coderabbit.yaml b/.coderabbit.yaml index f56d75abf..256439954 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -39,6 +39,6 @@ reviews: The compatibility gate is an exact protocol match, not a release-semver range. If a change makes a CLI/package from the previous protocol generation unable to interoperate, require both `cli/contract.json` `protocolVersion` and `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION` to be incremented in the same PR. - Do not request a protocol bump for ordinary CLI features, bug fixes, UI changes, documentation, or additive wire-format changes that older counterparts can safely ignore. `cliVersion` and `default-tools.json` `version` are release-please managed release metadata. If a PR bumps `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION`, require a matching update to `CliConstants.MINIMUM_REQUIRED_CLI_VERSION` after the corresponding published CLI release tag is available, because setup must install a CLI release that advertises the required protocol. + Do not request a protocol bump for ordinary CLI features, bug fixes, UI changes, documentation, or additive wire-format changes that older counterparts can safely ignore. `projectRunnerVersion` and `default-tools.json` `version` are release-please managed release metadata. If a PR bumps `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION`, require a matching update to `CliConstants.MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` after the corresponding published project runner release tag is available, because setup must install a project runner release that advertises the required protocol. chat: auto_reply: true diff --git a/.uloop/cli-pin.json b/.uloop/cli-pin.json deleted file mode 100644 index cc8d48560..000000000 --- a/.uloop/cli-pin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "cliVersion": "3.0.0-beta.43", - "minimumDispatcherVersion": "3.0.1-beta.6", - "packageName": "io.github.hatayama.uloopmcp", - "packageVersion": "3.0.0-beta.44", - "requiredProtocolVersion": 2, - "schemaVersion": 1 -} diff --git a/Assets/Tests/Editor/CliPinSynchronizerTests.cs b/Assets/Tests/Editor/CliPinSynchronizerTests.cs index abb966593..3bc7741cb 100644 --- a/Assets/Tests/Editor/CliPinSynchronizerTests.cs +++ b/Assets/Tests/Editor/CliPinSynchronizerTests.cs @@ -79,39 +79,6 @@ public void SyncProjectPinFile_WhenDestinationMatches_ShouldSkipWrite() } } - [Test] - public void SyncProjectPinFile_WhenLegacyPinExists_ShouldCopyLegacyPin() - { - // Tests that existing dispatchers can still bootstrap from the legacy project CLI pin. - string root = CreateTestRoot(); - string packageRoot = Path.Combine(root, "package"); - string projectRoot = Path.Combine(root, "project"); - - try - { - Directory.CreateDirectory(packageRoot); - Directory.CreateDirectory(projectRoot); - File.WriteAllText( - Path.Combine(packageRoot, UnityCliLoopConstants.ULOOP_LEGACY_CLI_PIN_FILE_NAME), - "{\"schemaVersion\":1,\"cliVersion\":\"3.0.0-beta.43\"}"); - - bool changed = CliPinSynchronizer.SyncProjectPinFile(packageRoot, projectRoot); - - Assert.That(changed, Is.True); - Assert.That( - File.ReadAllText( - Path.Combine( - projectRoot, - UnityCliLoopConstants.ULOOP_DIR, - UnityCliLoopConstants.ULOOP_LEGACY_CLI_PIN_FILE_NAME)), - Is.EqualTo("{\"schemaVersion\":1,\"cliVersion\":\"3.0.0-beta.43\"}")); - } - finally - { - Directory.Delete(root, recursive: true); - } - } - [Test] public void SyncProjectPinFile_WhenPackagePinChanges_ShouldUpdateProjectPin() { diff --git a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs index b319b7736..a390fe97a 100644 --- a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs +++ b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs @@ -122,22 +122,6 @@ public async Task ProcessRequest_WhenCliMetadataIsMissing_ReturnsCliUpdateRequir Assert.That(data["updateCommand"]?.ToString(), Is.EqualTo(ExpectedCliUpdateCommand())); } - [Test] - public async Task ProcessRequest_WhenClientSendsOnlySemverVersion_ReturnsCliUpdateRequiredError() - { - // Verifies CLIs released before the protocol handshake are treated as outdated. - string response = await JsonRpcProcessor.ProcessRequest( - "{\"jsonrpc\":\"2.0\",\"method\":\"get-version\",\"params\":{},\"id\":1," + - "\"uloop\":{\"cliVersion\":\"3.0.0-beta.24\"}}", - CancellationToken.None); - JObject data = ParseErrorData(response); - - Assert.That(data["type"]?.ToString(), Is.EqualTo("cli_update_required")); - Assert.That(data["currentProtocolVersion"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(data["currentCliVersion"]?.ToString(), Is.EqualTo("3.0.0-beta.24")); - Assert.That(data["updateCommand"]?.ToString(), Is.EqualTo(ExpectedCliUpdateCommand())); - } - [Test] public async Task ProcessRequest_WhenProtocolVersionIsNotAnInteger_ReturnsCliUpdateRequiredError() { diff --git a/Assets/Tests/Editor/NativeCliInstallerTests.cs b/Assets/Tests/Editor/NativeCliInstallerTests.cs index 12ea75b5e..5ddea6fc8 100644 --- a/Assets/Tests/Editor/NativeCliInstallerTests.cs +++ b/Assets/Tests/Editor/NativeCliInstallerTests.cs @@ -135,20 +135,6 @@ public void GetInstallCommand_WhenVPrefixedVersionUsesDispatcherReleaseTag() Does.Contain("https://raw.githubusercontent.com/hatayama/unity-cli-loop/dispatcher-v3.0.0/scripts/install.sh")); } - [Test] - public void GetInstallCommand_WhenCliPrefixedVersionUsesDispatcherReleaseTag() - { - // Verifies legacy CLI release tags are normalized before selecting dispatcher installer assets. - NativeCliInstallCommand command = NativeCliInstaller.BuildInstallCommand( - RuntimePlatform.OSXEditor, - "cli-v3.0.0-beta.40", - false, - "/bin/zsh"); - - Assert.That(command.Arguments, Does.Contain("dispatcher-v3.0.0-beta.40")); - Assert.That(command.Arguments, Does.Not.Contain("dispatcher-vcli-v3.0.0-beta.40")); - } - [Test] public void GetInstallCommand_WhenLocalPackageOnMacUsesPackageLocalInstaller() { diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index be3b646bd..9bd8ade31 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -41,7 +41,6 @@ public static class CliConstants public const string WINDOWS_PATH_SEPARATOR = ";"; public const string RELEASE_TAG_PREFIX = "v"; public const string PROJECT_RUNNER_RELEASE_TAG_PREFIX = "uloop-project-runner-v"; - public const string LEGACY_CLI_RELEASE_TAG_PREFIX = "cli-v"; public const string DISPATCHER_RELEASE_TAG_PREFIX = "dispatcher-v"; public const string BETA_VERSION_MARKER = "-beta."; public const string SKILL_DIR_PREFIX = "uloop-"; diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs index a945d9462..e768fb693 100644 --- a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs @@ -106,13 +106,7 @@ private static string ReadClientProjectRunnerVersion(JObject request) } string projectRunnerVersion = metadata["projectRunnerVersion"]?.ToString(); - if (!string.IsNullOrWhiteSpace(projectRunnerVersion)) - { - return projectRunnerVersion; - } - - string legacyCliVersion = metadata["cliVersion"]?.ToString(); - return string.IsNullOrWhiteSpace(legacyCliVersion) ? null : legacyCliVersion; + return string.IsNullOrWhiteSpace(projectRunnerVersion) ? null : projectRunnerVersion; } private static int? ReadClientProtocolVersion(JObject request) diff --git a/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs b/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs index 89953091f..e65506f4a 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliPinSynchronizer.cs @@ -48,15 +48,10 @@ internal static bool SyncProjectPinFile(string packageRoot, string projectRoot) return false; } - bool projectRunnerPinChanged = SyncProjectPinFileByName( + return SyncProjectPinFileByName( packageRoot, projectRoot, UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); - bool legacyCliPinChanged = SyncProjectPinFileByName( - packageRoot, - projectRoot, - UnityCliLoopConstants.ULOOP_LEGACY_CLI_PIN_FILE_NAME); - return projectRunnerPinChanged || legacyCliPinChanged; } private static bool SyncProjectPinFileByName(string packageRoot, string projectRoot, string pinFileName) diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs index f76ace1be..6698b31f6 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs @@ -962,10 +962,6 @@ private static string BuildReleaseTag(string cliReleaseTag) { return $"{CliConstants.DISPATCHER_RELEASE_TAG_PREFIX}{cliReleaseTag.Substring(CliConstants.PROJECT_RUNNER_RELEASE_TAG_PREFIX.Length)}"; } - if (cliReleaseTag.StartsWith(CliConstants.LEGACY_CLI_RELEASE_TAG_PREFIX, StringComparison.Ordinal)) - { - return $"{CliConstants.DISPATCHER_RELEASE_TAG_PREFIX}{cliReleaseTag.Substring(CliConstants.LEGACY_CLI_RELEASE_TAG_PREFIX.Length)}"; - } if (cliReleaseTag.StartsWith(CliConstants.RELEASE_TAG_PREFIX, StringComparison.Ordinal)) { return $"{CliConstants.DISPATCHER_RELEASE_TAG_PREFIX}{cliReleaseTag.Substring(CliConstants.RELEASE_TAG_PREFIX.Length)}"; diff --git a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs index c31a3e5bd..ff0eaff2a 100644 --- a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs @@ -57,7 +57,6 @@ public static UnityEditor.PackageManager.PackageInfo PackageInfo public const string ULOOP_DIR = ".uloop"; public const string ULOOP_TOOL_SETTINGS_FILE_NAME = "settings.tools.json"; public const string ULOOP_PROJECT_RUNNER_PIN_FILE_NAME = "project-runner-pin.json"; - public const string ULOOP_LEGACY_CLI_PIN_FILE_NAME = "cli-pin.json"; // Command name constants public const string TOOL_NAME_COMPILE = "compile"; diff --git a/Packages/src/cli-pin.json b/Packages/src/cli-pin.json deleted file mode 100644 index cc8d48560..000000000 --- a/Packages/src/cli-pin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "cliVersion": "3.0.0-beta.43", - "minimumDispatcherVersion": "3.0.1-beta.6", - "packageName": "io.github.hatayama.uloopmcp", - "packageVersion": "3.0.0-beta.44", - "requiredProtocolVersion": 2, - "schemaVersion": 1 -} diff --git a/Packages/src/cli-pin.json.meta b/Packages/src/cli-pin.json.meta deleted file mode 100644 index ba9cd3265..000000000 --- a/Packages/src/cli-pin.json.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: dff12f569bc6496fbf20a6b550c80e1a -TextScriptImporter: - externalObjects: {} -userData: -assetBundleName: -assetBundleVariant: diff --git a/README.md b/README.md index 89f0da7de..f52f3f8ad 100644 --- a/README.md +++ b/README.md @@ -613,7 +613,6 @@ The `.uloop/` directory at the project root stores CLI cache, tool registry, and | File | Purpose | Git-track? | |------|---------|------------| | `project-runner-pin.json` | Project runner version contract used by the global dispatcher | Yes | -| `cli-pin.json` | Legacy dispatcher bootstrap contract during the project runner pin rename | Yes | | `settings.tools.json` | Per-tool enable/disable preferences | Optional | | `tools.json` | Auto-generated CLI tool registry | No | | `outputs/` | Runtime outputs (test results, screenshots, hierarchy dumps) | No | @@ -624,7 +623,6 @@ The `.uloop/` directory at the project root stores CLI cache, tool registry, and > ```gitignore > **/.uloop/* > !**/.uloop/project-runner-pin.json -> !**/.uloop/cli-pin.json > !**/.uloop/settings.tools.json > ``` > diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/cli/internal/automation/protocol_minimum_version_guard.go index 16d4a69f8..7a4b42a7a 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/cli/internal/automation/protocol_minimum_version_guard.go @@ -15,17 +15,16 @@ import ( ) const ( - protocolMinimumVersionFile = "Packages/src/Editor/Domain/CliConstants.cs" - protocolMinimumVersionMarker = "" - projectRunnerReleaseTagPrefix = "uloop-project-runner-v" - legacyProjectRunnerReleaseTagPrefix = "cli-v" + protocolMinimumVersionFile = "Packages/src/Editor/Domain/CliConstants.cs" + protocolMinimumVersionMarker = "" + projectRunnerReleaseTagPrefix = "uloop-project-runner-v" ) var ( - requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) - minimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) - legacyMinimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) - requiredMinimumProjectRunnerAssets = []string{ + requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) + minimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) + preRenameMinimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) + requiredMinimumProjectRunnerAssets = []string{ "uloop-project-runner-darwin-amd64.tar.gz", "uloop-project-runner-darwin-amd64.tar.gz.sha256", "uloop-project-runner-darwin-arm64.tar.gz", @@ -33,14 +32,6 @@ var ( "uloop-project-runner-windows-amd64.zip", "uloop-project-runner-windows-amd64.zip.sha256", } - requiredLegacyMinimumProjectRunnerAssets = []string{ - "uloop-cli-darwin-amd64.tar.gz", - "uloop-cli-darwin-amd64.tar.gz.sha256", - "uloop-cli-darwin-arm64.tar.gz", - "uloop-cli-darwin-arm64.tar.gz.sha256", - "uloop-cli-windows-amd64.zip", - "uloop-cli-windows-amd64.zip.sha256", - } ) type minimumProjectRunnerRelease struct { @@ -227,9 +218,9 @@ func parseMinimumProjectRunnerVersion(text string) (string, bool) { return minimumMatches[1], true } - legacyMinimumMatches := legacyMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) - if len(legacyMinimumMatches) == 2 { - return legacyMinimumMatches[1], true + preRenameMinimumMatches := preRenameMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) + if len(preRenameMinimumMatches) == 2 { + return preRenameMinimumMatches[1], true } return "", false } @@ -281,36 +272,18 @@ func verifyMinimumCliReleaseProtocolAtRef( repoRoot string, values ProtocolMinimumVersionValues, ) (string, error) { - releases := minimumProjectRunnerReleases(values.MinimumProjectRunnerVersion) - unavailableReleases := []string{} - for _, release := range releases { - contractContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, release.Tag, "cli/contract.json") - if err != nil { - unavailableReleases = append(unavailableReleases, release.Tag) - continue - } - if err := verifyMinimumProjectRunnerReleaseProtocol(release.Tag, values, []byte(contractContent)); err != nil { - return "", err - } - return verifyMinimumCliReleaseIsPublished(ctx, repoRoot, release) + release := minimumProjectRunnerRelease{ + Tag: projectRunnerReleaseTagPrefix + values.MinimumProjectRunnerVersion, + RequiredAssets: requiredMinimumProjectRunnerAssets, } - - return "", fmt.Errorf( - "project runner release %s does not provide cli/contract.json", - strings.Join(unavailableReleases, " or ")) -} - -func minimumProjectRunnerReleases(version string) []minimumProjectRunnerRelease { - return []minimumProjectRunnerRelease{ - { - Tag: projectRunnerReleaseTagPrefix + version, - RequiredAssets: requiredMinimumProjectRunnerAssets, - }, - { - Tag: legacyProjectRunnerReleaseTagPrefix + version, - RequiredAssets: requiredLegacyMinimumProjectRunnerAssets, - }, + contractContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, release.Tag, "cli/contract.json") + if err != nil { + return "", fmt.Errorf("project runner release %s does not provide cli/contract.json", release.Tag) + } + if err := verifyMinimumProjectRunnerReleaseProtocol(release.Tag, values, []byte(contractContent)); err != nil { + return "", err } + return verifyMinimumCliReleaseIsPublished(ctx, repoRoot, release) } func minimumCliReleaseProtocolFile(ctx context.Context, repoRoot string, ref string) ([]byte, error) { diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/cli/internal/automation/protocol_minimum_version_guard_test.go index 41327b476..e31fbe178 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/internal/automation/protocol_minimum_version_guard_test.go @@ -82,10 +82,10 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseMatches_Passes(t *test assertProtocolMinimumVersionLogContains(t, result.gitLog, "uloop-project-runner-v3.0.0-beta.33:cli/contract.json") } -func TestRunProtocolMinimumVersionGuard_WhenBaseUsesLegacyMinimumConstant_Passes(t *testing.T) { +func TestRunProtocolMinimumVersionGuard_WhenBaseUsesPreRenameMinimumConstant_Passes(t *testing.T) { // Verifies rename PRs can compare against base branches that still use the old CLI minimum constant name. result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ - baseContent: buildLegacyProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), + baseContent: buildPreRenameProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), }) @@ -260,48 +260,6 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenRefIsProvided_ReadsValuesAtRef(t assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view uloop-project-runner-v3.0.0-beta.33") } -func TestRunMinimumCliReleaseProtocolCheck_WhenOnlyLegacyReleaseExists_Passes(t *testing.T) { - // Verifies release backfill checks can validate minimum releases published before the project runner tag rename. - workDir := t.TempDir() - mockBin := filepath.Join(workDir, "bin") - err := os.MkdirAll(mockBin, 0o755) - if err != nil { - t.Fatalf("failed to create mock bin: %v", err) - } - - gitLogPath := filepath.Join(workDir, "git.log") - ghLogPath := filepath.Join(workDir, "gh.log") - writeProtocolMinimumVersionMockGit(t, filepath.Join(mockBin, "git")) - writeProtocolMinimumVersionMockGH(t, filepath.Join(mockBin, "gh")) - prepareProtocolMinimumVersionGitContents(t, workDir, protocolMinimumVersionRefCase{ - headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), - releaseContent: `{"schemaVersion":1,"protocolVersion":2,"cliVersion":"3.0.0-beta.40"}`, - releaseContentPrefix: "cli-v", - }) - - t.Setenv("PATH", mockBin+string(os.PathListSeparator)+os.Getenv("PATH")) - t.Setenv("ULOOP_REPOSITORY_ROOT", workDir) - t.Setenv("GIT_LOG", gitLogPath) - t.Setenv("GH_LOG", ghLogPath) - t.Setenv("GH_RELEASE_VIEW", `{"isDraft":false,"assets":[{"name":"uloop-cli-darwin-amd64.tar.gz","size":1},{"name":"uloop-cli-darwin-amd64.tar.gz.sha256","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz","size":1},{"name":"uloop-cli-darwin-arm64.tar.gz.sha256","size":1},{"name":"uloop-cli-windows-amd64.zip","size":1},{"name":"uloop-cli-windows-amd64.zip.sha256","size":1}]}`) - - stdout := bytes.Buffer{} - stderr := bytes.Buffer{} - exitCode := RunMinimumCliReleaseProtocolCheck( - context.Background(), - &stdout, - &stderr, - "protocol-release") - - if exitCode != 0 { - t.Fatalf("expected exit code 0, got %d\nstderr: %s", exitCode, stderr.String()) - } - assertProtocolMinimumVersionLogContains(t, stdout.String(), "Minimum project runner release cli-v3.0.0-beta.40 advertises protocol 2.") - assertProtocolMinimumVersionLogContains(t, readFile(t, gitLogPath), "uloop-project-runner-v3.0.0-beta.40:cli/contract.json") - assertProtocolMinimumVersionLogContains(t, readFile(t, gitLogPath), "cli-v3.0.0-beta.40:cli/contract.json") - assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view cli-v3.0.0-beta.40") -} - func TestRunProtocolMinimumVersionComment_WhenWarningExists_UpsertsComment(t *testing.T) { // Verifies PR comments explain protocol bump installer target omissions. result := runProtocolMinimumVersionCommentCase(t, protocolMinimumVersionCommentCase{ @@ -351,11 +309,10 @@ func TestRunProtocolMinimumVersionComment_WhenMinimumReleaseProtocolDiffers_Upse } type protocolMinimumVersionRefCase struct { - baseContent string - headContent string - releaseContent string - releaseContentPrefix string - releaseView string + baseContent string + headContent string + releaseContent string + releaseView string } type protocolMinimumVersionGuardRunResult struct { @@ -477,9 +434,6 @@ func prepareProtocolMinimumVersionGitContents(t *testing.T, workDir string, test writeFile(t, releaseContentPath, testCase.releaseContent) t.Setenv("GIT_RELEASE_CONTENT", releaseContentPath) } - if testCase.releaseContentPrefix != "" { - t.Setenv("GIT_RELEASE_CONTENT_PREFIX", testCase.releaseContentPrefix) - } t.Setenv("GIT_BASE_CONTENT", baseContentPath) t.Setenv("GIT_HEAD_CONTENT", headContentPath) } @@ -495,7 +449,7 @@ public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "` + minimumProjec }` } -func buildLegacyProtocolMinimumVersionConstants(requiredProtocolVersion int, minimumCliVersion string) string { +func buildPreRenameProtocolMinimumVersionConstants(requiredProtocolVersion int, minimumCliVersion string) string { return `namespace Tests { public static class CliConstants { public const int REQUIRED_CLI_PROTOCOL_VERSION = ` + @@ -530,29 +484,13 @@ fi protocol-release:*) cat "$GIT_HEAD_CONTENT" ;; uloop-project-runner-v*:cli/contract.json) if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then - if [ "${GIT_RELEASE_CONTENT_PREFIX:-uloop-project-runner-v}" != "uloop-project-runner-v" ]; then - echo "release not found" >&2 - exit 1 - fi - cat "$GIT_RELEASE_CONTENT" - else - echo "release not found" >&2 - exit 1 - fi - ;; - cli-v*:cli/contract.json) - if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then - if [ "${GIT_RELEASE_CONTENT_PREFIX:-uloop-project-runner-v}" != "cli-v" ]; then - echo "release not found" >&2 - exit 1 - fi cat "$GIT_RELEASE_CONTENT" else echo "release not found" >&2 exit 1 fi ;; - *) echo "unexpected git show ref: $2" >&2; exit 1 ;; + *) echo "unexpected git show ref: $2" >&2; exit 1 ;; esac exit 0 fi diff --git a/cli/internal/cli/dispatcher.go b/cli/internal/cli/dispatcher.go index fe368d745..d92a507a9 100644 --- a/cli/internal/cli/dispatcher.go +++ b/cli/internal/cli/dispatcher.go @@ -18,24 +18,19 @@ import ( ) const ( - dispatcherCacheDirEnvName = "ULOOP_CACHE_DIR" - dispatcherDisableSelfUpdateEnvName = "ULOOP_DISABLE_SELF_UPDATE" - dispatcherCacheDirectoryName = "uloop" - dispatcherVersionsDirectoryName = "versions" - dispatcherUpdateStateFileName = "dispatcher-update.json" - dispatcherProjectPinRelativePath = ".uloop/project-runner-pin.json" - dispatcherLegacyProjectPinRelativePath = ".uloop/cli-pin.json" - dispatcherPackagePinFileName = "project-runner-pin.json" - dispatcherLegacyPackagePinFileName = "cli-pin.json" - dispatcherUnityPackageName = "io.github.hatayama.uloopmcp" - dispatcherRealCLIUnixFileName = "uloop-project-runner" - dispatcherRealCLIWindowsFileName = "uloop-project-runner.exe" - dispatcherLegacyCLIUnixFileName = "uloop-cli" - dispatcherLegacyCLIWindowsFileName = "uloop-cli.exe" - dispatcherLegacyReleaseTagPrefix = "cli-v" - dispatcherReleaseRepository = "hatayama/unity-cli-loop" - dispatcherReleaseBaseURL = "https://github.com/" + dispatcherReleaseRepository + "/releases/download" - dispatcherSelfUpdateInterval = 24 * time.Hour + dispatcherCacheDirEnvName = "ULOOP_CACHE_DIR" + dispatcherDisableSelfUpdateEnvName = "ULOOP_DISABLE_SELF_UPDATE" + dispatcherCacheDirectoryName = "uloop" + dispatcherVersionsDirectoryName = "versions" + dispatcherUpdateStateFileName = "dispatcher-update.json" + dispatcherProjectPinRelativePath = ".uloop/project-runner-pin.json" + dispatcherPackagePinFileName = "project-runner-pin.json" + dispatcherUnityPackageName = "io.github.hatayama.uloopmcp" + dispatcherRealCLIUnixFileName = "uloop-project-runner" + dispatcherRealCLIWindowsFileName = "uloop-project-runner.exe" + dispatcherReleaseRepository = "hatayama/unity-cli-loop" + dispatcherReleaseBaseURL = "https://github.com/" + dispatcherReleaseRepository + "/releases/download" + dispatcherSelfUpdateInterval = 24 * time.Hour ) var ( diff --git a/cli/internal/cli/dispatcher_download.go b/cli/internal/cli/dispatcher_download.go index 1c3518064..ccdd66451 100644 --- a/cli/internal/cli/dispatcher_download.go +++ b/cli/internal/cli/dispatcher_download.go @@ -36,7 +36,7 @@ func resolveDispatcherRealCLI(ctx context.Context, pin dispatcherPin, stderr io. if err != nil { return "", err } - realCLIPath := dispatcherCachedRealCLIPathForPin(cacheRoot, pin, runtime.GOOS, runtime.GOARCH) + realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, pin.ProjectRunnerVersion, runtime.GOOS, runtime.GOARCH) if isExecutableFile(realCLIPath) { return realCLIPath, nil } @@ -45,9 +45,6 @@ func resolveDispatcherRealCLI(ctx context.Context, pin dispatcherPin, stderr io. } func dispatcherSiblingRealCLIPath(pin dispatcherPin) (string, bool) { - if pin.LegacyRelease { - return "", false - } if pin.ProjectRunnerVersion != version { return "", false } @@ -95,25 +92,14 @@ func dispatcherCacheRoot(goos string) (string, error) { } func dispatcherCachedRealCLIPath(cacheRoot string, projectRunnerVersion string, goos string, goarch string) string { - return dispatcherCachedRealCLIPathForPin(cacheRoot, dispatcherPin{ProjectRunnerVersion: projectRunnerVersion}, goos, goarch) -} - -func dispatcherCachedRealCLIPathForPin(cacheRoot string, pin dispatcherPin, goos string, goarch string) string { return filepath.Join( cacheRoot, dispatcherVersionsDirectoryName, - dispatcherCacheVersionDirectoryName(pin), + projectRunnerVersion, dispatcherPlatformName(goos, goarch), dispatcherRealCLIFileName(goos)) } -func dispatcherCacheVersionDirectoryName(pin dispatcherPin) string { - if pin.LegacyRelease { - return dispatcherLegacyReleaseTagPrefix + pin.ProjectRunnerVersion - } - return pin.ProjectRunnerVersion -} - func dispatcherPlatformName(goos string, goarch string) string { return goos + "-" + goarch } @@ -147,11 +133,11 @@ func downloadDispatcherRealCLI(ctx context.Context, cacheRoot string, projectRun } func downloadDispatcherRealCLIForPin(ctx context.Context, cacheRoot string, pin dispatcherPin, goos string, goarch string, stderr io.Writer) (string, error) { - assetName, err := dispatcherReleaseAssetNameForPin(goos, goarch, pin) + assetName, err := dispatcherReleaseAssetName(goos, goarch) if err != nil { return "", err } - realCLIPath := dispatcherCachedRealCLIPathForPin(cacheRoot, pin, goos, goarch) + realCLIPath := dispatcherCachedRealCLIPath(cacheRoot, pin.ProjectRunnerVersion, goos, goarch) if err := os.MkdirAll(filepath.Dir(realCLIPath), 0o755); err != nil { return "", err } @@ -166,7 +152,7 @@ func downloadDispatcherRealCLIForPin(ctx context.Context, cacheRoot string, pin archivePath := filepath.Join(tempDir, assetName) checksumPath := archivePath + ".sha256" - assetURL := dispatcherReleaseAssetURLForPin(pin, assetName) + assetURL := dispatcherReleaseAssetURL(pin.ProjectRunnerVersion, assetName) writeFormat(stderr, "uloop: downloading pinned project runner %s for %s...\n", pin.ProjectRunnerVersion, dispatcherPlatformName(goos, goarch)) if err := downloadDispatcherFile(ctx, assetURL, archivePath); err != nil { return "", err @@ -179,7 +165,7 @@ func downloadDispatcherRealCLIForPin(ctx context.Context, cacheRoot string, pin } tempRealCLIPath := filepath.Join(tempDir, dispatcherRealCLIFileName(goos)) - if err := extractDispatcherRealCLIForPin(archivePath, assetName, tempRealCLIPath, goos, pin); err != nil { + if err := extractDispatcherRealCLI(archivePath, assetName, tempRealCLIPath, goos); err != nil { return "", err } if err := os.Chmod(tempRealCLIPath, 0o755); err != nil { @@ -208,14 +194,7 @@ func installDownloadedDispatcherRealCLI(tempRealCLIPath string, realCLIPath stri } func dispatcherReleaseAssetName(goos string, goarch string) (string, error) { - return dispatcherReleaseAssetNameForPin(goos, goarch, dispatcherPin{}) -} - -func dispatcherReleaseAssetNameForPin(goos string, goarch string, pin dispatcherPin) (string, error) { assetPrefix := "uloop-project-runner" - if pin.LegacyRelease { - assetPrefix = "uloop-cli" - } switch goos { case "darwin": if goarch != "arm64" && goarch != "amd64" { @@ -236,13 +215,6 @@ func dispatcherReleaseAssetURL(projectRunnerVersion string, assetName string) st return dispatcherReleaseBaseURL + "/" + sharedupdate.ProjectRunnerReleaseTag(projectRunnerVersion) + "/" + assetName } -func dispatcherReleaseAssetURLForPin(pin dispatcherPin, assetName string) string { - if pin.LegacyRelease { - return dispatcherReleaseBaseURL + "/" + dispatcherLegacyReleaseTagPrefix + pin.ProjectRunnerVersion + "/" + assetName - } - return dispatcherReleaseAssetURL(pin.ProjectRunnerVersion, assetName) -} - func downloadDispatcherFile(ctx context.Context, url string, destinationPath string) error { request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -301,18 +273,14 @@ func verifyDispatcherChecksum(assetPath string, checksumPath string) error { } func extractDispatcherRealCLI(archivePath string, assetName string, destinationPath string, goos string) error { - return extractDispatcherRealCLIForPin(archivePath, assetName, destinationPath, goos, dispatcherPin{}) -} - -func extractDispatcherRealCLIForPin(archivePath string, assetName string, destinationPath string, goos string, pin dispatcherPin) error { if strings.HasSuffix(assetName, ".zip") { - return extractDispatcherRealCLIFromZipForPin(archivePath, destinationPath, goos, pin) + return extractDispatcherRealCLIFromZip(archivePath, destinationPath, goos) } - return extractDispatcherRealCLIFromTarGzForPin(archivePath, destinationPath, goos, pin) + return extractDispatcherRealCLIFromTarGz(archivePath, destinationPath, goos) } -func extractDispatcherRealCLIFromTarGzForPin(archivePath string, destinationPath string, goos string, pin dispatcherPin) error { - entryFileName := dispatcherArchiveCLIFileName(goos, pin) +func extractDispatcherRealCLIFromTarGz(archivePath string, destinationPath string, goos string) error { + entryFileName := dispatcherRealCLIFileName(goos) found, err := extractDispatcherCLIFromTarGzEntry(archivePath, destinationPath, entryFileName) if err != nil || found { return err @@ -355,7 +323,7 @@ func extractDispatcherCLIFromTarGzEntry(archivePath string, destinationPath stri return false, nil } -func extractDispatcherRealCLIFromZipForPin(archivePath string, destinationPath string, goos string, pin dispatcherPin) error { +func extractDispatcherRealCLIFromZip(archivePath string, destinationPath string, goos string) error { reader, err := zip.OpenReader(archivePath) if err != nil { return err @@ -363,7 +331,7 @@ func extractDispatcherRealCLIFromZipForPin(archivePath string, destinationPath s defer func() { _ = reader.Close() }() - entryFileName := dispatcherArchiveCLIFileName(goos, pin) + entryFileName := dispatcherRealCLIFileName(goos) found, err := extractDispatcherCLIFromZipEntry(reader, destinationPath, entryFileName) if err != nil || found { return err @@ -371,16 +339,6 @@ func extractDispatcherRealCLIFromZipForPin(archivePath string, destinationPath s return fmt.Errorf("archive does not contain %s", entryFileName) } -func dispatcherArchiveCLIFileName(goos string, pin dispatcherPin) string { - if pin.LegacyRelease { - if goos == "windows" { - return dispatcherLegacyCLIWindowsFileName - } - return dispatcherLegacyCLIUnixFileName - } - return dispatcherRealCLIFileName(goos) -} - func extractDispatcherCLIFromZipEntry(reader *zip.ReadCloser, destinationPath string, entryFileName string) (bool, error) { for _, entry := range reader.File { if entry.FileInfo().IsDir() || !dispatcherArchiveEntryMatchesFileName(entry.Name, entryFileName) { diff --git a/cli/internal/cli/dispatcher_pin.go b/cli/internal/cli/dispatcher_pin.go index fb26839f9..e0a894a93 100644 --- a/cli/internal/cli/dispatcher_pin.go +++ b/cli/internal/cli/dispatcher_pin.go @@ -12,7 +12,6 @@ import ( var ( dispatcherMinimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) - dispatcherLegacyMinimumCliVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) dispatcherMinimumVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_DISPATCHER_VERSION\s*=\s*"([^"]+)"`) dispatcherRequiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) dispatcherProjectRunnerVersionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$`) @@ -23,10 +22,8 @@ type dispatcherPin struct { PackageName string `json:"packageName"` PackageVersion string `json:"packageVersion"` ProjectRunnerVersion string `json:"projectRunnerVersion"` - LegacyCliVersion string `json:"cliVersion"` RequiredProtocolVersion int `json:"requiredProtocolVersion"` MinimumDispatcherVersion string `json:"minimumDispatcherVersion"` - LegacyRelease bool `json:"-"` SourcePath string `json:"-"` } @@ -73,24 +70,19 @@ func loadDispatcherPin(projectRoot string) (dispatcherPin, error) { func dispatcherPinCandidatePaths(projectRoot string) []dispatcherPinCandidatePath { paths := []dispatcherPinCandidatePath{ {Path: filepath.Join(projectRoot, dispatcherProjectPinRelativePath), Required: true}, - {Path: filepath.Join(projectRoot, dispatcherLegacyProjectPinRelativePath), Required: true}, {Path: filepath.Join(projectRoot, "Packages", "src", dispatcherPackagePinFileName)}, - {Path: filepath.Join(projectRoot, "Packages", "src", dispatcherLegacyPackagePinFileName)}, {Path: filepath.Join(projectRoot, "Packages", dispatcherUnityPackageName, dispatcherPackagePinFileName)}, - {Path: filepath.Join(projectRoot, "Packages", dispatcherUnityPackageName, dispatcherLegacyPackagePinFileName)}, - } - for _, packagePinFileName := range []string{dispatcherPackagePinFileName, dispatcherLegacyPackagePinFileName} { - packageCachePattern := filepath.Join( - projectRoot, - "Library", - "PackageCache", - dispatcherUnityPackageName+"@*", - packagePinFileName) - matches, err := filepath.Glob(packageCachePattern) - if err == nil { - for _, match := range matches { - paths = append(paths, dispatcherPinCandidatePath{Path: match}) - } + } + packageCachePattern := filepath.Join( + projectRoot, + "Library", + "PackageCache", + dispatcherUnityPackageName+"@*", + dispatcherPackagePinFileName) + matches, err := filepath.Glob(packageCachePattern) + if err == nil { + for _, match := range matches { + paths = append(paths, dispatcherPinCandidatePath{Path: match}) } } return paths @@ -126,10 +118,6 @@ func readDispatcherPin(pinPath string) (dispatcherPin, error) { if err := json.Unmarshal(content, &pin); err != nil { return dispatcherPin{}, fmt.Errorf("failed to parse %s: %w", pinPath, err) } - if strings.TrimSpace(pin.ProjectRunnerVersion) == "" && strings.TrimSpace(pin.LegacyCliVersion) != "" { - pin.ProjectRunnerVersion = pin.LegacyCliVersion - pin.LegacyRelease = true - } pin.ProjectRunnerVersion = normalizeDispatcherVersion(pin.ProjectRunnerVersion) if pin.ProjectRunnerVersion == "" { return dispatcherPin{}, fmt.Errorf("%s does not define projectRunnerVersion", pinPath) @@ -156,7 +144,7 @@ func readDispatcherPinFromCliConstants(constantsPath string) (dispatcherPin, err return dispatcherPin{}, err } text := string(content) - versionMatch, legacyRelease := dispatcherMinimumProjectRunnerVersionMatch(text) + versionMatch := dispatcherMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) if len(versionMatch) != 2 { return dispatcherPin{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION", constantsPath) } @@ -184,19 +172,10 @@ func readDispatcherPinFromCliConstants(constantsPath string) (dispatcherPin, err ProjectRunnerVersion: projectRunnerVersion, RequiredProtocolVersion: protocolVersion, MinimumDispatcherVersion: minimumDispatcherVersion, - LegacyRelease: legacyRelease, SourcePath: constantsPath, }, nil } -func dispatcherMinimumProjectRunnerVersionMatch(text string) ([]string, bool) { - versionMatch := dispatcherMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) - if len(versionMatch) == 2 { - return versionMatch, false - } - return dispatcherLegacyMinimumCliVersionPattern.FindStringSubmatch(text), true -} - func normalizeDispatcherVersion(value string) string { trimmed := strings.TrimSpace(value) if strings.HasPrefix(trimmed, "v") || strings.HasPrefix(trimmed, "V") { diff --git a/cli/internal/cli/dispatcher_test.go b/cli/internal/cli/dispatcher_test.go index ec5112525..ec96b817b 100644 --- a/cli/internal/cli/dispatcher_test.go +++ b/cli/internal/cli/dispatcher_test.go @@ -436,67 +436,6 @@ func TestDownloadDispatcherRealCLIWritesDownloadStatus(t *testing.T) { assertFileContent(t, realCLIPath, "real") } -func TestDownloadDispatcherRealCLIUsesLegacyReleaseAssets(t *testing.T) { - // Verifies legacy project pins download the old cli-v release assets and install them under the runner cache path. - tempDir := t.TempDir() - archivePath := filepath.Join(tempDir, "uloop-cli-darwin-arm64.tar.gz") - writeDispatcherTarGzArchive(t, archivePath, []dispatcherArchiveTestEntry{ - {Name: "uloop-cli", Content: "legacy real"}, - }) - archiveContent, err := os.ReadFile(archivePath) - if err != nil { - t.Fatalf("failed to read archive: %v", err) - } - checksum := sha256.Sum256(archiveContent) - checksumContent := []byte(hex.EncodeToString(checksum[:]) + " " + filepath.Base(archivePath) + "\n") - - previousHTTPClient := dispatcherHTTPClient - defer func() { - dispatcherHTTPClient = previousHTTPClient - }() - requestPaths := []string{} - dispatcherHTTPClient = &http.Client{ - Transport: dispatcherRoundTripFunc(func(request *http.Request) (*http.Response, error) { - requestPaths = append(requestPaths, request.URL.Path) - content := []byte{} - statusCode := http.StatusNotFound - if strings.HasSuffix(request.URL.Path, "/cli-v3.0.0-beta.88/uloop-cli-darwin-arm64.tar.gz") { - content = archiveContent - statusCode = http.StatusOK - } - if strings.HasSuffix(request.URL.Path, "/cli-v3.0.0-beta.88/uloop-cli-darwin-arm64.tar.gz.sha256") { - content = checksumContent - statusCode = http.StatusOK - } - return &http.Response{ - StatusCode: statusCode, - Status: http.StatusText(statusCode), - Body: io.NopCloser(bytes.NewReader(content)), - }, nil - }), - } - - var stderr bytes.Buffer - pin := dispatcherPin{ProjectRunnerVersion: "3.0.0-beta.88", LegacyRelease: true} - realCLIPath, err := downloadDispatcherRealCLIForPin( - context.Background(), - t.TempDir(), - pin, - "darwin", - "arm64", - &stderr) - if err != nil { - t.Fatalf("downloadDispatcherRealCLIForPin failed: %v", err) - } - if !strings.Contains(strings.Join(requestPaths, "\n"), "/cli-v3.0.0-beta.88/uloop-cli-darwin-arm64.tar.gz") { - t.Fatalf("legacy asset URL was not requested: %v", requestPaths) - } - if !strings.Contains(realCLIPath, filepath.Join(dispatcherVersionsDirectoryName, "cli-v3.0.0-beta.88")) { - t.Fatalf("legacy cache path should include tag namespace: %s", realCLIPath) - } - assertFileContent(t, realCLIPath, "legacy real") -} - func TestInstallDownloadedDispatcherRealCLIKeepsExistingExecutable(t *testing.T) { // Verifies concurrent downloads do not delete an executable another dispatcher already cached. tempDir := t.TempDir() @@ -537,30 +476,6 @@ func TestLoadDispatcherPinFallsBackToPackagePin(t *testing.T) { } } -func TestLoadDispatcherPinReadsLegacyProjectPin(t *testing.T) { - // Verifies existing v3-beta project pins remain readable after the project runner pin rename. - projectRoot := createDispatcherUnityProject(t) - pinPath := filepath.Join(projectRoot, dispatcherLegacyProjectPinRelativePath) - if err := os.MkdirAll(filepath.Dir(pinPath), 0o755); err != nil { - t.Fatalf("failed to create pin directory: %v", err) - } - content := `{"schemaVersion":1,"packageName":"io.github.hatayama.uloopmcp","packageVersion":"3.0.0-beta.1","cliVersion":"3.0.0-beta.55","requiredProtocolVersion":2,"minimumDispatcherVersion":"3.0.1-beta.2"}` - if err := os.WriteFile(pinPath, []byte(content), 0o644); err != nil { - t.Fatalf("failed to write legacy pin: %v", err) - } - - pin, err := loadDispatcherPin(projectRoot) - if err != nil { - t.Fatalf("loadDispatcherPin failed: %v", err) - } - if pin.ProjectRunnerVersion != "3.0.0-beta.55" { - t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) - } - if !pin.LegacyRelease { - t.Fatal("expected legacy release pin") - } -} - func TestLoadDispatcherPinSkipsInvalidPackageCandidate(t *testing.T) { // Verifies stale package pins do not block a valid PackageCache pin during first startup. projectRoot := createDispatcherUnityProject(t) @@ -662,32 +577,6 @@ public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "1.0.0";` } } -func TestLoadDispatcherPinFallsBackToLegacyCliConstants(t *testing.T) { - // Verifies package constants from before the project runner rename still resolve the pinned release. - projectRoot := createDispatcherUnityProject(t) - constantsPath := filepath.Join(projectRoot, "Packages", "src", "Editor", "Domain", "CliConstants.cs") - if err := os.MkdirAll(filepath.Dir(constantsPath), 0o755); err != nil { - t.Fatalf("failed to create constants directory: %v", err) - } - content := `public const int REQUIRED_CLI_PROTOCOL_VERSION = 3; -public const string MINIMUM_REQUIRED_CLI_VERSION = "3.0.0-beta.56"; -public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "1.0.0";` - if err := os.WriteFile(constantsPath, []byte(content), 0o644); err != nil { - t.Fatalf("failed to write constants: %v", err) - } - - pin, err := loadDispatcherPin(projectRoot) - if err != nil { - t.Fatalf("loadDispatcherPin failed: %v", err) - } - if pin.ProjectRunnerVersion != "3.0.0-beta.56" { - t.Fatalf("projectRunnerVersion mismatch: %s", pin.ProjectRunnerVersion) - } - if !pin.LegacyRelease { - t.Fatal("expected legacy release constants") - } -} - func TestLoadDispatcherPinFromCliConstantsNormalizesVersionPrefix(t *testing.T) { // Verifies v-prefixed fallback constants are normalized before dispatcher resolution. projectRoot := createDispatcherUnityProject(t) diff --git a/cli/internal/cli/update_test.go b/cli/internal/cli/update_test.go index 329ecc8ae..a6ac48177 100644 --- a/cli/internal/cli/update_test.go +++ b/cli/internal/cli/update_test.go @@ -126,27 +126,6 @@ func TestUpdateCommandForDarwinNormalizesProjectRunnerReleaseTag(t *testing.T) { } } -func TestUpdateCommandForDarwinNormalizesLegacyCliReleaseTag(t *testing.T) { - // Verifies legacy CLI release tags resolve to the matching dispatcher release. - commandName, args, err := updateCommandForOSWithOptions("darwin", updateOptions{ - targetVersion: "cli-v3.0.0-beta.6", - }) - if err != nil { - t.Fatalf("updateCommandForOSWithOptions failed: %v", err) - } - - if commandName != "sh" { - t.Fatalf("command mismatch: %s", commandName) - } - joinedArgs := strings.Join(args, " ") - if !strings.Contains(joinedArgs, "dispatcher-v3.0.0-beta.6/scripts/install.sh") { - t.Fatalf("installer URL mismatch: %s", joinedArgs) - } - if strings.Contains(joinedArgs, "dispatcher-vcli-v3.0.0-beta.6") { - t.Fatalf("installer version contains legacy CLI prefix: %s", joinedArgs) - } -} - func TestUpdateCommandForWindowsUsesRequestedVersion(t *testing.T) { // Verifies Windows dispatcher update can target the minimum release version requested by Unity. commandName, args, err := updateCommandForOSWithOptions("windows", updateOptions{ @@ -192,18 +171,6 @@ func TestParseUpdateOptionsNormalizesProjectRunnerReleaseTag(t *testing.T) { } } -func TestParseUpdateOptionsNormalizesLegacyCliReleaseTag(t *testing.T) { - // Verifies parsed legacy CLI release tags are normalized before dispatcher tag selection. - options, err := parseUpdateOptions([]string{"--to-version", "cli-v3.0.0-beta.6"}) - if err != nil { - t.Fatalf("parseUpdateOptions failed: %v", err) - } - - if options.targetVersion != "3.0.0-beta.6" { - t.Fatalf("target version mismatch: %#v", options) - } -} - func TestParseUpdateOptionsAcceptsEqualsSyntax(t *testing.T) { // Verifies AI-readable update commands may use a single --to-version=value token. options, err := parseUpdateOptions([]string{"--to-version=3.0.0-beta.6"}) diff --git a/cli/internal/update/command.go b/cli/internal/update/command.go index b28846a67..3be110930 100644 --- a/cli/internal/update/command.go +++ b/cli/internal/update/command.go @@ -58,9 +58,6 @@ func NormalizeTargetVersion(value string) string { if strings.HasPrefix(lower, projectRunnerReleaseTagPrefix) { return trimmed[len(projectRunnerReleaseTagPrefix):] } - if strings.HasPrefix(lower, legacyCliReleaseTagPrefix) { - return trimmed[len(legacyCliReleaseTagPrefix):] - } if strings.HasPrefix(lower, "v") { return trimmed[1:] } diff --git a/cli/internal/update/installer.go b/cli/internal/update/installer.go index 35d30567e..212e6f9ad 100644 --- a/cli/internal/update/installer.go +++ b/cli/internal/update/installer.go @@ -10,7 +10,6 @@ const ( repositoryRawBaseURL = "https://raw.githubusercontent.com/hatayama/unity-cli-loop" projectRunnerReleaseTagPrefix = "uloop-project-runner-v" - legacyCliReleaseTagPrefix = "cli-v" dispatcherTagPrefix = "dispatcher-v" betaVersionMarker = "-beta." ) From 33f5747865bb527b2f6558f7e0c17ef7c737fa55 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 08:55:09 +0900 Subject: [PATCH 10/16] Tighten project runner rename automation guards Address review feedback by keeping legacy cliVersion metadata out of the dispatcher contract, finishing project runner naming in architecture tests, validating release tag SemVer suffixes, and normalizing v-prefixed minimum runner versions before constructing release tags. --- cli/contract_test.go | 1 + .../architecture/architecture_test.go | 4 +- .../protocol_minimum_version_guard.go | 20 ++++++++-- .../protocol_minimum_version_guard_test.go | 40 +++++++++++++++++++ scripts/resolve-native-cli-release-target.sh | 15 ++++++- .../test-resolve-native-cli-release-target.sh | 9 ++++- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/cli/contract_test.go b/cli/contract_test.go index bc3383f23..6f46a1b7f 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -42,6 +42,7 @@ func TestDispatcherContractDoesNotDeclareCliReleaseFields(t *testing.T) { // Verifies dispatcher releases stay independent from project-local CLI release metadata. fields := requireContractFieldMap(t, dispatcherContractFileName) requireContractFieldMissing(t, fields, "projectRunnerVersion") + requireContractFieldMissing(t, fields, "cliVersion") requireContractFieldMissing(t, fields, "protocolVersion") } diff --git a/cli/internal/architecture/architecture_test.go b/cli/internal/architecture/architecture_test.go index 5484ced9f..ebed4bcfc 100644 --- a/cli/internal/architecture/architecture_test.go +++ b/cli/internal/architecture/architecture_test.go @@ -86,8 +86,8 @@ func TestDispatcherCommandOnlyDependsOnDispatcherEntrypoint(t *testing.T) { assertCommandOnlyDependsOnInternalEntrypoint(t, "./cmd/dispatcher", cliModulePath+"/internal/dispatcher") } -// Tests that the project-local CLI command only enters the project CLI package. -func TestProjectCliCommandOnlyDependsOnProjectCliEntrypoint(t *testing.T) { +// Tests that the project runner command only enters the project runner package. +func TestProjectRunnerCommandOnlyDependsOnProjectRunnerEntrypoint(t *testing.T) { assertCommandOnlyDependsOnInternalEntrypoint(t, "./cmd/project-runner", cliModulePath+"/internal/projectrunner") } diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/cli/internal/automation/protocol_minimum_version_guard.go index 7a4b42a7a..03992acae 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/cli/internal/automation/protocol_minimum_version_guard.go @@ -207,7 +207,7 @@ func ParseProtocolMinimumVersionValues(content []byte) (ProtocolMinimumVersionVa func VerifyMinimumCliReleaseProtocol(values ProtocolMinimumVersionValues, contractContent []byte) error { return verifyMinimumProjectRunnerReleaseProtocol( - projectRunnerReleaseTagPrefix+values.MinimumProjectRunnerVersion, + minimumProjectRunnerReleaseTag(values.MinimumProjectRunnerVersion), values, contractContent) } @@ -215,16 +215,28 @@ func VerifyMinimumCliReleaseProtocol(values ProtocolMinimumVersionValues, contra func parseMinimumProjectRunnerVersion(text string) (string, bool) { minimumMatches := minimumProjectRunnerVersionPattern.FindStringSubmatch(text) if len(minimumMatches) == 2 { - return minimumMatches[1], true + return normalizeProjectRunnerVersion(minimumMatches[1]), true } preRenameMinimumMatches := preRenameMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) if len(preRenameMinimumMatches) == 2 { - return preRenameMinimumMatches[1], true + return normalizeProjectRunnerVersion(preRenameMinimumMatches[1]), true } return "", false } +func minimumProjectRunnerReleaseTag(version string) string { + return projectRunnerReleaseTagPrefix + normalizeProjectRunnerVersion(version) +} + +func normalizeProjectRunnerVersion(version string) string { + trimmedVersion := strings.TrimSpace(version) + if strings.HasPrefix(trimmedVersion, "v") || strings.HasPrefix(trimmedVersion, "V") { + return trimmedVersion[1:] + } + return trimmedVersion +} + func verifyMinimumProjectRunnerReleaseProtocol( releaseTag string, values ProtocolMinimumVersionValues, @@ -273,7 +285,7 @@ func verifyMinimumCliReleaseProtocolAtRef( values ProtocolMinimumVersionValues, ) (string, error) { release := minimumProjectRunnerRelease{ - Tag: projectRunnerReleaseTagPrefix + values.MinimumProjectRunnerVersion, + Tag: minimumProjectRunnerReleaseTag(values.MinimumProjectRunnerVersion), RequiredAssets: requiredMinimumProjectRunnerAssets, } contractContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, release.Tag, "cli/contract.json") diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/cli/internal/automation/protocol_minimum_version_guard_test.go index e31fbe178..09067dd2d 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/internal/automation/protocol_minimum_version_guard_test.go @@ -260,6 +260,46 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenRefIsProvided_ReadsValuesAtRef(t assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view uloop-project-runner-v3.0.0-beta.33") } +func TestRunMinimumCliReleaseProtocolCheck_WhenMinimumVersionHasPrefix_NormalizesReleaseTag(t *testing.T) { + // Verifies v-prefixed package constants map to a project runner tag with one prefix. + workDir := t.TempDir() + mockBin := filepath.Join(workDir, "bin") + err := os.MkdirAll(mockBin, 0o755) + if err != nil { + t.Fatalf("failed to create mock bin: %v", err) + } + + gitLogPath := filepath.Join(workDir, "git.log") + ghLogPath := filepath.Join(workDir, "gh.log") + writeProtocolMinimumVersionMockGit(t, filepath.Join(mockBin, "git")) + writeProtocolMinimumVersionMockGH(t, filepath.Join(mockBin, "gh")) + prepareProtocolMinimumVersionGitContents(t, workDir, protocolMinimumVersionRefCase{ + baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), + headContent: buildProtocolMinimumVersionConstants(2, "v3.0.0-beta.33"), + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, + }) + + t.Setenv("PATH", mockBin+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("ULOOP_REPOSITORY_ROOT", workDir) + t.Setenv("GIT_LOG", gitLogPath) + t.Setenv("GH_LOG", ghLogPath) + + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + exitCode := RunMinimumCliReleaseProtocolCheck( + context.Background(), + &stdout, + &stderr, + "protocol-release") + + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d\nstderr: %s", exitCode, stderr.String()) + } + assertProtocolMinimumVersionLogContains(t, stdout.String(), "Minimum project runner release uloop-project-runner-v3.0.0-beta.33 advertises protocol 2.") + assertProtocolMinimumVersionLogContains(t, readFile(t, gitLogPath), "uloop-project-runner-v3.0.0-beta.33:cli/contract.json") + assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view uloop-project-runner-v3.0.0-beta.33") +} + func TestRunProtocolMinimumVersionComment_WhenWarningExists_UpsertsComment(t *testing.T) { // Verifies PR comments explain protocol bump installer target omissions. result := runProtocolMinimumVersionCommentCase(t, protocolMinimumVersionCommentCase{ diff --git a/scripts/resolve-native-cli-release-target.sh b/scripts/resolve-native-cli-release-target.sh index 4b79123b6..58d34341f 100755 --- a/scripts/resolve-native-cli-release-target.sh +++ b/scripts/resolve-native-cli-release-target.sh @@ -26,6 +26,11 @@ scripts/package-go-cli.sh scripts/verify-native-cli-release-assets.sh " +is_semver_version() { + version=$1 + printf '%s\n' "$version" | grep -Eq '^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?([+][0-9A-Za-z][0-9A-Za-z.-]*)?$' +} + release_json() { release_tag=$1 release_error_file=$(mktemp) @@ -254,7 +259,7 @@ fi RELEASE_TAG="${INPUT_RELEASE_TAG:-uloop-project-runner-v$VERSION}" case "$RELEASE_TAG" in - uloop-project-runner-v[0-9]*) + uloop-project-runner-v*) ;; *) echo "Invalid release tag: $RELEASE_TAG" >&2 @@ -262,8 +267,14 @@ case "$RELEASE_TAG" in ;; esac +RELEASE_TAG_VERSION=${RELEASE_TAG#uloop-project-runner-v} +if ! is_semver_version "$RELEASE_TAG_VERSION"; then + echo "Invalid release tag: $RELEASE_TAG" >&2 + exit 1 +fi + case "$RELEASE_TAG" in - *[!A-Za-z0-9._-]*) + *[!A-Za-z0-9._+-]*) echo "Invalid release tag: $RELEASE_TAG" >&2 exit 1 ;; diff --git a/scripts/test-resolve-native-cli-release-target.sh b/scripts/test-resolve-native-cli-release-target.sh index 8bb4a9e92..d499f666e 100755 --- a/scripts/test-resolve-native-cli-release-target.sh +++ b/scripts/test-resolve-native-cli-release-target.sh @@ -257,6 +257,7 @@ run_failure_case() { branch_name=$4 expected_error=$5 current_release_state=${6:-missing} + input_release_tag=${7:-} work_dir="$TMP_DIR/$name" mock_bin="$work_dir/bin" @@ -284,7 +285,7 @@ run_failure_case() { EVENT_NAME="$event_name" \ EVENT_REF_NAME="$branch_name" \ BEFORE_SHA=before \ - INPUT_RELEASE_TAG= \ + INPUT_RELEASE_TAG="$input_release_tag" \ INPUT_DRY_RUN=false \ "$SCRIPT" > output.txt 2> stderr.txt status=$? @@ -394,6 +395,11 @@ test_release_lookup_error_fails() { run_failure_case release-lookup-error 3.0.0-beta.3 push v3-beta "gh auth failed" error } +# Verifies explicit project runner tags must use a full SemVer suffix. +test_invalid_release_tag_version_fails() { + run_failure_case invalid-release-tag-version 3.0.0-beta.3 push v3-beta "Invalid release tag: uloop-project-runner-v3-beta" missing uloop-project-runner-v3-beta +} + assert_script_contains "cli/contract.json" test_complete_current_release_skips test_package_version_change_without_cli_change_skips @@ -414,3 +420,4 @@ test_recovery_target_uses_exact_version_boundary test_main_prerelease_fails test_v3_beta_stable_fails test_release_lookup_error_fails +test_invalid_release_tag_version_fails From 42cf11580a92fdcd90944311cda6b54fac8b19ba Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 09:01:37 +0900 Subject: [PATCH 11/16] Tighten project runner release tag SemVer validation Reject malformed explicit project runner release tags with numeric prerelease leading zeroes or empty prerelease identifiers so release automation does not accept invalid SemVer input. --- scripts/resolve-native-cli-release-target.sh | 2 +- scripts/test-resolve-native-cli-release-target.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/resolve-native-cli-release-target.sh b/scripts/resolve-native-cli-release-target.sh index 58d34341f..98b3a326d 100755 --- a/scripts/resolve-native-cli-release-target.sh +++ b/scripts/resolve-native-cli-release-target.sh @@ -28,7 +28,7 @@ scripts/verify-native-cli-release-assets.sh is_semver_version() { version=$1 - printf '%s\n' "$version" | grep -Eq '^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?([+][0-9A-Za-z][0-9A-Za-z.-]*)?$' + printf '%s\n' "$version" | grep -Eq '^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)([.](0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*)?([+][0-9A-Za-z-]+([.][0-9A-Za-z-]+)*)?$' } release_json() { diff --git a/scripts/test-resolve-native-cli-release-target.sh b/scripts/test-resolve-native-cli-release-target.sh index d499f666e..0526c23b1 100755 --- a/scripts/test-resolve-native-cli-release-target.sh +++ b/scripts/test-resolve-native-cli-release-target.sh @@ -400,6 +400,16 @@ test_invalid_release_tag_version_fails() { run_failure_case invalid-release-tag-version 3.0.0-beta.3 push v3-beta "Invalid release tag: uloop-project-runner-v3-beta" missing uloop-project-runner-v3-beta } +# Verifies numeric prerelease identifiers cannot have leading zeroes. +test_invalid_release_tag_numeric_prerelease_fails() { + run_failure_case invalid-release-tag-numeric-prerelease 3.0.0-beta.3 push v3-beta "Invalid release tag: uloop-project-runner-v3.0.0-01" missing uloop-project-runner-v3.0.0-01 +} + +# Verifies prerelease identifiers cannot be empty. +test_invalid_release_tag_empty_prerelease_identifier_fails() { + run_failure_case invalid-release-tag-empty-prerelease 3.0.0-beta.3 push v3-beta "Invalid release tag: uloop-project-runner-v3.0.0-alpha..1" missing uloop-project-runner-v3.0.0-alpha..1 +} + assert_script_contains "cli/contract.json" test_complete_current_release_skips test_package_version_change_without_cli_change_skips @@ -421,3 +431,5 @@ test_main_prerelease_fails test_v3_beta_stable_fails test_release_lookup_error_fails test_invalid_release_tag_version_fails +test_invalid_release_tag_numeric_prerelease_fails +test_invalid_release_tag_empty_prerelease_identifier_fails From b00e26d2454716c681d5f57df46f8cb31f599dcb Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 09:07:24 +0900 Subject: [PATCH 12/16] Validate minimum project runner versions in protocol guard Reject invalid minimum project runner versions before constructing release tags so protocol minimum checks follow the same SemVer expectations as project runner release automation. --- .../automation/protocol_minimum_version_guard.go | 5 +++++ .../protocol_minimum_version_guard_test.go | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/cli/internal/automation/protocol_minimum_version_guard.go index 03992acae..e6a09496f 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/cli/internal/automation/protocol_minimum_version_guard.go @@ -12,6 +12,8 @@ import ( "regexp" "strconv" "strings" + + sharedversion "github.com/hatayama/unity-cli-loop/cli/internal/version" ) const ( @@ -201,6 +203,9 @@ func ParseProtocolMinimumVersionValues(content []byte) (ProtocolMinimumVersionVa if !ok { return ProtocolMinimumVersionValues{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION", protocolMinimumVersionFile) } + if _, ok := sharedversion.Compare(minimumProjectRunnerVersion, minimumProjectRunnerVersion); !ok { + return ProtocolMinimumVersionValues{}, fmt.Errorf("%s MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION must be semver, got %q", protocolMinimumVersionFile, minimumProjectRunnerVersion) + } values.MinimumProjectRunnerVersion = minimumProjectRunnerVersion return values, nil } diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/cli/internal/automation/protocol_minimum_version_guard_test.go index 09067dd2d..68067a15f 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/internal/automation/protocol_minimum_version_guard_test.go @@ -95,6 +95,18 @@ func TestRunProtocolMinimumVersionGuard_WhenBaseUsesPreRenameMinimumConstant_Pas assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") } +func TestParseProtocolMinimumVersionValues_WhenMinimumVersionIsInvalid_Fails(t *testing.T) { + // Verifies invalid minimum project runner versions fail before release tag construction. + _, err := ParseProtocolMinimumVersionValues([]byte(buildProtocolMinimumVersionConstants(2, "3.0.0-01"))) + + if err == nil { + t.Fatal("expected invalid minimum project runner version to fail") + } + if !strings.Contains(err.Error(), "MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION must be semver") { + t.Fatalf("expected semver error, got %v", err) + } +} + func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseProtocolDiffers_Fails(t *testing.T) { // Verifies changing the minimum version text is not enough when the release uses the old protocol. result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ From e36d256b29f358a546f9693e6a98cb5b30f76917 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 09:23:15 +0900 Subject: [PATCH 13/16] Require project runner minimum version constants Keep legacy minimum-version parsing limited to base-ref comparison so rename PRs can still compare against v3-beta while current package constants must use MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION. --- .../protocol_minimum_version_guard.go | 76 +++++------------- .../protocol_minimum_version_guard_test.go | 12 +++ .../protocol_minimum_version_parse.go | 77 +++++++++++++++++++ 3 files changed, 110 insertions(+), 55 deletions(-) create mode 100644 cli/internal/automation/protocol_minimum_version_parse.go diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/cli/internal/automation/protocol_minimum_version_guard.go index e6a09496f..8ccf4cd1d 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/cli/internal/automation/protocol_minimum_version_guard.go @@ -9,11 +9,8 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "strconv" "strings" - - sharedversion "github.com/hatayama/unity-cli-loop/cli/internal/version" ) const ( @@ -22,19 +19,14 @@ const ( projectRunnerReleaseTagPrefix = "uloop-project-runner-v" ) -var ( - requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) - minimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) - preRenameMinimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) - requiredMinimumProjectRunnerAssets = []string{ - "uloop-project-runner-darwin-amd64.tar.gz", - "uloop-project-runner-darwin-amd64.tar.gz.sha256", - "uloop-project-runner-darwin-arm64.tar.gz", - "uloop-project-runner-darwin-arm64.tar.gz.sha256", - "uloop-project-runner-windows-amd64.zip", - "uloop-project-runner-windows-amd64.zip.sha256", - } -) +var requiredMinimumProjectRunnerAssets = []string{ + "uloop-project-runner-darwin-amd64.tar.gz", + "uloop-project-runner-darwin-amd64.tar.gz.sha256", + "uloop-project-runner-darwin-arm64.tar.gz", + "uloop-project-runner-darwin-arm64.tar.gz.sha256", + "uloop-project-runner-windows-amd64.zip", + "uloop-project-runner-windows-amd64.zip.sha256", +} type minimumProjectRunnerRelease struct { Tag string @@ -149,7 +141,7 @@ func AnalyzeProtocolMinimumVersionGuardForRefs( return ProtocolMinimumVersionGuardResult{}, fmt.Errorf("failed to resolve git repository root: %w", err) } - baseValues, err := protocolMinimumVersionValuesAtRef(ctx, repoRoot, config.BaseRef) + baseValues, err := protocolMinimumVersionBaseValuesAtRef(ctx, repoRoot, config.BaseRef) if err != nil { return ProtocolMinimumVersionGuardResult{}, err } @@ -185,31 +177,6 @@ func AnalyzeProtocolMinimumVersionGuard( } } -func ParseProtocolMinimumVersionValues(content []byte) (ProtocolMinimumVersionValues, error) { - text := string(content) - values := ProtocolMinimumVersionValues{} - - requiredMatches := requiredProtocolVersionPattern.FindStringSubmatch(text) - if len(requiredMatches) == 2 { - requiredProtocolVersion, err := strconv.Atoi(requiredMatches[1]) - if err != nil { - return ProtocolMinimumVersionValues{}, fmt.Errorf("REQUIRED_CLI_PROTOCOL_VERSION is not an integer: %w", err) - } - values.RequiredProtocolVersion = requiredProtocolVersion - values.HasRequiredProtocol = true - } - - minimumProjectRunnerVersion, ok := parseMinimumProjectRunnerVersion(text) - if !ok { - return ProtocolMinimumVersionValues{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION", protocolMinimumVersionFile) - } - if _, ok := sharedversion.Compare(minimumProjectRunnerVersion, minimumProjectRunnerVersion); !ok { - return ProtocolMinimumVersionValues{}, fmt.Errorf("%s MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION must be semver, got %q", protocolMinimumVersionFile, minimumProjectRunnerVersion) - } - values.MinimumProjectRunnerVersion = minimumProjectRunnerVersion - return values, nil -} - func VerifyMinimumCliReleaseProtocol(values ProtocolMinimumVersionValues, contractContent []byte) error { return verifyMinimumProjectRunnerReleaseProtocol( minimumProjectRunnerReleaseTag(values.MinimumProjectRunnerVersion), @@ -217,19 +184,6 @@ func VerifyMinimumCliReleaseProtocol(values ProtocolMinimumVersionValues, contra contractContent) } -func parseMinimumProjectRunnerVersion(text string) (string, bool) { - minimumMatches := minimumProjectRunnerVersionPattern.FindStringSubmatch(text) - if len(minimumMatches) == 2 { - return normalizeProjectRunnerVersion(minimumMatches[1]), true - } - - preRenameMinimumMatches := preRenameMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) - if len(preRenameMinimumMatches) == 2 { - return normalizeProjectRunnerVersion(preRenameMinimumMatches[1]), true - } - return "", false -} - func minimumProjectRunnerReleaseTag(version string) string { return projectRunnerReleaseTagPrefix + normalizeProjectRunnerVersion(version) } @@ -424,6 +378,18 @@ func protocolMinimumVersionValuesAtRef( return ParseProtocolMinimumVersionValues([]byte(content)) } +func protocolMinimumVersionBaseValuesAtRef( + ctx context.Context, + repoRoot string, + ref string, +) (ProtocolMinimumVersionValues, error) { + content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, protocolMinimumVersionFile) + if err != nil { + return ProtocolMinimumVersionValues{}, err + } + return parseProtocolMinimumVersionBaseValues([]byte(content)) +} + func protocolMinimumVersionFileAtRef( ctx context.Context, repoRoot string, diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/cli/internal/automation/protocol_minimum_version_guard_test.go index 68067a15f..0cdc3d4fa 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/internal/automation/protocol_minimum_version_guard_test.go @@ -95,6 +95,18 @@ func TestRunProtocolMinimumVersionGuard_WhenBaseUsesPreRenameMinimumConstant_Pas assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") } +func TestParseProtocolMinimumVersionValues_WhenPreRenameMinimumConstantIsUsed_Fails(t *testing.T) { + // Verifies current package constants must use the project runner minimum version name. + _, err := ParseProtocolMinimumVersionValues([]byte(buildPreRenameProtocolMinimumVersionConstants(2, "3.0.0-beta.40"))) + + if err == nil { + t.Fatal("expected pre-rename minimum version constant to fail") + } + if !strings.Contains(err.Error(), "does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION") { + t.Fatalf("expected missing project runner minimum version error, got %v", err) + } +} + func TestParseProtocolMinimumVersionValues_WhenMinimumVersionIsInvalid_Fails(t *testing.T) { // Verifies invalid minimum project runner versions fail before release tag construction. _, err := ParseProtocolMinimumVersionValues([]byte(buildProtocolMinimumVersionConstants(2, "3.0.0-01"))) diff --git a/cli/internal/automation/protocol_minimum_version_parse.go b/cli/internal/automation/protocol_minimum_version_parse.go new file mode 100644 index 000000000..e2f61ad7a --- /dev/null +++ b/cli/internal/automation/protocol_minimum_version_parse.go @@ -0,0 +1,77 @@ +package automation + +import ( + "fmt" + "regexp" + "strconv" + + sharedversion "github.com/hatayama/unity-cli-loop/cli/internal/version" +) + +var ( + requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) + minimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION\s*=\s*"([^"]+)"`) + preRenameMinimumProjectRunnerVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_CLI_VERSION\s*=\s*"([^"]+)"`) +) + +func ParseProtocolMinimumVersionValues(content []byte) (ProtocolMinimumVersionValues, error) { + text := string(content) + values, err := parseProtocolMinimumVersionValuesWithMinimumVersion(text) + if err != nil { + return ProtocolMinimumVersionValues{}, err + } + return values, nil +} + +func parseProtocolMinimumVersionBaseValues(content []byte) (ProtocolMinimumVersionValues, error) { + text := string(content) + if _, ok := parseMinimumProjectRunnerVersion(text); ok { + return ParseProtocolMinimumVersionValues(content) + } + + minimumMatches := preRenameMinimumProjectRunnerVersionPattern.FindStringSubmatch(text) + if len(minimumMatches) != 2 { + return ParseProtocolMinimumVersionValues(content) + } + return parseProtocolMinimumVersionValues(text, normalizeProjectRunnerVersion(minimumMatches[1]), "MINIMUM_REQUIRED_CLI_VERSION") +} + +func parseProtocolMinimumVersionValuesWithMinimumVersion(text string) (ProtocolMinimumVersionValues, error) { + minimumProjectRunnerVersion, ok := parseMinimumProjectRunnerVersion(text) + if !ok { + return ProtocolMinimumVersionValues{}, fmt.Errorf("%s does not define MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION", protocolMinimumVersionFile) + } + return parseProtocolMinimumVersionValues(text, minimumProjectRunnerVersion, "MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION") +} + +func parseMinimumProjectRunnerVersion(text string) (string, bool) { + minimumMatches := minimumProjectRunnerVersionPattern.FindStringSubmatch(text) + if len(minimumMatches) == 2 { + return normalizeProjectRunnerVersion(minimumMatches[1]), true + } + return "", false +} + +func parseProtocolMinimumVersionValues( + text string, + minimumProjectRunnerVersion string, + minimumVersionConstantName string, +) (ProtocolMinimumVersionValues, error) { + values := ProtocolMinimumVersionValues{ + MinimumProjectRunnerVersion: minimumProjectRunnerVersion, + } + + requiredMatches := requiredProtocolVersionPattern.FindStringSubmatch(text) + if len(requiredMatches) == 2 { + requiredProtocolVersion, err := strconv.Atoi(requiredMatches[1]) + if err != nil { + return ProtocolMinimumVersionValues{}, fmt.Errorf("REQUIRED_CLI_PROTOCOL_VERSION is not an integer: %w", err) + } + values.RequiredProtocolVersion = requiredProtocolVersion + values.HasRequiredProtocol = true + } + if _, ok := sharedversion.Compare(minimumProjectRunnerVersion, minimumProjectRunnerVersion); !ok { + return ProtocolMinimumVersionValues{}, fmt.Errorf("%s %s must be semver, got %q", protocolMinimumVersionFile, minimumVersionConstantName, minimumProjectRunnerVersion) + } + return values, nil +} From 03a06ca5201c149f686b2e3134c2f8c8f50481e3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 09:44:37 +0900 Subject: [PATCH 14/16] Align minimum project runner release tag Point the Unity package setup target at the first renamed project-runner release so package release verification no longer looks for the old cli namespace version under the new tag prefix. --- Packages/src/Editor/Domain/CliConstants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index 9bd8ade31..3eaf9e399 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -12,7 +12,7 @@ public static class CliConstants public const int REQUIRED_CLI_PROTOCOL_VERSION = 2; // Why: setup installs this pinned project runner release; protocol bump PRs can advance it only after // the matching project runner tag is published. - public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "3.0.0-beta.40"; + public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "3.0.0-beta.43"; public const string MINIMUM_REQUIRED_PROJECT_RUNNER_RELEASE_TAG = PROJECT_RUNNER_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION; // Why: global uloop is a dispatcher; project-local CLI versions are downloaded separately. public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "3.0.1-beta.6"; From 8a1cc13537ce1c6f0bdf118e692c6253142b77fb Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 09:55:52 +0900 Subject: [PATCH 15/16] Allow project runner release bootstrap check Permit the first renamed project-runner release to pass protocol minimum checks when the base branch still uses the pre-rename minimum constant, and whitelist the renamed project pin file in gitignore. --- .gitignore | 2 +- .../protocol_minimum_version_guard.go | 48 ++++++++++++++++++- .../protocol_minimum_version_guard_test.go | 35 ++++++++++++-- .../protocol_minimum_version_parse.go | 7 ++- 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 108a0808d..ae8e36434 100644 --- a/.gitignore +++ b/.gitignore @@ -96,7 +96,7 @@ Packages/src/Docs/** # uloop CLI cache and outputs **/.uloop/* -!**/.uloop/cli-pin.json +!**/.uloop/project-runner-pin.json # Node.js / TypeScript **/node_modules/ diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/cli/internal/automation/protocol_minimum_version_guard.go index 8ccf4cd1d..e42d7605b 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/cli/internal/automation/protocol_minimum_version_guard.go @@ -42,6 +42,7 @@ type ProtocolMinimumVersionValues struct { RequiredProtocolVersion int HasRequiredProtocol bool MinimumProjectRunnerVersion string + UsesPreRenameMinimumVersion bool } type ProtocolMinimumVersionGuardResult struct { @@ -154,7 +155,9 @@ func AnalyzeProtocolMinimumVersionGuardForRefs( if protocolMinimumVersionGuardNeedsReleaseCheck(result) { _, err = verifyMinimumCliReleaseProtocolAtRef(ctx, repoRoot, result.Head) if err != nil { - result.MinimumCliReleaseProtocolError = err.Error() + if !protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner(ctx, repoRoot, config.HeadRef, result) { + result.MinimumCliReleaseProtocolError = err.Error() + } } } return result, nil @@ -353,6 +356,29 @@ func protocolMinimumVersionGuardNeedsReleaseCheck(result ProtocolMinimumVersionG return result.RequiredProtocolChanged || result.MinimumProjectRunnerVersionChanged } +func protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner( + ctx context.Context, + repoRoot string, + headRef string, + result ProtocolMinimumVersionGuardResult, +) bool { + if !result.Base.UsesPreRenameMinimumVersion { + return false + } + if result.RequiredProtocolChanged { + return false + } + if !result.MinimumProjectRunnerVersionChanged { + return false + } + + headProjectRunnerVersion, err := protocolMinimumProjectRunnerVersionAtRef(ctx, repoRoot, headRef) + if err != nil { + return false + } + return result.Head.MinimumProjectRunnerVersion == headProjectRunnerVersion +} + func protocolMinimumVersionValueLabel(values ProtocolMinimumVersionValues) string { if !values.HasRequiredProtocol { return "``" @@ -390,6 +416,26 @@ func protocolMinimumVersionBaseValuesAtRef( return parseProtocolMinimumVersionBaseValues([]byte(content)) } +func protocolMinimumProjectRunnerVersionAtRef( + ctx context.Context, + repoRoot string, + ref string, +) (string, error) { + content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, cliContractFile) + if err != nil { + return "", err + } + + contract := minimumCliReleaseContract{} + if err := json.Unmarshal([]byte(content), &contract); err != nil { + return "", fmt.Errorf("%s is invalid JSON: %w", cliContractFile, err) + } + if contract.ProjectRunnerVersion == "" { + return "", fmt.Errorf("%s does not define projectRunnerVersion", cliContractFile) + } + return contract.ProjectRunnerVersion, nil +} + func protocolMinimumVersionFileAtRef( ctx context.Context, repoRoot string, diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/cli/internal/automation/protocol_minimum_version_guard_test.go index 0cdc3d4fa..1f6bae65b 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/internal/automation/protocol_minimum_version_guard_test.go @@ -95,6 +95,20 @@ func TestRunProtocolMinimumVersionGuard_WhenBaseUsesPreRenameMinimumConstant_Pas assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") } +func TestRunProtocolMinimumVersionGuard_WhenBaseUsesPreRenameAndHeadMinimumMatchesCurrentProjectRunner_Passes(t *testing.T) { + // Verifies the project runner tag rename can bootstrap its first renamed release. + result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ + baseContent: buildPreRenameProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), + headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.43"), + headContractContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.43"}`, + }) + + if result.exitCode != 0 { + t.Fatalf("expected exit code 0, got %d\nstderr: %s", result.exitCode, result.stderr) + } + assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") +} + func TestParseProtocolMinimumVersionValues_WhenPreRenameMinimumConstantIsUsed_Fails(t *testing.T) { // Verifies current package constants must use the project runner minimum version name. _, err := ParseProtocolMinimumVersionValues([]byte(buildPreRenameProtocolMinimumVersionConstants(2, "3.0.0-beta.40"))) @@ -373,10 +387,11 @@ func TestRunProtocolMinimumVersionComment_WhenMinimumReleaseProtocolDiffers_Upse } type protocolMinimumVersionRefCase struct { - baseContent string - headContent string - releaseContent string - releaseView string + baseContent string + headContent string + headContractContent string + releaseContent string + releaseView string } type protocolMinimumVersionGuardRunResult struct { @@ -494,6 +509,11 @@ func prepareProtocolMinimumVersionGitContents(t *testing.T, workDir string, test releaseContentPath := filepath.Join(workDir, "release-contract.json") writeFile(t, baseContentPath, testCase.baseContent) writeFile(t, headContentPath, testCase.headContent) + if testCase.headContractContent != "" { + headContractContentPath := filepath.Join(workDir, "head-contract.json") + writeFile(t, headContractContentPath, testCase.headContractContent) + t.Setenv("GIT_HEAD_CONTRACT_CONTENT", headContractContentPath) + } if testCase.releaseContent != "" { writeFile(t, releaseContentPath, testCase.releaseContent) t.Setenv("GIT_RELEASE_CONTENT", releaseContentPath) @@ -544,6 +564,13 @@ fi if [ "$1" = "show" ]; then case "$2" in origin/v3-beta:*) cat "$GIT_BASE_CONTENT" ;; + protocol-pr-head:cli/contract.json) + if [ -n "${GIT_HEAD_CONTRACT_CONTENT:-}" ]; then + cat "$GIT_HEAD_CONTRACT_CONTENT" + else + cat "$GIT_HEAD_CONTENT" + fi + ;; protocol-pr-head:*) cat "$GIT_HEAD_CONTENT" ;; protocol-release:*) cat "$GIT_HEAD_CONTENT" ;; uloop-project-runner-v*:cli/contract.json) diff --git a/cli/internal/automation/protocol_minimum_version_parse.go b/cli/internal/automation/protocol_minimum_version_parse.go index e2f61ad7a..b9eeaa5c9 100644 --- a/cli/internal/automation/protocol_minimum_version_parse.go +++ b/cli/internal/automation/protocol_minimum_version_parse.go @@ -33,7 +33,12 @@ func parseProtocolMinimumVersionBaseValues(content []byte) (ProtocolMinimumVersi if len(minimumMatches) != 2 { return ParseProtocolMinimumVersionValues(content) } - return parseProtocolMinimumVersionValues(text, normalizeProjectRunnerVersion(minimumMatches[1]), "MINIMUM_REQUIRED_CLI_VERSION") + values, err := parseProtocolMinimumVersionValues(text, normalizeProjectRunnerVersion(minimumMatches[1]), "MINIMUM_REQUIRED_CLI_VERSION") + if err != nil { + return ProtocolMinimumVersionValues{}, err + } + values.UsesPreRenameMinimumVersion = true + return values, nil } func parseProtocolMinimumVersionValuesWithMinimumVersion(text string) (ProtocolMinimumVersionValues, error) { From bf6a7dad3cdf40ed3ce1bc22cc585a6fc3f36d59 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 29 Jun 2026 14:31:22 +0900 Subject: [PATCH 16/16] Bump project runner IPC protocol generation Move the renamed project-runner IPC contract to protocol 3 and keep the first renamed release bootstrap exemption limited to the missing release-contract case so other release verification failures still surface. --- .uloop/project-runner-pin.json | 2 +- Packages/src/Editor/Domain/CliConstants.cs | 2 +- Packages/src/project-runner-pin.json | 2 +- cli/contract.json | 2 +- .../protocol_minimum_version_guard.go | 11 ++++++++--- .../protocol_minimum_version_guard_test.go | 19 +++++++++++++++++-- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.uloop/project-runner-pin.json b/.uloop/project-runner-pin.json index 40dfa3a99..9b475c365 100644 --- a/.uloop/project-runner-pin.json +++ b/.uloop/project-runner-pin.json @@ -3,6 +3,6 @@ "minimumDispatcherVersion": "3.0.1-beta.6", "packageName": "io.github.hatayama.uloopmcp", "packageVersion": "3.0.0-beta.44", - "requiredProtocolVersion": 2, + "requiredProtocolVersion": 3, "schemaVersion": 1 } diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index 3eaf9e399..95343f978 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -9,7 +9,7 @@ public static class CliConstants // Why: the runtime IPC gate compares this contract generation, not release numbers. // Bump it together with cli/contract.json protocolVersion only when this package can // no longer interoperate with a different CLI protocol generation. - public const int REQUIRED_CLI_PROTOCOL_VERSION = 2; + public const int REQUIRED_CLI_PROTOCOL_VERSION = 3; // Why: setup installs this pinned project runner release; protocol bump PRs can advance it only after // the matching project runner tag is published. public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "3.0.0-beta.43"; diff --git a/Packages/src/project-runner-pin.json b/Packages/src/project-runner-pin.json index 40dfa3a99..9b475c365 100644 --- a/Packages/src/project-runner-pin.json +++ b/Packages/src/project-runner-pin.json @@ -3,6 +3,6 @@ "minimumDispatcherVersion": "3.0.1-beta.6", "packageName": "io.github.hatayama.uloopmcp", "packageVersion": "3.0.0-beta.44", - "requiredProtocolVersion": 2, + "requiredProtocolVersion": 3, "schemaVersion": 1 } diff --git a/cli/contract.json b/cli/contract.json index 6a73ac7ad..712a3b9cb 100644 --- a/cli/contract.json +++ b/cli/contract.json @@ -1,5 +1,5 @@ { "schemaVersion": 1, - "protocolVersion": 2, + "protocolVersion": 3, "projectRunnerVersion": "3.0.0-beta.43" } diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/cli/internal/automation/protocol_minimum_version_guard.go index e42d7605b..ae80af5bc 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/cli/internal/automation/protocol_minimum_version_guard.go @@ -155,7 +155,7 @@ func AnalyzeProtocolMinimumVersionGuardForRefs( if protocolMinimumVersionGuardNeedsReleaseCheck(result) { _, err = verifyMinimumCliReleaseProtocolAtRef(ctx, repoRoot, result.Head) if err != nil { - if !protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner(ctx, repoRoot, config.HeadRef, result) { + if !protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner(ctx, repoRoot, config.HeadRef, result, err) { result.MinimumCliReleaseProtocolError = err.Error() } } @@ -361,11 +361,12 @@ func protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner( repoRoot string, headRef string, result ProtocolMinimumVersionGuardResult, + err error, ) bool { - if !result.Base.UsesPreRenameMinimumVersion { + if !protocolMinimumVersionReleaseContractIsMissing(err) { return false } - if result.RequiredProtocolChanged { + if !result.Base.UsesPreRenameMinimumVersion { return false } if !result.MinimumProjectRunnerVersionChanged { @@ -379,6 +380,10 @@ func protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner( return result.Head.MinimumProjectRunnerVersion == headProjectRunnerVersion } +func protocolMinimumVersionReleaseContractIsMissing(err error) bool { + return strings.Contains(err.Error(), "does not provide cli/contract.json") +} + func protocolMinimumVersionValueLabel(values ProtocolMinimumVersionValues) string { if !values.HasRequiredProtocol { return "``" diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/cli/internal/automation/protocol_minimum_version_guard_test.go index 1f6bae65b..ca5445692 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/internal/automation/protocol_minimum_version_guard_test.go @@ -99,8 +99,8 @@ func TestRunProtocolMinimumVersionGuard_WhenBaseUsesPreRenameAndHeadMinimumMatch // Verifies the project runner tag rename can bootstrap its first renamed release. result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ baseContent: buildPreRenameProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), - headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.43"), - headContractContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.43"}`, + headContent: buildProtocolMinimumVersionConstants(3, "3.0.0-beta.43"), + headContractContent: `{"schemaVersion":1,"protocolVersion":3,"projectRunnerVersion":"3.0.0-beta.43"}`, }) if result.exitCode != 0 { @@ -109,6 +109,21 @@ func TestRunProtocolMinimumVersionGuard_WhenBaseUsesPreRenameAndHeadMinimumMatch assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") } +func TestRunProtocolMinimumVersionGuard_WhenBootstrapReleaseProtocolDiffers_Fails(t *testing.T) { + // Verifies bootstrap exemption does not hide readable release contract mismatches. + result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ + baseContent: buildPreRenameProtocolMinimumVersionConstants(2, "3.0.0-beta.40"), + headContent: buildProtocolMinimumVersionConstants(3, "3.0.0-beta.43"), + headContractContent: `{"schemaVersion":1,"protocolVersion":3,"projectRunnerVersion":"3.0.0-beta.43"}`, + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.43"}`, + }) + + if result.exitCode != 1 { + t.Fatalf("expected exit code 1, got %d\nstdout: %s", result.exitCode, result.stdout) + } + assertProtocolMinimumVersionLogContains(t, result.stderr, "advertises protocol 2") +} + func TestParseProtocolMinimumVersionValues_WhenPreRenameMinimumConstantIsUsed_Fails(t *testing.T) { // Verifies current package constants must use the project runner minimum version name. _, err := ParseProtocolMinimumVersionValues([]byte(buildPreRenameProtocolMinimumVersionConstants(2, "3.0.0-beta.40")))