Skip to content

fix: Read minimum version requirements from the project runner pin#1506

Merged
hatayama merged 2 commits into
v3-betafrom
refactor/single-source-minimum-versions
Jul 5, 2026
Merged

fix: Read minimum version requirements from the project runner pin#1506
hatayama merged 2 commits into
v3-betafrom
refactor/single-source-minimum-versions

Conversation

@hatayama

@hatayama hatayama commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Part 5 of the version-gate consolidation (G4), following #1501/#1503/#1504/#1505. MINIMUM_REQUIRED_DISPATCHER_VERSION duplicated the pin's minimumDispatcherVersion, and MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION had no runtime reader left after #1501 — only CI guards still parsed both constants out of C# source with regexes. The pin JSON is now the single source and the four C# constants (two values plus derived release tags) are gone.

After this PR the manually maintained version values in the whole system are down to three: the protocolVersion pair (Go/C# wire contract) and the pin's minimumDispatcherVersion.

  • Add CliPinReader so the package reads its own shipped pin; setup, installation detection, and the settings window take the dispatcher floor from it and fail fast when the pin cannot be loaded
  • Repoint the protocol minimum guard at the pin's projectRunnerVersion while preserving its intent: a protocol bump still requires a published project runner release advertising the new protocol
  • Drop the CliConstants regex from the dispatcher minimum guard; it now validates the two pins against each other and the published release
  • Keep the Go/C# protocol equality test; REQUIRED_CLI_PROTOCOL_VERSION is the one remaining C# version constant

Validation

  • scripts/check-go-cli.sh, scripts/test-release-please-config.sh, scripts/test-stamp-release-inputs.sh, scripts/test-sync-release-please-package-releases.sh
  • check-release-triggers: passed
  • uloop compile: 0 errors / 0 warnings
  • EditMode: CliSetupApplicationServiceTests / CliSetupCompatibilityTests / CliInstallationDetectorTests / CliPathSetupFlowTests 30/30 passed

Review in cubic

MINIMUM_REQUIRED_DISPATCHER_VERSION duplicated the pin's
minimumDispatcherVersion, and MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION had
no runtime reader left after the dispatcher regex fallback was removed —
only CI guards still parsed both constants out of C# source with regexes.
Each duplicate declaration was one more value to keep in sync by hand, so
the pin JSON is now the single source and the four C# constants (two
values plus derived release tags) are gone.

- Add CliPinReader so the package reads its own shipped pin; setup,
  installation detection, and the settings window take the dispatcher
  floor from it and fail fast when the pin cannot be loaded
- Repoint the protocol minimum guard at the pin's projectRunnerVersion
  while preserving its intent: a protocol bump still requires a published
  project runner release advertising the new protocol
- Drop the CliConstants regex from the dispatcher minimum guard; it now
  validates the two pins against each other and the published release
- Keep the Go/C# protocol equality test; REQUIRED_CLI_PROTOCOL_VERSION is
  the one remaining C# version constant
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces hard-coded minimum dispatcher/project-runner version constants in Unity's CliConstants with values read from the shipped Unity package pin JSON via a new CliPinReader. Corresponding Go release-automation and CLI-contract guards are updated to parse minimum versions from pin files instead of regex-extracted constants, with tests updated accordingly.

Changes

Unity Editor pin-derived version reading

Layer / File(s) Summary
CliPinReader module and constant removal
Packages/src/Editor/Application/CliPinReader.cs, Packages/src/Editor/Domain/CliConstants.cs
New CliPin, CliPinLoadResult, and CliPinReader types load/validate pin JSON and build dispatcher release tags; minimum version constants are removed from CliConstants.
Application service and detector wiring
Packages/src/Editor/Application/CliSetupApplicationService.cs, Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs, Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs
Minimum required CLI version/release tag and shell-detection compatibility checks now source values from CliPinReader instead of CliConstants.
Unity test updates
Assets/Tests/Editor/CliInstallationDetectorTests.cs, Assets/Tests/Editor/CliSetupApplicationServiceTests.cs, Assets/Tests/Editor/CliSetupCompatibilityTests.cs
Tests assert against pin-derived values or local test constants, add helper methods to load expected pin values, and pass the new detector parameter.

Go release-automation and CLI-contract guard updates

