diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d5f2071853..f534fd8a23 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -72,11 +72,6 @@ jobs: working-directory: cli/release-automation run: go run ./cmd/check-release-triggers --base "origin/${{ github.base_ref }}" --head HEAD - - name: Check dispatcher contract - if: github.event_name == 'pull_request' - working-directory: cli/release-automation - run: go run ./cmd/check-dispatcher-contract --base "origin/${{ github.base_ref }}" --head HEAD - - name: Install golangci-lint run: | go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.0 diff --git a/Assets/Tests/Editor/CliInstallationDetectorTests.cs b/Assets/Tests/Editor/CliInstallationDetectorTests.cs index f148675280..f0b955e89b 100644 --- a/Assets/Tests/Editor/CliInstallationDetectorTests.cs +++ b/Assets/Tests/Editor/CliInstallationDetectorTests.cs @@ -194,12 +194,12 @@ public void ParseShellCliInstallationOutput_WhenLegacyCliJsonExists_ReturnsVersi [Test] public void ParseShellCliInstallationOutput_WhenDispatcherJsonExists_ReturnsDispatcherDetection() { - // Verifies setup detection recognizes the dispatcher contract exposed by global uloop. + // Verifies setup detection recognizes the dispatcher release exposed by global uloop. string output = "__ULOOP_PATH_START__\n" + "/Users/ExampleUser/.local/bin/uloop\n" + "__ULOOP_PATH_END__\n" + "__ULOOP_CONTRACT_START__\n" - + "{\"DispatcherVersion\":\"3.0.0\",\"DispatcherContractVersion\":1}\n" + + "{\"DispatcherVersion\":\"3.0.0\"}\n" + "__ULOOP_CONTRACT_END__\n" + "__ULOOP_CONTRACT_STATUS_START__\n" + "0\n" @@ -219,34 +219,6 @@ public void ParseShellCliInstallationOutput_WhenDispatcherJsonExists_ReturnsDisp Assert.That(detection.ExecutablePath, Is.EqualTo("/Users/ExampleUser/.local/bin/uloop")); } - [Test] - public void ParseShellCliInstallationOutput_WhenDispatcherContractVersionDiffers_ReturnsVersionWithoutProtocol() - { - // Verifies dispatcher setup detection rejects mismatched launcher contract generations. - string output = "__ULOOP_PATH_START__\n" - + "/Users/ExampleUser/.local/bin/uloop\n" - + "__ULOOP_PATH_END__\n" - + "__ULOOP_CONTRACT_START__\n" - + "{\"DispatcherVersion\":\"3.0.0\",\"DispatcherContractVersion\":2}\n" - + "__ULOOP_CONTRACT_END__\n" - + "__ULOOP_CONTRACT_STATUS_START__\n" - + "0\n" - + "__ULOOP_CONTRACT_STATUS_END__\n" - + "__ULOOP_VERSION_START__\n" - + "3.0.0\n" - + "__ULOOP_VERSION_END__\n" - + "__ULOOP_VERSION_STATUS_START__\n" - + "0\n" - + "__ULOOP_VERSION_STATUS_END__\n"; - - CliInstallationDetection detection = - CliInstallationDetector.ParseShellCliInstallationOutput(output); - - Assert.That(detection.Version, Is.EqualTo("3.0.0")); - Assert.That(detection.IsDispatcher, Is.False); - Assert.That(detection.ExecutablePath, Is.EqualTo("/Users/ExampleUser/.local/bin/uloop")); - } - [Test] public void IsShellDetectionUsableForPathSetup_WhenLegacyCliVersionIsHigh_ReturnsFalse() { diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index 95343f978c..8d7cd23fb3 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -17,8 +17,6 @@ public static class CliConstants // 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; - // Why: dispatcher setup compatibility is a launcher contract generation, not the IPC protocol generation. - public const int REQUIRED_DISPATCHER_CONTRACT_VERSION = 1; 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 e157dd75ff..c1c7ef91c7 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs @@ -48,7 +48,6 @@ public sealed class CliInstallationDetector : ICliInstallationDetector 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"; private string _cachedCliVersion; private bool _cachedCliIsDispatcher; @@ -392,15 +391,10 @@ private static CliInstallationDetection ParseCliContractOutput(string output, st private static CliInstallationDetection ParseDispatcherContract(JObject parsed, string executablePath) { + // Why: dispatcher compatibility is enforced by the pin's minimumDispatcherVersion (semver floor), + // so identifying the dispatcher only needs its release version. string dispatcherVersion = parsed[VERSION_JSON_DISPATCHER_VERSION_PROPERTY]?.ToString(); - JToken dispatcherContractVersionToken = parsed[VERSION_JSON_DISPATCHER_CONTRACT_VERSION_PROPERTY]; - int? dispatcherContractVersion = ReadIntegerToken(dispatcherContractVersionToken); - if (string.IsNullOrEmpty(dispatcherVersion) || dispatcherContractVersion == null) - { - return new CliInstallationDetection(null, executablePath); - } - - if (dispatcherContractVersion.Value != CliConstants.REQUIRED_DISPATCHER_CONTRACT_VERSION) + if (string.IsNullOrEmpty(dispatcherVersion)) { return new CliInstallationDetection(null, executablePath); } @@ -411,33 +405,6 @@ private static CliInstallationDetection ParseDispatcherContract(JObject parsed, true); } - private static int? ReadIntegerToken(JToken integerToken) - { - if (integerToken == null || integerToken.Type != JTokenType.Integer) - { - return null; - } - - JValue integerValue = integerToken as JValue; - object rawInteger = integerValue?.Value; - if (rawInteger is int integer) - { - return integer; - } - - if (!(rawInteger is long longInteger)) - { - return null; - } - - if (longInteger < int.MinValue || longInteger > int.MaxValue) - { - return null; - } - - return (int)longInteger; - } - private static string ExecuteAndGetOutput(ProcessStartInfo startInfo, CancellationToken ct) { UnityEngine.Debug.Assert(startInfo != null, "startInfo must not be null"); diff --git a/cli/common/clicontract/contract.go b/cli/common/clicontract/contract.go index e7f93c50f4..de8d1a55ba 100644 --- a/cli/common/clicontract/contract.go +++ b/cli/common/clicontract/contract.go @@ -9,7 +9,6 @@ import ( const ( contractFileName = "contract.json" - schemaVersion = 1 ) //go:embed contract.json @@ -22,7 +21,6 @@ var ( ) 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"` @@ -61,9 +59,6 @@ func parseContract(content []byte) (Contract, error) { if err := json.Unmarshal(content, &contract); err != nil { return Contract{}, fmt.Errorf("CLI contract is invalid JSON: %w", err) } - if contract.SchemaVersion != schemaVersion { - return Contract{}, fmt.Errorf("CLI contract schema version mismatch: %d", contract.SchemaVersion) - } if err := requireString(contract.ProjectRunnerVersion, "projectRunnerVersion"); err != nil { return Contract{}, err } diff --git a/cli/common/clicontract/contract.json b/cli/common/clicontract/contract.json index 134e05c80e..97c8e44f8e 100644 --- a/cli/common/clicontract/contract.json +++ b/cli/common/clicontract/contract.json @@ -1,5 +1,4 @@ { - "schemaVersion": 1, "protocolVersion": 3, "projectRunnerVersion": "3.0.0-beta.46" } diff --git a/cli/common/clicontract/contract_test.go b/cli/common/clicontract/contract_test.go index 1285518b16..32e1e9c6b3 100644 --- a/cli/common/clicontract/contract_test.go +++ b/cli/common/clicontract/contract_test.go @@ -43,4 +43,5 @@ func TestCliContractDoesNotDeclareDispatcherReleaseFields(t *testing.T) { fields := clitest.RequireContractFieldMap(t, contractFiles, contractFileName) clitest.RequireContractFieldMissing(t, fields, "dispatcherVersion") clitest.RequireContractFieldMissing(t, fields, "dispatcherContractVersion") + clitest.RequireContractFieldMissing(t, fields, "schemaVersion") } diff --git a/cli/common/clicore/runner_version.go b/cli/common/clicore/runner_version.go index dd306d522f..5978dd8965 100644 --- a/cli/common/clicore/runner_version.go +++ b/cli/common/clicore/runner_version.go @@ -7,20 +7,10 @@ import ( clicontract "github.com/hatayama/unity-cli-loop/common/clicontract" ) -// Version returns the project runner release version advertised by CLI commands. -func Version() string { - return clicontract.ProjectRunnerVersion() -} - -// ProtocolVersion returns the IPC protocol generation advertised by CLI commands. -func ProtocolVersion() int { - return clicontract.ProtocolVersion() -} - func WriteVersionJSON(stdout io.Writer) { content, err := json.Marshal(map[string]any{ - "ProjectRunnerVersion": Version(), - "ProtocolVersion": ProtocolVersion(), + "ProjectRunnerVersion": clicontract.ProjectRunnerVersion(), + "ProtocolVersion": clicontract.ProtocolVersion(), }) if err != nil { panic(err) diff --git a/cli/common/clicore/tool_readiness.go b/cli/common/clicore/tool_readiness.go index 8d64e748c6..b039e98531 100644 --- a/cli/common/clicore/tool_readiness.go +++ b/cli/common/clicore/tool_readiness.go @@ -7,6 +7,7 @@ import ( "fmt" "time" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/project" "github.com/hatayama/unity-cli-loop/common/unityipc" "github.com/hatayama/unity-cli-loop/common/unityprocess" @@ -133,11 +134,11 @@ func probeToolReadiness(ctx context.Context, projectRoot string, executeDynamicC } if !executeDynamicCodeAvailable { - _, err := unityipc.NewClient(connection, Version()).Send(probeContext, "get-version", map[string]any{}) + _, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send(probeContext, "get-version", map[string]any{}) return err } - response, err := unityipc.NewClient(connection, Version()).Send(probeContext, "execute-dynamic-code", executeDynamicCodeReadinessProbeParams()) + response, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send(probeContext, "execute-dynamic-code", executeDynamicCodeReadinessProbeParams()) if err != nil { return err } diff --git a/cli/dispatcher/dispatchercontract/contract.go b/cli/dispatcher/dispatchercontract/contract.go index bcc4e6d9b8..c161d12aef 100644 --- a/cli/dispatcher/dispatchercontract/contract.go +++ b/cli/dispatcher/dispatchercontract/contract.go @@ -11,7 +11,6 @@ import ( const ( dispatcherContractFileName = "dispatcher-contract.json" - schemaVersion = 1 ) //go:embed dispatcher-contract.json @@ -20,13 +19,9 @@ var contractFiles embed.FS var DispatcherCurrent = mustLoadDispatcherContract() type DispatcherContract struct { - SchemaVersion int `json:"schemaVersion"` // DispatcherVersion is the launcher release version. It moves only when the dispatcher // itself is released, not when project-local CLI releases move. DispatcherVersion string `json:"dispatcherVersion"` - // DispatcherContractVersion is the launcher capability generation this binary provides. - // It moves only when project pins need a newer dispatcher contract. - DispatcherContractVersion int `json:"dispatcherContractVersion"` } func mustLoadDispatcherContract() DispatcherContract { @@ -39,13 +34,7 @@ func mustLoadDispatcherContract() DispatcherContract { if err := json.Unmarshal(content, &contract); err != nil { panic(fmt.Sprintf("Dispatcher contract is invalid JSON: %v", err)) } - if contract.SchemaVersion != schemaVersion { - panic(fmt.Sprintf("Dispatcher contract schema version mismatch: %d", contract.SchemaVersion)) - } requireString(contract.DispatcherVersion, "dispatcherVersion") - if contract.DispatcherContractVersion < 1 { - panic(fmt.Sprintf("Dispatcher contract dispatcherContractVersion must be at least 1, got %d", contract.DispatcherContractVersion)) - } return contract } diff --git a/cli/dispatcher/dispatchercontract/contract_test.go b/cli/dispatcher/dispatchercontract/contract_test.go index dbc37b825e..fd8aff05a6 100644 --- a/cli/dispatcher/dispatchercontract/contract_test.go +++ b/cli/dispatcher/dispatchercontract/contract_test.go @@ -6,13 +6,6 @@ import ( "github.com/hatayama/unity-cli-loop/common/clitest" ) -func TestCliContractProvidesDispatcherContractVersion(t *testing.T) { - // Verifies that the contract declares which dispatcher capability generation the binary provides. - if DispatcherCurrent.DispatcherContractVersion < 1 { - t.Fatalf("dispatcherContractVersion must be at least 1, got %d", DispatcherCurrent.DispatcherContractVersion) - } -} - func TestDispatcherContractProvidesRuntimeVersion(t *testing.T) { // Verifies that the launcher owns a release version independent from project-local CLI releases. clitest.RequireValidContractVersion(t, "dispatcherVersion", DispatcherCurrent.DispatcherVersion) @@ -24,4 +17,6 @@ func TestDispatcherContractDoesNotDeclareCliReleaseFields(t *testing.T) { clitest.RequireContractFieldMissing(t, fields, "projectRunnerVersion") clitest.RequireContractFieldMissing(t, fields, "cliVersion") clitest.RequireContractFieldMissing(t, fields, "protocolVersion") + clitest.RequireContractFieldMissing(t, fields, "dispatcherContractVersion") + clitest.RequireContractFieldMissing(t, fields, "schemaVersion") } diff --git a/cli/dispatcher/dispatchercontract/dispatcher-contract.json b/cli/dispatcher/dispatchercontract/dispatcher-contract.json index e78428b528..cca2a38e47 100644 --- a/cli/dispatcher/dispatchercontract/dispatcher-contract.json +++ b/cli/dispatcher/dispatchercontract/dispatcher-contract.json @@ -1,5 +1,3 @@ { - "schemaVersion": 1, - "dispatcherVersion": "3.0.1-beta.12", - "dispatcherContractVersion": 1 + "dispatcherVersion": "3.0.1-beta.12" } diff --git a/cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go b/cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go index 0c60373acb..953182e92f 100644 --- a/cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go +++ b/cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go @@ -25,8 +25,8 @@ func TestRunDispatcherVersionJSONReportsDispatcherIdentity(t *testing.T) { if payload["DispatcherVersion"] != dispatcherVersion { t.Fatalf("DispatcherVersion mismatch: %v", payload["DispatcherVersion"]) } - if _, ok := payload["DispatcherContractVersion"]; !ok { - t.Fatalf("DispatcherContractVersion missing: %#v", payload) + if _, ok := payload["DispatcherContractVersion"]; ok { + t.Fatalf("DispatcherContractVersion must not be emitted: %#v", payload) } } diff --git a/cli/dispatcher/internal/dispatcher/dispatcher_download.go b/cli/dispatcher/internal/dispatcher/dispatcher_download.go index efaa4b8c27..46536b06b3 100644 --- a/cli/dispatcher/internal/dispatcher/dispatcher_download.go +++ b/cli/dispatcher/internal/dispatcher/dispatcher_download.go @@ -18,6 +18,7 @@ import ( "strings" "time" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clicore" "github.com/hatayama/unity-cli-loop/dispatcher/internal/nativepath" sharedupdate "github.com/hatayama/unity-cli-loop/dispatcher/internal/update" @@ -52,7 +53,7 @@ func resolveDispatcherRealCLI(ctx context.Context, pin dispatcherPin, stderr io. } func dispatcherSiblingRealCLIPath(pin dispatcherPin) (string, bool) { - if pin.ProjectRunnerVersion != clicore.Version() { + if pin.ProjectRunnerVersion != clicontract.ProjectRunnerVersion() { return "", false } executablePath, err := os.Executable() diff --git a/cli/dispatcher/internal/dispatcher/dispatcher_test.go b/cli/dispatcher/internal/dispatcher/dispatcher_test.go index 7c51ea695a..d47f45c72b 100644 --- a/cli/dispatcher/internal/dispatcher/dispatcher_test.go +++ b/cli/dispatcher/internal/dispatcher/dispatcher_test.go @@ -18,6 +18,7 @@ import ( "testing" "time" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clicore" "github.com/hatayama/unity-cli-loop/dispatcher/internal/nativepath" ) @@ -37,8 +38,8 @@ func TestRunDispatcherUsesProjectPinAndCachedRealCLI(t *testing.T) { // Verifies dispatcher reads the project pin and executes the cached real CLI. projectRoot := createDispatcherUnityProject(t) cacheRoot := t.TempDir() - writeDispatcherProjectPin(t, projectRoot, clicore.Version()) - expectedCLIPath := writeCachedDispatcherRealCLI(t, cacheRoot, clicore.Version()) + writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion()) + expectedCLIPath := writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion()) t.Setenv(nativepath.CacheDirEnvName, cacheRoot) t.Setenv(dispatcherDisableSelfUpdateEnvName, "1") t.Chdir(projectRoot) @@ -72,8 +73,8 @@ func TestRunDispatcherPreservesExplicitProjectPathForRealCLI(t *testing.T) { // Verifies dispatcher accepts trailing --project-path and passes the original arguments onward. projectRoot := createDispatcherUnityProject(t) cacheRoot := t.TempDir() - writeDispatcherProjectPin(t, projectRoot, clicore.Version()) - writeCachedDispatcherRealCLI(t, cacheRoot, clicore.Version()) + writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion()) + writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion()) t.Setenv(nativepath.CacheDirEnvName, cacheRoot) t.Setenv(dispatcherDisableSelfUpdateEnvName, "1") t.Chdir(t.TempDir()) @@ -99,8 +100,8 @@ func TestRunDispatcherForwardsProjectScopedVersionToPinnedRunner(t *testing.T) { // Verifies --project-path --version is forwarded so the pinned runner reports its own version. projectRoot := createDispatcherUnityProject(t) cacheRoot := t.TempDir() - writeDispatcherProjectPin(t, projectRoot, clicore.Version()) - writeCachedDispatcherRealCLI(t, cacheRoot, clicore.Version()) + writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion()) + writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion()) t.Setenv(nativepath.CacheDirEnvName, cacheRoot) t.Setenv(dispatcherDisableSelfUpdateEnvName, "1") t.Chdir(t.TempDir()) @@ -129,8 +130,8 @@ func TestRunDispatcherForwardsProjectScopedVersionJSONToPinnedRunner(t *testing. // Verifies --project-path --version --json is forwarded so the pinned runner reports its own version payload. projectRoot := createDispatcherUnityProject(t) cacheRoot := t.TempDir() - writeDispatcherProjectPin(t, projectRoot, clicore.Version()) - writeCachedDispatcherRealCLI(t, cacheRoot, clicore.Version()) + writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion()) + writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion()) t.Setenv(nativepath.CacheDirEnvName, cacheRoot) t.Setenv(dispatcherDisableSelfUpdateEnvName, "1") t.Chdir(t.TempDir()) diff --git a/cli/dispatcher/internal/dispatcher/dispatcher_version.go b/cli/dispatcher/internal/dispatcher/dispatcher_version.go index 302489cecf..eaf2bf43f1 100644 --- a/cli/dispatcher/internal/dispatcher/dispatcher_version.go +++ b/cli/dispatcher/internal/dispatcher/dispatcher_version.go @@ -8,15 +8,11 @@ import ( "github.com/hatayama/unity-cli-loop/dispatcher/dispatchercontract" ) -var ( - dispatcherVersion = dispatchercontract.DispatcherCurrent.DispatcherVersion - dispatcherContractVersion = dispatchercontract.DispatcherCurrent.DispatcherContractVersion -) +var dispatcherVersion = dispatchercontract.DispatcherCurrent.DispatcherVersion func writeDispatcherVersionJSON(stdout io.Writer) { content, err := json.Marshal(map[string]any{ - "DispatcherVersion": dispatcherVersion, - "DispatcherContractVersion": dispatcherContractVersion, + "DispatcherVersion": dispatcherVersion, }) if err != nil { panic(err) diff --git a/cli/dispatcher/internal/dispatcher/run_help.go b/cli/dispatcher/internal/dispatcher/run_help.go index efe5fa85e4..7fb816f578 100644 --- a/cli/dispatcher/internal/dispatcher/run_help.go +++ b/cli/dispatcher/internal/dispatcher/run_help.go @@ -5,6 +5,7 @@ import ( "os" "strings" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clicore" "github.com/hatayama/unity-cli-loop/common/project" ) @@ -17,7 +18,7 @@ const ( func printHelp(stdout io.Writer) { printMainHelp( stdout, - clicore.Version(), + clicontract.ProjectRunnerVersion(), nativeCLIDescription, clicore.ToolsCache{}, false) @@ -37,7 +38,7 @@ func printHelpForResolvedProject(stdout io.Writer, explicitProjectPath string) { } cache, ok := clicore.LoadProjectToolCache(connection.ProjectRoot) - printMainHelp(stdout, clicore.Version(), nativeCLIDescription, cache, ok) + printMainHelp(stdout, clicontract.ProjectRunnerVersion(), nativeCLIDescription, cache, ok) } func printLauncherHelp(stdout io.Writer) { diff --git a/cli/dispatcher/shared-inputs-stamp.json b/cli/dispatcher/shared-inputs-stamp.json index 0d1ae3a46d..5a3fd87ccf 100644 --- a/cli/dispatcher/shared-inputs-stamp.json +++ b/cli/dispatcher/shared-inputs-stamp.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "sharedInputsHash": "53be2808987ea6b968d22112258b077a6e900868" + "sharedInputsHash": "9af384d70ef14c3600cb092b48ff3646a00d16b9" } diff --git a/cli/project-runner/internal/projectrunner/compile_wait.go b/cli/project-runner/internal/projectrunner/compile_wait.go index 3ea3b7fdb6..2128fb9ce1 100644 --- a/cli/project-runner/internal/projectrunner/compile_wait.go +++ b/cli/project-runner/internal/projectrunner/compile_wait.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clicore" "github.com/hatayama/unity-cli-loop/common/unityipc" ) @@ -159,7 +160,7 @@ func queryCompileStatusFromUnity(ctx context.Context, connection unityipc.Connec probeContext, cancel := context.WithTimeout(ctx, compileStatusProbeTimeout) defer cancel() - response, err := unityipc.NewClient(connection, clicore.Version()).Send( + response, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send( probeContext, compileStatusCommandName, map[string]any{compileRequestIDParam: requestID}, @@ -202,7 +203,7 @@ func logCliDebugModeResolved(connection unityipc.Connection, command string) { "debug_enabled": true, "debug_source": "env", "project_identity": clicore.ProjectIdentity(connection.ProjectRoot), - "cli_version": clicore.Version(), + "cli_version": clicontract.ProjectRunnerVersion(), }, }) } diff --git a/cli/project-runner/internal/projectrunner/connection_retry_flow.go b/cli/project-runner/internal/projectrunner/connection_retry_flow.go index 2de01922b1..5003de5f76 100644 --- a/cli/project-runner/internal/projectrunner/connection_retry_flow.go +++ b/cli/project-runner/internal/projectrunner/connection_retry_flow.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clicore" "github.com/hatayama/unity-cli-loop/common/unityipc" "github.com/hatayama/unity-cli-loop/common/unityprocess" @@ -14,7 +15,7 @@ func newConnectionRetryClient( responseTimeout time.Duration, mainThreadStallHandler func(float64), ) *unityipc.Client { - client := unityipc.NewClient(connection, clicore.Version()) + client := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()) if responseTimeout > 0 { client = client.WithResponseTimeout(responseTimeout) } diff --git a/cli/project-runner/internal/projectrunner/control_play_mode_wait.go b/cli/project-runner/internal/projectrunner/control_play_mode_wait.go index dd275a53ad..f27d9ac516 100644 --- a/cli/project-runner/internal/projectrunner/control_play_mode_wait.go +++ b/cli/project-runner/internal/projectrunner/control_play_mode_wait.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clicore" "github.com/hatayama/unity-cli-loop/common/unityipc" ) @@ -198,7 +199,7 @@ func requestControlPlayModeStatus( probeContext, cancel := context.WithTimeout(ctx, controlPlayModeStatusTimeout) defer cancel() - result, err := unityipc.NewClient(connection, clicore.Version()).Send( + result, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send( probeContext, controlPlayModeCommandName, map[string]any{ diff --git a/cli/project-runner/internal/projectrunner/pause_point_logs.go b/cli/project-runner/internal/projectrunner/pause_point_logs.go index b64d6e7f55..51f92faa4f 100644 --- a/cli/project-runner/internal/projectrunner/pause_point_logs.go +++ b/cli/project-runner/internal/projectrunner/pause_point_logs.go @@ -5,7 +5,7 @@ import ( "encoding/json" "strings" - "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/unityipc" ) @@ -83,7 +83,7 @@ func fetchMatchingLogsFromUnity( probeContext, cancel := context.WithTimeout(ctx, pausePointStatusProbeTimeout) defer cancel() - result, err := unityipc.NewClient(connection, clicore.Version()).Send( + result, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send( probeContext, pausePointGetLogsCommandName, map[string]any{ diff --git a/cli/project-runner/internal/projectrunner/pause_point_wait.go b/cli/project-runner/internal/projectrunner/pause_point_wait.go index 120371a55c..960ecb3c7a 100644 --- a/cli/project-runner/internal/projectrunner/pause_point_wait.go +++ b/cli/project-runner/internal/projectrunner/pause_point_wait.go @@ -8,6 +8,7 @@ import ( "strconv" "time" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clicore" "github.com/hatayama/unity-cli-loop/common/unityipc" ) @@ -424,7 +425,7 @@ func queryPausePointStatusFromUnity( probeContext, cancel := context.WithTimeout(ctx, pausePointStatusProbeTimeout) defer cancel() - result, err := unityipc.NewClient(connection, clicore.Version()).Send( + result, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send( probeContext, pausePointStatusCommandName, map[string]any{"Id": id}, @@ -448,7 +449,7 @@ func clearPausePointStatusFromUnity( probeContext, cancel := context.WithTimeout(ctx, pausePointStatusProbeTimeout) defer cancel() - result, err := unityipc.NewClient(connection, clicore.Version()).Send( + result, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send( probeContext, pausePointClearStatusCommandName, map[string]any{"Id": id}, diff --git a/cli/project-runner/internal/projectrunner/run_test.go b/cli/project-runner/internal/projectrunner/run_test.go index 86d66f5c7d..29af7b6042 100644 --- a/cli/project-runner/internal/projectrunner/run_test.go +++ b/cli/project-runner/internal/projectrunner/run_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clitest" ) @@ -14,10 +14,10 @@ func TestRunProjectLocalVersionJSONIncludesProtocolVersion(t *testing.T) { // Verifies Unity setup can inspect protocol compatibility without parsing human help text. payload := clitest.RunVersionJSON(t, RunProjectLocal) - if payload["ProjectRunnerVersion"] != clicore.Version() { + if payload["ProjectRunnerVersion"] != clicontract.ProjectRunnerVersion() { t.Fatalf("projectRunnerVersion mismatch: %#v", payload) } - if payload["ProtocolVersion"] != float64(clicore.ProtocolVersion()) { + if payload["ProtocolVersion"] != float64(clicontract.ProtocolVersion()) { t.Fatalf("protocolVersion mismatch: %#v", payload) } } diff --git a/cli/project-runner/internal/projectrunner/runner_usage.go b/cli/project-runner/internal/projectrunner/runner_usage.go index 8f7d0909cc..8eeefc6ef4 100644 --- a/cli/project-runner/internal/projectrunner/runner_usage.go +++ b/cli/project-runner/internal/projectrunner/runner_usage.go @@ -3,6 +3,7 @@ package projectrunner import ( "io" + "github.com/hatayama/unity-cli-loop/common/clicontract" "github.com/hatayama/unity-cli-loop/common/clicore" ) @@ -20,7 +21,7 @@ func tryHandleRunnerInfoRequest(args []string, stdout io.Writer) (bool, int) { return true, 0 } if clicore.IsVersionRequest(args) { - clicore.WriteLine(stdout, clicore.Version()) + clicore.WriteLine(stdout, clicontract.ProjectRunnerVersion()) return true, 0 } return false, 0 diff --git a/cli/project-runner/shared-inputs-stamp.json b/cli/project-runner/shared-inputs-stamp.json index fd82c4a355..5c16f2a4ee 100644 --- a/cli/project-runner/shared-inputs-stamp.json +++ b/cli/project-runner/shared-inputs-stamp.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "sharedInputsHash": "2cbd95205bef8d25a2269f01d49475346b3506a3" + "sharedInputsHash": "96a846581558cb16f2e478e59199ccef16642f0a" } diff --git a/cli/release-automation/cmd/check-dispatcher-contract/main.go b/cli/release-automation/cmd/check-dispatcher-contract/main.go deleted file mode 100644 index 08cae27fd0..0000000000 --- a/cli/release-automation/cmd/check-dispatcher-contract/main.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "context" - "flag" - "os" - - "github.com/hatayama/unity-cli-loop/tools/release-automation/internal/automation" -) - -func main() { - baseRef := flag.String("base", "", "base git ref to compare from") - headRef := flag.String("head", "HEAD", "head git ref to compare to") - flag.Parse() - - os.Exit(automation.RunDispatcherContractGuard( - context.Background(), - os.Stdout, - os.Stderr, - automation.DispatcherContractGuardConfig{ - BaseRef: *baseRef, - HeadRef: *headRef, - })) -} 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 02f6b5d398..896380cc79 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 @@ -91,18 +91,6 @@ func TestContractFileAtRefWithLegacyFallback_WhenRefMissing_PropagatesShowError( if content != "" { t.Fatalf("expected no content on missing ref, got %q", content) } - - // Confirm the dispatcher guard does not treat a missing ref as initial. - missing, guardErr := isMissingDispatcherContractAtRefError( - context.Background(), - fixture.repoRoot, - "no-such-ref") - if guardErr != nil { - t.Fatalf("expected guard to run without executor error, got %v", guardErr) - } - if missing { - t.Fatal("expected missing-ref to NOT be treated as an initial contract by the guard") - } } // Verifies a non-ExitError from the presence probe after a show failure diff --git a/cli/release-automation/internal/automation/dispatcher_contract_guard.go b/cli/release-automation/internal/automation/dispatcher_contract_guard.go deleted file mode 100644 index 6e02d3c2ed..0000000000 --- a/cli/release-automation/internal/automation/dispatcher_contract_guard.go +++ /dev/null @@ -1,203 +0,0 @@ -package automation - -import ( - "context" - "encoding/json" - "fmt" - "io" - "strconv" - "strings" -) - -type DispatcherContractGuardConfig struct { - BaseRef string - HeadRef string -} - -type DispatcherContractValues struct { - HasContract bool - DispatcherContractVersion int -} - -type DispatcherContractGuardResult struct { - Base DispatcherContractValues - Head DispatcherContractValues - DispatcherContractVersionDecreased bool -} - -type dispatcherContractDocument struct { - DispatcherContractVersion int `json:"dispatcherContractVersion"` -} - -func RunDispatcherContractGuard( - ctx context.Context, - stdout io.Writer, - stderr io.Writer, - config DispatcherContractGuardConfig, -) int { - result, err := AnalyzeDispatcherContractGuardForRefs(ctx, config) - if err != nil { - writeDispatcherContractLine(stderr, err) - return 1 - } - if dispatcherContractGuardNeedsAction(result) { - writeDispatcherContractLine(stderr, FormatDispatcherContractWarning(result)) - return 1 - } - - writeDispatcherContractLine(stdout, "Dispatcher contract guard passed.") - return 0 -} - -func AnalyzeDispatcherContractGuardForRefs( - ctx context.Context, - config DispatcherContractGuardConfig, -) (DispatcherContractGuardResult, error) { - if config.BaseRef == "" { - return DispatcherContractGuardResult{}, fmt.Errorf("--base is required") - } - if config.HeadRef == "" { - config.HeadRef = "HEAD" - } - - repoRoot, err := gitRepoRoot(ctx) - if err != nil { - return DispatcherContractGuardResult{}, fmt.Errorf("failed to resolve git repository root: %w", err) - } - - headValues, err := dispatcherContractValuesAtRef(ctx, repoRoot, config.HeadRef) - if err != nil { - return DispatcherContractGuardResult{}, fmt.Errorf("failed to read head %s: %w", dispatcherContractFile, err) - } - - // The base ref may predate the directory split, where the contract lived at - // the legacy cli/ path, or predate the contract entirely. Both cases are an - // initial introduction, not a regression. - baseContent, baseContentErr := dispatcherContractFileAtRef(ctx, repoRoot, config.BaseRef) - baseValues, err := parseDispatcherContractBaseValues(ctx, repoRoot, config.BaseRef, baseContent, baseContentErr) - if err != nil { - return DispatcherContractGuardResult{}, err - } - - return AnalyzeDispatcherContractGuard(baseValues, headValues), nil -} - -func AnalyzeDispatcherContractGuard( - base DispatcherContractValues, - head DispatcherContractValues, -) DispatcherContractGuardResult { - return DispatcherContractGuardResult{ - Base: base, - Head: head, - DispatcherContractVersionDecreased: base.HasContract && - head.HasContract && - head.DispatcherContractVersion < base.DispatcherContractVersion, - } -} - -func dispatcherContractGuardNeedsAction(result DispatcherContractGuardResult) bool { - return result.DispatcherContractVersionDecreased -} - -func parseDispatcherContractBaseValues( - ctx context.Context, - repoRoot string, - baseRef string, - content string, - readErr error, -) (DispatcherContractValues, error) { - if readErr != nil { - missing, guardErr := isMissingDispatcherContractAtRefError(ctx, repoRoot, baseRef) - if guardErr != nil { - // Preserve the original read failure so operators see the show - // failure that started the classification rather than only the - // probe failure that made the classification impossible. - return DispatcherContractValues{}, fmt.Errorf("failed to classify base %s read failure: %w (original read error: %v)", dispatcherContractFile, guardErr, readErr) - } - if missing { - return DispatcherContractValues{}, nil - } - return DispatcherContractValues{}, fmt.Errorf("failed to read base %s: %w", dispatcherContractFile, readErr) - } - - values, err := ParseDispatcherContractValues([]byte(content)) - if err != nil { - return DispatcherContractValues{}, fmt.Errorf("failed to parse base %s: %w", dispatcherContractFile, err) - } - return values, nil -} - -// isMissingDispatcherContractAtRefError reports whether the base ref exists yet -// carries no dispatcher contract at any known generation. Only that -// combination is an initial introduction; a missing ref, or any git execution -// failure, must propagate instead of being silently treated as "no contract". -func isMissingDispatcherContractAtRefError( - ctx context.Context, - repoRoot string, - baseRef string, -) (bool, error) { - refExists, err := refExistsAtRef(ctx, repoRoot, baseRef) - if err != nil { - return false, err - } - if !refExists { - return false, nil - } - anyExists, err := anyOfFilesExistsAtRef(ctx, repoRoot, baseRef, dispatcherContractPathChain) - if err != nil { - return false, err - } - return !anyExists, nil -} - -// ParseDispatcherContractValues extracts only what the guard compares. -// dispatcherVersion is intentionally not read or validated here: the guard -// never consumes it, and the dispatcher module's own contract tests pin its -// semver format on every PR. -func ParseDispatcherContractValues(content []byte) (DispatcherContractValues, error) { - contract := dispatcherContractDocument{} - if err := json.Unmarshal(content, &contract); err != nil { - return DispatcherContractValues{}, fmt.Errorf("%s is invalid JSON: %w", dispatcherContractFile, err) - } - - if contract.DispatcherContractVersion < 1 { - return DispatcherContractValues{}, fmt.Errorf( - "%s dispatcherContractVersion must be at least 1, got %s", - dispatcherContractFile, - strconv.Itoa(contract.DispatcherContractVersion)) - } - - return DispatcherContractValues{ - HasContract: true, - DispatcherContractVersion: contract.DispatcherContractVersion, - }, nil -} - -func FormatDispatcherContractWarning(result DispatcherContractGuardResult) string { - builder := strings.Builder{} - builder.WriteString("dispatcherContractVersion moved backwards.\n\n") - builder.WriteString("- Base dispatcher contract version: `") - builder.WriteString(strconv.Itoa(result.Base.DispatcherContractVersion)) - builder.WriteString("`\n") - builder.WriteString("- Head dispatcher contract version: `") - builder.WriteString(strconv.Itoa(result.Head.DispatcherContractVersion)) - builder.WriteString("`\n\n") - builder.WriteString("Dispatcher contract generations only move forward. Restore the base value or bump it when the dispatcher contract itself changes.") - return builder.String() -} - -func dispatcherContractValuesAtRef( - ctx context.Context, - repoRoot string, - ref string, -) (DispatcherContractValues, error) { - content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, dispatcherContractFile) - if err != nil { - return DispatcherContractValues{}, err - } - return ParseDispatcherContractValues([]byte(content)) -} - -func writeDispatcherContractLine(writer io.Writer, values ...any) { - _, _ = fmt.Fprintln(writer, values...) -} diff --git a/cli/release-automation/internal/automation/dispatcher_contract_guard_test.go b/cli/release-automation/internal/automation/dispatcher_contract_guard_test.go deleted file mode 100644 index e4c217232b..0000000000 --- a/cli/release-automation/internal/automation/dispatcher_contract_guard_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package automation - -import ( - "context" - "errors" - "os" - "path/filepath" - "strings" - "testing" -) - -// Verifies dispatcher contract generations cannot move backwards. -func TestDispatcherContractGuardRejectsContractVersionDecrease(t *testing.T) { - result := AnalyzeDispatcherContractGuard( - DispatcherContractValues{HasContract: true, DispatcherContractVersion: 2}, - DispatcherContractValues{HasContract: true, DispatcherContractVersion: 1}, - ) - - if !result.DispatcherContractVersionDecreased { - t.Fatal("expected a contract version decrease to be detected") - } - if !dispatcherContractGuardNeedsAction(result) { - t.Fatal("expected the guard to fail on a contract version decrease") - } -} - -// Verifies unchanged and increased contract versions pass the guard. -func TestDispatcherContractGuardAcceptsSameAndIncreasedContractVersions(t *testing.T) { - base := DispatcherContractValues{HasContract: true, DispatcherContractVersion: 1} - for _, headContractVersion := range []int{1, 2} { - head := DispatcherContractValues{HasContract: true, DispatcherContractVersion: headContractVersion} - result := AnalyzeDispatcherContractGuard(base, head) - if dispatcherContractGuardNeedsAction(result) { - t.Fatalf("expected head contract version %d to pass", headContractVersion) - } - } -} - -// Verifies the first dispatcher contract can be introduced without comparing against a missing base. -func TestDispatcherContractGuardAcceptsInitialContract(t *testing.T) { - result := AnalyzeDispatcherContractGuard( - DispatcherContractValues{}, - DispatcherContractValues{HasContract: true, DispatcherContractVersion: 1}, - ) - - if dispatcherContractGuardNeedsAction(result) { - t.Fatal("expected an initial contract introduction to pass") - } -} - -// Verifies a base ref without the contract at either the split or legacy path -// is treated as an initial introduction (zero values, no error). -func TestDispatcherContractGuardTreatsMissingBaseContractAsInitial(t *testing.T) { - repoRoot := setupDispatcherContractGuardMockGit(t, dispatcherContractGuardMockState{ - refExists: true, - primaryExists: false, - legacyExists: false, - }) - readErr := errors.New("fatal: dispatcher/dispatcher-contract.json missing at origin/main") - - values, err := parseDispatcherContractBaseValues( - context.Background(), - repoRoot, - "origin/main", - "", - readErr) - if err != nil { - t.Fatalf("expected a missing base contract to be tolerated, got %v", err) - } - if values.HasContract { - t.Fatal("expected a missing base contract to produce zero values") - } -} - -// Verifies base contract read failures other than "both paths missing at an -// existing ref" propagate; a real read failure must not be silently ignored. -func TestDispatcherContractGuardRejectsUnexpectedBaseReadError(t *testing.T) { - repoRoot := setupDispatcherContractGuardMockGit(t, dispatcherContractGuardMockState{ - refExists: true, - primaryExists: true, - legacyExists: false, - }) - readErr := errors.New("fatal: unable to read tree") - - _, err := parseDispatcherContractBaseValues( - context.Background(), - repoRoot, - "origin/main", - "", - readErr) - - if err == nil { - t.Fatal("expected an unexpected read error to propagate") - } -} - -// Verifies a base ref that is not resolvable is not silently treated as -// initial; the read error propagates instead. -func TestDispatcherContractGuardPropagatesMissingBaseRef(t *testing.T) { - repoRoot := setupDispatcherContractGuardMockGit(t, dispatcherContractGuardMockState{ - refExists: false, - }) - readErr := errors.New("fatal: bad revision 'no-such-ref'") - - _, err := parseDispatcherContractBaseValues( - context.Background(), - repoRoot, - "no-such-ref", - "", - readErr) - - if err == nil { - t.Fatal("expected a missing ref to propagate the read error") - } -} - -// Verifies that when the existence probe used to classify a base read -// failure itself fails with a non-ExitError, the original read error text is -// still present in the returned error so operators see the real failure. -func TestDispatcherContractGuardPreservesReadErrWhenProbeFailsWithNonExitError(t *testing.T) { - workDir := t.TempDir() - emptyBin := filepath.Join(workDir, "empty-bin") - if err := os.MkdirAll(emptyBin, 0o755); err != nil { - t.Fatalf("failed to create empty bin: %v", err) - } - // Shadow PATH with a directory that has no git so exec.LookPath fails - // at command startup, producing a non-ExitError from the probe. - t.Setenv("PATH", emptyBin) - - readErr := errors.New("fatal: original show failure sentinel") - - _, err := parseDispatcherContractBaseValues( - context.Background(), - workDir, - "origin/main", - "", - readErr) - - if err == nil { - t.Fatal("expected an error when the classifier probe itself fails") - } - if !strings.Contains(err.Error(), "fatal: original show failure sentinel") { - t.Fatalf("expected original read error text to be preserved, got: %v", err) - } -} - -type dispatcherContractGuardMockState struct { - refExists bool - primaryExists bool - previousExists bool - // middleExists mirrors the root-modules contract path. Tests that predate - // the middle generation leave it zero-valued, meaning - // "absent", which matches historic two-generation behavior. - middleExists bool - legacyExists bool -} - -func setupDispatcherContractGuardMockGit(t *testing.T, state dispatcherContractGuardMockState) string { - t.Helper() - - workDir, binDir := setupMockGitBin(t) - writeExistenceMockGit(t, binDir, mockGitExistenceFixture{ - refResolves: state.refExists, - paths: map[string]mockGitPathBehavior{ - dispatcherContractFile: {exists: state.primaryExists}, - cliDispatcherRootContractFile: {exists: state.previousExists}, - rootModulesDispatcherContractFile: {exists: state.middleExists}, - legacyDispatcherContractFile: {exists: state.legacyExists}, - }, - }) - return workDir -} - -// Verifies a base ref that only carries the middle-generation dispatcher -// contract path is NOT misclassified as "initial": the guard must probe every -// generation so a tag published between the v3 module split and the cli/ group -// move still counts as an existing base contract. -func TestDispatcherContractGuardDetectsMiddleGenerationBaseContract(t *testing.T) { - repoRoot := setupDispatcherContractGuardMockGit(t, dispatcherContractGuardMockState{ - refExists: true, - primaryExists: false, - middleExists: true, - legacyExists: false, - }) - readErr := errors.New("fatal: cli/dispatcher/dispatcher-contract.json missing at origin/main") - - _, err := parseDispatcherContractBaseValues( - context.Background(), - repoRoot, - "origin/main", - "", - readErr) - - if err == nil { - t.Fatal("expected a middle-generation base contract to not be silently treated as initial") - } -} - -// Verifies the head contract must parse as a valid dispatcher contract. -// dispatcherVersion format is not validated here: this guard never consumes -// it, and TestDispatcherContractProvidesRuntimeVersion in the dispatcher -// module already pins the semver format on every PR. -func TestParseDispatcherContractValuesValidation(t *testing.T) { - cases := []struct { - name string - content string - }{ - {name: "invalid JSON", content: "{"}, - {name: "contract version below 1", content: `{"dispatcherVersion": "3.0.1", "dispatcherContractVersion": 0}`}, - } - - for _, testCase := range cases { - _, err := ParseDispatcherContractValues([]byte(testCase.content)) - if err == nil { - t.Fatalf("expected %s to be rejected", testCase.name) - } - } -} - -// Verifies a valid contract parses into its version values. -func TestParseDispatcherContractValuesReadsContract(t *testing.T) { - values, err := ParseDispatcherContractValues([]byte(`{"dispatcherVersion": "3.0.1-beta.11", "dispatcherContractVersion": 1}`)) - if err != nil { - t.Fatalf("expected a valid contract to parse, got %v", err) - } - if !values.HasContract || values.DispatcherContractVersion != 1 { - t.Fatalf("unexpected parsed values: %+v", values) - } -} - -// Verifies the warning explains that the contract generation moved backwards. -func TestFormatDispatcherContractWarningExplainsDecrease(t *testing.T) { - result := AnalyzeDispatcherContractGuard( - DispatcherContractValues{HasContract: true, DispatcherContractVersion: 2}, - DispatcherContractValues{HasContract: true, DispatcherContractVersion: 1}, - ) - - warning := FormatDispatcherContractWarning(result) - - for _, expected := range []string{"dispatcherContractVersion", "moved backwards", "`2`", "`1`"} { - if !strings.Contains(warning, expected) { - t.Fatalf("expected warning to contain %q, got:\n%s", expected, warning) - } - } -} 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 a611586e1d..044bbdb276 100644 --- a/cli/release-automation/internal/automation/dispatcher_minimum_version_guard.go +++ b/cli/release-automation/internal/automation/dispatcher_minimum_version_guard.go @@ -8,7 +8,6 @@ import ( "os" "path/filepath" "regexp" - "strconv" "strings" ) @@ -41,7 +40,6 @@ const ( dispatcherReleaseTagPrefix = "dispatcher-v" unityPackageCliPinFile = "Packages/src/project-runner-pin.json" unityProjectCliPinFile = ".uloop/project-runner-pin.json" - minimumDispatcherContractVersion = 1 minimumDispatcherVersionDescription = "minimumDispatcherVersion" ) @@ -59,7 +57,6 @@ var minimumDispatcherVersionPattern = regexp.MustCompile(`MINIMUM_REQUIRED_DISPA type dispatcherMinimumVersionValues struct { CurrentProjectRunnerVersion string CurrentDispatcherVersion string - CurrentDispatcherContractVersion int MinimumDispatcherVersion string PackagePinProjectRunnerVersion string PackagePinMinimumDispatcherVersion string @@ -72,13 +69,11 @@ type dispatcherMinimumVersionCliContract struct { } type dispatcherMinimumVersionContract struct { - DispatcherVersion string `json:"dispatcherVersion"` - DispatcherContractVersion int `json:"dispatcherContractVersion"` + DispatcherVersion string `json:"dispatcherVersion"` } type dispatcherMinimumVersionReleaseContract struct { - DispatcherVersion string `json:"dispatcherVersion"` - DispatcherContractVersion *json.RawMessage `json:"dispatcherContractVersion"` + DispatcherVersion string `json:"dispatcherVersion"` } type dispatcherMinimumVersionCliPin struct { @@ -189,7 +184,6 @@ func parseDispatcherMinimumVersionValues( values := dispatcherMinimumVersionValues{ CurrentProjectRunnerVersion: cliContract.ProjectRunnerVersion, CurrentDispatcherVersion: dispatcherContract.DispatcherVersion, - CurrentDispatcherContractVersion: dispatcherContract.DispatcherContractVersion, MinimumDispatcherVersion: minimumDispatcherVersion, PackagePinProjectRunnerVersion: packagePin.ProjectRunnerVersion, PackagePinMinimumDispatcherVersion: packagePin.MinimumDispatcherVersion, @@ -218,11 +212,6 @@ func parseDispatcherMinimumVersionContract(content []byte) (dispatcherMinimumVer if contract.DispatcherVersion == "" { return dispatcherMinimumVersionContract{}, fmt.Errorf("%s does not define dispatcherVersion", dispatcherContractFile) } - if contract.DispatcherContractVersion < minimumDispatcherContractVersion { - return dispatcherMinimumVersionContract{}, dispatcherContractVersionTooLowError( - dispatcherContractFile, - contract.DispatcherContractVersion) - } return contract, nil } @@ -289,7 +278,7 @@ func verifyDispatcherMinimumVersionAtRef( values dispatcherMinimumVersionValues, ) error { if values.MinimumDispatcherVersion == values.CurrentDispatcherVersion { - return verifyCurrentDispatcherMinimumVersion(values) + return nil } releaseTag := dispatcherReleaseTagPrefix + values.MinimumDispatcherVersion @@ -315,13 +304,6 @@ func dispatcherContractFileAtRef(ctx context.Context, repoRoot string, ref strin dispatcherContractPathChain[1:]...) } -func verifyCurrentDispatcherMinimumVersion(values dispatcherMinimumVersionValues) error { - if values.CurrentDispatcherContractVersion < minimumDispatcherContractVersion { - return dispatcherContractVersionTooLowError(dispatcherContractFile, values.CurrentDispatcherContractVersion) - } - return nil -} - func verifyMinimumCliReleaseDispatcherContract(values dispatcherMinimumVersionValues, contractContent []byte) error { contract := dispatcherMinimumVersionReleaseContract{} if err := json.Unmarshal(contractContent, &contract); err != nil { @@ -334,54 +316,9 @@ func verifyMinimumCliReleaseDispatcherContract(values dispatcherMinimumVersionVa values.MinimumDispatcherVersion, contract.DispatcherVersion) } - - releaseLabel := dispatcherReleaseTagPrefix + values.MinimumDispatcherVersion - dispatcherContractVersion, hasDispatcherContractVersion, err := dispatcherMinimumReleaseContractVersion( - releaseLabel, - contract.DispatcherContractVersion) - if err != nil { - return err - } - if !hasDispatcherContractVersion { - return fmt.Errorf("dispatcher release %s does not define dispatcherContractVersion", releaseLabel) - } - if dispatcherContractVersion < minimumDispatcherContractVersion { - return dispatcherContractVersionTooLowError("dispatcher release "+releaseLabel, dispatcherContractVersion) - } - if dispatcherContractVersion < values.CurrentDispatcherContractVersion { - return fmt.Errorf( - "unity package requires dispatcher contract %d, but dispatcher release %s advertises dispatcher contract %d", - values.CurrentDispatcherContractVersion, - releaseLabel, - dispatcherContractVersion) - } return nil } -func dispatcherMinimumReleaseContractVersion(releaseLabel string, value *json.RawMessage) (int, bool, error) { - if value == nil { - return 0, false, nil - } - - rawValue := strings.TrimSpace(string(*value)) - dispatcherContractVersion, err := strconv.Atoi(rawValue) - if err != nil { - return 0, true, fmt.Errorf( - "dispatcher release %s dispatcherContractVersion must be an integer, got %s", - releaseLabel, - rawValue) - } - return dispatcherContractVersion, true, nil -} - -func dispatcherContractVersionTooLowError(subject string, value int) error { - return fmt.Errorf( - "%s dispatcherContractVersion must be at least %d, got %d", - subject, - minimumDispatcherContractVersion, - value) -} - func writeDispatcherMinimumVersionLine(writer io.Writer, values ...any) { _, _ = fmt.Fprintln(writer, values...) } 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 d81fd2d4bf..53e501b839 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 @@ -10,30 +10,12 @@ import ( "testing" ) -// Verifies release PRs cannot point minimumDispatcherVersion at a dispatcher tag without contract metadata. -func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleaseLacksDispatcherContract_Fails(t *testing.T) { - result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ - currentProjectRunnerVersion: "3.0.0-beta.40", - currentDispatcherVersion: "1.0.1", - currentDispatcherContractVersion: 1, - minimumDispatcherVersion: "1.0.0", - releaseContract: `{"schemaVersion":1,"dispatcherVersion":"1.0.0"}`, - }) - - if result.exitCode != 1 { - t.Fatalf("expected exit code 1, got %d\nstdout: %s", result.exitCode, result.stdout) - } - assertDispatcherMinimumVersionLogContains(t, result.stderr, "does not define dispatcherContractVersion") - assertDispatcherMinimumVersionLogContains(t, result.gitLog, "dispatcher-v1.0.0:"+dispatcherContractFile) -} - // 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{ - currentProjectRunnerVersion: "3.0.0-beta.40", - currentDispatcherVersion: "1.0.0", - currentDispatcherContractVersion: 1, - minimumDispatcherVersion: "1.0.0", + currentProjectRunnerVersion: "3.0.0-beta.40", + currentDispatcherVersion: "1.0.0", + minimumDispatcherVersion: "1.0.0", }) if result.exitCode != 0 { @@ -47,11 +29,10 @@ func TestRunDispatcherMinimumVersionCheck_WhenMinimumIsCurrentRelease_Passes(t * // before dispatchercontract existed fall back to the previous cli/dispatcher path. func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleaseIsPreviousCliGeneration_FallsBackToCliDispatcherRootPath(t *testing.T) { result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ - currentProjectRunnerVersion: "3.0.0-beta.40", - currentDispatcherVersion: "1.0.1", - currentDispatcherContractVersion: 1, - minimumDispatcherVersion: "1.0.0", - previousReleaseContract: `{"schemaVersion":1,"dispatcherVersion":"1.0.0","dispatcherContractVersion":1}`, + currentProjectRunnerVersion: "3.0.0-beta.40", + currentDispatcherVersion: "1.0.1", + minimumDispatcherVersion: "1.0.0", + previousReleaseContract: `{"dispatcherVersion":"1.0.0"}`, }) if result.exitCode != 0 { @@ -67,11 +48,10 @@ func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleaseIsPreviousCliGenerat // later cli/ grouping move fall back to the middle-generation dispatcher path. func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleaseIsMiddleGeneration_FallsBackToRootModulesPath(t *testing.T) { result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ - currentProjectRunnerVersion: "3.0.0-beta.40", - currentDispatcherVersion: "1.0.1", - currentDispatcherContractVersion: 1, - minimumDispatcherVersion: "1.0.0", - middleReleaseContract: `{"schemaVersion":1,"dispatcherVersion":"1.0.0","dispatcherContractVersion":1}`, + currentProjectRunnerVersion: "3.0.0-beta.40", + currentDispatcherVersion: "1.0.1", + minimumDispatcherVersion: "1.0.0", + middleReleaseContract: `{"dispatcherVersion":"1.0.0"}`, }) if result.exitCode != 0 { @@ -87,11 +67,10 @@ func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleaseIsMiddleGeneration_F // remain readable through the oldest cli/dispatcher-contract.json path. func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleasePredatesDirectorySplit_FallsBackToLegacyPath(t *testing.T) { result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ - currentProjectRunnerVersion: "3.0.0-beta.40", - currentDispatcherVersion: "1.0.1", - currentDispatcherContractVersion: 1, - minimumDispatcherVersion: "1.0.0", - legacyReleaseContract: `{"schemaVersion":1,"dispatcherVersion":"1.0.0","dispatcherContractVersion":1}`, + currentProjectRunnerVersion: "3.0.0-beta.40", + currentDispatcherVersion: "1.0.1", + minimumDispatcherVersion: "1.0.0", + legacyReleaseContract: `{"dispatcherVersion":"1.0.0"}`, }) if result.exitCode != 0 { @@ -109,7 +88,6 @@ func TestRunDispatcherMinimumVersionCheck_WhenProjectPinDiffersFromPackagePin_Fa result := runDispatcherMinimumVersionCheckCase(t, dispatcherMinimumVersionCase{ currentProjectRunnerVersion: "3.0.0-beta.40", currentDispatcherVersion: "1.0.0", - currentDispatcherContractVersion: 1, minimumDispatcherVersion: "1.0.0", projectPinMinimumDispatcherVersion: "0.9.0", }) @@ -120,25 +98,9 @@ func TestRunDispatcherMinimumVersionCheck_WhenProjectPinDiffersFromPackagePin_Fa 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{ - currentProjectRunnerVersion: "3.0.0-beta.40", - currentDispatcherVersion: "1.0.0", - currentDispatcherContractVersion: 0, - minimumDispatcherVersion: "1.0.0", - }) - - if result.exitCode != 1 { - t.Fatalf("expected exit code 1, got %d\nstdout: %s", result.exitCode, result.stdout) - } - assertDispatcherMinimumVersionLogContains(t, result.stderr, "dispatcherContractVersion must be at least 1, got 0") -} - type dispatcherMinimumVersionCase struct { currentProjectRunnerVersion string currentDispatcherVersion string - currentDispatcherContractVersion int minimumDispatcherVersion string projectPinMinimumDispatcherVersion string releaseContract string @@ -219,8 +181,7 @@ func prepareDispatcherMinimumVersionFiles(t *testing.T, workDir string, testCase writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, cliContractFile), buildDispatcherMinimumVersionCliContract( testCase.currentProjectRunnerVersion)) writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, dispatcherContractFile), buildDispatcherMinimumVersionContract( - currentDispatcherVersion, - testCase.currentDispatcherContractVersion)) + currentDispatcherVersion)) writeDispatcherMinimumVersionFile(t, filepath.Join(workDir, protocolMinimumVersionFile), buildDispatcherMinimumVersionConstants( 2, testCase.currentProjectRunnerVersion, @@ -243,13 +204,11 @@ func writeDispatcherMinimumVersionFile(t *testing.T, path string, content string } func buildDispatcherMinimumVersionCliContract(projectRunnerVersion string) string { - return `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"` + projectRunnerVersion + `"}` + return `{"protocolVersion":2,"projectRunnerVersion":"` + projectRunnerVersion + `"}` } -func buildDispatcherMinimumVersionContract(dispatcherVersion string, dispatcherContractVersion int) string { - return `{"schemaVersion":1,"dispatcherVersion":"` + dispatcherVersion + `","dispatcherContractVersion":` + - strconv.Itoa(dispatcherContractVersion) + - `}` +func buildDispatcherMinimumVersionContract(dispatcherVersion string) string { + return `{"dispatcherVersion":"` + dispatcherVersion + `"}` } func buildDispatcherMinimumVersionConstants( diff --git a/scripts/test-resolve-dispatcher-release-target.sh b/scripts/test-resolve-dispatcher-release-target.sh index 2045279613..612eb32971 100755 --- a/scripts/test-resolve-dispatcher-release-target.sh +++ b/scripts/test-resolve-dispatcher-release-target.sh @@ -77,9 +77,7 @@ write_contract() { mkdir -p cli/dispatcher/dispatchercontract printf '%s\n' \ '{' \ - ' "schemaVersion": 1,' \ - ' "dispatcherVersion": "'"$version"'",' \ - ' "dispatcherContractVersion": 1' \ + ' "dispatcherVersion": "'"$version"'"' \ '}' > cli/dispatcher/dispatchercontract/dispatcher-contract.json }