Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,13 @@ reviews:
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
path_instructions:
- path: "**/*"
instructions: |
For changes that touch the Unity package/native CLI IPC contract, explicitly check whether the integer protocol generation must be bumped. Relevant IPC surfaces include JSON-RPC method names, request/response shapes, request metadata under `uloop`, readiness/dispatch/heartbeat handshakes, tool catalog schema, `cli/internal/unityipc/**`, `Packages/src/Editor/Infrastructure/Api/**`, `cli/contract.json`, and `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION`.

The compatibility gate is an exact protocol match, not a release-semver range. If a change makes a CLI/package from the previous protocol generation unable to interoperate, require both `cli/contract.json` `protocolVersion` and `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION` to be incremented in the same PR.

Do not request a protocol bump for ordinary CLI features, bug fixes, UI changes, documentation, or additive wire-format changes that older counterparts can safely ignore. `cliVersion`, `default-tools.json` `version`, and `CliConstants.MINIMUM_REQUIRED_CLI_VERSION` are release/install concerns and must not be hand-edited to keep up with feature PRs.
chat:
auto_reply: true
16 changes: 5 additions & 11 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,8 @@ jobs:
go-version-file: cli/.go-version
cache: false

- name: Check CLI minimum version bump
if: github.event_name == 'pull_request'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_BASE_REF: ${{ github.base_ref }}
CLI_MINIMUM_VERSION_FAIL_ON_WARNING: true
run: scripts/check-cli-minimum-version-warning.sh

- name: Test release automation shell helpers
run: |
scripts/test-comment-cli-minimum-version-warning.sh
scripts/test-cli-minimum-version-warning-workflow.sh
scripts/test-go-cli-toolchain.sh
scripts/test-resolve-native-cli-release-target.sh
scripts/test-is-release-please-release-commit.sh
Expand All @@ -54,6 +43,11 @@ jobs:
scripts/test-native-cli-publish-workflow.sh
scripts/test-install-release-filter.sh

- name: Check IPC protocol reminder
if: github.event_name == 'pull_request'
working-directory: cli
run: go run ./cmd/check-ipc-protocol-reminder --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
Expand Down
38 changes: 0 additions & 38 deletions .github/workflows/cli-minimum-version-warning.yml

This file was deleted.

35 changes: 30 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,36 @@ Comments in the code, commit messages, PR titles, and PR descriptions must all b

Every test method must have a short comment that states what behavior the test verifies.

## Commit-Time Version Checks

Before committing a C# package version bump, check whether `CliConstants.MINIMUM_REQUIRED_CLI_VERSION` must also change.
If the package release depends on behavior from a newer native CLI contract, update the minimum required CLI version and add or update focused tests.
If the minimum CLI version stays unchanged, make sure that decision is intentional before committing.
## CLI / Unity Package Compatibility

Runtime compatibility between the Unity package and the native CLI is gated on an integer
protocol version, not on release numbers. Two declarations must always stay equal:

- Go side: `protocolVersion` in `cli/contract.json` (the generation the CLI advertises over IPC).
- C# side: `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION` (the exact generation the package accepts).

`TestProtocolVersionMatchesUnityPackage` fails the build if they diverge, so never bump one alone.
The runtime gate expects equality because the protocol version is a contract generation, not a
minimum-compatible range.
Pull request CI also runs a non-blocking IPC protocol reminder when IPC-facing files changed
without protocol declaration changes; treat it as a review prompt, not as proof that a bump is
required.

Bump both, together, in the same PR only when the IPC contract changes in a way that makes
CLI and package builds from different protocol generations unable to interoperate — for example renaming
or removing a request field, changing the readiness/dispatch handshake, or altering a response
shape the other side parses. Ordinary CLI features and bug fixes that keep the wire format
compatible must not bump it.

Do not touch the protocol version to "keep up with releases":

- `cli/contract.json` `cliVersion` and `cli/internal/tools/default-tools.json` `version` are
stamped by release-please only. Never edit them by hand in a feature PR.
- `CliConstants.MINIMUM_REQUIRED_CLI_VERSION` is the release that setup installs. It must always
point at a published CLI release, so it advances after a release, never inside a feature PR.
- Runtime protocol mismatch guidance must not pin updates to `MINIMUM_REQUIRED_CLI_VERSION`; that
value may be older than the protocol the package now requires. Use the unpinned CLI update path
for older clients and tell newer clients to align the package and CLI releases.

## Generated Skill Files

Expand Down
80 changes: 79 additions & 1 deletion Assets/Tests/Editor/CliInstallationDetectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ public void SelectPreferredDetection_WhenShellCommandShadowsPackageOwnedCliUsesS
// Verifies that the settings UI reports the same CLI command the user's terminal runs.
CliInstallationDetection packageOwnedDetection = new(
"3.0.0-beta.3",
1,
"/Users/ExampleUser/.local/bin/uloop");
CliInstallationDetection shellDetection = new(
"2.1.0",
null,
"/Users/ExampleUser/.npm-global/bin/uloop");

CliInstallationDetection result = CliInstallationDetector.SelectPreferredDetection(
Expand All @@ -36,8 +38,10 @@ public void SelectPreferredDetection_WhenShellCommandMissingUsesPackageOwnedCli(
// Verifies that package-owned installs still count when the shell cannot resolve uloop.
CliInstallationDetection packageOwnedDetection = new(
"3.0.0-beta.3",
1,
"/Users/ExampleUser/.local/bin/uloop");
CliInstallationDetection shellDetection = new(
null,
null,
null);

Expand All @@ -55,8 +59,10 @@ public void SelectPreferredDetection_WhenShellCommandExistsButVersionFailsUsesSh
// Verifies that a broken PATH command is surfaced instead of hidden by the package-owned binary.
CliInstallationDetection packageOwnedDetection = new(
"3.0.0-beta.3",
1,
"/Users/ExampleUser/.local/bin/uloop");
CliInstallationDetection shellDetection = new(
null,
null,
"/Users/ExampleUser/.npm-global/bin/uloop");

Expand All @@ -73,10 +79,12 @@ public void SelectPreferredDetection_WhenPackageOwnedCliMissingUsesShellPath()
{
// Verifies that legacy CLI installs still surface as update candidates.
CliInstallationDetection packageOwnedDetection = new(
null,
null,
"/Users/ExampleUser/.local/bin/uloop");
CliInstallationDetection shellDetection = new(
"2.1.0",
null,
"/Users/ExampleUser/.npm-global/bin/uloop");

CliInstallationDetection result = CliInstallationDetector.SelectPreferredDetection(
Expand All @@ -93,9 +101,11 @@ public void SelectPreferredDetection_WhenShellVersionExistsWithoutPathUsesShellV
// Verifies that installed state does not depend on command path availability.
CliInstallationDetection packageOwnedDetection = new(
"3.0.0-beta.3",
1,
"/Users/ExampleUser/.local/bin/uloop");
CliInstallationDetection shellDetection = new(
"2.1.0",
null,
null);

CliInstallationDetection result = CliInstallationDetector.SelectPreferredDetection(
Expand All @@ -113,10 +123,12 @@ public void BuildShellCliDetectionCommand_UsesShortVersionFlag()
string command = CliInstallationDetector.BuildShellCliDetectionCommand("uloop");

Assert.That(command, Does.Contain("command -v uloop"));
Assert.That(command, Does.Contain("uloop --version --json"));
Assert.That(command, Does.Contain("uloop_contract_status=$?"));
Assert.That(command, Does.Contain("uloop -v"));
Assert.That(command, Does.Contain("uloop_version_status=$?"));
Assert.That(command, Does.Contain("__ULOOP_CONTRACT_STATUS_START__"));
Assert.That(command, Does.Contain("__ULOOP_VERSION_STATUS_START__"));
Assert.That(command, Does.Not.Contain("uloop --version"));
}

[Test]
Expand All @@ -128,6 +140,7 @@ public void BuildShellCliDetectionCommandForShell_WhenRuntimeShellIsFish_UsesFis
"/opt/homebrew/bin/fish");

Assert.That(command, Does.Contain("set uloop_version_status $status"));
Assert.That(command, Does.Contain("set uloop_contract_status $status"));
Assert.That(command, Does.Not.Contain("uloop_version_status=$?"));
}

Expand All @@ -139,6 +152,12 @@ public void ParseShellCliInstallationOutput_WhenPathAndVersionExist_ReturnsDetec
+ "__ULOOP_PATH_START__\n"
+ "/Users/ExampleUser/.npm-global/bin/uloop\n"
+ "__ULOOP_PATH_END__\n"
+ "__ULOOP_CONTRACT_START__\n"
+ "unknown option: --json\n"
+ "__ULOOP_CONTRACT_END__\n"
+ "__ULOOP_CONTRACT_STATUS_START__\n"
+ "1\n"
+ "__ULOOP_CONTRACT_STATUS_END__\n"
+ "__ULOOP_VERSION_START__\n"
+ "2.1.1\n"
+ "__ULOOP_VERSION_END__\n"
Expand All @@ -150,9 +169,66 @@ public void ParseShellCliInstallationOutput_WhenPathAndVersionExist_ReturnsDetec
CliInstallationDetector.ParseShellCliInstallationOutput(output);

Assert.That(detection.Version, Is.EqualTo("2.1.1"));
Assert.That(detection.ProtocolVersion, Is.Null);
Assert.That(detection.ExecutablePath, Is.EqualTo("/Users/ExampleUser/.npm-global/bin/uloop"));
}

[Test]
public void ParseShellCliInstallationOutput_WhenContractJsonExists_ReturnsProtocolDetection()
{
// Verifies that new CLIs expose protocol metadata for setup compatibility checks.
string output = "__ULOOP_PATH_START__\n"
+ "/Users/ExampleUser/.npm-global/bin/uloop\n"
+ "__ULOOP_PATH_END__\n"
+ "__ULOOP_CONTRACT_START__\n"
+ "{\"cliVersion\":\"3.0.0-beta.31\",\"protocolVersion\":1}\n"
+ "__ULOOP_CONTRACT_END__\n"
+ "__ULOOP_CONTRACT_STATUS_START__\n"
+ "0\n"
+ "__ULOOP_CONTRACT_STATUS_END__\n"
+ "__ULOOP_VERSION_START__\n"
+ "2.1.1\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-beta.31"));
Assert.That(detection.ProtocolVersion, Is.EqualTo(1));
Assert.That(detection.ExecutablePath, Is.EqualTo("/Users/ExampleUser/.npm-global/bin/uloop"));
}

[Test]
public void ParseShellCliInstallationOutput_WhenProtocolVersionIsOutsideIntRange_ReturnsVersionWithoutProtocol()
{
// Verifies oversized protocol metadata cannot break setup compatibility detection.
string output = "__ULOOP_PATH_START__\n"
+ "/tmp/uloop\n"
+ "__ULOOP_PATH_END__\n"
+ "__ULOOP_CONTRACT_START__\n"
+ "{\"cliVersion\":\"3.0.0-beta.31\",\"protocolVersion\":2147483648}\n"
+ "__ULOOP_CONTRACT_END__\n"
+ "__ULOOP_CONTRACT_STATUS_START__\n"
+ "0\n"
+ "__ULOOP_CONTRACT_STATUS_END__\n"
+ "__ULOOP_VERSION_START__\n"
+ "2.1.1\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-beta.31"));
Assert.That(detection.ProtocolVersion, Is.Null);
Assert.That(detection.ExecutablePath, Is.EqualTo("/tmp/uloop"));
}

[Test]
public void ParseShellCliInstallationOutput_WhenOnlyVersionExists_ReturnsInstalledDetection()
{
Expand All @@ -170,6 +246,7 @@ public void ParseShellCliInstallationOutput_WhenOnlyVersionExists_ReturnsInstall
CliInstallationDetector.ParseShellCliInstallationOutput(output);

Assert.That(detection.Version, Is.EqualTo("2.1.1"));
Assert.That(detection.ProtocolVersion, Is.Null);
Assert.That(detection.ExecutablePath, Is.Null);
}

Expand All @@ -191,6 +268,7 @@ public void ParseShellCliInstallationOutput_WhenVersionCommandFails_ReturnsPathW
CliInstallationDetector.ParseShellCliInstallationOutput(output);

Assert.That(detection.Version, Is.Null);
Assert.That(detection.ProtocolVersion, Is.Null);
Assert.That(detection.ExecutablePath, Is.EqualTo("/Users/ExampleUser/.npm-global/bin/uloop"));
}

Expand Down
1 change: 1 addition & 0 deletions Assets/Tests/Editor/CliPathSetupFlowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public FakeCliInstallationDetector(params bool[] visibilityResults)

public bool IsCliInstalled() => true;
public string GetCachedCliVersion() => "3.0.0-beta.14";
public int? GetCachedCliProtocolVersion() => 1;
public string GetCachedCliExecutablePath() => "/Users/ExampleUser/.local/bin/uloop";
public bool IsCheckCompleted() => true;
public Task RefreshCliVersionAsync(CancellationToken ct) => Task.CompletedTask;
Expand Down
1 change: 1 addition & 0 deletions Assets/Tests/Editor/CliSetupApplicationServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public FakeCliInstallationDetector(string[] versions)
}
public bool IsCliInstalled() => GetCachedCliVersion() != null;
public string GetCachedCliVersion() => _versions[_versionIndex];
public int? GetCachedCliProtocolVersion() => null;
public string GetCachedCliExecutablePath() => "";
public bool IsCheckCompleted() => true;
public Task RefreshCliVersionAsync(CancellationToken ct) => Task.CompletedTask;
Expand Down
28 changes: 28 additions & 0 deletions Assets/Tests/Editor/CliVersionComparerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ public void IsVersionLessThan_ReturnsExpectedResult(
Assert.That(result, Is.EqualTo(expected));
}

[TestCase("3.0.0-beta.2", "3.0.0-beta.1", true)]
[TestCase("3.0.0-beta.1", "3.0.0-beta.1", false)]
[TestCase("3.0.0-beta.0", "3.0.0-beta.1", false)]
public void IsVersionGreaterThan_ReturnsExpectedResult(
string leftVersion,
string rightVersion,
bool expected)
{
// Verifies strict greater-than comparison for CLI setup downgrade detection.
bool result = CliVersionComparer.IsVersionGreaterThan(leftVersion, rightVersion);

Assert.That(result, Is.EqualTo(expected));
}

[TestCase("3.0.0-beta.1", "3.0.0-beta.1", true)]
[TestCase("v3.0.0-beta.1", "3.0.0-beta.1", true)]
[TestCase("3.0.0-beta.2", "3.0.0-beta.1", false)]
public void IsVersionEqual_ReturnsExpectedResult(
string leftVersion,
string rightVersion,
bool expected)
{
// Verifies semantic equality comparison for CLI setup exact-match detection.
bool result = CliVersionComparer.IsVersionEqual(leftVersion, rightVersion);

Assert.That(result, Is.EqualTo(expected));
}

[Test]
public void IsVersionGreaterThanOrEqual_WhenVersionIsInvalid_ReturnsFalse()
{
Expand Down
Loading
Loading