diff --git a/Assets/Tests/Editor/CliInstallationDetectorTests.cs b/Assets/Tests/Editor/CliInstallationDetectorTests.cs index f0b955e89b..9d4c632c1a 100644 --- a/Assets/Tests/Editor/CliInstallationDetectorTests.cs +++ b/Assets/Tests/Editor/CliInstallationDetectorTests.cs @@ -230,7 +230,8 @@ public void IsShellDetectionUsableForPathSetup_WhenLegacyCliVersionIsHigh_Return bool result = CliInstallationDetector.IsShellDetectionUsableForPathSetup( detection, UnityEngine.RuntimePlatform.OSXEditor, - (path, platform) => false); + (path, platform) => false, + "3.0.1-beta.6"); Assert.That(result, Is.False); } diff --git a/Assets/Tests/Editor/CliSetupApplicationServiceTests.cs b/Assets/Tests/Editor/CliSetupApplicationServiceTests.cs index 27390889a1..73ba3ee678 100644 --- a/Assets/Tests/Editor/CliSetupApplicationServiceTests.cs +++ b/Assets/Tests/Editor/CliSetupApplicationServiceTests.cs @@ -5,7 +5,6 @@ using UnityEngine; using io.github.hatayama.UnityCliLoop.Application; -using io.github.hatayama.UnityCliLoop.Domain; namespace io.github.hatayama.UnityCliLoop.Tests.Editor { @@ -17,7 +16,7 @@ public class CliSetupApplicationServiceTests [Test] public async Task InstallGlobalCliAsync_UsesMinimumRequiredDispatcherReleaseTag() { - // Verifies that manual installs target the independent dispatcher release stream. + // Verifies that manual installs target the dispatcher release derived from the package pin. FakeNativeCliInstaller nativeCliInstaller = new(); CliSetupApplicationService service = new( new FakeCliInstallationDetector(new string[] { null }), @@ -27,35 +26,35 @@ public async Task InstallGlobalCliAsync_UsesMinimumRequiredDispatcherReleaseTag( Assert.That( nativeCliInstaller.InstalledVersion, - Is.EqualTo(CliConstants.MINIMUM_REQUIRED_DISPATCHER_RELEASE_TAG)); + Is.EqualTo(ExpectedDispatcherReleaseTag())); } [Test] public void GetMinimumRequiredCliVersion_UsesDispatcherVersion() { - // Verifies setup reports the minimum dispatcher required by the package. + // Verifies setup reads the minimum dispatcher version from the package pin JSON. CliSetupApplicationService service = new( new FakeCliInstallationDetector(new string[] { null }), new FakeNativeCliInstaller()); - Assert.That(service.GetMinimumRequiredCliVersion(), Is.EqualTo(CliConstants.MINIMUM_REQUIRED_DISPATCHER_VERSION)); + Assert.That(service.GetMinimumRequiredCliVersion(), Is.EqualTo(ExpectedMinimumDispatcherVersion())); } [Test] public void GetMinimumRequiredCliReleaseTag_UsesDispatcherReleaseTag() { - // Verifies setup reports the prefixed release tag for the required dispatcher. + // Verifies setup derives the prefixed release tag from the package pin instead of a duplicated constant. CliSetupApplicationService service = new( new FakeCliInstallationDetector(new string[] { null }), new FakeNativeCliInstaller()); - Assert.That(service.GetMinimumRequiredCliReleaseTag(), Is.EqualTo(CliConstants.MINIMUM_REQUIRED_DISPATCHER_RELEASE_TAG)); + Assert.That(service.GetMinimumRequiredCliReleaseTag(), Is.EqualTo(ExpectedDispatcherReleaseTag())); } [Test] public void GetGlobalCliInstallCommand_UsesMinimumRequiredDispatcherReleaseTag() { - // Verifies that fallback manual commands point at the independent dispatcher release stream. + // Verifies that fallback manual commands point at the pin-derived dispatcher release tag. FakeNativeCliInstaller nativeCliInstaller = new(); CliSetupApplicationService service = new( new FakeCliInstallationDetector(new string[] { null }), @@ -67,7 +66,19 @@ public void GetGlobalCliInstallCommand_UsesMinimumRequiredDispatcherReleaseTag() Assert.That( command.ManualCommand, - Is.EqualTo("install " + CliConstants.MINIMUM_REQUIRED_DISPATCHER_RELEASE_TAG)); + Is.EqualTo("install " + ExpectedDispatcherReleaseTag())); + } + + private static string ExpectedMinimumDispatcherVersion() + { + CliPinLoadResult result = CliPinReader.LoadPackagePin(); + Assert.That(result.Success, Is.True, result.ErrorMessage); + return result.Pin.MinimumDispatcherVersion; + } + + private static string ExpectedDispatcherReleaseTag() + { + return CliPinReader.BuildDispatcherReleaseTag(ExpectedMinimumDispatcherVersion()); } private sealed class FakeCliInstallationDetector : ICliInstallationDetector diff --git a/Assets/Tests/Editor/CliSetupCompatibilityTests.cs b/Assets/Tests/Editor/CliSetupCompatibilityTests.cs index 35d849253e..06b44abf6d 100644 --- a/Assets/Tests/Editor/CliSetupCompatibilityTests.cs +++ b/Assets/Tests/Editor/CliSetupCompatibilityTests.cs @@ -1,6 +1,5 @@ using NUnit.Framework; -using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.Presentation; namespace io.github.hatayama.UnityCliLoop.Tests.Editor @@ -10,6 +9,9 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor /// public class CliSetupCompatibilityTests { + // Why: fixed test value keeps compat logic assertions independent of the shipping pin version. + private const string TEST_MINIMUM_DISPATCHER_VERSION = "3.0.0"; + [TestCase(null, false, false, false)] [TestCase("2.1.10", false, true, false)] [TestCase("3.0.0", false, true, false)] @@ -26,7 +28,7 @@ public void Evaluate_ReturnsExpectedDispatcherSetupState( CliSetupCompatibilityState state = CliSetupCompatibility.Evaluate( cliVersion, isDispatcher, - CliConstants.MINIMUM_REQUIRED_DISPATCHER_VERSION); + TEST_MINIMUM_DISPATCHER_VERSION); Assert.That(state.NeedsUpdate, Is.EqualTo(expectedNeedsUpdate)); Assert.That(state.IsCompatible, Is.EqualTo(expectedCompatible)); @@ -40,7 +42,7 @@ public void Evaluate_WhenDispatcherVersionIsInvalid_RequestsUpdate() CliSetupCompatibilityState state = CliSetupCompatibility.Evaluate( "not-a-version", true, - CliConstants.MINIMUM_REQUIRED_DISPATCHER_VERSION); + TEST_MINIMUM_DISPATCHER_VERSION); Assert.That(state.NeedsUpdate, Is.True); Assert.That(state.IsCompatible, Is.False); diff --git a/Packages/src/Editor/Application/CliPinReader.cs b/Packages/src/Editor/Application/CliPinReader.cs new file mode 100644 index 0000000000..00344b3cbf --- /dev/null +++ b/Packages/src/Editor/Application/CliPinReader.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; + +using Newtonsoft.Json.Linq; + +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Application +{ + /// + /// Immutable snapshot of the CLI pin JSON shipped with the package. + /// + public readonly struct CliPin + { + public CliPin(string projectRunnerVersion, string minimumDispatcherVersion) + { + ProjectRunnerVersion = projectRunnerVersion; + MinimumDispatcherVersion = minimumDispatcherVersion; + } + + public string ProjectRunnerVersion { get; } + public string MinimumDispatcherVersion { get; } + } + + /// + /// Result of loading the CLI pin JSON. + /// + public readonly struct CliPinLoadResult + { + private CliPinLoadResult(bool success, CliPin pin, string errorMessage) + { + Success = success; + Pin = pin; + ErrorMessage = errorMessage; + } + + public bool Success { get; } + public CliPin Pin { get; } + public string ErrorMessage { get; } + + public static CliPinLoadResult FromSuccess(CliPin pin) + { + return new CliPinLoadResult(true, pin, string.Empty); + } + + public static CliPinLoadResult FromFailure(string errorMessage) + { + return new CliPinLoadResult(false, default, errorMessage); + } + } + + /// + /// Reads the CLI pin JSON that ships with the Unity package so callers can + /// consult a single source of truth for minimum-version requirements. + /// + public static class CliPinReader + { + // Why: keep pin JSON keys named in one place so callers do not spread string literals. + private const string PIN_JSON_PROJECT_RUNNER_VERSION_KEY = "projectRunnerVersion"; + private const string PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY = "minimumDispatcherVersion"; + + public static CliPinLoadResult LoadPackagePin() + { + string pinPath = Path.Combine( + UnityCliLoopConstants.PackageResolvedPath, + UnityCliLoopConstants.ULOOP_PROJECT_RUNNER_PIN_FILE_NAME); + return LoadPinFromPath(pinPath); + } + + // Why: exposed so tests and edge-case callers can point at an alternate pin file layout. + public static CliPinLoadResult LoadPinFromPath(string pinPath) + { + if (string.IsNullOrWhiteSpace(pinPath)) + { + return CliPinLoadResult.FromFailure("Unity CLI Loop pin file path is empty."); + } + + if (!File.Exists(pinPath)) + { + return CliPinLoadResult.FromFailure( + $"Unity CLI Loop pin file not found at {pinPath}."); + } + + string content = File.ReadAllText(pinPath); + if (string.IsNullOrWhiteSpace(content)) + { + return CliPinLoadResult.FromFailure( + $"Unity CLI Loop pin file at {pinPath} is empty."); + } + + JObject parsed = JObject.Parse(content); + string projectRunnerVersion = parsed[PIN_JSON_PROJECT_RUNNER_VERSION_KEY]?.ToString(); + string minimumDispatcherVersion = parsed[PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY]?.ToString(); + if (string.IsNullOrWhiteSpace(projectRunnerVersion)) + { + return CliPinLoadResult.FromFailure( + $"Unity CLI Loop pin file at {pinPath} is missing {PIN_JSON_PROJECT_RUNNER_VERSION_KEY}."); + } + if (string.IsNullOrWhiteSpace(minimumDispatcherVersion)) + { + return CliPinLoadResult.FromFailure( + $"Unity CLI Loop pin file at {pinPath} is missing {PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY}."); + } + + return CliPinLoadResult.FromSuccess( + new CliPin(projectRunnerVersion, minimumDispatcherVersion)); + } + + // Why: composes the dispatcher release tag from the pin so callers do not have to know the prefix. + public static string BuildDispatcherReleaseTag(string minimumDispatcherVersion) + { + return CliConstants.DISPATCHER_RELEASE_TAG_PREFIX + minimumDispatcherVersion; + } + + // Why: setup/detection paths must fail closed when the pin is unreadable rather than defaulting + // to "compatible", so both call sites share one Fail-Fast helper instead of duplicating it. + public static string LoadMinimumDispatcherVersionOrThrow() + { + CliPinLoadResult pinResult = LoadPackagePin(); + if (!pinResult.Success) + { + throw new InvalidOperationException( + "Unity CLI Loop cannot resolve minimum dispatcher version from the package pin: " + + pinResult.ErrorMessage); + } + return pinResult.Pin.MinimumDispatcherVersion; + } + } +} diff --git a/Packages/src/Editor/Application/CliPinReader.cs.meta b/Packages/src/Editor/Application/CliPinReader.cs.meta new file mode 100644 index 0000000000..605a7d62f9 --- /dev/null +++ b/Packages/src/Editor/Application/CliPinReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a1b3c5d7e9f4b0e9a2c1f0d5e8b3c7f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Application/CliSetupApplicationService.cs b/Packages/src/Editor/Application/CliSetupApplicationService.cs index 296557e89c..1214d2de96 100644 --- a/Packages/src/Editor/Application/CliSetupApplicationService.cs +++ b/Packages/src/Editor/Application/CliSetupApplicationService.cs @@ -125,14 +125,16 @@ public void InvalidateCliCache() public string GetMinimumRequiredCliVersion() { - // Why: v3 setup installs the global dispatcher; project-local CLI versions are pinned separately. - return CliConstants.MINIMUM_REQUIRED_DISPATCHER_VERSION; + // Why: v3 setup installs the global dispatcher and reads the minimum from the package pin JSON + // so that the single source of truth stays consistent with the dispatcher installation flow. + return CliPinReader.LoadMinimumDispatcherVersionOrThrow(); } public string GetMinimumRequiredCliReleaseTag() { - // Why: v3 setup installs the global dispatcher; project-local CLI versions are pinned separately. - return CliConstants.MINIMUM_REQUIRED_DISPATCHER_RELEASE_TAG; + // Why: v3 setup installs the global dispatcher, and the release tag is derived from the pin so it + // cannot drift from the version reported by GetMinimumRequiredCliVersion. + return CliPinReader.BuildDispatcherReleaseTag(CliPinReader.LoadMinimumDispatcherVersionOrThrow()); } public bool IsPackageOwnedCurrentUserInstallPath( diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index 8d7cd23fb3..a66485a8c6 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -10,13 +10,6 @@ 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 = 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"; - 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"; - public const string MINIMUM_REQUIRED_DISPATCHER_RELEASE_TAG = DISPATCHER_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_DISPATCHER_VERSION; public const string VERSION_FLAG = "--version"; public const string SHORT_VERSION_FLAG = "-v"; public const string JSON_FLAG = "--json"; diff --git a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs index c1c7ef91c7..e812882e2e 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs @@ -118,6 +118,10 @@ public Task IsCliVisibleFromShellAsync(RuntimePlatform platform, Cancellat return Task.FromResult(true); } + // Why: resolve the minimum dispatcher version once on the caller thread so the background + // detection task does not perform Unity package IO from a worker thread. + string minimumDispatcherVersion = CliPinReader.LoadMinimumDispatcherVersionOrThrow(); + return Task.Run( () => { @@ -125,7 +129,8 @@ public Task IsCliVisibleFromShellAsync(RuntimePlatform platform, Cancellat return IsShellDetectionUsableForPathSetup( detection, platform, - NativeCliInstaller.IsPackageOwnedCurrentUserInstallPath); + NativeCliInstaller.IsPackageOwnedCurrentUserInstallPath, + minimumDispatcherVersion); }, ct); } @@ -241,9 +246,11 @@ internal static ProcessStartInfo BuildShellCliDetectionStartInfo( internal static bool IsShellDetectionUsableForPathSetup( CliInstallationDetection detection, RuntimePlatform platform, - Func isPackageOwnedCurrentUserInstallPath) + Func isPackageOwnedCurrentUserInstallPath, + string minimumDispatcherVersion) { UnityEngine.Debug.Assert(isPackageOwnedCurrentUserInstallPath != null, "isPackageOwnedCurrentUserInstallPath must not be null"); + UnityEngine.Debug.Assert(!string.IsNullOrEmpty(minimumDispatcherVersion), "minimumDispatcherVersion must not be null or empty"); if (isPackageOwnedCurrentUserInstallPath(detection.ExecutablePath, platform)) { @@ -257,7 +264,7 @@ internal static bool IsShellDetectionUsableForPathSetup( return CliVersionComparer.IsVersionGreaterThanOrEqual( detection.Version, - CliConstants.MINIMUM_REQUIRED_DISPATCHER_VERSION); + minimumDispatcherVersion); } internal static string BuildShellCliDetectionCommandForShell( diff --git a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs index 9dc105773d..58fb8aa72a 100644 --- a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs +++ b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs @@ -985,10 +985,12 @@ private static CliSetupCompatibilityState EvaluateCliSetupCompatibility( string cliVersion, bool cliIsDispatcher) { + // Why: route through the setup facade so the pin JSON stays the single source for the minimum + // dispatcher version instead of duplicating the constant in the presentation layer. return CliSetupCompatibility.Evaluate( cliVersion, cliIsDispatcher, - CliConstants.MINIMUM_REQUIRED_DISPATCHER_VERSION); + CliSetupApplicationFacade.GetMinimumRequiredCliVersion()); } private async Task HandleUninstallCli() diff --git a/cli/common/clicontract/protocol_version_consistency_test.go b/cli/common/clicontract/protocol_version_consistency_test.go index 161da97591..4b842ecc3b 100644 --- a/cli/common/clicontract/protocol_version_consistency_test.go +++ b/cli/common/clicontract/protocol_version_consistency_test.go @@ -17,10 +17,7 @@ const ( unityProjectCliPinPath = "../../../.uloop/project-runner-pin.json" ) -var ( - unityRequiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) - unityMinimumDispatcherVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_DISPATCHER_VERSION\s*=\s*"([^"]+)"`) -) +var unityRequiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) type unityPackageCliPin struct { ProjectRunnerVersion string `json:"projectRunnerVersion"` @@ -44,9 +41,9 @@ func TestProtocolVersionMatchesUnityPackage(t *testing.T) { } } -// TestUnityPackageCliPinMatchesReleaseContracts verifies the dispatcher pin copied into -// projects points at the project runner release from cli/contract.json and at the minimum -// dispatcher version declared in CliConstants. +// TestUnityPackageCliPinMatchesReleaseContracts verifies the pin JSON shipped with the package +// advertises the same project runner release as cli/contract.json and defines a minimum +// dispatcher version. func TestUnityPackageCliPinMatchesReleaseContracts(t *testing.T) { pin := readJSONFile[unityPackageCliPin](t, unityPackageCliPinPath) @@ -56,9 +53,6 @@ func TestUnityPackageCliPinMatchesReleaseContracts(t *testing.T) { if pin.MinimumDispatcherVersion == "" { t.Fatalf("expected %s minimumDispatcherVersion to be set", unityPackageCliPinPath) } - if pin.MinimumDispatcherVersion != readUnityMinimumRequiredDispatcherVersion(t) { - t.Fatalf("expected %s minimumDispatcherVersion to match %s MINIMUM_REQUIRED_DISPATCHER_VERSION", unityPackageCliPinPath, unityProtocolConstantPath) - } } // TestUnityProjectCliPinMatchesPackageCliPin verifies the committed project pin does not @@ -93,22 +87,6 @@ func readUnityRequiredProtocolVersion(t *testing.T) int { return unityProtocolVersion } -func readUnityMinimumRequiredDispatcherVersion(t *testing.T) string { - t.Helper() - - content, err := os.ReadFile(filepath.Clean(unityProtocolConstantPath)) - if err != nil { - t.Fatalf("failed to read Unity dispatcher version constant from %s: %v", unityProtocolConstantPath, err) - } - - matches := unityMinimumDispatcherVersionPattern.FindStringSubmatch(string(content)) - if len(matches) != 2 { - t.Fatalf("%s does not define MINIMUM_REQUIRED_DISPATCHER_VERSION", unityProtocolConstantPath) - } - - return matches[1] -} - func readJSONFile[T any](t *testing.T, path string) T { t.Helper() diff --git a/cli/dispatcher/internal/dispatcher/dispatcher_test.go b/cli/dispatcher/internal/dispatcher/dispatcher_test.go index d47f45c72b..5e866df4ed 100644 --- a/cli/dispatcher/internal/dispatcher/dispatcher_test.go +++ b/cli/dispatcher/internal/dispatcher/dispatcher_test.go @@ -778,9 +778,7 @@ func TestLoadDispatcherPinFailsWhenPinFileMissing(t *testing.T) { 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_PROJECT_RUNNER_VERSION = "3.0.0-beta.56"; -public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "1.0.0";` + content := `public const int REQUIRED_CLI_PROTOCOL_VERSION = 3;` if err := os.WriteFile(constantsPath, []byte(content), 0o644); err != nil { t.Fatalf("failed to write constants: %v", err) } diff --git a/cli/release-automation/internal/automation/contract_file_at_ref_test.go b/cli/release-automation/internal/automation/contract_file_at_ref_test.go index 896380cc79..b41f1d3d0d 100644 --- a/cli/release-automation/internal/automation/contract_file_at_ref_test.go +++ b/cli/release-automation/internal/automation/contract_file_at_ref_test.go @@ -256,10 +256,9 @@ func TestContractFileAtRefWithLegacyFallback_WhenPrimaryReadSucceeds_DoesNotCons } } -// Verifies the "no path found at an existing ref" outcome still classifies as -// missing through the sentinel-based classifier, so the bootstrap exemption -// keeps working when the path list grows. -func TestContractFileAtRefWithLegacyFallback_WhenAllPathsAbsentAtExistingRef_ClassifiesAsMissingByClassifier(t *testing.T) { +// Verifies that when every legacy path is also absent at a resolvable ref, +// the primary show error is surfaced so operators keep the initial signal. +func TestContractFileAtRefWithLegacyFallback_WhenAllPathsAbsentAtExistingRef_ReturnsPrimaryShowError(t *testing.T) { workDir, binDir := setupMockGitBin(t) writeExistenceMockGit(t, binDir, mockGitExistenceFixture{ @@ -281,13 +280,6 @@ func TestContractFileAtRefWithLegacyFallback_WhenAllPathsAbsentAtExistingRef_Cla if err == nil { t.Fatal("expected an error when every path is absent") } - - // The runner-side caller wraps this outcome with the sentinel message. The - // classifier must accept it regardless of the checked-path count. - sentinelErr := errors.New(runnerContractMissingAtReleaseMessage("uloop-project-runner-v0.0.0")) - if !protocolMinimumVersionReleaseContractIsMissing(sentinelErr) { - t.Fatalf("expected classifier to recognize sentinel message, got: %v", sentinelErr) - } } // Verifies that when execution reaches the legacy loop and the first legacy diff --git a/cli/release-automation/internal/automation/dispatcher_minimum_version_guard.go b/cli/release-automation/internal/automation/dispatcher_minimum_version_guard.go index 044bbdb276..dcd653f8be 100644 --- a/cli/release-automation/internal/automation/dispatcher_minimum_version_guard.go +++ b/cli/release-automation/internal/automation/dispatcher_minimum_version_guard.go @@ -7,7 +7,6 @@ import ( "io" "os" "path/filepath" - "regexp" "strings" ) @@ -52,12 +51,9 @@ var ( dispatcherContractPathChain = []string{dispatcherContractFile, cliDispatcherRootContractFile, rootModulesDispatcherContractFile, legacyDispatcherContractFile} ) -var minimumDispatcherVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_DISPATCHER_VERSION\s*=\s*"([^"]+)"`) - type dispatcherMinimumVersionValues struct { CurrentProjectRunnerVersion string CurrentDispatcherVersion string - MinimumDispatcherVersion string PackagePinProjectRunnerVersion string PackagePinMinimumDispatcherVersion string ProjectPinProjectRunnerVersion string @@ -115,10 +111,6 @@ func dispatcherMinimumVersionValuesAtRef( if err != nil { return dispatcherMinimumVersionValues{}, err } - constantsContent, err := dispatcherMinimumVersionFileAtRef(ctx, repoRoot, ref, protocolMinimumVersionFile) - if err != nil { - return dispatcherMinimumVersionValues{}, err - } packagePinContent, err := dispatcherMinimumVersionFileAtRef(ctx, repoRoot, ref, unityPackageCliPinFile) if err != nil { return dispatcherMinimumVersionValues{}, err @@ -131,7 +123,6 @@ func dispatcherMinimumVersionValuesAtRef( return parseDispatcherMinimumVersionValues( []byte(cliContractContent), []byte(dispatcherContractContent), - []byte(constantsContent), []byte(packagePinContent), []byte(projectPinContent)) } @@ -156,7 +147,6 @@ func dispatcherMinimumVersionFileAtRef( func parseDispatcherMinimumVersionValues( cliContractContent []byte, dispatcherContractContent []byte, - constantsContent []byte, packagePinContent []byte, projectPinContent []byte, ) (dispatcherMinimumVersionValues, error) { @@ -168,10 +158,6 @@ func parseDispatcherMinimumVersionValues( if err != nil { return dispatcherMinimumVersionValues{}, err } - minimumDispatcherVersion, err := parseMinimumDispatcherVersion(constantsContent) - if err != nil { - return dispatcherMinimumVersionValues{}, err - } packagePin, err := parseDispatcherMinimumVersionPin(unityPackageCliPinFile, packagePinContent) if err != nil { return dispatcherMinimumVersionValues{}, err @@ -184,7 +170,6 @@ func parseDispatcherMinimumVersionValues( values := dispatcherMinimumVersionValues{ CurrentProjectRunnerVersion: cliContract.ProjectRunnerVersion, CurrentDispatcherVersion: dispatcherContract.DispatcherVersion, - MinimumDispatcherVersion: minimumDispatcherVersion, PackagePinProjectRunnerVersion: packagePin.ProjectRunnerVersion, PackagePinMinimumDispatcherVersion: packagePin.MinimumDispatcherVersion, ProjectPinProjectRunnerVersion: projectPin.ProjectRunnerVersion, @@ -215,14 +200,6 @@ func parseDispatcherMinimumVersionContract(content []byte) (dispatcherMinimumVer return contract, nil } -func parseMinimumDispatcherVersion(content []byte) (string, error) { - matches := minimumDispatcherVersionPattern.FindStringSubmatch(string(content)) - if len(matches) != 2 { - return "", fmt.Errorf("%s does not define MINIMUM_REQUIRED_DISPATCHER_VERSION", protocolMinimumVersionFile) - } - return matches[1], nil -} - func parseDispatcherMinimumVersionPin(path string, content []byte) (dispatcherMinimumVersionCliPin, error) { pin := dispatcherMinimumVersionCliPin{} if err := json.Unmarshal(content, &pin); err != nil { @@ -252,14 +229,6 @@ func validateDispatcherMinimumVersionValues(values dispatcherMinimumVersionValue unityPackageCliPinFile, values.PackagePinProjectRunnerVersion) } - if values.PackagePinMinimumDispatcherVersion != values.MinimumDispatcherVersion { - return fmt.Errorf("%s %s %q does not match %s MINIMUM_REQUIRED_DISPATCHER_VERSION %q", - unityPackageCliPinFile, - minimumDispatcherVersionDescription, - values.PackagePinMinimumDispatcherVersion, - protocolMinimumVersionFile, - values.MinimumDispatcherVersion) - } if values.ProjectPinMinimumDispatcherVersion != values.PackagePinMinimumDispatcherVersion { return fmt.Errorf("%s %s %q does not match %s %s %q", unityProjectCliPinFile, @@ -277,11 +246,13 @@ func verifyDispatcherMinimumVersionAtRef( repoRoot string, values dispatcherMinimumVersionValues, ) error { - if values.MinimumDispatcherVersion == values.CurrentDispatcherVersion { + // Why: skip release existence checks when the minimum equals the current in-tree dispatcher release, + // since the release-please PR publishes that dispatcher before promoting the package. + if values.PackagePinMinimumDispatcherVersion == values.CurrentDispatcherVersion { return nil } - releaseTag := dispatcherReleaseTagPrefix + values.MinimumDispatcherVersion + releaseTag := dispatcherReleaseTagPrefix + values.PackagePinMinimumDispatcherVersion contractContent, err := dispatcherContractFileAtRef(ctx, repoRoot, releaseTag) if err != nil { return fmt.Errorf( @@ -309,11 +280,11 @@ func verifyMinimumCliReleaseDispatcherContract(values dispatcherMinimumVersionVa if err := json.Unmarshal(contractContent, &contract); err != nil { return fmt.Errorf("dispatcher release contract is invalid JSON: %w", err) } - if contract.DispatcherVersion != "" && contract.DispatcherVersion != values.MinimumDispatcherVersion { + if contract.DispatcherVersion != "" && contract.DispatcherVersion != values.PackagePinMinimumDispatcherVersion { return fmt.Errorf( "dispatcher release %s%s contract declares dispatcherVersion %q", dispatcherReleaseTagPrefix, - values.MinimumDispatcherVersion, + values.PackagePinMinimumDispatcherVersion, contract.DispatcherVersion) } return nil diff --git a/cli/release-automation/internal/automation/dispatcher_minimum_version_guard_test.go b/cli/release-automation/internal/automation/dispatcher_minimum_version_guard_test.go index 53e501b839..e6f18597d4 100644 --- a/cli/release-automation/internal/automation/dispatcher_minimum_version_guard_test.go +++ b/cli/release-automation/internal/automation/dispatcher_minimum_version_guard_test.go @@ -5,7 +5,6 @@ import ( "context" "os" "path/filepath" - "strconv" "strings" "testing" ) @@ -83,7 +82,7 @@ func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleasePredatesDirectorySpl assertDispatcherMinimumVersionLogContains(t, result.gitLog, "dispatcher-v1.0.0:"+legacyDispatcherContractFile) } -// Verifies committed pin files cannot drift from the C# minimum dispatcher version. +// Verifies committed project pin cannot drift from the package pin's minimum dispatcher version. func TestRunDispatcherMinimumVersionCheck_WhenProjectPinDiffersFromPackagePin_Fails(t *testing.T) { result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ currentProjectRunnerVersion: "3.0.0-beta.40", @@ -182,10 +181,6 @@ func prepareDispatcherMinimumVersionFiles(t *testing.T, workDir string, testCase testCase.currentProjectRunnerVersion)) writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, dispatcherContractFile), buildDispatcherMinimumVersionContract( currentDispatcherVersion)) - writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, protocolMinimumVersionFile), buildDispatcherMinimumVersionConstants( - 2, - testCase.currentProjectRunnerVersion, - testCase.minimumDispatcherVersion)) writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, unityPackageCliPinFile), buildDispatcherMinimumVersionPin( testCase.currentProjectRunnerVersion, testCase.minimumDispatcherVersion)) @@ -211,22 +206,6 @@ func buildDispatcherMinimumVersionContract(dispatcherVersion string) string { return `{"dispatcherVersion":"` + dispatcherVersion + `"}` } -func buildDispatcherMinimumVersionConstants( - requiredProtocolVersion int, - minimumProjectRunnerVersion string, - minimumDispatcherVersion string, -) string { - return `namespace Tests { -public static class CliConstants { -public const int REQUIRED_CLI_PROTOCOL_VERSION = ` + - strconv.Itoa(requiredProtocolVersion) + - `; -public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "` + minimumProjectRunnerVersion + `"; -public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "` + minimumDispatcherVersion + `"; -} -}` -} - func buildDispatcherMinimumVersionPin(projectRunnerVersion string, minimumDispatcherVersion string) string { return `{"projectRunnerVersion":"` + projectRunnerVersion + diff --git a/cli/release-automation/internal/automation/protocol_minimum_version_guard.go b/cli/release-automation/internal/automation/protocol_minimum_version_guard.go index c841e7989f..f1d68059ea 100644 --- a/cli/release-automation/internal/automation/protocol_minimum_version_guard.go +++ b/cli/release-automation/internal/automation/protocol_minimum_version_guard.go @@ -41,7 +41,6 @@ type ProtocolMinimumVersionValues struct { RequiredProtocolVersion int HasRequiredProtocol bool MinimumProjectRunnerVersion string - UsesPreRenameMinimumVersion bool } type ProtocolMinimumVersionGuardResult struct { @@ -95,12 +94,17 @@ func RunMinimumCliReleaseProtocolCheck(ctx context.Context, stdout io.Writer, st return 1 } - content, err := minimumCliReleaseProtocolFile(ctx, repoRoot, ref) + constantsContent, err := minimumCliReleaseFile(ctx, repoRoot, ref, protocolMinimumVersionFile) if err != nil { writeProtocolMinimumVersionLine(stderr, fmt.Sprintf("failed to read %s: %v", protocolMinimumVersionFile, err)) return 1 } - values, err := ParseProtocolMinimumVersionValues(content) + pinContent, err := minimumCliReleaseFile(ctx, repoRoot, ref, unityPackageCliPinFile) + if err != nil { + writeProtocolMinimumVersionLine(stderr, fmt.Sprintf("failed to read %s: %v", unityPackageCliPinFile, err)) + return 1 + } + values, err := ParseProtocolMinimumVersionValues(constantsContent, pinContent) if err != nil { writeProtocolMinimumVersionLine(stderr, err) return 1 @@ -141,7 +145,7 @@ func AnalyzeProtocolMinimumVersionGuardForRefs( return ProtocolMinimumVersionGuardResult{}, fmt.Errorf("failed to resolve git repository root: %w", err) } - baseValues, err := protocolMinimumVersionBaseValuesAtRef(ctx, repoRoot, config.BaseRef) + baseValues, err := protocolMinimumVersionValuesAtRef(ctx, repoRoot, config.BaseRef) if err != nil { return ProtocolMinimumVersionGuardResult{}, err } @@ -154,9 +158,7 @@ func AnalyzeProtocolMinimumVersionGuardForRefs( if protocolMinimumVersionGuardNeedsReleaseCheck(result) { _, err = verifyMinimumCliReleaseProtocolAtRef(ctx, repoRoot, result.Head) if err != nil { - if !protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner(ctx, repoRoot, config.HeadRef, result, err) { - result.MinimumCliReleaseProtocolError = err.Error() - } + result.MinimumCliReleaseProtocolError = err.Error() } } return result, nil @@ -259,12 +261,12 @@ func verifyMinimumCliReleaseProtocolAtRef( return verifyMinimumCliReleaseIsPublished(ctx, repoRoot, release) } -func minimumCliReleaseProtocolFile(ctx context.Context, repoRoot string, ref string) ([]byte, error) { +func minimumCliReleaseFile(ctx context.Context, repoRoot string, ref string, file string) ([]byte, error) { if ref == "" { - return os.ReadFile(filepath.Join(repoRoot, protocolMinimumVersionFile)) + return os.ReadFile(filepath.Join(repoRoot, file)) } - content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, protocolMinimumVersionFile) + content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, file) if err != nil { return nil, err } @@ -319,11 +321,11 @@ func FormatProtocolMinimumVersionWarning(result ProtocolMinimumVersionGuardResul builder.WriteString(protocolMinimumVersionMarker) builder.WriteString("\n") if result.NeedsMinimumVersionUpdate { - builder.WriteString("Protocol version changed, but `MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` did not.\n\n") + builder.WriteString("Protocol version changed, but `" + unityPackageCliPinFile + "` `projectRunnerVersion` did not.\n\n") } else if result.RequiredProtocolChanged { - 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") + builder.WriteString("Protocol version changed, but `" + unityPackageCliPinFile + "` `projectRunnerVersion` does not point to a published project runner release that advertises the required protocol.\n\n") } else { - 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("`" + unityPackageCliPinFile + "` `projectRunnerVersion` 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)) @@ -340,7 +342,7 @@ func FormatProtocolMinimumVersionWarning(result ProtocolMinimumVersionGuardResul builder.WriteString("\n") } builder.WriteString("\n") - builder.WriteString("Update `MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` to a published project runner release that advertises the new protocol before releasing the Unity package.") + builder.WriteString("Update `" + unityPackageCliPinFile + "` `projectRunnerVersion` to a published project runner release that advertises the new protocol before releasing the Unity package.") return builder.String() } @@ -355,34 +357,6 @@ func protocolMinimumVersionGuardNeedsReleaseCheck(result ProtocolMinimumVersionG return result.RequiredProtocolChanged || result.MinimumProjectRunnerVersionChanged } -func protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner( - ctx context.Context, - repoRoot string, - headRef string, - result ProtocolMinimumVersionGuardResult, - err error, -) bool { - if !protocolMinimumVersionReleaseContractIsMissing(err) { - return false - } - if !result.Base.UsesPreRenameMinimumVersion { - return false - } - if !result.MinimumProjectRunnerVersionChanged { - return false - } - - headProjectRunnerVersion, err := protocolMinimumProjectRunnerVersionAtRef(ctx, repoRoot, headRef) - if err != nil { - return false - } - return result.Head.MinimumProjectRunnerVersion == headProjectRunnerVersion -} - -func protocolMinimumVersionReleaseContractIsMissing(err error) bool { - return strings.Contains(err.Error(), releaseContractMissingSentinel) -} - func protocolMinimumVersionValueLabel(values ProtocolMinimumVersionValues) string { if !values.HasRequiredProtocol { return "``" @@ -401,43 +375,15 @@ func protocolMinimumVersionValuesAtRef( repoRoot string, ref string, ) (ProtocolMinimumVersionValues, error) { - content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, protocolMinimumVersionFile) + constantsContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, protocolMinimumVersionFile) if err != nil { return ProtocolMinimumVersionValues{}, err } - return ParseProtocolMinimumVersionValues([]byte(content)) -} - -func protocolMinimumVersionBaseValuesAtRef( - ctx context.Context, - repoRoot string, - ref string, -) (ProtocolMinimumVersionValues, error) { - content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, protocolMinimumVersionFile) + pinContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, unityPackageCliPinFile) if err != nil { return ProtocolMinimumVersionValues{}, err } - return parseProtocolMinimumVersionBaseValues([]byte(content)) -} - -func protocolMinimumProjectRunnerVersionAtRef( - ctx context.Context, - repoRoot string, - ref string, -) (string, error) { - content, err := runnerContractFileAtRef(ctx, repoRoot, ref) - 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 + return ParseProtocolMinimumVersionValues([]byte(constantsContent), []byte(pinContent)) } func writeProtocolMinimumVersionLine(writer io.Writer, values ...any) { diff --git a/cli/release-automation/internal/automation/protocol_minimum_version_guard_test.go b/cli/release-automation/internal/automation/protocol_minimum_version_guard_test.go index 9685c50c05..e8699517cc 100644 --- a/cli/release-automation/internal/automation/protocol_minimum_version_guard_test.go +++ b/cli/release-automation/internal/automation/protocol_minimum_version_guard_test.go @@ -70,9 +70,11 @@ func TestAnalyzeProtocolMinimumVersionGuard_WhenProtocolDoesNotChange_DoesNotWar func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseMatches_Passes(t *testing.T) { // 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,"projectRunnerVersion":"3.0.0-beta.33"}`, + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.33", + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, }) if result.exitCode != 0 { @@ -86,8 +88,10 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseMatches_Passes(t *test // the later cli/ grouping move fall back to the middle-generation contract path. func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseIsMiddleGeneration_FallsBackToRootModulesContractPath(t *testing.T) { result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ - baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), - headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.33", middleReleaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, }) @@ -103,8 +107,10 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleasePredatesDirectorySplit // Verifies project runner releases published before the v3 module split are still // readable through the legacy contract.json path when both newer paths are missing. result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ - baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), - headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.33", legacyReleaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, }) @@ -117,68 +123,30 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleasePredatesDirectorySplit assertProtocolMinimumVersionLogContains(t, result.gitLog, "uloop-project-runner-v3.0.0-beta.33:"+legacyRunnerContractFile) } -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: buildPreRenameProtocolMinimumVersionConstants(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_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(3, "3.0.0-beta.43"), - headContractContent: `{"schemaVersion":1,"protocolVersion":3,"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 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"))) +func TestParseProtocolMinimumVersionValues_WhenPinIsMissingProjectRunnerVersion_Fails(t *testing.T) { + // Verifies the pin JSON must define projectRunnerVersion for the guard to run. + _, err := ParseProtocolMinimumVersionValues( + []byte(buildProtocolMinimumVersionConstants(2)), + []byte(`{}`)) if err == nil { - t.Fatal("expected pre-rename minimum version constant to fail") + t.Fatal("expected missing pin projectRunnerVersion 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) + if !strings.Contains(err.Error(), "does not define projectRunnerVersion") { + t.Fatalf("expected missing projectRunnerVersion 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"))) + _, err := ParseProtocolMinimumVersionValues( + []byte(buildProtocolMinimumVersionConstants(2)), + []byte(buildProtocolMinimumVersionPin("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") { + if !strings.Contains(err.Error(), "projectRunnerVersion must be semver") { t.Fatalf("expected semver error, got %v", err) } } @@ -186,9 +154,11 @@ func TestParseProtocolMinimumVersionValues_WhenMinimumVersionIsInvalid_Fails(t * 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{ - baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), - headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":1,"projectRunnerVersion":"3.0.0-beta.33"}`, + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.33", + releaseContent: `{"schemaVersion":1,"protocolVersion":1,"projectRunnerVersion":"3.0.0-beta.33"}`, }) if result.exitCode != 1 { @@ -201,10 +171,12 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseProtocolDiffers_Fails( func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseIsDraft_Fails(t *testing.T) { // 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,"projectRunnerVersion":"3.0.0-beta.33"}`, - releaseView: `{"isDraft":true,"assets":[]}`, + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.33", + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, + releaseView: `{"isDraft":true,"assets":[]}`, }) if result.exitCode != 1 { @@ -217,10 +189,12 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseIsDraft_Fails(t *testi func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseAssetsAreMissing_Fails(t *testing.T) { // 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,"projectRunnerVersion":"3.0.0-beta.33"}`, - releaseView: `{"isDraft":false,"assets":[]}`, + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.33", + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, + releaseView: `{"isDraft":false,"assets":[]}`, }) if result.exitCode != 1 { @@ -233,15 +207,17 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseAssetsAreMissing_Fails func TestRunProtocolMinimumVersionGuard_WhenOnlyMinimumReleaseProtocolDiffers_Fails(t *testing.T) { // Verifies installer target changes are validated even without a protocol declaration bump. 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,"projectRunnerVersion":"3.0.0-beta.33"}`, + baseProtocol: 2, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "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_PROJECT_RUNNER_VERSION` changed") + assertProtocolMinimumVersionLogContains(t, result.stderr, unityPackageCliPinFile+"` `projectRunnerVersion` changed") assertProtocolMinimumVersionLogContains(t, result.stderr, "advertises protocol 1") } @@ -322,9 +298,11 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenRefIsProvided_ReadsValuesAtRef(t 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, "3.0.0-beta.33"), - releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "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")) @@ -345,11 +323,12 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenRefIsProvided_ReadsValuesAtRef(t } 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, gitLogPath), "protocol-release:"+unityPackageCliPinFile) 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. + // Verifies v-prefixed pin versions map to a project runner tag with one prefix. workDir := t.TempDir() mockBin := filepath.Join(workDir, "bin") err := os.MkdirAll(mockBin, 0o755) @@ -362,9 +341,11 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenMinimumVersionHasPrefix_Normalize 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"}`, + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "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")) @@ -391,9 +372,11 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenMinimumVersionHasPrefix_Normalize func TestRunProtocolMinimumVersionComment_WhenWarningExists_UpsertsComment(t *testing.T) { // Verifies PR comments explain protocol bump installer target omissions. result := runProtocolMinimumVersionCommentCase(t, protocolMinimumVersionCommentCase{ - baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), - headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.32"), - commentIDs: "123", + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.32", + commentIDs: "123", }) if result.exitCode != 0 { @@ -407,10 +390,12 @@ func TestRunProtocolMinimumVersionComment_WhenWarningExists_UpsertsComment(t *te func TestRunProtocolMinimumVersionComment_WhenWarningIsResolved_DeletesComment(t *testing.T) { // Verifies stale protocol minimum comments are removed after the PR is fixed. 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,"projectRunnerVersion":"3.0.0-beta.33"}`, - commentIDs: "123", + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.33", + releaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, + commentIDs: "123", }) if result.exitCode != 0 { @@ -423,10 +408,12 @@ func TestRunProtocolMinimumVersionComment_WhenWarningIsResolved_DeletesComment(t func TestRunProtocolMinimumVersionComment_WhenMinimumReleaseProtocolDiffers_UpsertsComment(t *testing.T) { // Verifies stale protocol minimum comments stay open until the selected release protocol matches. 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,"projectRunnerVersion":"3.0.0-beta.33"}`, - commentIDs: "123", + baseProtocol: 1, + baseProjectRunner: "3.0.0-beta.32", + headProtocol: 2, + headProjectRunner: "3.0.0-beta.33", + releaseContent: `{"schemaVersion":1,"protocolVersion":1,"projectRunnerVersion":"3.0.0-beta.33"}`, + commentIDs: "123", }) if result.exitCode != 0 { @@ -437,8 +424,10 @@ func TestRunProtocolMinimumVersionComment_WhenMinimumReleaseProtocolDiffers_Upse } type protocolMinimumVersionRefCase struct { - baseContent string - headContent string + baseProtocol int + baseProjectRunner string + headProtocol int + headProjectRunner string headContractContent string releaseContent string middleReleaseContent string @@ -497,10 +486,12 @@ func runProtocolMinimumVersionGuardCase(t *testing.T, testCase protocolMinimumVe } type protocolMinimumVersionCommentCase struct { - baseContent string - headContent string - releaseContent string - commentIDs string + baseProtocol int + baseProjectRunner string + headProtocol int + headProjectRunner string + releaseContent string + commentIDs string } type protocolMinimumVersionCommentResult struct { @@ -525,9 +516,11 @@ func runProtocolMinimumVersionCommentCase(t *testing.T, testCase protocolMinimum writeProtocolMinimumVersionMockGit(t, filepath.Join(mockBin, "git")) writeProtocolMinimumVersionMockGH(t, filepath.Join(mockBin, "gh")) prepareProtocolMinimumVersionGitContents(t, workDir, protocolMinimumVersionRefCase{ - baseContent: testCase.baseContent, - headContent: testCase.headContent, - releaseContent: testCase.releaseContent, + baseProtocol: testCase.baseProtocol, + baseProjectRunner: testCase.baseProjectRunner, + headProtocol: testCase.headProtocol, + headProjectRunner: testCase.headProjectRunner, + releaseContent: testCase.releaseContent, }) t.Setenv("PATH", mockBin+string(os.PathListSeparator)+os.Getenv("PATH")) @@ -556,17 +549,21 @@ func runProtocolMinimumVersionCommentCase(t *testing.T, testCase protocolMinimum func prepareProtocolMinimumVersionGitContents(t *testing.T, workDir string, testCase protocolMinimumVersionRefCase) { t.Helper() - baseContentPath := filepath.Join(workDir, "base.cs") - headContentPath := filepath.Join(workDir, "head.cs") - releaseContentPath := filepath.Join(workDir, "release-contract.json") - writeFile(t, baseContentPath, testCase.baseContent) - writeFile(t, headContentPath, testCase.headContent) + baseConstantsPath := filepath.Join(workDir, "base-constants.cs") + basePinPath := filepath.Join(workDir, "base-pin.json") + headConstantsPath := filepath.Join(workDir, "head-constants.cs") + headPinPath := filepath.Join(workDir, "head-pin.json") + writeFile(t, baseConstantsPath, buildProtocolMinimumVersionConstants(testCase.baseProtocol)) + writeFile(t, basePinPath, buildProtocolMinimumVersionPin(testCase.baseProjectRunner)) + writeFile(t, headConstantsPath, buildProtocolMinimumVersionConstants(testCase.headProtocol)) + writeFile(t, headPinPath, buildProtocolMinimumVersionPin(testCase.headProjectRunner)) 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 != "" { + releaseContentPath := filepath.Join(workDir, "release-contract.json") writeFile(t, releaseContentPath, testCase.releaseContent) t.Setenv("GIT_RELEASE_CONTENT", releaseContentPath) } @@ -580,30 +577,24 @@ func prepareProtocolMinimumVersionGitContents(t *testing.T, workDir string, test writeFile(t, legacyReleaseContentPath, testCase.legacyReleaseContent) t.Setenv("GIT_LEGACY_RELEASE_CONTENT", legacyReleaseContentPath) } - t.Setenv("GIT_BASE_CONTENT", baseContentPath) - t.Setenv("GIT_HEAD_CONTENT", headContentPath) + t.Setenv("GIT_BASE_CONSTANTS", baseConstantsPath) + t.Setenv("GIT_BASE_PIN", basePinPath) + t.Setenv("GIT_HEAD_CONSTANTS", headConstantsPath) + t.Setenv("GIT_HEAD_PIN", headPinPath) } -func buildProtocolMinimumVersionConstants(requiredProtocolVersion int, minimumProjectRunnerVersion string) string { +func buildProtocolMinimumVersionConstants(requiredProtocolVersion int) string { return `namespace Tests { public static class CliConstants { public const int REQUIRED_CLI_PROTOCOL_VERSION = ` + strconv.Itoa(requiredProtocolVersion) + `; -public const string MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION = "` + minimumProjectRunnerVersion + `"; } - }` +}` } -func buildPreRenameProtocolMinimumVersionConstants(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 + `"; -} -}` +func buildProtocolMinimumVersionPin(projectRunnerVersion string) string { + return `{"projectRunnerVersion":"` + projectRunnerVersion + `","minimumDispatcherVersion":"1.0.0"}` } func writeProtocolMinimumVersionMockGit(t *testing.T, path string) { @@ -623,44 +614,47 @@ if [ "$1" = "-C" ]; then shift 2 fi - if [ "$1" = "show" ]; then - case "$2" in - origin/v3-beta:*) cat "$GIT_BASE_CONTENT" ;; - protocol-pr-head:cli/common/clicontract/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/common/clicontract/contract.json) - if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then - cat "$GIT_RELEASE_CONTENT" - else - echo "fatal: path 'cli/common/clicontract/contract.json' exists on disk, but not in '$2'" >&2 - exit 1 - fi - ;; - uloop-project-runner-v*:common/clicontract/contract.json) - if [ -n "${GIT_MIDDLE_RELEASE_CONTENT:-}" ]; then - cat "$GIT_MIDDLE_RELEASE_CONTENT" - else - echo "middle release not found" >&2 - exit 1 - fi - ;; - uloop-project-runner-v*:cli/contract.json) - if [ -n "${GIT_LEGACY_RELEASE_CONTENT:-}" ]; then - cat "$GIT_LEGACY_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:Packages/src/project-runner-pin.json) cat "$GIT_BASE_PIN" ;; + origin/v3-beta:*) cat "$GIT_BASE_CONSTANTS" ;; + protocol-pr-head:cli/common/clicontract/contract.json) + if [ -n "${GIT_HEAD_CONTRACT_CONTENT:-}" ]; then + cat "$GIT_HEAD_CONTRACT_CONTENT" + else + cat "$GIT_HEAD_CONSTANTS" + fi + ;; + protocol-pr-head:Packages/src/project-runner-pin.json) cat "$GIT_HEAD_PIN" ;; + protocol-pr-head:*) cat "$GIT_HEAD_CONSTANTS" ;; + protocol-release:Packages/src/project-runner-pin.json) cat "$GIT_HEAD_PIN" ;; + protocol-release:*) cat "$GIT_HEAD_CONSTANTS" ;; + uloop-project-runner-v*:cli/common/clicontract/contract.json) + if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then + cat "$GIT_RELEASE_CONTENT" + else + echo "fatal: path 'cli/common/clicontract/contract.json' exists on disk, but not in '$2'" >&2 + exit 1 + fi + ;; + uloop-project-runner-v*:common/clicontract/contract.json) + if [ -n "${GIT_MIDDLE_RELEASE_CONTENT:-}" ]; then + cat "$GIT_MIDDLE_RELEASE_CONTENT" + else + echo "middle release not found" >&2 + exit 1 + fi + ;; + uloop-project-runner-v*:cli/contract.json) + if [ -n "${GIT_LEGACY_RELEASE_CONTENT:-}" ]; then + cat "$GIT_LEGACY_RELEASE_CONTENT" + else + echo "release not found" >&2 + exit 1 + fi + ;; + *) echo "unexpected git show ref: $2" >&2; exit 1 ;; + esac exit 0 fi @@ -710,7 +704,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-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}]}' + 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/release-automation/internal/automation/protocol_minimum_version_parse.go b/cli/release-automation/internal/automation/protocol_minimum_version_parse.go index 298aa351c1..adfeda64d2 100644 --- a/cli/release-automation/internal/automation/protocol_minimum_version_parse.go +++ b/cli/release-automation/internal/automation/protocol_minimum_version_parse.go @@ -1,6 +1,7 @@ package automation import ( + "encoding/json" "fmt" "regexp" "strconv" @@ -8,65 +9,27 @@ import ( sharedversion "github.com/hatayama/unity-cli-loop/common/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*"([^"]+)"`) -) +// Why: REQUIRED_CLI_PROTOCOL_VERSION is the enduring IPC contract number and stays in CliConstants.cs, +// so the guard keeps a regex probe for it. The minimum project-runner version now lives in the pin JSON. +var requiredProtocolVersionPattern = regexp.MustCompile(`REQUIRED_CLI_PROTOCOL_VERSION\s*=\s*(\d+)`) -func ParseProtocolMinimumVersionValues(content []byte) (ProtocolMinimumVersionValues, error) { - text := string(content) - values, err := parseProtocolMinimumVersionValuesWithMinimumVersion(text) - if err != nil { - return ProtocolMinimumVersionValues{}, err - } - return values, nil +type unityPackageCliPinProjectRunnerVersion struct { + ProjectRunnerVersion string `json:"projectRunnerVersion"` } -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) - } - values, err := parseProtocolMinimumVersionValues(text, normalizeProjectRunnerVersion(minimumMatches[1]), "MINIMUM_REQUIRED_CLI_VERSION") +// ParseProtocolMinimumVersionValues extracts the required protocol version from the CliConstants.cs +// source text and the minimum project-runner version from the package pin JSON. +func ParseProtocolMinimumVersionValues(constantsContent []byte, pinContent []byte) (ProtocolMinimumVersionValues, error) { + pinVersion, err := parseMinimumProjectRunnerVersionFromPin(pinContent) if err != nil { return ProtocolMinimumVersionValues{}, err } - values.UsesPreRenameMinimumVersion = true - return values, nil -} - -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, + MinimumProjectRunnerVersion: pinVersion, } - requiredMatches := requiredProtocolVersionPattern.FindStringSubmatch(text) + requiredMatches := requiredProtocolVersionPattern.FindStringSubmatch(string(constantsContent)) if len(requiredMatches) == 2 { requiredProtocolVersion, err := strconv.Atoi(requiredMatches[1]) if err != nil { @@ -75,8 +38,19 @@ func parseProtocolMinimumVersionValues( 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) + if _, ok := sharedversion.Compare(pinVersion, pinVersion); !ok { + return ProtocolMinimumVersionValues{}, fmt.Errorf("%s projectRunnerVersion must be semver, got %q", unityPackageCliPinFile, pinVersion) } return values, nil } + +func parseMinimumProjectRunnerVersionFromPin(pinContent []byte) (string, error) { + pin := unityPackageCliPinProjectRunnerVersion{} + if err := json.Unmarshal(pinContent, &pin); err != nil { + return "", fmt.Errorf("%s is invalid JSON: %w", unityPackageCliPinFile, err) + } + if pin.ProjectRunnerVersion == "" { + return "", fmt.Errorf("%s does not define projectRunnerVersion", unityPackageCliPinFile) + } + return normalizeProjectRunnerVersion(pin.ProjectRunnerVersion), nil +}