Layer / File(s) Summary
Protocol minimum version parsing
cli/release-automation/internal/automation/protocol_minimum_version_parse.go
ParseProtocolMinimumVersionValues now takes constants and pin content, extracting projectRunnerVersion from pin JSON with semver validation, dropping regex-based and pre-rename fallback logic.
Protocol minimum version guard wiring
cli/release-automation/internal/automation/protocol_minimum_version_guard.go
Guard checks read both constants and pin files, remove UsesPreRenameMinimumVersion and bootstrap allowance logic, and update warning text to reference the pin file.
Dispatcher minimum version guard wiring
cli/release-automation/internal/automation/dispatcher_minimum_version_guard.go
Dispatcher guard drops regex parsing of MINIMUM_REQUIRED_DISPATCHER_VERSION and uses PackagePinMinimumDispatcherVersion for validation and release checks.
Go test fixtures and assertions
cli/release-automation/internal/automation/*_test.go, cli/dispatcher/internal/dispatcher/dispatcher_test.go, cli/common/clicontract/protocol_version_consistency_test.go
Tests switch to structured protocol/pin fixtures, remove obsolete constant builders and pre-rename cases, and update assertions to reference pin-derived wording.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • hatayama/unity-cli-loop#1412: Introduces the dispatcher minimum version guard checks that this PR's dispatcher pin-derivation logic modifies.
  • hatayama/unity-cli-loop#1413: Modifies the pin's minimumDispatcherVersion value consumed by the new CliPinReader and dispatcher release logic.
  • hatayama/unity-cli-loop#1417: Wires dispatcher-based compatibility checks that this PR routes through CliPinReader-derived minimum versions.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: reading minimum version requirements from the project runner pin.
Description check ✅ Passed The description is directly related to the changeset and accurately summarizes the refactor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/single-source-minimum-versions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli/release-automation/internal/automation/protocol_minimum_version_guard.go">

<violation number="1" location="cli/release-automation/internal/automation/protocol_minimum_version_guard.go:161">
P2: The protocol minimum guard now hard-fails every release verification error, including the known missing-contract bootstrap path, so the first renamed-tag migration can be blocked even when no protocol mismatch is readable. Consider restoring a narrow exemption for only the release-contract-missing bootstrap case while still surfacing real protocol/content mismatches.

(Based on your team's feedback about scoped bootstrap exemption for missing release contracts.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if !protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner(ctx, repoRoot, config.HeadRef, result, err) {
result.MinimumCliReleaseProtocolError = err.Error()
}
result.MinimumCliReleaseProtocolError = err.Error()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The protocol minimum guard now hard-fails every release verification error, including the known missing-contract bootstrap path, so the first renamed-tag migration can be blocked even when no protocol mismatch is readable. Consider restoring a narrow exemption for only the release-contract-missing bootstrap case while still surfacing real protocol/content mismatches.

(Based on your team's feedback about scoped bootstrap exemption for missing release contracts.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/release-automation/internal/automation/protocol_minimum_version_guard.go, line 161:

<comment>The protocol minimum guard now hard-fails every release verification error, including the known missing-contract bootstrap path, so the first renamed-tag migration can be blocked even when no protocol mismatch is readable. Consider restoring a narrow exemption for only the release-contract-missing bootstrap case while still surfacing real protocol/content mismatches.

(Based on your team's feedback about scoped bootstrap exemption for missing release contracts.) </comment>

<file context>
@@ -154,9 +158,7 @@ func AnalyzeProtocolMinimumVersionGuardForRefs(
-			if !protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner(ctx, repoRoot, config.HeadRef, result, err) {
-				result.MinimumCliReleaseProtocolError = err.Error()
-			}
+			result.MinimumCliReleaseProtocolError = err.Error()
 		}
 	}
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this one, deliberately. The removed exemption was not a general release-contract-missing escape hatch: it was gated on the base ref still using the deleted MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION constant (UsesPreRenameMinimumVersion), a precondition that can never be true again after this PR. All live uloop-project-runner-v3.0.0-beta.43..46 tags — including the current pin target — resolve their contract through the existing 3-generation runnerContractPathChain fallback and advertise protocolVersion, so there is no reachable legitimate missing-contract case today. If a future layout rename adds a fourth generation, the right fix is appending the new path to runnerContractPathChain in that same PR; silently exempting missing contracts would mask exactly that omission.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent comment was wrong here: that exemption was only reachable through the deleted UsesPreRenameMinimumVersion precondition, and the live tags already resolve through runnerContractPathChain. I agree the right fix for a future fourth-generation layout would be to add the new path in that PR, not exempt missing contracts.

This comment was influenced by this learning. Open the link to edit it, or reply here to edit or delete it.

Comment thread Packages/src/Editor/Application/CliSetupApplicationService.cs Outdated
Setup and installation detection each carried their own load-or-throw
copy of the same pin read, which invites drift in error handling. Both
now call CliPinReader.LoadMinimumDispatcherVersionOrThrow().
@hatayama
hatayama merged commit 18bf780 into v3-beta Jul 5, 2026
9 of 10 checks passed
@hatayama
hatayama deleted the refactor/single-source-minimum-versions branch July 5, 2026 06:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Assets/Tests/Editor/CliInstallationDetectorTests.cs (1)

222-237: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for the dispatcher-version comparison branch.

The updated call only exercises the !detection.IsDispatcher early-return path; the new minimumDispatcherVersion comparison branch (CliVersionComparer.IsVersionGreaterThanOrEqual(detection.Version, minimumDispatcherVersion)) added to IsShellDetectionUsableForPathSetup isn't covered by any test here.

✅ Suggested additional test
[Test]
public void IsShellDetectionUsableForPathSetup_WhenDispatcherVersionBelowMinimum_ReturnsFalse()
{
    CliInstallationDetection detection = new(
        "2.9.9",
        "/Users/ExampleUser/.npm-global/bin/uloop",
        isDispatcher: true);

    bool result = CliInstallationDetector.IsShellDetectionUsableForPathSetup(
        detection,
        UnityEngine.RuntimePlatform.OSXEditor,
        (path, platform) => false,
        "3.0.0");

    Assert.That(result, Is.False);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Assets/Tests/Editor/CliInstallationDetectorTests.cs` around lines 222 - 237,
Add a test that covers the dispatcher-version gate in
CliInstallationDetector.IsShellDetectionUsableForPathSetup, since the current
test only verifies the !detection.IsDispatcher early return. Create a
CliInstallationDetection with isDispatcher: true and a version lower than the
minimumDispatcherVersion argument, then assert the method returns false. Use the
existing test class and the IsShellDetectionUsableForPathSetup path to confirm
the CliVersionComparer.IsVersionGreaterThanOrEqual branch is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Packages/src/Editor/Application/CliPinReader.cs`:
- Around line 85-108: Wrap the JObject.Parse call in CliPinReader.LoadCliPin (or
the surrounding parsing path used by LoadPackagePin and
LoadMinimumDispatcherVersionOrThrow) so malformed pin JSON returns
CliPinLoadResult.FromFailure instead of throwing a raw JsonException. Keep the
existing failure pattern consistent with the missing-file, empty-content, and
missing-key checks by catching parse exceptions and including the pinPath plus a
descriptive parse error in the failure message.

---

Nitpick comments:
In `@Assets/Tests/Editor/CliInstallationDetectorTests.cs`:
- Around line 222-237: Add a test that covers the dispatcher-version gate in
CliInstallationDetector.IsShellDetectionUsableForPathSetup, since the current
test only verifies the !detection.IsDispatcher early return. Create a
CliInstallationDetection with isDispatcher: true and a version lower than the
minimumDispatcherVersion argument, then assert the method returns false. Use the
existing test class and the IsShellDetectionUsableForPathSetup path to confirm
the CliVersionComparer.IsVersionGreaterThanOrEqual branch is exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 545996d6-3282-4ee6-8fb7-a091a703b61b

📥 Commits

Reviewing files that changed from the base of the PR and between f39045e and b510c5a.

⛔ Files ignored due to path filters (1)
  • Packages/src/Editor/Application/CliPinReader.cs.meta is excluded by none and included by none
📒 Files selected for processing (16)
  • Assets/Tests/Editor/CliInstallationDetectorTests.cs
  • Assets/Tests/Editor/CliSetupApplicationServiceTests.cs
  • Assets/Tests/Editor/CliSetupCompatibilityTests.cs
  • Packages/src/Editor/Application/CliPinReader.cs
  • Packages/src/Editor/Application/CliSetupApplicationService.cs
  • Packages/src/Editor/Domain/CliConstants.cs
  • Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs
  • Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs
  • cli/common/clicontract/protocol_version_consistency_test.go
  • cli/dispatcher/internal/dispatcher/dispatcher_test.go
  • cli/release-automation/internal/automation/contract_file_at_ref_test.go
  • cli/release-automation/internal/automation/dispatcher_minimum_version_guard.go
  • cli/release-automation/internal/automation/dispatcher_minimum_version_guard_test.go
  • cli/release-automation/internal/automation/protocol_minimum_version_guard.go
  • cli/release-automation/internal/automation/protocol_minimum_version_guard_test.go
  • cli/release-automation/internal/automation/protocol_minimum_version_parse.go
💤 Files with no reviewable changes (1)
  • Packages/src/Editor/Domain/CliConstants.cs

Comment on lines +85 to +108
string content = File.ReadAllText(pinPath);
if (string.IsNullOrWhiteSpace(content))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is empty.");
}

JObject parsed = JObject.Parse(content);
string projectRunnerVersion = parsed[PIN_JSON_PROJECT_RUNNER_VERSION_KEY]?.ToString();
string minimumDispatcherVersion = parsed[PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY]?.ToString();
if (string.IsNullOrWhiteSpace(projectRunnerVersion))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is missing {PIN_JSON_PROJECT_RUNNER_VERSION_KEY}.");
}
if (string.IsNullOrWhiteSpace(minimumDispatcherVersion))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is missing {PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY}.");
}

return CliPinLoadResult.FromSuccess(
new CliPin(projectRunnerVersion, minimumDispatcherVersion));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unwrap JObject.Parse failures into CliPinLoadResult.FromFailure.

Every other failure mode here (missing file, empty content, missing keys) is surfaced via CliPinLoadResult.FromFailure with a descriptive message. A malformed pin JSON, however, will let JObject.Parse throw a raw JsonException straight through LoadPackagePin() / LoadMinimumDispatcherVersionOrThrow(), bypassing the friendly error wrapping and surfacing an opaque stack trace during setup instead of "Unity CLI Loop cannot resolve minimum dispatcher version from the package pin: ...". CliInstallationDetector.ParseCliContractOutput already catches JsonException for similarly untrusted content, so this would keep the pattern consistent.

🛡️ Proposed fix
-            JObject parsed = JObject.Parse(content);
-            string projectRunnerVersion = parsed[PIN_JSON_PROJECT_RUNNER_VERSION_KEY]?.ToString();
-            string minimumDispatcherVersion = parsed[PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY]?.ToString();
+            JObject parsed;
+            try
+            {
+                parsed = JObject.Parse(content);
+            }
+            catch (Newtonsoft.Json.JsonException ex)
+            {
+                return CliPinLoadResult.FromFailure(
+                    $"Unity CLI Loop pin file at {pinPath} contains invalid JSON: {ex.Message}");
+            }
+            string projectRunnerVersion = parsed[PIN_JSON_PROJECT_RUNNER_VERSION_KEY]?.ToString();
+            string minimumDispatcherVersion = parsed[PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY]?.ToString();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
string content = File.ReadAllText(pinPath);
if (string.IsNullOrWhiteSpace(content))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is empty.");
}
JObject parsed = JObject.Parse(content);
string projectRunnerVersion = parsed[PIN_JSON_PROJECT_RUNNER_VERSION_KEY]?.ToString();
string minimumDispatcherVersion = parsed[PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY]?.ToString();
if (string.IsNullOrWhiteSpace(projectRunnerVersion))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is missing {PIN_JSON_PROJECT_RUNNER_VERSION_KEY}.");
}
if (string.IsNullOrWhiteSpace(minimumDispatcherVersion))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is missing {PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY}.");
}
return CliPinLoadResult.FromSuccess(
new CliPin(projectRunnerVersion, minimumDispatcherVersion));
}
string content = File.ReadAllText(pinPath);
if (string.IsNullOrWhiteSpace(content))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is empty.");
}
JObject parsed;
try
{
parsed = JObject.Parse(content);
}
catch (Newtonsoft.Json.JsonException ex)
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} contains invalid JSON: {ex.Message}");
}
string projectRunnerVersion = parsed[PIN_JSON_PROJECT_RUNNER_VERSION_KEY]?.ToString();
string minimumDispatcherVersion = parsed[PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY]?.ToString();
if (string.IsNullOrWhiteSpace(projectRunnerVersion))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is missing {PIN_JSON_PROJECT_RUNNER_VERSION_KEY}.");
}
if (string.IsNullOrWhiteSpace(minimumDispatcherVersion))
{
return CliPinLoadResult.FromFailure(
$"Unity CLI Loop pin file at {pinPath} is missing {PIN_JSON_MINIMUM_DISPATCHER_VERSION_KEY}.");
}
return CliPinLoadResult.FromSuccess(
new CliPin(projectRunnerVersion, minimumDispatcherVersion));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Packages/src/Editor/Application/CliPinReader.cs` around lines 85 - 108, Wrap
the JObject.Parse call in CliPinReader.LoadCliPin (or the surrounding parsing
path used by LoadPackagePin and LoadMinimumDispatcherVersionOrThrow) so
malformed pin JSON returns CliPinLoadResult.FromFailure instead of throwing a
raw JsonException. Keep the existing failure pattern consistent with the
missing-file, empty-content, and missing-key checks by catching parse exceptions
and including the pinPath plus a descriptive parse error in the failure message.

Source: Learnings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant