From e8a05d00e5ff8c2ca504a4e58e997ad9dc558e3f Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 4 Jul 2026 16:47:22 +0900 Subject: [PATCH 1/2] Remove the dispatcher contract integer generation and embedded schemaVersions The dispatcher and the Unity package were double-gated: an integer generation (dispatcherContractVersion vs REQUIRED_DISPATCHER_CONTRACT_VERSION, compared through --version --json) on top of the semver floor the pin's minimumDispatcherVersion already enforces. The dispatcher self-updates and releases monotonically, so the floor alone expresses capability requirements and the equality gate only added a value to keep in sync (it was never bumped). The embedded contract JSONs' schemaVersion fields gated nothing because the JSON is compiled into the same binary that parses it. The IPC protocolVersion is now the only integer compatibility generation in the system. - Drop dispatcherContractVersion and schemaVersion from dispatcher-contract.json and schemaVersion from contract.json, with their parse gates and getters - Stop emitting DispatcherContractVersion from --version --json and stop parsing it in CliInstallationDetector - Delete the check-dispatcher-contract guard, its cmd, and its workflow step; shrink the dispatcher minimum version guard accordingly - Refresh shared-inputs stamps for the cli/common contract change --- .github/workflows/build-and-test.yml | 5 - .../Editor/CliInstallationDetectorTests.cs | 32 +-- Packages/src/Editor/Domain/CliConstants.cs | 2 - .../CLI/CliInstallationDetector.cs | 39 +-- cli/common/clicontract/contract.go | 5 - cli/common/clicontract/contract.json | 1 - cli/common/clicontract/contract_test.go | 1 + cli/dispatcher/dispatchercontract/contract.go | 11 - .../dispatchercontract/contract_test.go | 9 +- .../dispatcher-contract.json | 4 +- .../dispatcher_bootstrap_routing_test.go | 4 +- .../internal/dispatcher/dispatcher_version.go | 8 +- cli/dispatcher/shared-inputs-stamp.json | 2 +- cli/project-runner/shared-inputs-stamp.json | 2 +- .../cmd/check-dispatcher-contract/main.go | 24 -- .../automation/contract_file_at_ref_test.go | 12 - .../automation/dispatcher_contract_guard.go | 203 --------------- .../dispatcher_contract_guard_test.go | 245 ------------------ .../dispatcher_minimum_version_guard.go | 69 +---- .../dispatcher_minimum_version_guard_test.go | 79 ++---- .../test-resolve-dispatcher-release-target.sh | 4 +- 21 files changed, 38 insertions(+), 723 deletions(-) delete mode 100644 cli/release-automation/cmd/check-dispatcher-contract/main.go delete mode 100644 cli/release-automation/internal/automation/dispatcher_contract_guard.go delete mode 100644 cli/release-automation/internal/automation/dispatcher_contract_guard_test.go 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/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_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/shared-inputs-stamp.json b/cli/dispatcher/shared-inputs-stamp.json index 0d1ae3a46d..fd35ea66fc 100644 --- a/cli/dispatcher/shared-inputs-stamp.json +++ b/cli/dispatcher/shared-inputs-stamp.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "sharedInputsHash": "53be2808987ea6b968d22112258b077a6e900868" + "sharedInputsHash": "061702fcf06b2121ae2c3bbf74841eb2a73e1327" } diff --git a/cli/project-runner/shared-inputs-stamp.json b/cli/project-runner/shared-inputs-stamp.json index fd82c4a355..2dfdf108fe 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": "ed299fe895afa3858cf2502b43b1836932089119" } 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 } From a4cdda46c7e0657cd8cc9d5cef30fcf9c1c96b0f Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 4 Jul 2026 16:54:25 +0900 Subject: [PATCH 2/2] Unify the project runner version getter on clicontract clicore.Version() and clicore.ProtocolVersion() were pure pass-through wrappers around clicontract, which owns the embedded contract data. Two names for the same value invite divergence in call sites, so every caller now reads clicontract directly and the wrappers are gone. --- cli/common/clicore/runner_version.go | 14 ++------------ cli/common/clicore/tool_readiness.go | 5 +++-- .../internal/dispatcher/dispatcher_download.go | 3 ++- .../internal/dispatcher/dispatcher_test.go | 17 +++++++++-------- cli/dispatcher/internal/dispatcher/run_help.go | 5 +++-- cli/dispatcher/shared-inputs-stamp.json | 2 +- .../internal/projectrunner/compile_wait.go | 5 +++-- .../projectrunner/connection_retry_flow.go | 3 ++- .../projectrunner/control_play_mode_wait.go | 3 ++- .../internal/projectrunner/pause_point_logs.go | 4 ++-- .../internal/projectrunner/pause_point_wait.go | 5 +++-- .../internal/projectrunner/run_test.go | 6 +++--- .../internal/projectrunner/runner_usage.go | 3 ++- cli/project-runner/shared-inputs-stamp.json | 2 +- 14 files changed, 38 insertions(+), 39 deletions(-) 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/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/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 fd35ea66fc..5a3fd87ccf 100644 --- a/cli/dispatcher/shared-inputs-stamp.json +++ b/cli/dispatcher/shared-inputs-stamp.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "sharedInputsHash": "061702fcf06b2121ae2c3bbf74841eb2a73e1327" + "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 2dfdf108fe..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": "ed299fe895afa3858cf2502b43b1836932089119" + "sharedInputsHash": "96a846581558cb16f2e478e59199ccef16642f0a" }