From bb8fcb0d1a0eb9910515e2bffd87b4454187b409 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 20:07:02 +0900 Subject: [PATCH 01/18] Gate package release commit matching on release-please subjects The release commit search matched any commit whose pathspec-limited diff re-added the manifest entry and the changelog heading. A commit that moves a package changelog always re-adds every changelog line in that diff because the rename source falls outside the pathspec, so a restructure commit could impersonate the release commit and break the sync against the published release target. Reuse is-release-please-release-commit.sh so only release-please subjects qualify, ahead of the directory split that moves cli/CHANGELOG.md. --- .../sync-release-please-package-releases.sh | 14 +++++- ...st-sync-release-please-package-releases.sh | 43 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/scripts/sync-release-please-package-releases.sh b/scripts/sync-release-please-package-releases.sh index b23764bcc7..2186797210 100755 --- a/scripts/sync-release-please-package-releases.sh +++ b/scripts/sync-release-please-package-releases.sh @@ -2,6 +2,9 @@ set -eu ROOT_DIR=${ULOOP_REPO_ROOT:-$(CDPATH= cd "$(dirname "$0")/.." && pwd)} +# SCRIPT_DIR must resolve from $0, not ROOT_DIR: ULOOP_REPO_ROOT may point at a +# repository that does not contain the helper scripts this script calls. +SCRIPT_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) CONFIG="$ROOT_DIR/release-please-config.json" MANIFEST="$ROOT_DIR/.release-please-manifest.json" CLI_PACKAGE_PATH="cli" @@ -69,8 +72,15 @@ release_commit_sha_for_package() { version=$2 changelog_path=$3 - git log --format=%H HEAD | - while IFS= read -r commit_sha; do + # Only release-please release commits may qualify. Content matching alone is + # not enough: a commit that moves a package changelog re-adds every changelog + # line in the pathspec-limited diff (the rename source is outside the + # pathspec), so it would otherwise impersonate the release commit. + git log --format='%H%x09%s' HEAD | + while IFS=' ' read -r commit_sha commit_subject; do + if ! "$SCRIPT_DIR/is-release-please-release-commit.sh" "$commit_subject"; then + continue + fi if release_commit_updates_package_version "$commit_sha" "$package_path" "$version" "$changelog_path"; then printf '%s\n' "$commit_sha" break diff --git a/scripts/test-sync-release-please-package-releases.sh b/scripts/test-sync-release-please-package-releases.sh index 7a5fca7889..2e8aa7b2e5 100755 --- a/scripts/test-sync-release-please-package-releases.sh +++ b/scripts/test-sync-release-please-package-releases.sh @@ -327,6 +327,37 @@ create_key_rename_repo() { printf '%s\n' "$work_dir" } +create_changelog_move_repo() { + name=$1 + work_dir="$TMP_DIR/$name" + mkdir -p "$work_dir" + + ( + cd "$work_dir" + git init -q + git config user.email "test@example.com" + git config user.name "Test User" + # A pathspec-limited diff hides the rename source, so a moved changelog + # always reappears as fully added lines. Rename detection is additionally + # disabled to keep that behavior deterministic across git versions. + git config diff.renames false + + write_release_files 3.0.0-beta.6 "." "CHANGELOG.md" + mv Packages/src/CHANGELOG.md CHANGELOG.md + git add . + git commit -q -m "chore: release v3-beta" + git tag uloop-project-runner-v3.0.0-beta.6 + git rev-parse HEAD > "$work_dir/release-sha.txt" + + write_release_files 3.0.0-beta.6 + git rm -q CHANGELOG.md + git add . + git commit -q -m "chore: move unity-package release root" + ) + + printf '%s\n' "$work_dir" +} + prepare_origin_branch() { work_dir=$1 branch_name=$2 @@ -543,6 +574,17 @@ test_key_rename_commit_is_not_treated_as_release_commit() { assert_not_contains "$work_dir/stderr.txt" "points at" } +# Verifies a package-root move that relocates the changelog file is not mistaken for the release-please release commit even when git rename detection is unavailable. +test_changelog_move_commit_is_not_treated_as_release_commit() { + work_dir=$(create_changelog_move_repo changelog-move-root) + release_sha=$(cat "$work_dir/release-sha.txt") + + run_sync "$work_dir" v3.0.0-beta.6 false "$release_sha" + + assert_contains "$work_dir/output.txt" "Release v3.0.0-beta.6 already exists." + assert_not_contains "$work_dir/stderr.txt" "points at" +} + # Verifies a root package release created by another workflow during creation is reused. test_concurrent_root_release_creation_is_reused() { work_dir=$(create_release_repo concurrent-root-create) @@ -568,4 +610,5 @@ test_waits_for_dispatcher_assets_before_creating_root_release test_waits_when_dispatcher_asset_list_fails test_retries_until_cli_assets_are_ready test_key_rename_commit_is_not_treated_as_release_commit +test_changelog_move_commit_is_not_treated_as_release_commit test_concurrent_root_release_creation_is_reused From a6d7ce43e0992756562f3cd80bae3c6d3dc5aa43 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 20:09:41 +0900 Subject: [PATCH 02/18] Introduce a repo-root Go workspace ahead of the module split The directory split will turn cli into multiple Go modules (common, project-runner, dispatcher, release tooling). A workspace lets every module resolve its siblings locally without publishing, and landing it first pins that the existing single-module checks stay green with a go.work present. The go directive matches cli/go.mod so the patch-level toolchain pin stays solely in cli/.go-version. --- go.work | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.work diff --git a/go.work b/go.work new file mode 100644 index 0000000000..69a9de8cb1 --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.26 + +use ./cli From 0e467a8cec1795976e2dfc99f70203a841b8f0c3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 20:25:18 +0900 Subject: [PATCH 03/18] Extract shared CLI packages into an independent common module Move clicore, project, skills, tools, unityipc, and version out of cli/internal into a new common module that depends on no other module in the repo, so the compiler itself enforces the shared-code boundary once dispatcher and project-runner become separate modules. The runner contract (contract.json and its loader) moves to common/clicontract because common code (clicore, unityipc) and the dispatcher both consume the runner version and protocol generation, and neither may depend on the runner module; the dispatcher contract stays at the cli module root until the dispatcher module split. Release-please extra-files now point at the moved stamped files via repo-root-relative paths. --- .github/workflows/build-and-test.yml | 2 +- cli/contract.go | 39 ++---------- cli/contract_test.go | 21 +------ cli/go.mod | 9 ++- .../dispatcher_version_bump_guard.go | 2 +- .../protocol_minimum_version_parse.go | 2 +- .../dispatcher/bootstrap_platform_errors.go | 2 +- cli/internal/dispatcher/command_help.go | 4 +- cli/internal/dispatcher/completion.go | 2 +- cli/internal/dispatcher/completion_options.go | 2 +- cli/internal/dispatcher/completion_test.go | 2 +- .../dispatcher/dispatcher_download.go | 2 +- cli/internal/dispatcher/dispatcher_process.go | 2 +- cli/internal/dispatcher/dispatcher_test.go | 2 +- .../dispatcher/dispatcher_update_version.go | 2 +- cli/internal/dispatcher/dispatcher_version.go | 2 +- .../dispatcher/error_envelope_test.go | 2 +- cli/internal/dispatcher/help_test.go | 2 +- cli/internal/dispatcher/install.go | 2 +- cli/internal/dispatcher/install_test.go | 2 +- cli/internal/dispatcher/launch.go | 4 +- cli/internal/dispatcher/launch_focus_log.go | 2 +- cli/internal/dispatcher/launch_options.go | 2 +- .../launch_process_exit_timeout_error.go | 2 +- cli/internal/dispatcher/launch_ready.go | 2 +- .../launch_startup_timeout_error.go | 2 +- cli/internal/dispatcher/launch_test.go | 4 +- cli/internal/dispatcher/run_dispatcher.go | 6 +- cli/internal/dispatcher/run_help.go | 4 +- cli/internal/dispatcher/skills.go | 4 +- cli/internal/dispatcher/skills_discovery.go | 2 +- cli/internal/dispatcher/skills_dispatch.go | 2 +- cli/internal/dispatcher/skills_display.go | 2 +- cli/internal/dispatcher/skills_sync.go | 2 +- cli/internal/dispatcher/skills_targets.go | 2 +- .../dispatcher/skills_v3_migration.go | 2 +- cli/internal/dispatcher/uninstall.go | 2 +- cli/internal/dispatcher/update.go | 2 +- cli/internal/dispatcher/update_test.go | 2 +- cli/internal/projectrunner/compile_wait.go | 4 +- .../projectrunner/compile_wait_test.go | 4 +- .../projectrunner/connection_retry.go | 4 +- .../projectrunner/connection_retry_flow.go | 4 +- .../projectrunner/connection_retry_test.go | 4 +- .../projectrunner/control_play_mode_wait.go | 4 +- .../control_play_mode_wait_test.go | 4 +- cli/internal/projectrunner/debug_timing.go | 4 +- .../projectrunner/debug_timing_test.go | 4 +- .../projectrunner/dynamic_code_file.go | 2 +- .../projectrunner/dynamic_code_file_test.go | 2 +- .../projectrunner/dynamic_code_wait.go | 4 +- .../projectrunner/execution_errors.go | 2 +- cli/internal/projectrunner/list_output.go | 2 +- .../projectrunner/list_output_test.go | 2 +- .../projectrunner/native_tool_settings.go | 2 +- .../projectrunner/pause_point_errors.go | 2 +- .../projectrunner/pause_point_logs.go | 4 +- .../projectrunner/pause_point_wait.go | 4 +- .../projectrunner/pause_point_wait_test.go | 4 +- cli/internal/projectrunner/run.go | 6 +- cli/internal/projectrunner/run_test.go | 2 +- cli/internal/projectrunner/runner_commands.go | 6 +- cli/internal/projectrunner/runner_usage.go | 2 +- cli/internal/projectrunner/tool_params.go | 2 +- cli/internal/projectrunner/tools_test.go | 2 +- cli/internal/update/command.go | 2 +- common/clicontract/contract.go | 51 ++++++++++++++++ {cli => common/clicontract}/contract.json | 0 common/clicontract/contract_test.go | 61 +++++++++++++++++++ .../protocol_version_consistency_test.go | 10 +-- .../clicore/argument_error.go | 0 .../clicore/busy_status.go | 0 {cli/internal => common}/clicore/cli_vibe.go | 0 .../clicore/cli_vibe_test.go | 0 .../clicore/command_registry.go | 0 .../clicore/command_registry_test.go | 0 .../clicore/error_editor_unresponsive.go | 2 +- .../clicore/error_envelope.go | 2 +- .../clicore/error_envelope_classification.go | 2 +- .../clicore/error_envelope_test.go | 2 +- {cli/internal => common}/clicore/focus.go | 0 .../internal => common}/clicore/focus_test.go | 0 .../clicore/global_args.go | 0 {cli/internal => common}/clicore/output.go | 0 .../clicore/output_test.go | 0 .../clicore/runner_version.go | 2 +- .../clicore/server_not_responding.go | 2 +- .../clicore/skill_sources.go | 2 +- .../clicore/skills_packages.go | 0 .../clicore/skills_packages_test.go | 0 {cli/internal => common}/clicore/spinner.go | 2 +- .../clicore/spinner_test.go | 2 +- .../clicore/string_helpers.go | 0 .../clicore/tool_catalog.go | 2 +- .../clicore/tool_catalog_test.go | 0 .../clicore/tool_option_help.go | 0 .../clicore/tool_options.go | 0 .../clicore/tool_readiness.go | 4 +- .../clicore/tool_readiness_test.go | 2 +- .../clicore/tool_settings.go | 0 .../clicore/transport_errors.go | 0 .../clicore/transport_errors_test.go | 0 common/go.mod | 7 +++ common/go.sum | 4 ++ {cli/internal => common}/project/project.go | 2 +- .../project/project_test.go | 2 +- {cli/internal => common}/skills/source.go | 0 {cli/internal => common}/tools/catalog.go | 0 .../tools/default-tools.json | 0 {cli/internal => common}/tools/types.go | 0 {cli/internal => common}/unityipc/client.go | 2 +- .../unityipc/client_heartbeat_test.go | 0 .../unityipc/client_test.go | 2 +- .../unityipc/client_windows_test.go | 0 .../unityipc/connection.go | 0 .../internal => common}/unityipc/dial_unix.go | 0 .../unityipc/dial_windows.go | 0 {cli/internal => common}/unityipc/framing.go | 0 .../unityipc/framing_test.go | 0 {cli/internal => common}/unityipc/outcome.go | 0 {cli/internal => common}/version/compare.go | 0 .../version/compare_test.go | 0 go.work | 5 +- release-please-config.json | 4 +- scripts/check-go-cli-source.sh | 1 + scripts/test-release-please-config.sh | 4 +- 126 files changed, 254 insertions(+), 170 deletions(-) create mode 100644 common/clicontract/contract.go rename {cli => common/clicontract}/contract.json (100%) create mode 100644 common/clicontract/contract_test.go rename {cli => common/clicontract}/protocol_version_consistency_test.go (95%) rename {cli/internal => common}/clicore/argument_error.go (100%) rename {cli/internal => common}/clicore/busy_status.go (100%) rename {cli/internal => common}/clicore/cli_vibe.go (100%) rename {cli/internal => common}/clicore/cli_vibe_test.go (100%) rename {cli/internal => common}/clicore/command_registry.go (100%) rename {cli/internal => common}/clicore/command_registry_test.go (100%) rename {cli/internal => common}/clicore/error_editor_unresponsive.go (94%) rename {cli/internal => common}/clicore/error_envelope.go (99%) rename {cli/internal => common}/clicore/error_envelope_classification.go (98%) rename {cli/internal => common}/clicore/error_envelope_test.go (99%) rename {cli/internal => common}/clicore/focus.go (100%) rename {cli/internal => common}/clicore/focus_test.go (100%) rename {cli/internal => common}/clicore/global_args.go (100%) rename {cli/internal => common}/clicore/output.go (100%) rename {cli/internal => common}/clicore/output_test.go (100%) rename {cli/internal => common}/clicore/runner_version.go (85%) rename {cli/internal => common}/clicore/server_not_responding.go (95%) rename {cli/internal => common}/clicore/skill_sources.go (98%) rename {cli/internal => common}/clicore/skills_packages.go (100%) rename {cli/internal => common}/clicore/skills_packages_test.go (100%) rename {cli/internal => common}/clicore/spinner.go (98%) rename {cli/internal => common}/clicore/spinner_test.go (98%) rename {cli/internal => common}/clicore/string_helpers.go (100%) rename {cli/internal => common}/clicore/tool_catalog.go (96%) rename {cli/internal => common}/clicore/tool_catalog_test.go (100%) rename {cli/internal => common}/clicore/tool_option_help.go (100%) rename {cli/internal => common}/clicore/tool_options.go (100%) rename {cli/internal => common}/clicore/tool_readiness.go (97%) rename {cli/internal => common}/clicore/tool_readiness_test.go (98%) rename {cli/internal => common}/clicore/tool_settings.go (100%) rename {cli/internal => common}/clicore/transport_errors.go (100%) rename {cli/internal => common}/clicore/transport_errors_test.go (100%) create mode 100644 common/go.mod create mode 100644 common/go.sum rename {cli/internal => common}/project/project.go (99%) rename {cli/internal => common}/project/project_test.go (99%) rename {cli/internal => common}/skills/source.go (100%) rename {cli/internal => common}/tools/catalog.go (100%) rename {cli/internal => common}/tools/default-tools.json (100%) rename {cli/internal => common}/tools/types.go (100%) rename {cli/internal => common}/unityipc/client.go (99%) rename {cli/internal => common}/unityipc/client_heartbeat_test.go (100%) rename {cli/internal => common}/unityipc/client_test.go (99%) rename {cli/internal => common}/unityipc/client_windows_test.go (100%) rename {cli/internal => common}/unityipc/connection.go (100%) rename {cli/internal => common}/unityipc/dial_unix.go (100%) rename {cli/internal => common}/unityipc/dial_windows.go (100%) rename {cli/internal => common}/unityipc/framing.go (100%) rename {cli/internal => common}/unityipc/framing_test.go (100%) rename {cli/internal => common}/unityipc/outcome.go (100%) rename {cli/internal => common}/version/compare.go (100%) rename {cli/internal => common}/version/compare_test.go (100%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index aad6ada98d..383c8d0f75 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -100,7 +100,7 @@ jobs: # separators, missing .exe suffixes, TCP-only fixtures) that predate this job. - name: Test native Go CLI transport on Windows shell: bash - run: cd cli && go test ./internal/unityipc/ + run: cd common && go test ./unityipc/ - name: Test install release filter in Git Bash shell: bash diff --git a/cli/contract.go b/cli/contract.go index ace313b6b8..dd8623cecb 100644 --- a/cli/contract.go +++ b/cli/contract.go @@ -1,3 +1,6 @@ +// Package clicontract holds the dispatcher-side release contract. The runner-side contract +// has moved to common/clicontract; the dispatcher contract itself temporarily remains here at +// the cli module root and moves into the dispatcher module in a later step. package clicontract import ( @@ -7,26 +10,14 @@ import ( ) const ( - contractFileName = "contract.json" dispatcherContractFileName = "dispatcher-contract.json" schemaVersion = 1 ) -//go:embed contract.json dispatcher-contract.json +//go:embed dispatcher-contract.json var contractFiles embed.FS -var ( - Current = mustLoadContract() - DispatcherCurrent = mustLoadDispatcherContract() -) - -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"` - ProjectRunnerVersion string `json:"projectRunnerVersion"` -} +var DispatcherCurrent = mustLoadDispatcherContract() type DispatcherContract struct { SchemaVersion int `json:"schemaVersion"` @@ -38,26 +29,6 @@ type DispatcherContract struct { DispatcherContractVersion int `json:"dispatcherContractVersion"` } -func mustLoadContract() Contract { - content, err := contractFiles.ReadFile(contractFileName) - if err != nil { - panic(fmt.Sprintf("CLI contract is not embedded: %v", err)) - } - - var contract Contract - if err := json.Unmarshal(content, &contract); err != nil { - panic(fmt.Sprintf("CLI contract is invalid JSON: %v", err)) - } - if contract.SchemaVersion != schemaVersion { - panic(fmt.Sprintf("CLI contract schema version mismatch: %d", contract.SchemaVersion)) - } - requireString(contract.ProjectRunnerVersion, "projectRunnerVersion") - if contract.ProtocolVersion < 1 { - panic(fmt.Sprintf("CLI contract protocolVersion must be at least 1, got %d", contract.ProtocolVersion)) - } - return contract -} - func mustLoadDispatcherContract() DispatcherContract { content, err := contractFiles.ReadFile(dispatcherContractFileName) if err != nil { diff --git a/cli/contract_test.go b/cli/contract_test.go index 6f46a1b7f7..4cedc5eca2 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -4,21 +4,9 @@ import ( "encoding/json" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/version" + "github.com/hatayama/unity-cli-loop/common/version" ) -func TestCliContractProvidesRuntimeVersion(t *testing.T) { - // Verifies that the project runner owns its runtime version from the single CLI module. - requireValidContractVersion(t, "projectRunnerVersion", Current.ProjectRunnerVersion) -} - -func TestCliContractProvidesProtocolVersion(t *testing.T) { - // Verifies that the contract declares which C#-side IPC protocol the binary speaks. - if Current.ProtocolVersion < 1 { - t.Fatalf("protocolVersion must be at least 1, got %d", Current.ProtocolVersion) - } -} - func TestCliContractProvidesDispatcherContractVersion(t *testing.T) { // Verifies that the contract declares which dispatcher capability generation the binary provides. if DispatcherCurrent.DispatcherContractVersion < 1 { @@ -31,13 +19,6 @@ func TestDispatcherContractProvidesRuntimeVersion(t *testing.T) { requireValidContractVersion(t, "dispatcherVersion", DispatcherCurrent.DispatcherVersion) } -func TestCliContractDoesNotDeclareDispatcherReleaseFields(t *testing.T) { - // Verifies release-please CLI version stamping cannot accidentally move dispatcher release metadata. - fields := requireContractFieldMap(t, contractFileName) - requireContractFieldMissing(t, fields, "dispatcherVersion") - requireContractFieldMissing(t, fields, "dispatcherContractVersion") -} - func TestDispatcherContractDoesNotDeclareCliReleaseFields(t *testing.T) { // Verifies dispatcher releases stay independent from project-local CLI release metadata. fields := requireContractFieldMap(t, dispatcherContractFileName) diff --git a/cli/go.mod b/cli/go.mod index 5e44c02074..156c5ee482 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -2,6 +2,11 @@ module github.com/hatayama/unity-cli-loop/cli go 1.26 -require github.com/Microsoft/go-winio v0.6.2 +require github.com/hatayama/unity-cli-loop/common v0.0.0-00010101000000-000000000000 -require golang.org/x/sys v0.10.0 // indirect +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + golang.org/x/sys v0.10.0 // indirect +) + +replace github.com/hatayama/unity-cli-loop/common => ../common diff --git a/cli/internal/automation/dispatcher_version_bump_guard.go b/cli/internal/automation/dispatcher_version_bump_guard.go index c0e4a723ad..1abed4ad42 100644 --- a/cli/internal/automation/dispatcher_version_bump_guard.go +++ b/cli/internal/automation/dispatcher_version_bump_guard.go @@ -10,7 +10,7 @@ import ( "strconv" "strings" - sharedversion "github.com/hatayama/unity-cli-loop/cli/internal/version" + sharedversion "github.com/hatayama/unity-cli-loop/common/version" ) var dispatcherReleaseInputPatterns = []string{ diff --git a/cli/internal/automation/protocol_minimum_version_parse.go b/cli/internal/automation/protocol_minimum_version_parse.go index b9eeaa5c9b..298aa351c1 100644 --- a/cli/internal/automation/protocol_minimum_version_parse.go +++ b/cli/internal/automation/protocol_minimum_version_parse.go @@ -5,7 +5,7 @@ import ( "regexp" "strconv" - sharedversion "github.com/hatayama/unity-cli-loop/cli/internal/version" + sharedversion "github.com/hatayama/unity-cli-loop/common/version" ) var ( diff --git a/cli/internal/dispatcher/bootstrap_platform_errors.go b/cli/internal/dispatcher/bootstrap_platform_errors.go index fbe1ce7685..9ef437d7ef 100644 --- a/cli/internal/dispatcher/bootstrap_platform_errors.go +++ b/cli/internal/dispatcher/bootstrap_platform_errors.go @@ -1,6 +1,6 @@ package dispatcher -import "github.com/hatayama/unity-cli-loop/cli/internal/clicore" +import "github.com/hatayama/unity-cli-loop/common/clicore" // unsupportedPlatformError builds a clicore.CLIError for bootstrap commands (install, update, // uninstall) that fail because the current OS is not supported, matching on the diff --git a/cli/internal/dispatcher/command_help.go b/cli/internal/dispatcher/command_help.go index 69a851bbd7..f980baa213 100644 --- a/cli/internal/dispatcher/command_help.go +++ b/cli/internal/dispatcher/command_help.go @@ -4,8 +4,8 @@ import ( "io" "sort" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/project" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/project" ) func tryHandleCommandHelp(command string, startPath string, projectPath string, stdout io.Writer, stderr io.Writer) (bool, int) { diff --git a/cli/internal/dispatcher/completion.go b/cli/internal/dispatcher/completion.go index 47f5d321b5..0a7439fe17 100644 --- a/cli/internal/dispatcher/completion.go +++ b/cli/internal/dispatcher/completion.go @@ -11,7 +11,7 @@ import ( "sort" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) const ( diff --git a/cli/internal/dispatcher/completion_options.go b/cli/internal/dispatcher/completion_options.go index e0a59b7be9..4e8bda04c9 100644 --- a/cli/internal/dispatcher/completion_options.go +++ b/cli/internal/dispatcher/completion_options.go @@ -1,6 +1,6 @@ package dispatcher -import "github.com/hatayama/unity-cli-loop/cli/internal/clicore" +import "github.com/hatayama/unity-cli-loop/common/clicore" var nativeCommandOptions = map[string][]string{ clicore.CompletionCommand: {installCompletionFlag, shellFlag}, diff --git a/cli/internal/dispatcher/completion_test.go b/cli/internal/dispatcher/completion_test.go index f0a600af9d..3b36121a68 100644 --- a/cli/internal/dispatcher/completion_test.go +++ b/cli/internal/dispatcher/completion_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func TestCompletionListCommandsIncludesNativeCommandsAndDefaultTools(t *testing.T) { diff --git a/cli/internal/dispatcher/dispatcher_download.go b/cli/internal/dispatcher/dispatcher_download.go index 003336e449..6d41f06d3c 100644 --- a/cli/internal/dispatcher/dispatcher_download.go +++ b/cli/internal/dispatcher/dispatcher_download.go @@ -18,8 +18,8 @@ import ( "strings" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" sharedupdate "github.com/hatayama/unity-cli-loop/cli/internal/update" + "github.com/hatayama/unity-cli-loop/common/clicore" ) var dispatcherHTTPClient = &http.Client{Timeout: 2 * time.Minute} diff --git a/cli/internal/dispatcher/dispatcher_process.go b/cli/internal/dispatcher/dispatcher_process.go index 6ae3cd981d..ac43425143 100644 --- a/cli/internal/dispatcher/dispatcher_process.go +++ b/cli/internal/dispatcher/dispatcher_process.go @@ -4,7 +4,7 @@ import ( "context" "io" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) // tryHandleProjectScopeHelpRequest serves project-scoped help locally; help diff --git a/cli/internal/dispatcher/dispatcher_test.go b/cli/internal/dispatcher/dispatcher_test.go index 5a300385e1..67c9eb7dec 100644 --- a/cli/internal/dispatcher/dispatcher_test.go +++ b/cli/internal/dispatcher/dispatcher_test.go @@ -18,7 +18,7 @@ import ( "testing" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) type dispatcherArchiveTestEntry struct { diff --git a/cli/internal/dispatcher/dispatcher_update_version.go b/cli/internal/dispatcher/dispatcher_update_version.go index 4b9dc0c655..df89375cf0 100644 --- a/cli/internal/dispatcher/dispatcher_update_version.go +++ b/cli/internal/dispatcher/dispatcher_update_version.go @@ -8,7 +8,7 @@ import ( "os" "os/exec" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) var dispatcherReadInstalledVersion = readInstalledDispatcherVersion diff --git a/cli/internal/dispatcher/dispatcher_version.go b/cli/internal/dispatcher/dispatcher_version.go index c336636765..a43a48378e 100644 --- a/cli/internal/dispatcher/dispatcher_version.go +++ b/cli/internal/dispatcher/dispatcher_version.go @@ -5,7 +5,7 @@ import ( "io" clicontract "github.com/hatayama/unity-cli-loop/cli" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) var ( diff --git a/cli/internal/dispatcher/error_envelope_test.go b/cli/internal/dispatcher/error_envelope_test.go index 448e1b5772..ad01bf4fec 100644 --- a/cli/internal/dispatcher/error_envelope_test.go +++ b/cli/internal/dispatcher/error_envelope_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func TestClassifyLaunchStartupTimeoutError(t *testing.T) { diff --git a/cli/internal/dispatcher/help_test.go b/cli/internal/dispatcher/help_test.go index 9fa749d76a..d8cd4f56b6 100644 --- a/cli/internal/dispatcher/help_test.go +++ b/cli/internal/dispatcher/help_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) // Tests that launcher help lists native commands and live-tool discovery guidance without baked-in tools. diff --git a/cli/internal/dispatcher/install.go b/cli/internal/dispatcher/install.go index 964b8a71b4..69d05d0567 100644 --- a/cli/internal/dispatcher/install.go +++ b/cli/internal/dispatcher/install.go @@ -9,8 +9,8 @@ import ( "runtime" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" "github.com/hatayama/unity-cli-loop/cli/internal/install" + "github.com/hatayama/unity-cli-loop/common/clicore" ) const ( diff --git a/cli/internal/dispatcher/install_test.go b/cli/internal/dispatcher/install_test.go index 5f40992a3f..68fae326fb 100644 --- a/cli/internal/dispatcher/install_test.go +++ b/cli/internal/dispatcher/install_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func TestRunDispatcherInstallHelpDoesNotRequireUnityProject(t *testing.T) { diff --git a/cli/internal/dispatcher/launch.go b/cli/internal/dispatcher/launch.go index f02215f879..335b6ec57f 100644 --- a/cli/internal/dispatcher/launch.go +++ b/cli/internal/dispatcher/launch.go @@ -13,8 +13,8 @@ import ( "strings" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/project" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/project" ) const ( diff --git a/cli/internal/dispatcher/launch_focus_log.go b/cli/internal/dispatcher/launch_focus_log.go index d725587eb4..8d51df3e28 100644 --- a/cli/internal/dispatcher/launch_focus_log.go +++ b/cli/internal/dispatcher/launch_focus_log.go @@ -3,7 +3,7 @@ package dispatcher import ( "context" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func logLaunchExistingFocus(ctx context.Context, projectRoot string, pid int) { diff --git a/cli/internal/dispatcher/launch_options.go b/cli/internal/dispatcher/launch_options.go index 794cfa54c8..a4d5752d22 100644 --- a/cli/internal/dispatcher/launch_options.go +++ b/cli/internal/dispatcher/launch_options.go @@ -4,7 +4,7 @@ import ( "strconv" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func applyLaunchOption(options *launchOptions, args []string, index int) (int, error) { diff --git a/cli/internal/dispatcher/launch_process_exit_timeout_error.go b/cli/internal/dispatcher/launch_process_exit_timeout_error.go index ef27c32328..ea15089707 100644 --- a/cli/internal/dispatcher/launch_process_exit_timeout_error.go +++ b/cli/internal/dispatcher/launch_process_exit_timeout_error.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) type launchProcessExitTimeoutError struct { diff --git a/cli/internal/dispatcher/launch_ready.go b/cli/internal/dispatcher/launch_ready.go index 2b22e23cf1..5104e27931 100644 --- a/cli/internal/dispatcher/launch_ready.go +++ b/cli/internal/dispatcher/launch_ready.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) const ( diff --git a/cli/internal/dispatcher/launch_startup_timeout_error.go b/cli/internal/dispatcher/launch_startup_timeout_error.go index 6caa30db32..a675ee9bb1 100644 --- a/cli/internal/dispatcher/launch_startup_timeout_error.go +++ b/cli/internal/dispatcher/launch_startup_timeout_error.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) type launchStartupTimeoutError struct { diff --git a/cli/internal/dispatcher/launch_test.go b/cli/internal/dispatcher/launch_test.go index a4e3eecb2e..e639fbfef7 100644 --- a/cli/internal/dispatcher/launch_test.go +++ b/cli/internal/dispatcher/launch_test.go @@ -13,8 +13,8 @@ import ( "testing" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) func TestParseLaunchOptionsSupportsCoreFlags(t *testing.T) { diff --git a/cli/internal/dispatcher/run_dispatcher.go b/cli/internal/dispatcher/run_dispatcher.go index 19479969e2..41beb36179 100644 --- a/cli/internal/dispatcher/run_dispatcher.go +++ b/cli/internal/dispatcher/run_dispatcher.go @@ -12,10 +12,10 @@ import ( "strings" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/project" sharedupdate "github.com/hatayama/unity-cli-loop/cli/internal/update" - sharedversion "github.com/hatayama/unity-cli-loop/cli/internal/version" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/project" + sharedversion "github.com/hatayama/unity-cli-loop/common/version" ) const ( diff --git a/cli/internal/dispatcher/run_help.go b/cli/internal/dispatcher/run_help.go index 1159d593b6..41cc5d37a5 100644 --- a/cli/internal/dispatcher/run_help.go +++ b/cli/internal/dispatcher/run_help.go @@ -5,8 +5,8 @@ import ( "os" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/project" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/project" ) const ( diff --git a/cli/internal/dispatcher/skills.go b/cli/internal/dispatcher/skills.go index 1b5d0968f1..0895dc81eb 100644 --- a/cli/internal/dispatcher/skills.go +++ b/cli/internal/dispatcher/skills.go @@ -6,8 +6,8 @@ import ( "path/filepath" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/project" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/project" ) const ( diff --git a/cli/internal/dispatcher/skills_discovery.go b/cli/internal/dispatcher/skills_discovery.go index 6b68106ba8..20b11b3dae 100644 --- a/cli/internal/dispatcher/skills_discovery.go +++ b/cli/internal/dispatcher/skills_discovery.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func collectSkillDefinitions(projectRoot string) ([]skillDefinition, error) { diff --git a/cli/internal/dispatcher/skills_dispatch.go b/cli/internal/dispatcher/skills_dispatch.go index af774024f0..84d69100d4 100644 --- a/cli/internal/dispatcher/skills_dispatch.go +++ b/cli/internal/dispatcher/skills_dispatch.go @@ -3,7 +3,7 @@ package dispatcher import ( "io" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func runV3MigrationSkillsSubcommand( diff --git a/cli/internal/dispatcher/skills_display.go b/cli/internal/dispatcher/skills_display.go index a7d4af9e64..b843a12e61 100644 --- a/cli/internal/dispatcher/skills_display.go +++ b/cli/internal/dispatcher/skills_display.go @@ -4,7 +4,7 @@ import ( "io" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func defaultSkillTargets() []skillTarget { diff --git a/cli/internal/dispatcher/skills_sync.go b/cli/internal/dispatcher/skills_sync.go index 64f8021ad5..7a2e8ae4e8 100644 --- a/cli/internal/dispatcher/skills_sync.go +++ b/cli/internal/dispatcher/skills_sync.go @@ -6,7 +6,7 @@ import ( "os" "path/filepath" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func syncSkillDirectory(sourceDir string, destinationDir string) error { diff --git a/cli/internal/dispatcher/skills_targets.go b/cli/internal/dispatcher/skills_targets.go index 6d4bbed826..559a038886 100644 --- a/cli/internal/dispatcher/skills_targets.go +++ b/cli/internal/dispatcher/skills_targets.go @@ -5,7 +5,7 @@ import ( "path/filepath" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) var userHomeDir = os.UserHomeDir diff --git a/cli/internal/dispatcher/skills_v3_migration.go b/cli/internal/dispatcher/skills_v3_migration.go index e8faf1ea46..f8e4a8dfb1 100644 --- a/cli/internal/dispatcher/skills_v3_migration.go +++ b/cli/internal/dispatcher/skills_v3_migration.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func runV3MigrationSkillInstall(projectRoot string, skills []skillDefinition, options skillCommandOptions, stdout io.Writer, stderr io.Writer) int { diff --git a/cli/internal/dispatcher/uninstall.go b/cli/internal/dispatcher/uninstall.go index 1ed63082f4..5eb1864cfd 100644 --- a/cli/internal/dispatcher/uninstall.go +++ b/cli/internal/dispatcher/uninstall.go @@ -9,8 +9,8 @@ import ( "path/filepath" "runtime" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" "github.com/hatayama/unity-cli-loop/cli/internal/uninstall" + "github.com/hatayama/unity-cli-loop/common/clicore" ) const ( diff --git a/cli/internal/dispatcher/update.go b/cli/internal/dispatcher/update.go index cf65c12fa4..f474267756 100644 --- a/cli/internal/dispatcher/update.go +++ b/cli/internal/dispatcher/update.go @@ -6,8 +6,8 @@ import ( "os/exec" "runtime" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" "github.com/hatayama/unity-cli-loop/cli/internal/update" + "github.com/hatayama/unity-cli-loop/common/clicore" ) const ( diff --git a/cli/internal/dispatcher/update_test.go b/cli/internal/dispatcher/update_test.go index b63155a2f2..d817230d6f 100644 --- a/cli/internal/dispatcher/update_test.go +++ b/cli/internal/dispatcher/update_test.go @@ -9,8 +9,8 @@ import ( "testing" clicontract "github.com/hatayama/unity-cli-loop/cli" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" "github.com/hatayama/unity-cli-loop/cli/internal/update" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func TestUpdateCommandForDarwinUsesDirectInstaller(t *testing.T) { diff --git a/cli/internal/projectrunner/compile_wait.go b/cli/internal/projectrunner/compile_wait.go index 1a47337169..1517e7d953 100644 --- a/cli/internal/projectrunner/compile_wait.go +++ b/cli/internal/projectrunner/compile_wait.go @@ -8,8 +8,8 @@ import ( "fmt" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/projectrunner/compile_wait_test.go b/cli/internal/projectrunner/compile_wait_test.go index 1ee85a02c6..5fdb36f694 100644 --- a/cli/internal/projectrunner/compile_wait_test.go +++ b/cli/internal/projectrunner/compile_wait_test.go @@ -12,8 +12,8 @@ import ( "testing" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) func TestEnsureCompileRequestIDPreservesSafeValue(t *testing.T) { diff --git a/cli/internal/projectrunner/connection_retry.go b/cli/internal/projectrunner/connection_retry.go index 1ac23e1edf..3bcf3b193d 100644 --- a/cli/internal/projectrunner/connection_retry.go +++ b/cli/internal/projectrunner/connection_retry.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) var ( diff --git a/cli/internal/projectrunner/connection_retry_flow.go b/cli/internal/projectrunner/connection_retry_flow.go index cb22d7c156..27966d13d7 100644 --- a/cli/internal/projectrunner/connection_retry_flow.go +++ b/cli/internal/projectrunner/connection_retry_flow.go @@ -4,8 +4,8 @@ import ( "context" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) func newConnectionRetryClient( diff --git a/cli/internal/projectrunner/connection_retry_test.go b/cli/internal/projectrunner/connection_retry_test.go index e66f0a0c4e..fa22263505 100644 --- a/cli/internal/projectrunner/connection_retry_test.go +++ b/cli/internal/projectrunner/connection_retry_test.go @@ -13,8 +13,8 @@ import ( "testing" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) // Verifies transient IPC connection failures focus Unity once and restore focus before reporting server-not-responding. diff --git a/cli/internal/projectrunner/control_play_mode_wait.go b/cli/internal/projectrunner/control_play_mode_wait.go index 5cfdc87f71..357f303016 100644 --- a/cli/internal/projectrunner/control_play_mode_wait.go +++ b/cli/internal/projectrunner/control_play_mode_wait.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/projectrunner/control_play_mode_wait_test.go b/cli/internal/projectrunner/control_play_mode_wait_test.go index 2ff4e4653d..9090c2ef3c 100644 --- a/cli/internal/projectrunner/control_play_mode_wait_test.go +++ b/cli/internal/projectrunner/control_play_mode_wait_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) // Verifies that control-play-mode polls a status-only request before returning stale PlayMode state. diff --git a/cli/internal/projectrunner/debug_timing.go b/cli/internal/projectrunner/debug_timing.go index ee4896ac9b..ce2da215a5 100644 --- a/cli/internal/projectrunner/debug_timing.go +++ b/cli/internal/projectrunner/debug_timing.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/projectrunner/debug_timing_test.go b/cli/internal/projectrunner/debug_timing_test.go index 651e65e8f6..79e0b67c89 100644 --- a/cli/internal/projectrunner/debug_timing_test.go +++ b/cli/internal/projectrunner/debug_timing_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) // Verifies that timing output stays disabled unless explicitly requested. diff --git a/cli/internal/projectrunner/dynamic_code_file.go b/cli/internal/projectrunner/dynamic_code_file.go index 92590acfd7..04d491b97b 100644 --- a/cli/internal/projectrunner/dynamic_code_file.go +++ b/cli/internal/projectrunner/dynamic_code_file.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) const dynamicCodeCodePropertyName = "Code" diff --git a/cli/internal/projectrunner/dynamic_code_file_test.go b/cli/internal/projectrunner/dynamic_code_file_test.go index f47db8e4aa..de5503415a 100644 --- a/cli/internal/projectrunner/dynamic_code_file_test.go +++ b/cli/internal/projectrunner/dynamic_code_file_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) // Verifies --code-file is extracted from execute-dynamic-code args in both flag forms. diff --git a/cli/internal/projectrunner/dynamic_code_wait.go b/cli/internal/projectrunner/dynamic_code_wait.go index b67c01b18a..297ba32c50 100644 --- a/cli/internal/projectrunner/dynamic_code_wait.go +++ b/cli/internal/projectrunner/dynamic_code_wait.go @@ -3,8 +3,8 @@ package projectrunner import ( "encoding/json" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/projectrunner/execution_errors.go b/cli/internal/projectrunner/execution_errors.go index 19a6183213..905bf81c51 100644 --- a/cli/internal/projectrunner/execution_errors.go +++ b/cli/internal/projectrunner/execution_errors.go @@ -3,7 +3,7 @@ package projectrunner import ( "fmt" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func compileWaitTimeoutError(projectRoot string) clicore.CLIError { diff --git a/cli/internal/projectrunner/list_output.go b/cli/internal/projectrunner/list_output.go index c769902b79..7452536d22 100644 --- a/cli/internal/projectrunner/list_output.go +++ b/cli/internal/projectrunner/list_output.go @@ -4,7 +4,7 @@ import ( "encoding/json" "sort" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) type listCatalog struct { diff --git a/cli/internal/projectrunner/list_output_test.go b/cli/internal/projectrunner/list_output_test.go index 1154ef8d13..ca387c2662 100644 --- a/cli/internal/projectrunner/list_output_test.go +++ b/cli/internal/projectrunner/list_output_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) // Tests that list output exposes the actual CLI option names instead of schema property names. diff --git a/cli/internal/projectrunner/native_tool_settings.go b/cli/internal/projectrunner/native_tool_settings.go index 6b19d7b99f..69239ca24e 100644 --- a/cli/internal/projectrunner/native_tool_settings.go +++ b/cli/internal/projectrunner/native_tool_settings.go @@ -1,6 +1,6 @@ package projectrunner -import "github.com/hatayama/unity-cli-loop/cli/internal/clicore" +import "github.com/hatayama/unity-cli-loop/common/clicore" func isSettingsManagedNativeToolCommand(command string) bool { switch command { diff --git a/cli/internal/projectrunner/pause_point_errors.go b/cli/internal/projectrunner/pause_point_errors.go index 2c4d1ac378..c1efc2ca36 100644 --- a/cli/internal/projectrunner/pause_point_errors.go +++ b/cli/internal/projectrunner/pause_point_errors.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func pausePointWaitError( diff --git a/cli/internal/projectrunner/pause_point_logs.go b/cli/internal/projectrunner/pause_point_logs.go index 9a015bed86..c62b4ac7c9 100644 --- a/cli/internal/projectrunner/pause_point_logs.go +++ b/cli/internal/projectrunner/pause_point_logs.go @@ -5,8 +5,8 @@ import ( "encoding/json" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/projectrunner/pause_point_wait.go b/cli/internal/projectrunner/pause_point_wait.go index 740ffb509c..5e24dc08e8 100644 --- a/cli/internal/projectrunner/pause_point_wait.go +++ b/cli/internal/projectrunner/pause_point_wait.go @@ -8,8 +8,8 @@ import ( "strconv" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/projectrunner/pause_point_wait_test.go b/cli/internal/projectrunner/pause_point_wait_test.go index 02031c4666..4c5a051482 100644 --- a/cli/internal/projectrunner/pause_point_wait_test.go +++ b/cli/internal/projectrunner/pause_point_wait_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) // Verifies wait-for-pause-point polls until Unity reports the marker hit. diff --git a/cli/internal/projectrunner/run.go b/cli/internal/projectrunner/run.go index a52d9e7204..336ead5b72 100644 --- a/cli/internal/projectrunner/run.go +++ b/cli/internal/projectrunner/run.go @@ -9,9 +9,9 @@ import ( "path/filepath" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/project" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/project" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) int { diff --git a/cli/internal/projectrunner/run_test.go b/cli/internal/projectrunner/run_test.go index e4991951ce..b4f9811149 100644 --- a/cli/internal/projectrunner/run_test.go +++ b/cli/internal/projectrunner/run_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func TestRunProjectLocalVersionJSONIncludesProtocolVersion(t *testing.T) { diff --git a/cli/internal/projectrunner/runner_commands.go b/cli/internal/projectrunner/runner_commands.go index 728bc8d3ba..e5066ab4f8 100644 --- a/cli/internal/projectrunner/runner_commands.go +++ b/cli/internal/projectrunner/runner_commands.go @@ -4,9 +4,9 @@ import ( "context" "io" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" - "github.com/hatayama/unity-cli-loop/cli/internal/project" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/project" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) func runResolvedProjectCommand( diff --git a/cli/internal/projectrunner/runner_usage.go b/cli/internal/projectrunner/runner_usage.go index db48f09693..259597d1b8 100644 --- a/cli/internal/projectrunner/runner_usage.go +++ b/cli/internal/projectrunner/runner_usage.go @@ -3,7 +3,7 @@ package projectrunner import ( "io" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) // tryHandleRunnerInfoRequest answers the project runner's own identity diff --git a/cli/internal/projectrunner/tool_params.go b/cli/internal/projectrunner/tool_params.go index 1eef170090..a0ef3eaa87 100644 --- a/cli/internal/projectrunner/tool_params.go +++ b/cli/internal/projectrunner/tool_params.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) func buildToolParams(args []string, tool clicore.ToolDefinition) (map[string]any, string, error) { diff --git a/cli/internal/projectrunner/tools_test.go b/cli/internal/projectrunner/tools_test.go index 39e394bd5a..2a77882a31 100644 --- a/cli/internal/projectrunner/tools_test.go +++ b/cli/internal/projectrunner/tools_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/clicore" + "github.com/hatayama/unity-cli-loop/common/clicore" ) // Tests that tool arguments are converted according to their schema types. diff --git a/cli/internal/update/command.go b/cli/internal/update/command.go index 1e4990c225..2ade30adf5 100644 --- a/cli/internal/update/command.go +++ b/cli/internal/update/command.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - sharedversion "github.com/hatayama/unity-cli-loop/cli/internal/version" + sharedversion "github.com/hatayama/unity-cli-loop/common/version" ) const ( diff --git a/common/clicontract/contract.go b/common/clicontract/contract.go new file mode 100644 index 0000000000..071a02e4dc --- /dev/null +++ b/common/clicontract/contract.go @@ -0,0 +1,51 @@ +package clicontract + +import ( + "embed" + "encoding/json" + "fmt" +) + +const ( + contractFileName = "contract.json" + schemaVersion = 1 +) + +//go:embed contract.json +var contractFiles embed.FS + +var Current = mustLoadContract() + +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"` + ProjectRunnerVersion string `json:"projectRunnerVersion"` +} + +func mustLoadContract() Contract { + content, err := contractFiles.ReadFile(contractFileName) + if err != nil { + panic(fmt.Sprintf("CLI contract is not embedded: %v", err)) + } + + var contract Contract + if err := json.Unmarshal(content, &contract); err != nil { + panic(fmt.Sprintf("CLI contract is invalid JSON: %v", err)) + } + if contract.SchemaVersion != schemaVersion { + panic(fmt.Sprintf("CLI contract schema version mismatch: %d", contract.SchemaVersion)) + } + requireString(contract.ProjectRunnerVersion, "projectRunnerVersion") + if contract.ProtocolVersion < 1 { + panic(fmt.Sprintf("CLI contract protocolVersion must be at least 1, got %d", contract.ProtocolVersion)) + } + return contract +} + +func requireString(value string, key string) { + if value == "" { + panic(fmt.Sprintf("contract field %s must not be empty", key)) + } +} diff --git a/cli/contract.json b/common/clicontract/contract.json similarity index 100% rename from cli/contract.json rename to common/clicontract/contract.json diff --git a/common/clicontract/contract_test.go b/common/clicontract/contract_test.go new file mode 100644 index 0000000000..04c69f2775 --- /dev/null +++ b/common/clicontract/contract_test.go @@ -0,0 +1,61 @@ +package clicontract + +import ( + "encoding/json" + "testing" + + "github.com/hatayama/unity-cli-loop/common/version" +) + +func TestCliContractProvidesRuntimeVersion(t *testing.T) { + // Verifies that the project runner owns its runtime version from the single CLI module. + requireValidContractVersion(t, "projectRunnerVersion", Current.ProjectRunnerVersion) +} + +func TestCliContractProvidesProtocolVersion(t *testing.T) { + // Verifies that the contract declares which C#-side IPC protocol the binary speaks. + if Current.ProtocolVersion < 1 { + t.Fatalf("protocolVersion must be at least 1, got %d", Current.ProtocolVersion) + } +} + +func TestCliContractDoesNotDeclareDispatcherReleaseFields(t *testing.T) { + // Verifies release-please CLI version stamping cannot accidentally move dispatcher release metadata. + fields := requireContractFieldMap(t, contractFileName) + requireContractFieldMissing(t, fields, "dispatcherVersion") + requireContractFieldMissing(t, fields, "dispatcherContractVersion") +} + +func requireValidContractVersion(t *testing.T, label string, value string) { + t.Helper() + + if value == "" { + t.Fatalf("%s must not be empty", label) + } + _, ok := version.Compare(value, value) + if !ok { + t.Fatalf("%s must be valid semver: %s", label, value) + } +} + +func requireContractFieldMap(t *testing.T, fileName string) map[string]any { + t.Helper() + + content, err := contractFiles.ReadFile(fileName) + if err != nil { + t.Fatalf("failed to read %s: %v", fileName, err) + } + fields := map[string]any{} + if err := json.Unmarshal(content, &fields); err != nil { + t.Fatalf("%s is invalid JSON: %v", fileName, err) + } + return fields +} + +func requireContractFieldMissing(t *testing.T, fields map[string]any, fieldName string) { + t.Helper() + + if _, ok := fields[fieldName]; ok { + t.Fatalf("contract must not declare %s", fieldName) + } +} diff --git a/cli/protocol_version_consistency_test.go b/common/clicontract/protocol_version_consistency_test.go similarity index 95% rename from cli/protocol_version_consistency_test.go rename to common/clicontract/protocol_version_consistency_test.go index b27672bb78..c4cec55ba5 100644 --- a/cli/protocol_version_consistency_test.go +++ b/common/clicontract/protocol_version_consistency_test.go @@ -10,12 +10,12 @@ import ( ) // unityProtocolConstantPath is the C# source that declares the protocol generation the -// Unity package accepts. It is relative to this package directory (the cli/ module root). +// Unity package accepts. It is relative to this package directory (common/clicontract). const ( - unityProtocolConstantPath = "../Packages/src/Editor/Domain/CliConstants.cs" - unityPackageManifestPath = "../Packages/src/package.json" - unityPackageCliPinPath = "../Packages/src/project-runner-pin.json" - unityProjectCliPinPath = "../.uloop/project-runner-pin.json" + unityProtocolConstantPath = "../../Packages/src/Editor/Domain/CliConstants.cs" + unityPackageManifestPath = "../../Packages/src/package.json" + unityPackageCliPinPath = "../../Packages/src/project-runner-pin.json" + unityProjectCliPinPath = "../../.uloop/project-runner-pin.json" ) var ( diff --git a/cli/internal/clicore/argument_error.go b/common/clicore/argument_error.go similarity index 100% rename from cli/internal/clicore/argument_error.go rename to common/clicore/argument_error.go diff --git a/cli/internal/clicore/busy_status.go b/common/clicore/busy_status.go similarity index 100% rename from cli/internal/clicore/busy_status.go rename to common/clicore/busy_status.go diff --git a/cli/internal/clicore/cli_vibe.go b/common/clicore/cli_vibe.go similarity index 100% rename from cli/internal/clicore/cli_vibe.go rename to common/clicore/cli_vibe.go diff --git a/cli/internal/clicore/cli_vibe_test.go b/common/clicore/cli_vibe_test.go similarity index 100% rename from cli/internal/clicore/cli_vibe_test.go rename to common/clicore/cli_vibe_test.go diff --git a/cli/internal/clicore/command_registry.go b/common/clicore/command_registry.go similarity index 100% rename from cli/internal/clicore/command_registry.go rename to common/clicore/command_registry.go diff --git a/cli/internal/clicore/command_registry_test.go b/common/clicore/command_registry_test.go similarity index 100% rename from cli/internal/clicore/command_registry_test.go rename to common/clicore/command_registry_test.go diff --git a/cli/internal/clicore/error_editor_unresponsive.go b/common/clicore/error_editor_unresponsive.go similarity index 94% rename from cli/internal/clicore/error_editor_unresponsive.go rename to common/clicore/error_editor_unresponsive.go index fb88b54bc9..9dadb6e0b3 100644 --- a/cli/internal/clicore/error_editor_unresponsive.go +++ b/common/clicore/error_editor_unresponsive.go @@ -1,6 +1,6 @@ package clicore -import "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" +import "github.com/hatayama/unity-cli-loop/common/unityipc" func connectionAttemptCause(err *unityipc.ConnectionAttemptError) string { if err == nil { diff --git a/cli/internal/clicore/error_envelope.go b/common/clicore/error_envelope.go similarity index 99% rename from cli/internal/clicore/error_envelope.go rename to common/clicore/error_envelope.go index 0e5fcbf74c..783e02a240 100644 --- a/cli/internal/clicore/error_envelope.go +++ b/common/clicore/error_envelope.go @@ -6,7 +6,7 @@ import ( "io" "net" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/clicore/error_envelope_classification.go b/common/clicore/error_envelope_classification.go similarity index 98% rename from cli/internal/clicore/error_envelope_classification.go rename to common/clicore/error_envelope_classification.go index 9cd480a5a8..f327de9219 100644 --- a/cli/internal/clicore/error_envelope_classification.go +++ b/common/clicore/error_envelope_classification.go @@ -5,7 +5,7 @@ import ( "errors" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) func ClassifyError(err error, context ErrorContext) CLIError { diff --git a/cli/internal/clicore/error_envelope_test.go b/common/clicore/error_envelope_test.go similarity index 99% rename from cli/internal/clicore/error_envelope_test.go rename to common/clicore/error_envelope_test.go index 8e927f6b1e..1591d2a09a 100644 --- a/cli/internal/clicore/error_envelope_test.go +++ b/common/clicore/error_envelope_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) // Verifies the stderr envelope written for a caller-facing error is machine-readable JSON. diff --git a/cli/internal/clicore/focus.go b/common/clicore/focus.go similarity index 100% rename from cli/internal/clicore/focus.go rename to common/clicore/focus.go diff --git a/cli/internal/clicore/focus_test.go b/common/clicore/focus_test.go similarity index 100% rename from cli/internal/clicore/focus_test.go rename to common/clicore/focus_test.go diff --git a/cli/internal/clicore/global_args.go b/common/clicore/global_args.go similarity index 100% rename from cli/internal/clicore/global_args.go rename to common/clicore/global_args.go diff --git a/cli/internal/clicore/output.go b/common/clicore/output.go similarity index 100% rename from cli/internal/clicore/output.go rename to common/clicore/output.go diff --git a/cli/internal/clicore/output_test.go b/common/clicore/output_test.go similarity index 100% rename from cli/internal/clicore/output_test.go rename to common/clicore/output_test.go diff --git a/cli/internal/clicore/runner_version.go b/common/clicore/runner_version.go similarity index 85% rename from cli/internal/clicore/runner_version.go rename to common/clicore/runner_version.go index 34c3128542..cfabbaf042 100644 --- a/cli/internal/clicore/runner_version.go +++ b/common/clicore/runner_version.go @@ -4,7 +4,7 @@ import ( "encoding/json" "io" - clicontract "github.com/hatayama/unity-cli-loop/cli" + clicontract "github.com/hatayama/unity-cli-loop/common/clicontract" ) var ( diff --git a/cli/internal/clicore/server_not_responding.go b/common/clicore/server_not_responding.go similarity index 95% rename from cli/internal/clicore/server_not_responding.go rename to common/clicore/server_not_responding.go index bd460c95e0..e3b68d52ff 100644 --- a/cli/internal/clicore/server_not_responding.go +++ b/common/clicore/server_not_responding.go @@ -3,7 +3,7 @@ package clicore import ( "fmt" - "github.com/hatayama/unity-cli-loop/cli/internal/project" + "github.com/hatayama/unity-cli-loop/common/project" ) // UnityServerNotRespondingError reports that a running Unity Editor accepted a diff --git a/cli/internal/clicore/skill_sources.go b/common/clicore/skill_sources.go similarity index 98% rename from cli/internal/clicore/skill_sources.go rename to common/clicore/skill_sources.go index e3fc0d5440..0b75cf9e79 100644 --- a/cli/internal/clicore/skill_sources.go +++ b/common/clicore/skill_sources.go @@ -5,7 +5,7 @@ import ( "path/filepath" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/skills" + "github.com/hatayama/unity-cli-loop/common/skills" ) const ( diff --git a/cli/internal/clicore/skills_packages.go b/common/clicore/skills_packages.go similarity index 100% rename from cli/internal/clicore/skills_packages.go rename to common/clicore/skills_packages.go diff --git a/cli/internal/clicore/skills_packages_test.go b/common/clicore/skills_packages_test.go similarity index 100% rename from cli/internal/clicore/skills_packages_test.go rename to common/clicore/skills_packages_test.go diff --git a/cli/internal/clicore/spinner.go b/common/clicore/spinner.go similarity index 98% rename from cli/internal/clicore/spinner.go rename to common/clicore/spinner.go index 92b9341043..fa281cb210 100644 --- a/cli/internal/clicore/spinner.go +++ b/common/clicore/spinner.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/clicore/spinner_test.go b/common/clicore/spinner_test.go similarity index 98% rename from cli/internal/clicore/spinner_test.go rename to common/clicore/spinner_test.go index 8f15643c0f..9f515f872c 100644 --- a/cli/internal/clicore/spinner_test.go +++ b/common/clicore/spinner_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) func TestSpinnerDoesNotWriteWhenDisabled(t *testing.T) { diff --git a/cli/internal/clicore/string_helpers.go b/common/clicore/string_helpers.go similarity index 100% rename from cli/internal/clicore/string_helpers.go rename to common/clicore/string_helpers.go diff --git a/cli/internal/clicore/tool_catalog.go b/common/clicore/tool_catalog.go similarity index 96% rename from cli/internal/clicore/tool_catalog.go rename to common/clicore/tool_catalog.go index 3623ee3a20..df5e619073 100644 --- a/cli/internal/clicore/tool_catalog.go +++ b/common/clicore/tool_catalog.go @@ -1,7 +1,7 @@ package clicore import ( - "github.com/hatayama/unity-cli-loop/cli/internal/tools" + "github.com/hatayama/unity-cli-loop/common/tools" ) const ( diff --git a/cli/internal/clicore/tool_catalog_test.go b/common/clicore/tool_catalog_test.go similarity index 100% rename from cli/internal/clicore/tool_catalog_test.go rename to common/clicore/tool_catalog_test.go diff --git a/cli/internal/clicore/tool_option_help.go b/common/clicore/tool_option_help.go similarity index 100% rename from cli/internal/clicore/tool_option_help.go rename to common/clicore/tool_option_help.go diff --git a/cli/internal/clicore/tool_options.go b/common/clicore/tool_options.go similarity index 100% rename from cli/internal/clicore/tool_options.go rename to common/clicore/tool_options.go diff --git a/cli/internal/clicore/tool_readiness.go b/common/clicore/tool_readiness.go similarity index 97% rename from cli/internal/clicore/tool_readiness.go rename to common/clicore/tool_readiness.go index eb629735a5..5de5087494 100644 --- a/cli/internal/clicore/tool_readiness.go +++ b/common/clicore/tool_readiness.go @@ -7,8 +7,8 @@ import ( "fmt" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/project" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/project" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/clicore/tool_readiness_test.go b/common/clicore/tool_readiness_test.go similarity index 98% rename from cli/internal/clicore/tool_readiness_test.go rename to common/clicore/tool_readiness_test.go index 6514c51d54..8507bb4aba 100644 --- a/cli/internal/clicore/tool_readiness_test.go +++ b/common/clicore/tool_readiness_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) // Verifies shared readiness waits keep the shorter non-launch timeout. diff --git a/cli/internal/clicore/tool_settings.go b/common/clicore/tool_settings.go similarity index 100% rename from cli/internal/clicore/tool_settings.go rename to common/clicore/tool_settings.go diff --git a/cli/internal/clicore/transport_errors.go b/common/clicore/transport_errors.go similarity index 100% rename from cli/internal/clicore/transport_errors.go rename to common/clicore/transport_errors.go diff --git a/cli/internal/clicore/transport_errors_test.go b/common/clicore/transport_errors_test.go similarity index 100% rename from cli/internal/clicore/transport_errors_test.go rename to common/clicore/transport_errors_test.go diff --git a/common/go.mod b/common/go.mod new file mode 100644 index 0000000000..58d9563f1c --- /dev/null +++ b/common/go.mod @@ -0,0 +1,7 @@ +module github.com/hatayama/unity-cli-loop/common + +go 1.26 + +require github.com/Microsoft/go-winio v0.6.2 + +require golang.org/x/sys v0.10.0 // indirect diff --git a/common/go.sum b/common/go.sum new file mode 100644 index 0000000000..a001052ff3 --- /dev/null +++ b/common/go.sum @@ -0,0 +1,4 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/cli/internal/project/project.go b/common/project/project.go similarity index 99% rename from cli/internal/project/project.go rename to common/project/project.go index f01546529d..306efda28e 100644 --- a/cli/internal/project/project.go +++ b/common/project/project.go @@ -10,7 +10,7 @@ import ( "sort" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) const ( diff --git a/cli/internal/project/project_test.go b/common/project/project_test.go similarity index 99% rename from cli/internal/project/project_test.go rename to common/project/project_test.go index e84effd201..c8ef4ff086 100644 --- a/cli/internal/project/project_test.go +++ b/common/project/project_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" + "github.com/hatayama/unity-cli-loop/common/unityipc" ) func TestCreateEndpointUsesStableProjectHash(t *testing.T) { diff --git a/cli/internal/skills/source.go b/common/skills/source.go similarity index 100% rename from cli/internal/skills/source.go rename to common/skills/source.go diff --git a/cli/internal/tools/catalog.go b/common/tools/catalog.go similarity index 100% rename from cli/internal/tools/catalog.go rename to common/tools/catalog.go diff --git a/cli/internal/tools/default-tools.json b/common/tools/default-tools.json similarity index 100% rename from cli/internal/tools/default-tools.json rename to common/tools/default-tools.json diff --git a/cli/internal/tools/types.go b/common/tools/types.go similarity index 100% rename from cli/internal/tools/types.go rename to common/tools/types.go diff --git a/cli/internal/unityipc/client.go b/common/unityipc/client.go similarity index 99% rename from cli/internal/unityipc/client.go rename to common/unityipc/client.go index d17c51ef1d..810990adb1 100644 --- a/cli/internal/unityipc/client.go +++ b/common/unityipc/client.go @@ -10,7 +10,7 @@ import ( "os" "time" - clicontract "github.com/hatayama/unity-cli-loop/cli" + clicontract "github.com/hatayama/unity-cli-loop/common/clicontract" ) const ( diff --git a/cli/internal/unityipc/client_heartbeat_test.go b/common/unityipc/client_heartbeat_test.go similarity index 100% rename from cli/internal/unityipc/client_heartbeat_test.go rename to common/unityipc/client_heartbeat_test.go diff --git a/cli/internal/unityipc/client_test.go b/common/unityipc/client_test.go similarity index 99% rename from cli/internal/unityipc/client_test.go rename to common/unityipc/client_test.go index 7953b21090..f55bc9aaeb 100644 --- a/cli/internal/unityipc/client_test.go +++ b/common/unityipc/client_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - clicontract "github.com/hatayama/unity-cli-loop/cli" + clicontract "github.com/hatayama/unity-cli-loop/common/clicontract" ) func TestFormatConnectionAttemptErrorExplainsDialFailureWithoutDisconnectClaim(t *testing.T) { diff --git a/cli/internal/unityipc/client_windows_test.go b/common/unityipc/client_windows_test.go similarity index 100% rename from cli/internal/unityipc/client_windows_test.go rename to common/unityipc/client_windows_test.go diff --git a/cli/internal/unityipc/connection.go b/common/unityipc/connection.go similarity index 100% rename from cli/internal/unityipc/connection.go rename to common/unityipc/connection.go diff --git a/cli/internal/unityipc/dial_unix.go b/common/unityipc/dial_unix.go similarity index 100% rename from cli/internal/unityipc/dial_unix.go rename to common/unityipc/dial_unix.go diff --git a/cli/internal/unityipc/dial_windows.go b/common/unityipc/dial_windows.go similarity index 100% rename from cli/internal/unityipc/dial_windows.go rename to common/unityipc/dial_windows.go diff --git a/cli/internal/unityipc/framing.go b/common/unityipc/framing.go similarity index 100% rename from cli/internal/unityipc/framing.go rename to common/unityipc/framing.go diff --git a/cli/internal/unityipc/framing_test.go b/common/unityipc/framing_test.go similarity index 100% rename from cli/internal/unityipc/framing_test.go rename to common/unityipc/framing_test.go diff --git a/cli/internal/unityipc/outcome.go b/common/unityipc/outcome.go similarity index 100% rename from cli/internal/unityipc/outcome.go rename to common/unityipc/outcome.go diff --git a/cli/internal/version/compare.go b/common/version/compare.go similarity index 100% rename from cli/internal/version/compare.go rename to common/version/compare.go diff --git a/cli/internal/version/compare_test.go b/common/version/compare_test.go similarity index 100% rename from cli/internal/version/compare_test.go rename to common/version/compare_test.go diff --git a/go.work b/go.work index 69a9de8cb1..ec1425724e 100644 --- a/go.work +++ b/go.work @@ -1,3 +1,6 @@ go 1.26 -use ./cli +use ( + ./cli + ./common +) diff --git a/release-please-config.json b/release-please-config.json index 0938eba795..518141fad2 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -46,12 +46,12 @@ "extra-files": [ { "type": "json", - "path": "internal/tools/default-tools.json", + "path": "/common/tools/default-tools.json", "jsonpath": "$.version" }, { "type": "json", - "path": "contract.json", + "path": "/common/clicontract/contract.json", "jsonpath": "$.projectRunnerVersion" }, { diff --git a/scripts/check-go-cli-source.sh b/scripts/check-go-cli-source.sh index d1cbef269c..3a5b27287a 100755 --- a/scripts/check-go-cli-source.sh +++ b/scripts/check-go-cli-source.sh @@ -35,4 +35,5 @@ run_module_checks() { ) } +run_module_checks "$ROOT_DIR/common" run_module_checks "$CLI_DIR" diff --git a/scripts/test-release-please-config.sh b/scripts/test-release-please-config.sh index 295a5ea010..f0a91721ed 100755 --- a/scripts/test-release-please-config.sh +++ b/scripts/test-release-please-config.sh @@ -143,8 +143,8 @@ assert_json_value '.packages["cli"].["changelog-path"]' 'CHANGELOG.md' # no-op (see the directory check below). assert_json_value '.packages["cli"].["exclude-paths"] | sort | join(",")' 'cli/cmd/dispatch-release-please-pr-checks,cli/cmd/dispatcher,cli/internal/automation,cli/internal/dispatcher' assert_json_value '.packages["cli"].["extra-files"] | length' '4' -assert_json_value '.packages["cli"].["extra-files"][0].path' 'internal/tools/default-tools.json' -assert_json_value '.packages["cli"].["extra-files"][1].path' 'contract.json' +assert_json_value '.packages["cli"].["extra-files"][0].path' '/common/tools/default-tools.json' +assert_json_value '.packages["cli"].["extra-files"][1].path' '/common/clicontract/contract.json' assert_json_value '.packages["cli"].["extra-files"][2].path' '/Packages/src/project-runner-pin.json' assert_json_value '.packages["cli"].["extra-files"][2].jsonpath' '$.projectRunnerVersion' assert_json_value '.packages["cli"].["extra-files"][3].path' '/.uloop/project-runner-pin.json' From 0e6f94aa787a702ff3b8e61a4d0a4dc890891304 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 20:40:40 +0900 Subject: [PATCH 04/18] Extract the dispatcher into its own Go module Move internal/dispatcher, install, uninstall, update, cmd/dispatcher, and the dispatcher contract out of the cli module into a dispatcher module that requires only common. The compiler now enforces that the dispatcher and the project runner cannot import each other: neither module requires the other. The dispatcher contract package is renamed to dispatchercontract because the clicontract name now belongs to the runner contract in common. Exclude-paths for the moved directories are dropped from the runner release config since the paths left its package root, and the boundary architecture tests now run go list inside the owning module. --- .../architecture/architecture_test.go | 29 ++++++++++--------- {cli => dispatcher}/cmd/dispatcher/main.go | 2 +- {cli => dispatcher}/contract.go | 8 ++--- {cli => dispatcher}/contract_test.go | 2 +- {cli => dispatcher}/dispatcher-contract.json | 0 dispatcher/go.mod | 12 ++++++++ dispatcher/go.sum | 4 +++ .../dispatcher/bootstrap_platform_errors.go | 0 .../internal/dispatcher/command_help.go | 0 .../internal/dispatcher/completion.go | 0 .../internal/dispatcher/completion_options.go | 0 .../internal/dispatcher/completion_test.go | 0 .../internal/dispatcher/dispatcher.go | 0 .../dispatcher_bootstrap_routing_test.go | 2 +- .../dispatcher/dispatcher_download.go | 2 +- .../internal/dispatcher/dispatcher_pin.go | 0 .../internal/dispatcher/dispatcher_process.go | 0 .../internal/dispatcher/dispatcher_test.go | 0 .../dispatcher/dispatcher_update_version.go | 0 .../dispatcher_update_version_test.go | 0 .../internal/dispatcher/dispatcher_version.go | 6 ++-- .../dispatcher/error_envelope_test.go | 0 .../internal/dispatcher/help_test.go | 0 .../internal/dispatcher/install.go | 2 +- .../internal/dispatcher/install_test.go | 0 .../internal/dispatcher/launch.go | 0 .../internal/dispatcher/launch_focus_log.go | 0 .../internal/dispatcher/launch_options.go | 0 .../launch_process_exit_timeout_error.go | 0 .../dispatcher/launch_process_unix.go | 0 .../dispatcher/launch_process_unix_test.go | 0 .../dispatcher/launch_process_windows.go | 0 .../internal/dispatcher/launch_ready.go | 0 .../launch_startup_timeout_error.go | 0 .../internal/dispatcher/launch_test.go | 0 .../internal/dispatcher/native_install_dir.go | 0 .../internal/dispatcher/run_dispatcher.go | 2 +- .../internal/dispatcher/run_help.go | 0 .../internal/dispatcher/skills.go | 0 .../internal/dispatcher/skills_content.go | 0 .../internal/dispatcher/skills_discovery.go | 0 .../internal/dispatcher/skills_dispatch.go | 0 .../internal/dispatcher/skills_display.go | 0 .../internal/dispatcher/skills_sync.go | 0 .../internal/dispatcher/skills_targets.go | 0 .../internal/dispatcher/skills_test.go | 0 .../dispatcher/skills_v3_migration.go | 0 .../internal/dispatcher/uninstall.go | 2 +- .../internal/dispatcher/uninstall_test.go | 0 .../internal/dispatcher/update.go | 2 +- .../internal/dispatcher/update_test.go | 12 ++++---- .../internal/install/command.go | 0 .../internal/install/command_test.go | 0 .../internal/install/posix_install.go | 0 .../internal/uninstall/command.go | 0 .../internal/uninstall/command_test.go | 0 .../internal/update/command.go | 0 .../internal/update/installer.go | 0 .../internal/update/installer_test.go | 0 go.work | 1 + go.work.sum | 3 ++ release-please-config.json | 4 +-- scripts/build-go-cli.sh | 7 +++-- scripts/check-go-cli-source.sh | 1 + scripts/resolve-dispatcher-release-target.sh | 4 +-- scripts/test-release-please-config.sh | 2 +- 66 files changed, 65 insertions(+), 44 deletions(-) rename {cli => dispatcher}/cmd/dispatcher/main.go (67%) rename {cli => dispatcher}/contract.go (85%) rename {cli => dispatcher}/contract_test.go (98%) rename {cli => dispatcher}/dispatcher-contract.json (100%) create mode 100644 dispatcher/go.mod create mode 100644 dispatcher/go.sum rename {cli => dispatcher}/internal/dispatcher/bootstrap_platform_errors.go (100%) rename {cli => dispatcher}/internal/dispatcher/command_help.go (100%) rename {cli => dispatcher}/internal/dispatcher/completion.go (100%) rename {cli => dispatcher}/internal/dispatcher/completion_options.go (100%) rename {cli => dispatcher}/internal/dispatcher/completion_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/dispatcher.go (100%) rename {cli => dispatcher}/internal/dispatcher/dispatcher_bootstrap_routing_test.go (98%) rename {cli => dispatcher}/internal/dispatcher/dispatcher_download.go (99%) rename {cli => dispatcher}/internal/dispatcher/dispatcher_pin.go (100%) rename {cli => dispatcher}/internal/dispatcher/dispatcher_process.go (100%) rename {cli => dispatcher}/internal/dispatcher/dispatcher_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/dispatcher_update_version.go (100%) rename {cli => dispatcher}/internal/dispatcher/dispatcher_update_version_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/dispatcher_version.go (62%) rename {cli => dispatcher}/internal/dispatcher/error_envelope_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/help_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/install.go (98%) rename {cli => dispatcher}/internal/dispatcher/install_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_focus_log.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_options.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_process_exit_timeout_error.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_process_unix.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_process_unix_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_process_windows.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_ready.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_startup_timeout_error.go (100%) rename {cli => dispatcher}/internal/dispatcher/launch_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/native_install_dir.go (100%) rename {cli => dispatcher}/internal/dispatcher/run_dispatcher.go (99%) rename {cli => dispatcher}/internal/dispatcher/run_help.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills_content.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills_discovery.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills_dispatch.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills_display.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills_sync.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills_targets.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/skills_v3_migration.go (100%) rename {cli => dispatcher}/internal/dispatcher/uninstall.go (98%) rename {cli => dispatcher}/internal/dispatcher/uninstall_test.go (100%) rename {cli => dispatcher}/internal/dispatcher/update.go (98%) rename {cli => dispatcher}/internal/dispatcher/update_test.go (94%) rename {cli => dispatcher}/internal/install/command.go (100%) rename {cli => dispatcher}/internal/install/command_test.go (100%) rename {cli => dispatcher}/internal/install/posix_install.go (100%) rename {cli => dispatcher}/internal/uninstall/command.go (100%) rename {cli => dispatcher}/internal/uninstall/command_test.go (100%) rename {cli => dispatcher}/internal/update/command.go (100%) rename {cli => dispatcher}/internal/update/installer.go (100%) rename {cli => dispatcher}/internal/update/installer_test.go (100%) create mode 100644 go.work.sum diff --git a/cli/internal/architecture/architecture_test.go b/cli/internal/architecture/architecture_test.go index 319837e213..d1bcc49631 100644 --- a/cli/internal/architecture/architecture_test.go +++ b/cli/internal/architecture/architecture_test.go @@ -13,6 +13,7 @@ import ( const ( cliModulePath = "github.com/hatayama/unity-cli-loop/cli" + dispatcherModulePath = "github.com/hatayama/unity-cli-loop/dispatcher" maxProductionFileLines = 500 ) @@ -98,31 +99,32 @@ func TestCliInternalPackagesStayInsideExplicitBoundaries(t *testing.T) { } } -// Tests that the dispatcher command only enters the dispatcher package. +// Tests that the dispatcher command, now in its own module, only enters the dispatcher package. func TestDispatcherCommandOnlyDependsOnDispatcherEntrypoint(t *testing.T) { - assertCommandOnlyDependsOnInternalEntrypoint(t, "./cmd/dispatcher", cliModulePath+"/internal/dispatcher") + dispatcherModuleDir := filepath.Join(findRepositoryRoot(t, findModuleRoot(t)), "dispatcher") + assertCommandOnlyDependsOnInternalEntrypoint(t, dispatcherModuleDir, dispatcherModulePath, "./cmd/dispatcher", dispatcherModulePath+"/internal/dispatcher") } // Tests that the project runner command only enters the project runner package. func TestProjectRunnerCommandOnlyDependsOnProjectRunnerEntrypoint(t *testing.T) { - assertCommandOnlyDependsOnInternalEntrypoint(t, "./cmd/project-runner", cliModulePath+"/internal/projectrunner") + assertCommandOnlyDependsOnInternalEntrypoint(t, findModuleRoot(t), cliModulePath, "./cmd/project-runner", cliModulePath+"/internal/projectrunner") } -// Tests that the dispatcher binary does not transitively pull in the project runner package. +// Tests that the dispatcher binary, now in its own module, does not transitively pull in the CLI module's project runner package. func TestDispatcherBinaryDoesNotTransitivelyDependOnProjectRunner(t *testing.T) { - assertBinaryDoesNotTransitivelyDependOn(t, "./cmd/dispatcher", cliModulePath+"/internal/projectrunner") + dispatcherModuleDir := filepath.Join(findRepositoryRoot(t, findModuleRoot(t)), "dispatcher") + assertBinaryDoesNotTransitivelyDependOn(t, dispatcherModuleDir, "./cmd/dispatcher", cliModulePath+"/internal/projectrunner") } -// Tests that the project runner binary does not transitively pull in the dispatcher package. +// Tests that the project runner binary does not transitively pull in the dispatcher module's dispatcher package. func TestProjectRunnerBinaryDoesNotTransitivelyDependOnDispatcher(t *testing.T) { - assertBinaryDoesNotTransitivelyDependOn(t, "./cmd/project-runner", cliModulePath+"/internal/dispatcher") + assertBinaryDoesNotTransitivelyDependOn(t, findModuleRoot(t), "./cmd/project-runner", dispatcherModulePath+"/internal/dispatcher") } -func assertBinaryDoesNotTransitivelyDependOn(t *testing.T, commandPath string, forbiddenPackage string) { +func assertBinaryDoesNotTransitivelyDependOn(t *testing.T, moduleDir string, commandPath string, forbiddenPackage string) { t.Helper() - moduleRoot := findModuleRoot(t) command := exec.Command("go", "list", "-deps", commandPath) - command.Dir = moduleRoot + command.Dir = moduleDir output, err := command.Output() if err != nil { t.Fatalf("go list -deps failed: %v", err) @@ -134,11 +136,10 @@ func assertBinaryDoesNotTransitivelyDependOn(t *testing.T, commandPath string, f } } -func assertCommandOnlyDependsOnInternalEntrypoint(t *testing.T, commandPath string, expectedEntrypoint string) { +func assertCommandOnlyDependsOnInternalEntrypoint(t *testing.T, moduleDir string, modulePath string, commandPath string, expectedEntrypoint string) { t.Helper() - moduleRoot := findModuleRoot(t) command := exec.Command("go", "list", "-json", commandPath) - command.Dir = moduleRoot + command.Dir = moduleDir output, err := command.Output() if err != nil { t.Fatalf("go list failed: %v", err) @@ -154,7 +155,7 @@ func assertCommandOnlyDependsOnInternalEntrypoint(t *testing.T, commandPath stri t.Fatalf("CLI command must not depend on removed split module package %s", dependency) } } - if !strings.HasPrefix(dependency, cliModulePath+"/internal/") { + if !strings.HasPrefix(dependency, modulePath+"/internal/") { continue } if dependency != expectedEntrypoint { diff --git a/cli/cmd/dispatcher/main.go b/dispatcher/cmd/dispatcher/main.go similarity index 67% rename from cli/cmd/dispatcher/main.go rename to dispatcher/cmd/dispatcher/main.go index d21bf541be..3239b36af9 100644 --- a/cli/cmd/dispatcher/main.go +++ b/dispatcher/cmd/dispatcher/main.go @@ -4,7 +4,7 @@ import ( "context" "os" - "github.com/hatayama/unity-cli-loop/cli/internal/dispatcher" + "github.com/hatayama/unity-cli-loop/dispatcher/internal/dispatcher" ) func main() { diff --git a/cli/contract.go b/dispatcher/contract.go similarity index 85% rename from cli/contract.go rename to dispatcher/contract.go index dd8623cecb..bcc4e6d9b8 100644 --- a/cli/contract.go +++ b/dispatcher/contract.go @@ -1,7 +1,7 @@ -// Package clicontract holds the dispatcher-side release contract. The runner-side contract -// has moved to common/clicontract; the dispatcher contract itself temporarily remains here at -// the cli module root and moves into the dispatcher module in a later step. -package clicontract +// Package dispatchercontract holds the dispatcher-side release contract. The runner-side +// contract lives separately in common/clicontract; this package owns the dispatcher release +// contract at the dispatcher module root. +package dispatchercontract import ( "embed" diff --git a/cli/contract_test.go b/dispatcher/contract_test.go similarity index 98% rename from cli/contract_test.go rename to dispatcher/contract_test.go index 4cedc5eca2..49d87de3c2 100644 --- a/cli/contract_test.go +++ b/dispatcher/contract_test.go @@ -1,4 +1,4 @@ -package clicontract +package dispatchercontract import ( "encoding/json" diff --git a/cli/dispatcher-contract.json b/dispatcher/dispatcher-contract.json similarity index 100% rename from cli/dispatcher-contract.json rename to dispatcher/dispatcher-contract.json diff --git a/dispatcher/go.mod b/dispatcher/go.mod new file mode 100644 index 0000000000..8c4740f600 --- /dev/null +++ b/dispatcher/go.mod @@ -0,0 +1,12 @@ +module github.com/hatayama/unity-cli-loop/dispatcher + +go 1.26 + +require github.com/hatayama/unity-cli-loop/common v0.0.0-00010101000000-000000000000 + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + golang.org/x/sys v0.10.0 // indirect +) + +replace github.com/hatayama/unity-cli-loop/common => ../common diff --git a/dispatcher/go.sum b/dispatcher/go.sum new file mode 100644 index 0000000000..a001052ff3 --- /dev/null +++ b/dispatcher/go.sum @@ -0,0 +1,4 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/cli/internal/dispatcher/bootstrap_platform_errors.go b/dispatcher/internal/dispatcher/bootstrap_platform_errors.go similarity index 100% rename from cli/internal/dispatcher/bootstrap_platform_errors.go rename to dispatcher/internal/dispatcher/bootstrap_platform_errors.go diff --git a/cli/internal/dispatcher/command_help.go b/dispatcher/internal/dispatcher/command_help.go similarity index 100% rename from cli/internal/dispatcher/command_help.go rename to dispatcher/internal/dispatcher/command_help.go diff --git a/cli/internal/dispatcher/completion.go b/dispatcher/internal/dispatcher/completion.go similarity index 100% rename from cli/internal/dispatcher/completion.go rename to dispatcher/internal/dispatcher/completion.go diff --git a/cli/internal/dispatcher/completion_options.go b/dispatcher/internal/dispatcher/completion_options.go similarity index 100% rename from cli/internal/dispatcher/completion_options.go rename to dispatcher/internal/dispatcher/completion_options.go diff --git a/cli/internal/dispatcher/completion_test.go b/dispatcher/internal/dispatcher/completion_test.go similarity index 100% rename from cli/internal/dispatcher/completion_test.go rename to dispatcher/internal/dispatcher/completion_test.go diff --git a/cli/internal/dispatcher/dispatcher.go b/dispatcher/internal/dispatcher/dispatcher.go similarity index 100% rename from cli/internal/dispatcher/dispatcher.go rename to dispatcher/internal/dispatcher/dispatcher.go diff --git a/cli/internal/dispatcher/dispatcher_bootstrap_routing_test.go b/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go similarity index 98% rename from cli/internal/dispatcher/dispatcher_bootstrap_routing_test.go rename to dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go index 685c304d91..8a494aa951 100644 --- a/cli/internal/dispatcher/dispatcher_bootstrap_routing_test.go +++ b/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/hatayama/unity-cli-loop/cli/internal/update" + "github.com/hatayama/unity-cli-loop/dispatcher/internal/update" ) // These tests lock the dispatcher-process routing of bootstrap commands across diff --git a/cli/internal/dispatcher/dispatcher_download.go b/dispatcher/internal/dispatcher/dispatcher_download.go similarity index 99% rename from cli/internal/dispatcher/dispatcher_download.go rename to dispatcher/internal/dispatcher/dispatcher_download.go index 6d41f06d3c..8f1c9b3428 100644 --- a/cli/internal/dispatcher/dispatcher_download.go +++ b/dispatcher/internal/dispatcher/dispatcher_download.go @@ -18,8 +18,8 @@ import ( "strings" "time" - sharedupdate "github.com/hatayama/unity-cli-loop/cli/internal/update" "github.com/hatayama/unity-cli-loop/common/clicore" + sharedupdate "github.com/hatayama/unity-cli-loop/dispatcher/internal/update" ) var dispatcherHTTPClient = &http.Client{Timeout: 2 * time.Minute} diff --git a/cli/internal/dispatcher/dispatcher_pin.go b/dispatcher/internal/dispatcher/dispatcher_pin.go similarity index 100% rename from cli/internal/dispatcher/dispatcher_pin.go rename to dispatcher/internal/dispatcher/dispatcher_pin.go diff --git a/cli/internal/dispatcher/dispatcher_process.go b/dispatcher/internal/dispatcher/dispatcher_process.go similarity index 100% rename from cli/internal/dispatcher/dispatcher_process.go rename to dispatcher/internal/dispatcher/dispatcher_process.go diff --git a/cli/internal/dispatcher/dispatcher_test.go b/dispatcher/internal/dispatcher/dispatcher_test.go similarity index 100% rename from cli/internal/dispatcher/dispatcher_test.go rename to dispatcher/internal/dispatcher/dispatcher_test.go diff --git a/cli/internal/dispatcher/dispatcher_update_version.go b/dispatcher/internal/dispatcher/dispatcher_update_version.go similarity index 100% rename from cli/internal/dispatcher/dispatcher_update_version.go rename to dispatcher/internal/dispatcher/dispatcher_update_version.go diff --git a/cli/internal/dispatcher/dispatcher_update_version_test.go b/dispatcher/internal/dispatcher/dispatcher_update_version_test.go similarity index 100% rename from cli/internal/dispatcher/dispatcher_update_version_test.go rename to dispatcher/internal/dispatcher/dispatcher_update_version_test.go diff --git a/cli/internal/dispatcher/dispatcher_version.go b/dispatcher/internal/dispatcher/dispatcher_version.go similarity index 62% rename from cli/internal/dispatcher/dispatcher_version.go rename to dispatcher/internal/dispatcher/dispatcher_version.go index a43a48378e..042604123e 100644 --- a/cli/internal/dispatcher/dispatcher_version.go +++ b/dispatcher/internal/dispatcher/dispatcher_version.go @@ -4,13 +4,13 @@ import ( "encoding/json" "io" - clicontract "github.com/hatayama/unity-cli-loop/cli" "github.com/hatayama/unity-cli-loop/common/clicore" + dispatchercontract "github.com/hatayama/unity-cli-loop/dispatcher" ) var ( - dispatcherVersion = clicontract.DispatcherCurrent.DispatcherVersion - dispatcherContractVersion = clicontract.DispatcherCurrent.DispatcherContractVersion + dispatcherVersion = dispatchercontract.DispatcherCurrent.DispatcherVersion + dispatcherContractVersion = dispatchercontract.DispatcherCurrent.DispatcherContractVersion ) func writeDispatcherVersionJSON(stdout io.Writer) { diff --git a/cli/internal/dispatcher/error_envelope_test.go b/dispatcher/internal/dispatcher/error_envelope_test.go similarity index 100% rename from cli/internal/dispatcher/error_envelope_test.go rename to dispatcher/internal/dispatcher/error_envelope_test.go diff --git a/cli/internal/dispatcher/help_test.go b/dispatcher/internal/dispatcher/help_test.go similarity index 100% rename from cli/internal/dispatcher/help_test.go rename to dispatcher/internal/dispatcher/help_test.go diff --git a/cli/internal/dispatcher/install.go b/dispatcher/internal/dispatcher/install.go similarity index 98% rename from cli/internal/dispatcher/install.go rename to dispatcher/internal/dispatcher/install.go index 69d05d0567..ed098fa11c 100644 --- a/cli/internal/dispatcher/install.go +++ b/dispatcher/internal/dispatcher/install.go @@ -9,8 +9,8 @@ import ( "runtime" "strings" - "github.com/hatayama/unity-cli-loop/cli/internal/install" "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/dispatcher/internal/install" ) const ( diff --git a/cli/internal/dispatcher/install_test.go b/dispatcher/internal/dispatcher/install_test.go similarity index 100% rename from cli/internal/dispatcher/install_test.go rename to dispatcher/internal/dispatcher/install_test.go diff --git a/cli/internal/dispatcher/launch.go b/dispatcher/internal/dispatcher/launch.go similarity index 100% rename from cli/internal/dispatcher/launch.go rename to dispatcher/internal/dispatcher/launch.go diff --git a/cli/internal/dispatcher/launch_focus_log.go b/dispatcher/internal/dispatcher/launch_focus_log.go similarity index 100% rename from cli/internal/dispatcher/launch_focus_log.go rename to dispatcher/internal/dispatcher/launch_focus_log.go diff --git a/cli/internal/dispatcher/launch_options.go b/dispatcher/internal/dispatcher/launch_options.go similarity index 100% rename from cli/internal/dispatcher/launch_options.go rename to dispatcher/internal/dispatcher/launch_options.go diff --git a/cli/internal/dispatcher/launch_process_exit_timeout_error.go b/dispatcher/internal/dispatcher/launch_process_exit_timeout_error.go similarity index 100% rename from cli/internal/dispatcher/launch_process_exit_timeout_error.go rename to dispatcher/internal/dispatcher/launch_process_exit_timeout_error.go diff --git a/cli/internal/dispatcher/launch_process_unix.go b/dispatcher/internal/dispatcher/launch_process_unix.go similarity index 100% rename from cli/internal/dispatcher/launch_process_unix.go rename to dispatcher/internal/dispatcher/launch_process_unix.go diff --git a/cli/internal/dispatcher/launch_process_unix_test.go b/dispatcher/internal/dispatcher/launch_process_unix_test.go similarity index 100% rename from cli/internal/dispatcher/launch_process_unix_test.go rename to dispatcher/internal/dispatcher/launch_process_unix_test.go diff --git a/cli/internal/dispatcher/launch_process_windows.go b/dispatcher/internal/dispatcher/launch_process_windows.go similarity index 100% rename from cli/internal/dispatcher/launch_process_windows.go rename to dispatcher/internal/dispatcher/launch_process_windows.go diff --git a/cli/internal/dispatcher/launch_ready.go b/dispatcher/internal/dispatcher/launch_ready.go similarity index 100% rename from cli/internal/dispatcher/launch_ready.go rename to dispatcher/internal/dispatcher/launch_ready.go diff --git a/cli/internal/dispatcher/launch_startup_timeout_error.go b/dispatcher/internal/dispatcher/launch_startup_timeout_error.go similarity index 100% rename from cli/internal/dispatcher/launch_startup_timeout_error.go rename to dispatcher/internal/dispatcher/launch_startup_timeout_error.go diff --git a/cli/internal/dispatcher/launch_test.go b/dispatcher/internal/dispatcher/launch_test.go similarity index 100% rename from cli/internal/dispatcher/launch_test.go rename to dispatcher/internal/dispatcher/launch_test.go diff --git a/cli/internal/dispatcher/native_install_dir.go b/dispatcher/internal/dispatcher/native_install_dir.go similarity index 100% rename from cli/internal/dispatcher/native_install_dir.go rename to dispatcher/internal/dispatcher/native_install_dir.go diff --git a/cli/internal/dispatcher/run_dispatcher.go b/dispatcher/internal/dispatcher/run_dispatcher.go similarity index 99% rename from cli/internal/dispatcher/run_dispatcher.go rename to dispatcher/internal/dispatcher/run_dispatcher.go index 41beb36179..7ebe007c8d 100644 --- a/cli/internal/dispatcher/run_dispatcher.go +++ b/dispatcher/internal/dispatcher/run_dispatcher.go @@ -12,10 +12,10 @@ import ( "strings" "time" - sharedupdate "github.com/hatayama/unity-cli-loop/cli/internal/update" "github.com/hatayama/unity-cli-loop/common/clicore" "github.com/hatayama/unity-cli-loop/common/project" sharedversion "github.com/hatayama/unity-cli-loop/common/version" + sharedupdate "github.com/hatayama/unity-cli-loop/dispatcher/internal/update" ) const ( diff --git a/cli/internal/dispatcher/run_help.go b/dispatcher/internal/dispatcher/run_help.go similarity index 100% rename from cli/internal/dispatcher/run_help.go rename to dispatcher/internal/dispatcher/run_help.go diff --git a/cli/internal/dispatcher/skills.go b/dispatcher/internal/dispatcher/skills.go similarity index 100% rename from cli/internal/dispatcher/skills.go rename to dispatcher/internal/dispatcher/skills.go diff --git a/cli/internal/dispatcher/skills_content.go b/dispatcher/internal/dispatcher/skills_content.go similarity index 100% rename from cli/internal/dispatcher/skills_content.go rename to dispatcher/internal/dispatcher/skills_content.go diff --git a/cli/internal/dispatcher/skills_discovery.go b/dispatcher/internal/dispatcher/skills_discovery.go similarity index 100% rename from cli/internal/dispatcher/skills_discovery.go rename to dispatcher/internal/dispatcher/skills_discovery.go diff --git a/cli/internal/dispatcher/skills_dispatch.go b/dispatcher/internal/dispatcher/skills_dispatch.go similarity index 100% rename from cli/internal/dispatcher/skills_dispatch.go rename to dispatcher/internal/dispatcher/skills_dispatch.go diff --git a/cli/internal/dispatcher/skills_display.go b/dispatcher/internal/dispatcher/skills_display.go similarity index 100% rename from cli/internal/dispatcher/skills_display.go rename to dispatcher/internal/dispatcher/skills_display.go diff --git a/cli/internal/dispatcher/skills_sync.go b/dispatcher/internal/dispatcher/skills_sync.go similarity index 100% rename from cli/internal/dispatcher/skills_sync.go rename to dispatcher/internal/dispatcher/skills_sync.go diff --git a/cli/internal/dispatcher/skills_targets.go b/dispatcher/internal/dispatcher/skills_targets.go similarity index 100% rename from cli/internal/dispatcher/skills_targets.go rename to dispatcher/internal/dispatcher/skills_targets.go diff --git a/cli/internal/dispatcher/skills_test.go b/dispatcher/internal/dispatcher/skills_test.go similarity index 100% rename from cli/internal/dispatcher/skills_test.go rename to dispatcher/internal/dispatcher/skills_test.go diff --git a/cli/internal/dispatcher/skills_v3_migration.go b/dispatcher/internal/dispatcher/skills_v3_migration.go similarity index 100% rename from cli/internal/dispatcher/skills_v3_migration.go rename to dispatcher/internal/dispatcher/skills_v3_migration.go diff --git a/cli/internal/dispatcher/uninstall.go b/dispatcher/internal/dispatcher/uninstall.go similarity index 98% rename from cli/internal/dispatcher/uninstall.go rename to dispatcher/internal/dispatcher/uninstall.go index 5eb1864cfd..61538c3443 100644 --- a/cli/internal/dispatcher/uninstall.go +++ b/dispatcher/internal/dispatcher/uninstall.go @@ -9,8 +9,8 @@ import ( "path/filepath" "runtime" - "github.com/hatayama/unity-cli-loop/cli/internal/uninstall" "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/dispatcher/internal/uninstall" ) const ( diff --git a/cli/internal/dispatcher/uninstall_test.go b/dispatcher/internal/dispatcher/uninstall_test.go similarity index 100% rename from cli/internal/dispatcher/uninstall_test.go rename to dispatcher/internal/dispatcher/uninstall_test.go diff --git a/cli/internal/dispatcher/update.go b/dispatcher/internal/dispatcher/update.go similarity index 98% rename from cli/internal/dispatcher/update.go rename to dispatcher/internal/dispatcher/update.go index f474267756..650cec136d 100644 --- a/cli/internal/dispatcher/update.go +++ b/dispatcher/internal/dispatcher/update.go @@ -6,8 +6,8 @@ import ( "os/exec" "runtime" - "github.com/hatayama/unity-cli-loop/cli/internal/update" "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/dispatcher/internal/update" ) const ( diff --git a/cli/internal/dispatcher/update_test.go b/dispatcher/internal/dispatcher/update_test.go similarity index 94% rename from cli/internal/dispatcher/update_test.go rename to dispatcher/internal/dispatcher/update_test.go index d817230d6f..534bb7b571 100644 --- a/cli/internal/dispatcher/update_test.go +++ b/dispatcher/internal/dispatcher/update_test.go @@ -8,9 +8,9 @@ import ( "strings" "testing" - clicontract "github.com/hatayama/unity-cli-loop/cli" - "github.com/hatayama/unity-cli-loop/cli/internal/update" "github.com/hatayama/unity-cli-loop/common/clicore" + dispatchercontract "github.com/hatayama/unity-cli-loop/dispatcher" + "github.com/hatayama/unity-cli-loop/dispatcher/internal/update" ) func TestUpdateCommandForDarwinUsesDirectInstaller(t *testing.T) { @@ -24,8 +24,8 @@ func TestUpdateCommandForDarwinUsesDirectInstaller(t *testing.T) { t.Fatalf("command mismatch: %s", commandName) } joinedArgs := strings.Join(args, " ") - expectedScriptURL := update.ScriptURL(clicontract.DispatcherCurrent.DispatcherVersion, update.PosixScriptName) - expectedReleaseTag := update.UpdateSelectorForVersion(clicontract.DispatcherCurrent.DispatcherVersion) + expectedScriptURL := update.ScriptURL(dispatchercontract.DispatcherCurrent.DispatcherVersion, update.PosixScriptName) + expectedReleaseTag := update.UpdateSelectorForVersion(dispatchercontract.DispatcherCurrent.DispatcherVersion) if !strings.Contains(joinedArgs, expectedScriptURL) { t.Fatalf("installer URL missing: %s", joinedArgs) } @@ -51,8 +51,8 @@ func TestUpdateCommandForWindowsUsesPowerShellInstaller(t *testing.T) { t.Fatalf("command mismatch: %s", commandName) } joinedArgs := strings.Join(args, " ") - expectedScriptURL := update.ScriptURL(clicontract.DispatcherCurrent.DispatcherVersion, update.WindowsScriptName) - expectedReleaseTag := update.UpdateSelectorForVersion(clicontract.DispatcherCurrent.DispatcherVersion) + expectedScriptURL := update.ScriptURL(dispatchercontract.DispatcherCurrent.DispatcherVersion, update.WindowsScriptName) + expectedReleaseTag := update.UpdateSelectorForVersion(dispatchercontract.DispatcherCurrent.DispatcherVersion) if !strings.Contains(joinedArgs, expectedScriptURL) { t.Fatalf("installer URL missing: %s", joinedArgs) } diff --git a/cli/internal/install/command.go b/dispatcher/internal/install/command.go similarity index 100% rename from cli/internal/install/command.go rename to dispatcher/internal/install/command.go diff --git a/cli/internal/install/command_test.go b/dispatcher/internal/install/command_test.go similarity index 100% rename from cli/internal/install/command_test.go rename to dispatcher/internal/install/command_test.go diff --git a/cli/internal/install/posix_install.go b/dispatcher/internal/install/posix_install.go similarity index 100% rename from cli/internal/install/posix_install.go rename to dispatcher/internal/install/posix_install.go diff --git a/cli/internal/uninstall/command.go b/dispatcher/internal/uninstall/command.go similarity index 100% rename from cli/internal/uninstall/command.go rename to dispatcher/internal/uninstall/command.go diff --git a/cli/internal/uninstall/command_test.go b/dispatcher/internal/uninstall/command_test.go similarity index 100% rename from cli/internal/uninstall/command_test.go rename to dispatcher/internal/uninstall/command_test.go diff --git a/cli/internal/update/command.go b/dispatcher/internal/update/command.go similarity index 100% rename from cli/internal/update/command.go rename to dispatcher/internal/update/command.go diff --git a/cli/internal/update/installer.go b/dispatcher/internal/update/installer.go similarity index 100% rename from cli/internal/update/installer.go rename to dispatcher/internal/update/installer.go diff --git a/cli/internal/update/installer_test.go b/dispatcher/internal/update/installer_test.go similarity index 100% rename from cli/internal/update/installer_test.go rename to dispatcher/internal/update/installer_test.go diff --git a/go.work b/go.work index ec1425724e..5dc5e589b7 100644 --- a/go.work +++ b/go.work @@ -3,4 +3,5 @@ go 1.26 use ( ./cli ./common + ./dispatcher ) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000000..cc90caa9bb --- /dev/null +++ b/go.work.sum @@ -0,0 +1,3 @@ +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= diff --git a/release-please-config.json b/release-please-config.json index 518141fad2..d594bb3269 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -38,9 +38,7 @@ "include-component-in-tag": true, "exclude-paths": [ "cli/cmd/dispatch-release-please-pr-checks", - "cli/cmd/dispatcher", - "cli/internal/automation", - "cli/internal/dispatcher" + "cli/internal/automation" ], "changelog-path": "CHANGELOG.md", "extra-files": [ diff --git a/scripts/build-go-cli.sh b/scripts/build-go-cli.sh index b4530e9293..f906503719 100755 --- a/scripts/build-go-cli.sh +++ b/scripts/build-go-cli.sh @@ -3,6 +3,7 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) CLI_DIR="$ROOT_DIR/cli" +DISPATCHER_DIR="$ROOT_DIR/dispatcher" . "$ROOT_DIR/scripts/go-cli-toolchain.sh" require_go_cli_toolchain "$ROOT_DIR" @@ -28,9 +29,9 @@ build_binary() { ) } -build_binary darwin arm64 uloop "$CLI_DIR" ./cmd/dispatcher +build_binary darwin arm64 uloop "$DISPATCHER_DIR" ./cmd/dispatcher build_binary darwin arm64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner -build_binary darwin amd64 uloop "$CLI_DIR" ./cmd/dispatcher +build_binary darwin amd64 uloop "$DISPATCHER_DIR" ./cmd/dispatcher build_binary darwin amd64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner -build_binary windows amd64 uloop "$CLI_DIR" ./cmd/dispatcher +build_binary windows amd64 uloop "$DISPATCHER_DIR" ./cmd/dispatcher build_binary windows amd64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner diff --git a/scripts/check-go-cli-source.sh b/scripts/check-go-cli-source.sh index 3a5b27287a..74405e0391 100755 --- a/scripts/check-go-cli-source.sh +++ b/scripts/check-go-cli-source.sh @@ -36,4 +36,5 @@ run_module_checks() { } run_module_checks "$ROOT_DIR/common" +run_module_checks "$ROOT_DIR/dispatcher" run_module_checks "$CLI_DIR" diff --git a/scripts/resolve-dispatcher-release-target.sh b/scripts/resolve-dispatcher-release-target.sh index 8fef4c161a..7c7911290c 100755 --- a/scripts/resolve-dispatcher-release-target.sh +++ b/scripts/resolve-dispatcher-release-target.sh @@ -123,9 +123,9 @@ dispatcher_release_is_prerelease() { [ "$ref_name" = "v3-beta" ] } -VERSION=$(jq -r '.dispatcherVersion' cli/dispatcher-contract.json) +VERSION=$(jq -r '.dispatcherVersion' dispatcher/dispatcher-contract.json) if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then - echo "Could not resolve dispatcherVersion from cli/dispatcher-contract.json." >&2 + echo "Could not resolve dispatcherVersion from dispatcher/dispatcher-contract.json." >&2 exit 1 fi diff --git a/scripts/test-release-please-config.sh b/scripts/test-release-please-config.sh index f0a91721ed..0bf62e42c3 100755 --- a/scripts/test-release-please-config.sh +++ b/scripts/test-release-please-config.sh @@ -141,7 +141,7 @@ assert_json_value '.packages["cli"].["changelog-path"]' 'CHANGELOG.md' # cli/dispatcher-contract.json cannot be listed here: release-please matches # exclude-paths as directory prefixes only, so a plain-file entry is a silent # no-op (see the directory check below). -assert_json_value '.packages["cli"].["exclude-paths"] | sort | join(",")' 'cli/cmd/dispatch-release-please-pr-checks,cli/cmd/dispatcher,cli/internal/automation,cli/internal/dispatcher' +assert_json_value '.packages["cli"].["exclude-paths"] | sort | join(",")' 'cli/cmd/dispatch-release-please-pr-checks,cli/internal/automation' assert_json_value '.packages["cli"].["extra-files"] | length' '4' assert_json_value '.packages["cli"].["extra-files"][0].path' '/common/tools/default-tools.json' assert_json_value '.packages["cli"].["extra-files"][1].path' '/common/clicontract/contract.json' From 8cf7e92a54763eb676b4ed6131a0bf5aec24ace0 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 21:05:48 +0900 Subject: [PATCH 05/18] Extract release automation into its own Go module Move internal/automation and the six release automation commands out of the cli module into tools/release-automation, requiring only common. Path constants inside the guards now describe the split layout (common/clicontract, dispatcher/), and the runner and dispatcher contracts read at git refs fall back to the legacy cli/ paths because releases tagged before the split still store the contracts there. Workflow steps and the release sync script now run the automation commands from the new module directory, and the cli package exclude list is gone because the excluded directories left the package root. --- .github/workflows/build-and-test.yml | 8 ++-- .../protocol-minimum-version-warning.yml | 2 +- .github/workflows/release-please.yml | 2 +- go.work | 1 + release-please-config.json | 4 -- scripts/check-go-cli-source.sh | 1 + .../sync-release-please-package-releases.sh | 2 +- scripts/test-release-please-config.sh | 12 ++--- ...st-sync-release-please-package-releases.sh | 2 +- .../check-dispatcher-minimum-version/main.go | 2 +- .../cmd/check-dispatcher-version-bump/main.go | 2 +- .../cmd/check-ipc-protocol-reminder/main.go | 2 +- .../check-protocol-minimum-version/main.go | 2 +- .../comment-protocol-minimum-version/main.go | 2 +- .../dispatch-release-please-pr-checks/main.go | 2 +- tools/release-automation/go.mod | 7 +++ .../dispatcher_minimum_version_guard.go | 25 ++++++++-- .../dispatcher_minimum_version_guard_test.go | 39 +++++++++++++-- .../dispatcher_version_bump_guard.go | 21 ++++----- .../dispatcher_version_bump_guard_test.go | 14 +++--- .../automation/ipc_protocol_reminder.go | 12 ++--- .../automation/ipc_protocol_reminder_test.go | 6 +-- .../protocol_minimum_version_comment.go | 0 .../protocol_minimum_version_guard.go | 38 +++++++++++++-- .../protocol_minimum_version_guard_test.go | 47 +++++++++++++++---- .../protocol_minimum_version_parse.go | 0 .../internal/automation/release_pr_body.go | 0 .../internal/automation/release_pr_checks.go | 0 .../automation/release_pr_checks_test.go | 4 +- 29 files changed, 185 insertions(+), 74 deletions(-) rename {cli => tools/release-automation}/cmd/check-dispatcher-minimum-version/main.go (80%) rename {cli => tools/release-automation}/cmd/check-dispatcher-version-bump/main.go (83%) rename {cli => tools/release-automation}/cmd/check-ipc-protocol-reminder/main.go (85%) rename {cli => tools/release-automation}/cmd/check-protocol-minimum-version/main.go (91%) rename {cli => tools/release-automation}/cmd/comment-protocol-minimum-version/main.go (66%) rename {cli => tools/release-automation}/cmd/dispatch-release-please-pr-checks/main.go (64%) create mode 100644 tools/release-automation/go.mod rename {cli => tools/release-automation}/internal/automation/dispatcher_minimum_version_guard.go (91%) rename {cli => tools/release-automation}/internal/automation/dispatcher_minimum_version_guard_test.go (83%) rename {cli => tools/release-automation}/internal/automation/dispatcher_version_bump_guard.go (94%) rename {cli => tools/release-automation}/internal/automation/dispatcher_version_bump_guard_test.go (91%) rename {cli => tools/release-automation}/internal/automation/ipc_protocol_reminder.go (95%) rename {cli => tools/release-automation}/internal/automation/ipc_protocol_reminder_test.go (96%) rename {cli => tools/release-automation}/internal/automation/protocol_minimum_version_comment.go (100%) rename {cli => tools/release-automation}/internal/automation/protocol_minimum_version_guard.go (90%) rename {cli => tools/release-automation}/internal/automation/protocol_minimum_version_guard_test.go (92%) rename {cli => tools/release-automation}/internal/automation/protocol_minimum_version_parse.go (100%) rename {cli => tools/release-automation}/internal/automation/release_pr_body.go (100%) rename {cli => tools/release-automation}/internal/automation/release_pr_checks.go (100%) rename {cli => tools/release-automation}/internal/automation/release_pr_checks_test.go (99%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 383c8d0f75..12367d73cc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -50,24 +50,24 @@ jobs: - name: Check IPC protocol reminder if: github.event_name == 'pull_request' - working-directory: cli + working-directory: tools/release-automation run: go run ./cmd/check-ipc-protocol-reminder --base "origin/${{ github.base_ref }}" --head HEAD - name: Check protocol minimum version bump if: github.event_name == 'pull_request' && !(startsWith(github.head_ref, 'release-please--branches--') && (github.event.pull_request.user.login == 'github-actions[bot]' || github.event.pull_request.user.login == 'release-please[bot]')) - working-directory: cli + working-directory: tools/release-automation env: GH_TOKEN: ${{ github.token }} run: go run ./cmd/check-protocol-minimum-version --base "origin/${{ github.base_ref }}" --head HEAD - name: Check dispatcher minimum version if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - working-directory: cli + working-directory: tools/release-automation run: go run ./cmd/check-dispatcher-minimum-version - name: Check dispatcher version bump if: github.event_name == 'pull_request' - working-directory: cli + working-directory: tools/release-automation run: go run ./cmd/check-dispatcher-version-bump --base "origin/${{ github.base_ref }}" --head HEAD - name: Install golangci-lint diff --git a/.github/workflows/protocol-minimum-version-warning.yml b/.github/workflows/protocol-minimum-version-warning.yml index a6ab92a981..db632f965d 100644 --- a/.github/workflows/protocol-minimum-version-warning.yml +++ b/.github/workflows/protocol-minimum-version-warning.yml @@ -30,7 +30,7 @@ jobs: run: git fetch --no-tags origin "pull/${{ github.event.pull_request.number }}/merge:protocol-minimum-version-pr-merge" - name: Comment on protocol minimum version guard - working-directory: cli + working-directory: tools/release-automation env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 98e41e811d..8ba90ec342 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -113,7 +113,7 @@ jobs: - name: Dispatch release PR checks if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true' - working-directory: cli + working-directory: tools/release-automation env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TARGET_BRANCH: ${{ steps.target.outputs.branch }} diff --git a/go.work b/go.work index 5dc5e589b7..ff704f6bcf 100644 --- a/go.work +++ b/go.work @@ -4,4 +4,5 @@ use ( ./cli ./common ./dispatcher + ./tools/release-automation ) diff --git a/release-please-config.json b/release-please-config.json index d594bb3269..8b425e5ad1 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -36,10 +36,6 @@ "prerelease-type": "beta", "include-v-in-tag": true, "include-component-in-tag": true, - "exclude-paths": [ - "cli/cmd/dispatch-release-please-pr-checks", - "cli/internal/automation" - ], "changelog-path": "CHANGELOG.md", "extra-files": [ { diff --git a/scripts/check-go-cli-source.sh b/scripts/check-go-cli-source.sh index 74405e0391..740e134f8c 100755 --- a/scripts/check-go-cli-source.sh +++ b/scripts/check-go-cli-source.sh @@ -37,4 +37,5 @@ run_module_checks() { run_module_checks "$ROOT_DIR/common" run_module_checks "$ROOT_DIR/dispatcher" +run_module_checks "$ROOT_DIR/tools/release-automation" run_module_checks "$CLI_DIR" diff --git a/scripts/sync-release-please-package-releases.sh b/scripts/sync-release-please-package-releases.sh index 2186797210..26c164c1ba 100755 --- a/scripts/sync-release-please-package-releases.sh +++ b/scripts/sync-release-please-package-releases.sh @@ -451,7 +451,7 @@ verify_minimum_cli_release_protocol() { release_ref=$1 ( - cd "$ROOT_DIR/cli" + cd "$ROOT_DIR/tools/release-automation" go run ./cmd/check-protocol-minimum-version --verify-release --ref "$release_ref" ) } diff --git a/scripts/test-release-please-config.sh b/scripts/test-release-please-config.sh index 0bf62e42c3..3b69587986 100755 --- a/scripts/test-release-please-config.sh +++ b/scripts/test-release-please-config.sh @@ -136,12 +136,10 @@ assert_json_value '.packages["Packages/src"].["extra-files"][2].jsonpath' '$.pac assert_json_value '.packages["cli"].component' 'uloop-project-runner' assert_json_value '.packages["cli"].["include-component-in-tag"]' 'true' assert_json_value '.packages["cli"].["changelog-path"]' 'CHANGELOG.md' -# Dispatcher-owned paths must stay out of uloop-project-runner releases so a -# dispatcher-only change cannot regenerate a project-runner release PR. -# cli/dispatcher-contract.json cannot be listed here: release-please matches -# exclude-paths as directory prefixes only, so a plain-file entry is a silent -# no-op (see the directory check below). -assert_json_value '.packages["cli"].["exclude-paths"] | sort | join(",")' 'cli/cmd/dispatch-release-please-pr-checks,cli/internal/automation' +# The release automation cmd and internal packages that used to live under cli +# moved to tools/release-automation, so there is nothing left under cli to +# exclude from uloop-project-runner releases. +assert_json_value '.packages["cli"] | has("exclude-paths")' 'false' assert_json_value '.packages["cli"].["extra-files"] | length' '4' assert_json_value '.packages["cli"].["extra-files"][0].path' '/common/tools/default-tools.json' assert_json_value '.packages["cli"].["extra-files"][1].path' '/common/clicontract/contract.json' @@ -157,7 +155,7 @@ assert_file_contains "$RELEASE_WORKFLOW" ' checks: read' assert_file_contains "$RELEASE_WORKFLOW" ' - name: Setup Go for package release sync' assert_file_contains "$RELEASE_WORKFLOW" ' - name: Setup Go for release PR automation' assert_file_contains "$RELEASE_WORKFLOW" ' - name: Dispatch release PR checks' -assert_file_contains "$RELEASE_WORKFLOW" ' working-directory: cli' +assert_file_contains "$RELEASE_WORKFLOW" ' working-directory: tools/release-automation' assert_file_contains "$RELEASE_WORKFLOW" ' run: go run ./cmd/dispatch-release-please-pr-checks' assert_step_contains "$RELEASE_WORKFLOW" ' - name: Setup Go for release PR automation' " if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true'" assert_step_contains "$RELEASE_WORKFLOW" ' - name: Dispatch release PR checks' " if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true'" diff --git a/scripts/test-sync-release-please-package-releases.sh b/scripts/test-sync-release-please-package-releases.sh index 2e8aa7b2e5..7a95997cff 100755 --- a/scripts/test-sync-release-please-package-releases.sh +++ b/scripts/test-sync-release-please-package-releases.sh @@ -168,7 +168,7 @@ write_release_files() { unity_package_key=${2:-Packages/src} unity_changelog_path=${3:-CHANGELOG.md} - mkdir -p Packages/src cli scripts + mkdir -p Packages/src cli scripts tools/release-automation cat > release-please-config.json < ../../common diff --git a/cli/internal/automation/dispatcher_minimum_version_guard.go b/tools/release-automation/internal/automation/dispatcher_minimum_version_guard.go similarity index 91% rename from cli/internal/automation/dispatcher_minimum_version_guard.go rename to tools/release-automation/internal/automation/dispatcher_minimum_version_guard.go index d3afe64046..fdd3084ca1 100644 --- a/cli/internal/automation/dispatcher_minimum_version_guard.go +++ b/tools/release-automation/internal/automation/dispatcher_minimum_version_guard.go @@ -13,8 +13,10 @@ import ( ) const ( - cliContractFile = "cli/contract.json" - dispatcherContractFile = "cli/dispatcher-contract.json" + cliContractFile = "common/clicontract/contract.json" + legacyRunnerContractFile = "cli/contract.json" + dispatcherContractFile = "dispatcher/dispatcher-contract.json" + legacyDispatcherContractFile = "cli/dispatcher-contract.json" dispatcherReleaseTagPrefix = "dispatcher-v" unityPackageCliPinFile = "Packages/src/project-runner-pin.json" unityProjectCliPinFile = ".uloop/project-runner-pin.json" @@ -265,13 +267,28 @@ func verifyDispatcherMinimumVersionAtRef( } releaseTag := dispatcherReleaseTagPrefix + values.MinimumDispatcherVersion - contractContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, releaseTag, dispatcherContractFile) + contractContent, err := dispatcherContractFileAtRef(ctx, repoRoot, releaseTag) if err != nil { - return fmt.Errorf("dispatcher release %s does not provide %s", releaseTag, dispatcherContractFile) + return fmt.Errorf("dispatcher release %s does not provide %s or %s", releaseTag, dispatcherContractFile, legacyDispatcherContractFile) } return verifyMinimumCliReleaseDispatcherContract(values, []byte(contractContent)) } +// dispatcherContractFileAtRef reads the dispatcher release contract at a git ref. +// Dispatcher releases published before the cli/ directory split still provide the +// contract at the pre-split path, so this falls back to it when the new path is +// missing at the given ref. +func dispatcherContractFileAtRef(ctx context.Context, repoRoot string, ref string) (string, error) { + content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, dispatcherContractFile) + if err == nil { + return content, nil + } + if !isMissingFileAtRefError(err, dispatcherContractFile) { + return "", err + } + return protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, legacyDispatcherContractFile) +} + func verifyCurrentDispatcherMinimumVersion(values dispatcherMinimumVersionValues) error { if values.CurrentDispatcherContractVersion < minimumDispatcherContractVersion { return dispatcherContractVersionTooLowError(dispatcherContractFile, values.CurrentDispatcherContractVersion) diff --git a/cli/internal/automation/dispatcher_minimum_version_guard_test.go b/tools/release-automation/internal/automation/dispatcher_minimum_version_guard_test.go similarity index 83% rename from cli/internal/automation/dispatcher_minimum_version_guard_test.go rename to tools/release-automation/internal/automation/dispatcher_minimum_version_guard_test.go index 47b11bc87a..d5a4ec6e1b 100644 --- a/cli/internal/automation/dispatcher_minimum_version_guard_test.go +++ b/tools/release-automation/internal/automation/dispatcher_minimum_version_guard_test.go @@ -24,7 +24,7 @@ func TestRunDispatcherMinimumVersionCheck_WhenMinimumReleaseLacksDispatcherContr 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:cli/dispatcher-contract.json") + assertDispatcherMinimumVersionLogContains(t, result.gitLog, "dispatcher-v1.0.0:dispatcher/dispatcher-contract.json") } // Verifies release PRs pass when the current dispatcher release itself is the minimum dispatcher version. @@ -40,7 +40,26 @@ func TestRunDispatcherMinimumVersionCheck_WhenMinimumIsCurrentRelease_Passes(t * t.Fatalf("expected exit code 0, got %d\nstderr: %s", result.exitCode, result.stderr) } assertDispatcherMinimumVersionLogContains(t, result.stdout, "Dispatcher minimum version guard passed.") - assertDispatcherMinimumVersionLogDoesNotContain(t, result.gitLog, "dispatcher-v1.0.0:cli/dispatcher-contract.json") + assertDispatcherMinimumVersionLogDoesNotContain(t, result.gitLog, "dispatcher-v1.0.0:dispatcher/dispatcher-contract.json") +} + +// Verifies dispatcher releases published before the cli/ directory split are still readable +// through the legacy dispatcher-contract.json path when the new path is missing at the tag. +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}`, + }) + + if result.exitCode != 0 { + t.Fatalf("expected exit code 0, got %d\nstderr: %s", result.exitCode, result.stderr) + } + assertDispatcherMinimumVersionLogContains(t, result.stdout, "Dispatcher minimum version guard passed.") + assertDispatcherMinimumVersionLogContains(t, result.gitLog, "dispatcher-v1.0.0:dispatcher/dispatcher-contract.json") + assertDispatcherMinimumVersionLogContains(t, result.gitLog, "dispatcher-v1.0.0:cli/dispatcher-contract.json") } // Verifies committed pin files cannot drift from the C# minimum dispatcher version. @@ -81,6 +100,7 @@ type dispatcherMinimumVersionCase struct { minimumDispatcherVersion string projectPinMinimumDispatcherVersion string releaseContract string + legacyReleaseContract string } type dispatcherMinimumVersionRunResult struct { @@ -112,6 +132,11 @@ func runDispatcherMinimumVersionCheckCase(t *testing.T, testCase dispatcherMinim writeFile(t, releaseContractPath, testCase.releaseContract) t.Setenv("GIT_RELEASE_CONTRACT", releaseContractPath) } + if testCase.legacyReleaseContract != "" { + legacyReleaseContractPath := filepath.Join(workDir, "legacy-release-contract.json") + writeFile(t, legacyReleaseContractPath, testCase.legacyReleaseContract) + t.Setenv("GIT_LEGACY_RELEASE_CONTRACT", legacyReleaseContractPath) + } stdout := bytes.Buffer{} stderr := bytes.Buffer{} @@ -216,9 +241,17 @@ fi if [ "$1" = "show" ]; then case "$2" in - dispatcher-v*:cli/dispatcher-contract.json) + dispatcher-v*:dispatcher/dispatcher-contract.json) if [ -n "${GIT_RELEASE_CONTRACT:-}" ]; then cat "$GIT_RELEASE_CONTRACT" + else + echo "fatal: path 'dispatcher/dispatcher-contract.json' exists on disk, but not in '$2'" >&2 + exit 1 + fi + ;; + dispatcher-v*:cli/dispatcher-contract.json) + if [ -n "${GIT_LEGACY_RELEASE_CONTRACT:-}" ]; then + cat "$GIT_LEGACY_RELEASE_CONTRACT" else echo "release not found" >&2 exit 1 diff --git a/cli/internal/automation/dispatcher_version_bump_guard.go b/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go similarity index 94% rename from cli/internal/automation/dispatcher_version_bump_guard.go rename to tools/release-automation/internal/automation/dispatcher_version_bump_guard.go index 1abed4ad42..20b4bb99a7 100644 --- a/cli/internal/automation/dispatcher_version_bump_guard.go +++ b/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go @@ -14,14 +14,14 @@ import ( ) var dispatcherReleaseInputPatterns = []string{ - "cli/cmd/dispatcher/main.go", - "cli/contract.go", + "dispatcher/cmd/dispatcher/main.go", + "dispatcher/contract.go", dispatcherContractFile, - "cli/internal/clicore/*.go", - "cli/internal/dispatcher/*.go", - "cli/internal/install/*.go", - "cli/internal/uninstall/*.go", - "cli/internal/update/*.go", + "common/clicore/*.go", + "dispatcher/internal/dispatcher/*.go", + "dispatcher/internal/install/*.go", + "dispatcher/internal/uninstall/*.go", + "dispatcher/internal/update/*.go", "scripts/install.ps1", "scripts/install.sh", } @@ -124,10 +124,7 @@ func parseDispatcherVersionBumpBaseValues( } func isMissingDispatcherContractAtRefError(err error) bool { - message := err.Error() - quotedPath := "'" + dispatcherContractFile + "'" - return strings.Contains(message, "path "+quotedPath+" exists on disk, but not in") || - strings.Contains(message, "Path "+quotedPath+" does not exist in") + return isMissingFileAtRefError(err, dispatcherContractFile) } func AnalyzeDispatcherVersionBumpGuard( @@ -204,7 +201,7 @@ func FormatDispatcherVersionBumpWarning(result DispatcherVersionBumpGuardResult) builder.WriteString(changedInput) builder.WriteString("`\n") } - builder.WriteString("\nUpdate `cli/dispatcher-contract.json` `dispatcherVersion` before merging dispatcher release changes.") + builder.WriteString("\nUpdate `dispatcher/dispatcher-contract.json` `dispatcherVersion` before merging dispatcher release changes.") return builder.String() } diff --git a/cli/internal/automation/dispatcher_version_bump_guard_test.go b/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go similarity index 91% rename from cli/internal/automation/dispatcher_version_bump_guard_test.go rename to tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go index 2f6df64f48..9773266d95 100644 --- a/cli/internal/automation/dispatcher_version_bump_guard_test.go +++ b/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go @@ -49,7 +49,7 @@ func TestDispatcherVersionBumpGuardPassesWhenOnlyReleaseAutomationChanges(t *tes // Verifies dispatcher release inputs require a new dispatcherVersion. func TestDispatcherVersionBumpGuardRequiresDispatcherVersionIncrease(t *testing.T) { result := AnalyzeDispatcherVersionBumpGuard( - []string{"cli/internal/dispatcher/run_dispatcher.go"}, + []string{"dispatcher/internal/dispatcher/run_dispatcher.go"}, DispatcherVersionBumpValues{ HasContract: true, DispatcherVersion: "1.0.0", @@ -72,7 +72,7 @@ func TestDispatcherVersionBumpGuardRequiresDispatcherVersionIncrease(t *testing. // Verifies changes under the dispatcher entrypoint package require a new dispatcherVersion. func TestDispatcherVersionBumpGuardCoversDispatcherInternalPackage(t *testing.T) { result := AnalyzeDispatcherVersionBumpGuard( - []string{"cli/internal/dispatcher/dispatcher.go"}, + []string{"dispatcher/internal/dispatcher/dispatcher.go"}, DispatcherVersionBumpValues{ HasContract: true, DispatcherVersion: "1.0.0", @@ -92,7 +92,7 @@ func TestDispatcherVersionBumpGuardCoversDispatcherInternalPackage(t *testing.T) // Verifies shared clicore code compiled into the dispatcher binary counts as a dispatcher release input. func TestDispatcherVersionBumpGuardCoversClicorePackage(t *testing.T) { result := AnalyzeDispatcherVersionBumpGuard( - []string{"cli/internal/clicore/output.go"}, + []string{"common/clicore/output.go"}, DispatcherVersionBumpValues{ HasContract: true, DispatcherVersion: "1.0.0", @@ -112,7 +112,7 @@ func TestDispatcherVersionBumpGuardCoversClicorePackage(t *testing.T) { // Verifies the first dispatcher contract can be introduced without comparing against a missing base. func TestDispatcherVersionBumpGuardAcceptsInitialDispatcherContract(t *testing.T) { result := AnalyzeDispatcherVersionBumpGuard( - []string{"cli/dispatcher-contract.json"}, + []string{"dispatcher/dispatcher-contract.json"}, DispatcherVersionBumpValues{}, DispatcherVersionBumpValues{ HasContract: true, @@ -129,7 +129,7 @@ func TestDispatcherVersionBumpGuardAcceptsInitialDispatcherContract(t *testing.T func TestDispatcherVersionBumpGuardAcceptsMissingBaseContractReadError(t *testing.T) { _, err := parseDispatcherVersionBumpBaseValues( "", - errors.New("git show failed: fatal: path 'cli/dispatcher-contract.json' exists on disk, but not in 'origin/v3-beta'")) + errors.New("git show failed: fatal: path 'dispatcher/dispatcher-contract.json' exists on disk, but not in 'origin/v3-beta'")) if err != nil { t.Fatalf("expected missing base contract to be accepted: %v", err) } @@ -142,7 +142,7 @@ func TestDispatcherVersionBumpGuardRejectsUnexpectedBaseContractReadError(t *tes if err == nil { t.Fatal("expected unexpected base contract read error") } - if !strings.Contains(err.Error(), "failed to read base cli/dispatcher-contract.json") { + if !strings.Contains(err.Error(), "failed to read base dispatcher/dispatcher-contract.json") { t.Fatalf("unexpected error: %v", err) } } @@ -150,7 +150,7 @@ func TestDispatcherVersionBumpGuardRejectsUnexpectedBaseContractReadError(t *tes // Verifies dispatcher contract generations cannot move backwards. func TestDispatcherVersionBumpGuardRejectsDispatcherContractVersionDecrease(t *testing.T) { result := AnalyzeDispatcherVersionBumpGuard( - []string{"cli/internal/dispatcher/run_dispatcher.go"}, + []string{"dispatcher/internal/dispatcher/run_dispatcher.go"}, DispatcherVersionBumpValues{ HasContract: true, DispatcherVersion: "1.0.0", diff --git a/cli/internal/automation/ipc_protocol_reminder.go b/tools/release-automation/internal/automation/ipc_protocol_reminder.go similarity index 95% rename from cli/internal/automation/ipc_protocol_reminder.go rename to tools/release-automation/internal/automation/ipc_protocol_reminder.go index 5c8de2997a..21234acf7d 100644 --- a/cli/internal/automation/ipc_protocol_reminder.go +++ b/tools/release-automation/internal/automation/ipc_protocol_reminder.go @@ -11,11 +11,11 @@ import ( ) var ipcProtocolReminderPatterns = []string{ - "cli/contract.json", - "cli/contract.go", + "common/clicontract/contract.json", + "common/clicontract/contract.go", "cli/layout-contract.json", - "cli/internal/unityipc/**", - "cli/internal/tools/**", + "common/unityipc/**", + "common/tools/**", "Packages/src/Editor/CompositionRoot/UnityCliLoopFirstPartyServerLifecycleBinding.cs", "Packages/src/Editor/Domain/CliConstants.cs", "Packages/src/Editor/Infrastructure/Api/**", @@ -25,7 +25,7 @@ var ipcProtocolReminderPatterns = []string{ } var ipcProtocolDeclarationPaths = []string{ - "cli/contract.json", + "common/clicontract/contract.json", "Packages/src/Editor/Domain/CliConstants.cs", } @@ -141,7 +141,7 @@ func AppendIPCProtocolReminderSummary(summaryPath string, result IPCProtocolRemi content.WriteString("\n") } if result.NeedsProtocolBumpReview { - content.WriteString("If this PR breaks interoperability with the previous protocol generation, bump both `cli/contract.json` `protocolVersion` and `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION` in the same PR.\n\n") + content.WriteString("If this PR breaks interoperability with the previous protocol generation, bump both `common/clicontract/contract.json` `protocolVersion` and `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION` in the same PR.\n\n") } file, err := os.OpenFile(summaryPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) diff --git a/cli/internal/automation/ipc_protocol_reminder_test.go b/tools/release-automation/internal/automation/ipc_protocol_reminder_test.go similarity index 96% rename from cli/internal/automation/ipc_protocol_reminder_test.go rename to tools/release-automation/internal/automation/ipc_protocol_reminder_test.go index bcfe471834..6d58368e75 100644 --- a/cli/internal/automation/ipc_protocol_reminder_test.go +++ b/tools/release-automation/internal/automation/ipc_protocol_reminder_test.go @@ -25,7 +25,7 @@ func TestAnalyzeIPCProtocolReminder_WhenNoIPCFilesChanged_DoesNotRemind(t *testi func TestAnalyzeIPCProtocolReminder_WhenIPCFilesChangedWithoutProtocolDeclarations_Reminds(t *testing.T) { // Verifies IPC-facing changes surface a non-blocking protocol bump review reminder. result := AnalyzeIPCProtocolReminder([]string{ - "cli/internal/unityipc/client.go", + "common/unityipc/client.go", "Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs", }) @@ -40,8 +40,8 @@ func TestAnalyzeIPCProtocolReminder_WhenIPCFilesChangedWithoutProtocolDeclaratio func TestAnalyzeIPCProtocolReminder_WhenProtocolDeclarationsChanged_DoesNotRemind(t *testing.T) { // Verifies explicit protocol declaration edits satisfy the reminder check. result := AnalyzeIPCProtocolReminder([]string{ - "cli/internal/unityipc/client.go", - "cli/contract.json", + "common/unityipc/client.go", + "common/clicontract/contract.json", "Packages/src/Editor/Domain/CliConstants.cs", }) diff --git a/cli/internal/automation/protocol_minimum_version_comment.go b/tools/release-automation/internal/automation/protocol_minimum_version_comment.go similarity index 100% rename from cli/internal/automation/protocol_minimum_version_comment.go rename to tools/release-automation/internal/automation/protocol_minimum_version_comment.go diff --git a/cli/internal/automation/protocol_minimum_version_guard.go b/tools/release-automation/internal/automation/protocol_minimum_version_guard.go similarity index 90% rename from cli/internal/automation/protocol_minimum_version_guard.go rename to tools/release-automation/internal/automation/protocol_minimum_version_guard.go index ae80af5bc0..448c80e9be 100644 --- a/cli/internal/automation/protocol_minimum_version_guard.go +++ b/tools/release-automation/internal/automation/protocol_minimum_version_guard.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "os" @@ -250,9 +251,9 @@ func verifyMinimumCliReleaseProtocolAtRef( Tag: minimumProjectRunnerReleaseTag(values.MinimumProjectRunnerVersion), RequiredAssets: requiredMinimumProjectRunnerAssets, } - contractContent, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, release.Tag, "cli/contract.json") + contractContent, err := runnerContractFileAtRef(ctx, repoRoot, release.Tag) if err != nil { - return "", fmt.Errorf("project runner release %s does not provide cli/contract.json", release.Tag) + return "", errors.New(runnerContractMissingAtReleaseMessage(release.Tag)) } if err := verifyMinimumProjectRunnerReleaseProtocol(release.Tag, values, []byte(contractContent)); err != nil { return "", err @@ -260,6 +261,35 @@ func verifyMinimumCliReleaseProtocolAtRef( return verifyMinimumCliReleaseIsPublished(ctx, repoRoot, release) } +func runnerContractMissingAtReleaseMessage(releaseTag string) string { + return fmt.Sprintf("project runner release %s does not provide %s or %s", releaseTag, cliContractFile, legacyRunnerContractFile) +} + +// runnerContractFileAtRef reads the CLI/runner IPC contract file at a git ref. +// Release tags published before the cli/ directory split still provide the +// contract at the pre-split path, so this falls back to it when the new path +// is missing at the given ref. +func runnerContractFileAtRef(ctx context.Context, repoRoot string, ref string) (string, error) { + content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, cliContractFile) + if err == nil { + return content, nil + } + if !isMissingFileAtRefError(err, cliContractFile) { + return "", err + } + return protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, legacyRunnerContractFile) +} + +// isMissingFileAtRefError reports whether err came from `git show ref:file` +// failing because file does not exist at ref, as opposed to any other git +// failure (auth, network, etc.) that must not be silently swallowed. +func isMissingFileAtRefError(err error, file string) bool { + message := err.Error() + quotedPath := "'" + file + "'" + return strings.Contains(message, "path "+quotedPath+" exists on disk, but not in") || + strings.Contains(message, "Path "+quotedPath+" does not exist in") +} + func minimumCliReleaseProtocolFile(ctx context.Context, repoRoot string, ref string) ([]byte, error) { if ref == "" { return os.ReadFile(filepath.Join(repoRoot, protocolMinimumVersionFile)) @@ -381,7 +411,7 @@ func protocolMinimumVersionBootstrapAllowsUnpublishedProjectRunner( } func protocolMinimumVersionReleaseContractIsMissing(err error) bool { - return strings.Contains(err.Error(), "does not provide cli/contract.json") + return strings.Contains(err.Error(), fmt.Sprintf("does not provide %s or %s", cliContractFile, legacyRunnerContractFile)) } func protocolMinimumVersionValueLabel(values ProtocolMinimumVersionValues) string { @@ -426,7 +456,7 @@ func protocolMinimumProjectRunnerVersionAtRef( repoRoot string, ref string, ) (string, error) { - content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, cliContractFile) + content, err := runnerContractFileAtRef(ctx, repoRoot, ref) if err != nil { return "", err } diff --git a/cli/internal/automation/protocol_minimum_version_guard_test.go b/tools/release-automation/internal/automation/protocol_minimum_version_guard_test.go similarity index 92% rename from cli/internal/automation/protocol_minimum_version_guard_test.go rename to tools/release-automation/internal/automation/protocol_minimum_version_guard_test.go index ca5445692b..0fe4d99e62 100644 --- a/cli/internal/automation/protocol_minimum_version_guard_test.go +++ b/tools/release-automation/internal/automation/protocol_minimum_version_guard_test.go @@ -79,6 +79,23 @@ func TestRunProtocolMinimumVersionGuard_WhenMinimumReleaseMatches_Passes(t *test t.Fatalf("expected exit code 0, got %d\nstderr: %s", result.exitCode, result.stderr) } assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") + assertProtocolMinimumVersionLogContains(t, result.gitLog, "uloop-project-runner-v3.0.0-beta.33:common/clicontract/contract.json") +} + +func TestRunProtocolMinimumVersionGuard_WhenMinimumReleasePredatesDirectorySplit_FallsBackToLegacyContractPath(t *testing.T) { + // Verifies project runner releases published before the cli/ directory split are still + // readable through the legacy contract.json path when the new path is missing at the tag. + result := runProtocolMinimumVersionGuardCase(t, protocolMinimumVersionRefCase{ + baseContent: buildProtocolMinimumVersionConstants(1, "3.0.0-beta.32"), + headContent: buildProtocolMinimumVersionConstants(2, "3.0.0-beta.33"), + legacyReleaseContent: `{"schemaVersion":1,"protocolVersion":2,"projectRunnerVersion":"3.0.0-beta.33"}`, + }) + + if result.exitCode != 0 { + t.Fatalf("expected exit code 0, got %d\nstderr: %s", result.exitCode, result.stderr) + } + assertProtocolMinimumVersionLogContains(t, result.stdout, "Protocol minimum version guard passed.") + assertProtocolMinimumVersionLogContains(t, result.gitLog, "uloop-project-runner-v3.0.0-beta.33:common/clicontract/contract.json") assertProtocolMinimumVersionLogContains(t, result.gitLog, "uloop-project-runner-v3.0.0-beta.33:cli/contract.json") } @@ -349,7 +366,7 @@ func TestRunMinimumCliReleaseProtocolCheck_WhenMinimumVersionHasPrefix_Normalize t.Fatalf("expected exit code 0, got %d\nstderr: %s", exitCode, stderr.String()) } assertProtocolMinimumVersionLogContains(t, stdout.String(), "Minimum project runner release uloop-project-runner-v3.0.0-beta.33 advertises protocol 2.") - assertProtocolMinimumVersionLogContains(t, readFile(t, gitLogPath), "uloop-project-runner-v3.0.0-beta.33:cli/contract.json") + assertProtocolMinimumVersionLogContains(t, readFile(t, gitLogPath), "uloop-project-runner-v3.0.0-beta.33:common/clicontract/contract.json") assertProtocolMinimumVersionLogContains(t, readFile(t, ghLogPath), "release view uloop-project-runner-v3.0.0-beta.33") } @@ -402,11 +419,12 @@ func TestRunProtocolMinimumVersionComment_WhenMinimumReleaseProtocolDiffers_Upse } type protocolMinimumVersionRefCase struct { - baseContent string - headContent string - headContractContent string - releaseContent string - releaseView string + baseContent string + headContent string + headContractContent string + releaseContent string + legacyReleaseContent string + releaseView string } type protocolMinimumVersionGuardRunResult struct { @@ -533,6 +551,11 @@ func prepareProtocolMinimumVersionGitContents(t *testing.T, workDir string, test writeFile(t, releaseContentPath, testCase.releaseContent) t.Setenv("GIT_RELEASE_CONTENT", releaseContentPath) } + if testCase.legacyReleaseContent != "" { + legacyReleaseContentPath := filepath.Join(workDir, "legacy-release-contract.json") + writeFile(t, legacyReleaseContentPath, testCase.legacyReleaseContent) + t.Setenv("GIT_LEGACY_RELEASE_CONTENT", legacyReleaseContentPath) + } t.Setenv("GIT_BASE_CONTENT", baseContentPath) t.Setenv("GIT_HEAD_CONTENT", headContentPath) } @@ -579,7 +602,7 @@ fi if [ "$1" = "show" ]; then case "$2" in origin/v3-beta:*) cat "$GIT_BASE_CONTENT" ;; - protocol-pr-head:cli/contract.json) + protocol-pr-head:common/clicontract/contract.json) if [ -n "${GIT_HEAD_CONTRACT_CONTENT:-}" ]; then cat "$GIT_HEAD_CONTRACT_CONTENT" else @@ -588,9 +611,17 @@ fi ;; protocol-pr-head:*) cat "$GIT_HEAD_CONTENT" ;; protocol-release:*) cat "$GIT_HEAD_CONTENT" ;; - uloop-project-runner-v*:cli/contract.json) + uloop-project-runner-v*:common/clicontract/contract.json) if [ -n "${GIT_RELEASE_CONTENT:-}" ]; then cat "$GIT_RELEASE_CONTENT" + else + echo "fatal: path 'common/clicontract/contract.json' exists on disk, but not in '$2'" >&2 + exit 1 + fi + ;; + uloop-project-runner-v*:cli/contract.json) + if [ -n "${GIT_LEGACY_RELEASE_CONTENT:-}" ]; then + cat "$GIT_LEGACY_RELEASE_CONTENT" else echo "release not found" >&2 exit 1 diff --git a/cli/internal/automation/protocol_minimum_version_parse.go b/tools/release-automation/internal/automation/protocol_minimum_version_parse.go similarity index 100% rename from cli/internal/automation/protocol_minimum_version_parse.go rename to tools/release-automation/internal/automation/protocol_minimum_version_parse.go diff --git a/cli/internal/automation/release_pr_body.go b/tools/release-automation/internal/automation/release_pr_body.go similarity index 100% rename from cli/internal/automation/release_pr_body.go rename to tools/release-automation/internal/automation/release_pr_body.go diff --git a/cli/internal/automation/release_pr_checks.go b/tools/release-automation/internal/automation/release_pr_checks.go similarity index 100% rename from cli/internal/automation/release_pr_checks.go rename to tools/release-automation/internal/automation/release_pr_checks.go diff --git a/cli/internal/automation/release_pr_checks_test.go b/tools/release-automation/internal/automation/release_pr_checks_test.go similarity index 99% rename from cli/internal/automation/release_pr_checks_test.go rename to tools/release-automation/internal/automation/release_pr_checks_test.go index 5b770c0331..48058693d5 100644 --- a/cli/internal/automation/release_pr_checks_test.go +++ b/tools/release-automation/internal/automation/release_pr_checks_test.go @@ -466,7 +466,7 @@ fi if [ "$1" = "show" ]; then case "$2" in - uloop-project-runner-v*:cli/contract.json) + uloop-project-runner-v*:common/clicontract/contract.json) if [ -n "${GIT_RELEASE_CONTRACT:-}" ]; then cat "$GIT_RELEASE_CONTRACT" else @@ -568,7 +568,7 @@ if "%~1"=="fetch" if "%~2"=="origin" exit /b 0 if "%~1"=="switch" if "%~2"=="--detach" if "%~3"=="FETCH_HEAD" exit /b 0 if "%~1"=="show" ( - echo %~2| findstr /r "^uloop-project-runner-v.*:cli/contract.json$" >nul + echo %~2| findstr /r "^uloop-project-runner-v.*:common/clicontract/contract.json$" >nul if not errorlevel 1 ( if not "%GIT_RELEASE_CONTRACT%"=="" ( type "%GIT_RELEASE_CONTRACT%" From 252ebd0a5fae30a5272d4b790a7efa242fe15098 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 21:06:15 +0900 Subject: [PATCH 06/18] Bump dispatcherVersion for the module split Every dispatcher source file moved into the dispatcher module in this branch, which changes the dispatcher release inputs, so the next dispatcher release must carry a new version per the bump guard. --- dispatcher/dispatcher-contract.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dispatcher/dispatcher-contract.json b/dispatcher/dispatcher-contract.json index e1d8770c0d..f24cca038f 100644 --- a/dispatcher/dispatcher-contract.json +++ b/dispatcher/dispatcher-contract.json @@ -1,5 +1,5 @@ { "schemaVersion": 1, - "dispatcherVersion": "3.0.1-beta.10", + "dispatcherVersion": "3.0.1-beta.11", "dispatcherContractVersion": 1 } From 4621c8078f2714fb43ba051de65f0aa46a973d0b Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 21:10:18 +0900 Subject: [PATCH 07/18] Read the bump guard base contract through the legacy fallback The dispatcher version bump guard read the base dispatcher contract only at the split path, so any base ref predating the directory split looked like an initial contract introduction and the bump requirement was silently skipped. Route the base read through the existing legacy-path fallback so pre-split bases still enforce a version increase; a base missing the contract at both paths remains the bootstrap case. --- .../automation/dispatcher_version_bump_guard.go | 11 +++++++++-- .../automation/dispatcher_version_bump_guard_test.go | 10 ++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go b/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go index 20b4bb99a7..062defb09d 100644 --- a/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go +++ b/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go @@ -96,7 +96,11 @@ func AnalyzeDispatcherVersionBumpGuardForRefs( return DispatcherVersionBumpGuardResult{}, fmt.Errorf("failed to read head %s: %w", dispatcherContractFile, err) } - baseContent, baseContentErr := protocolMinimumVersionFileAtRef(ctx, repoRoot, config.BaseRef, dispatcherContractFile) + // The base ref may predate the directory split, where the contract lived at + // the legacy cli/ path. Without the fallback every pre-split base would look + // like an initial contract introduction and the bump requirement would be + // silently skipped. + baseContent, baseContentErr := dispatcherContractFileAtRef(ctx, repoRoot, config.BaseRef) baseValues, err := parseDispatcherVersionBumpBaseValues(baseContent, baseContentErr) if err != nil { return DispatcherVersionBumpGuardResult{}, err @@ -124,7 +128,10 @@ func parseDispatcherVersionBumpBaseValues( } func isMissingDispatcherContractAtRefError(err error) bool { - return isMissingFileAtRefError(err, dispatcherContractFile) + // dispatcherContractFileAtRef falls back to the legacy path, so a base ref + // without any dispatcher contract surfaces as the legacy file missing. + return isMissingFileAtRefError(err, dispatcherContractFile) || + isMissingFileAtRefError(err, legacyDispatcherContractFile) } func AnalyzeDispatcherVersionBumpGuard( diff --git a/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go b/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go index 9773266d95..45ec478b32 100644 --- a/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go +++ b/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go @@ -135,6 +135,16 @@ func TestDispatcherVersionBumpGuardAcceptsMissingBaseContractReadError(t *testin } } +// Verifies a base ref missing the contract at both the split and the legacy path is treated as initial introduction. +func TestDispatcherVersionBumpGuardAcceptsMissingLegacyBaseContractReadError(t *testing.T) { + _, err := parseDispatcherVersionBumpBaseValues( + "", + errors.New("git show failed: fatal: path 'cli/dispatcher-contract.json' exists on disk, but not in 'origin/v3-beta'")) + if err != nil { + t.Fatalf("expected missing legacy base contract to be accepted: %v", err) + } +} + // Verifies base contract read failures other than missing files do not get silently ignored. func TestDispatcherVersionBumpGuardRejectsUnexpectedBaseContractReadError(t *testing.T) { _, err := parseDispatcherVersionBumpBaseValues("", errors.New("git show failed: gh auth failed")) From ab52614f1c2ed903b23388775adab050165ce53b Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 21:43:05 +0900 Subject: [PATCH 08/18] Fix dispatcher release target test fixture to use the split contract path The dispatcher module split moved dispatcher-contract.json from cli/ to dispatcher/, and resolve-dispatcher-release-target.sh now reads the new path. The test fixture still wrote the contract to the legacy cli/ path, so every case failed with a missing-file jq error. --- scripts/test-resolve-dispatcher-release-target.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test-resolve-dispatcher-release-target.sh b/scripts/test-resolve-dispatcher-release-target.sh index 1ba398735e..00e0992959 100755 --- a/scripts/test-resolve-dispatcher-release-target.sh +++ b/scripts/test-resolve-dispatcher-release-target.sh @@ -74,13 +74,13 @@ MOCK_GH write_contract() { version=$1 - mkdir -p cli + mkdir -p dispatcher printf '%s\n' \ '{' \ ' "schemaVersion": 1,' \ ' "dispatcherVersion": "'"$version"'",' \ ' "dispatcherContractVersion": 1' \ - '}' > cli/dispatcher-contract.json + '}' > dispatcher/dispatcher-contract.json } run_case() { From 19721c0804a5388d9323a0420ce6a12a068a922b Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 21:46:30 +0900 Subject: [PATCH 09/18] Rename the cli module to project-runner and move shared tooling to the repo root After the dispatcher and release-automation splits, the cli/ directory only contained the project runner, so the name no longer described the module. Renaming the directory and Go module path finishes the physical split of the release boundaries. - release-please package key "cli" -> "project-runner"; the component uloop-project-runner and its tag series stay unchanged, and the manifest carries the current version over to the renamed key - native binaries now build into a repo-root dist/ tree shared by the dispatcher and project runner outputs - the Go toolchain version file, golangci configs, and the layout contract (now schema v2 declaring all four modules) move to the repo root as the single source of truth - workflows, packaging, release-target, and sync scripts follow the new paths; module-wide checks and vulnerability scans cover all four modules --- .github/workflows/build-and-test.yml | 12 +-- .github/workflows/code-complexity.yml | 13 +-- .github/workflows/dispatcher-publish.yml | 14 ++-- .github/workflows/native-cli-publish.yml | 16 ++-- .../protocol-minimum-version-warning.yml | 2 +- .github/workflows/release-please.yml | 8 +- .github/workflows/security-scan.yml | 32 ++++--- .gitignore | 7 +- cli/.go-version => .go-version | 0 ...complexity.yml => .golangci-complexity.yml | 0 cli/.golangci.yml => .golangci.yml | 0 .release-please-manifest.json | 2 +- cli/layout-contract.json | 13 --- go.work | 2 +- layout-contract.json | 22 +++++ {cli => project-runner}/CHANGELOG.md | 0 .../cmd/project-runner/main.go | 2 +- {cli => project-runner}/go.mod | 2 +- {cli => project-runner}/go.sum | 0 .../architecture/architecture_test.go | 84 ++++++++++++------- .../github_actions_security_test.go | 0 .../internal/projectrunner/compile_wait.go | 0 .../projectrunner/compile_wait_test.go | 0 .../projectrunner/connection_retry.go | 0 .../projectrunner/connection_retry_flow.go | 0 .../projectrunner/connection_retry_test.go | 0 .../projectrunner/control_play_mode_wait.go | 0 .../control_play_mode_wait_test.go | 0 .../internal/projectrunner/debug_timing.go | 0 .../projectrunner/debug_timing_test.go | 0 .../projectrunner/dynamic_code_file.go | 0 .../projectrunner/dynamic_code_file_test.go | 0 .../projectrunner/dynamic_code_wait.go | 0 .../projectrunner/execution_errors.go | 0 .../internal/projectrunner/list_output.go | 0 .../projectrunner/list_output_test.go | 0 .../projectrunner/native_tool_settings.go | 0 .../projectrunner/pause_point_errors.go | 0 .../projectrunner/pause_point_logs.go | 0 .../projectrunner/pause_point_wait.go | 0 .../projectrunner/pause_point_wait_test.go | 0 .../internal/projectrunner/projectrunner.go | 0 .../internal/projectrunner/run.go | 0 .../internal/projectrunner/run_test.go | 0 .../internal/projectrunner/runner_commands.go | 0 .../internal/projectrunner/runner_usage.go | 0 .../projectrunner/runner_usage_test.go | 0 .../internal/projectrunner/tool_params.go | 0 .../internal/projectrunner/tools_test.go | 0 release-please-config.json | 2 +- scripts/build-go-cli.sh | 10 +-- scripts/check-build-link-go-cli.sh | 6 +- scripts/check-code-complexity.sh | 6 +- scripts/check-go-cli-source.sh | 8 +- scripts/go-cli-toolchain.sh | 2 +- scripts/package-dispatcher.sh | 5 +- scripts/package-go-cli.sh | 3 +- scripts/refresh-neighbor-game-skills-macos.sh | 4 +- .../refresh-neighbor-game-skills-windows.sh | 4 +- scripts/resolve-native-cli-release-target.sh | 22 +++-- scripts/run-posix-e2e.sh | 6 +- .../sync-release-please-package-releases.sh | 2 +- scripts/test-go-cli-toolchain.sh | 4 +- scripts/test-native-cli-publish-workflow.sh | 4 +- scripts/test-release-please-config.sh | 30 +++---- .../test-resolve-native-cli-release-target.sh | 8 +- ...st-sync-release-please-package-releases.sh | 8 +- scripts/test-use-local-uloop.sh | 4 +- scripts/use-local-uloop.sh | 4 +- scripts/verify-dispatcher-release-assets.sh | 2 +- scripts/verify-go-cli-dist.sh | 12 +-- scripts/verify-native-cli-release-assets.sh | 2 +- .../dispatcher_version_bump_guard_test.go | 2 +- .../automation/ipc_protocol_reminder.go | 2 +- 74 files changed, 215 insertions(+), 178 deletions(-) rename cli/.go-version => .go-version (100%) rename cli/.golangci-complexity.yml => .golangci-complexity.yml (100%) rename cli/.golangci.yml => .golangci.yml (100%) delete mode 100644 cli/layout-contract.json create mode 100644 layout-contract.json rename {cli => project-runner}/CHANGELOG.md (100%) rename {cli => project-runner}/cmd/project-runner/main.go (65%) rename {cli => project-runner}/go.mod (82%) rename {cli => project-runner}/go.sum (100%) rename {cli => project-runner}/internal/architecture/architecture_test.go (75%) rename {cli => project-runner}/internal/architecture/github_actions_security_test.go (100%) rename {cli => project-runner}/internal/projectrunner/compile_wait.go (100%) rename {cli => project-runner}/internal/projectrunner/compile_wait_test.go (100%) rename {cli => project-runner}/internal/projectrunner/connection_retry.go (100%) rename {cli => project-runner}/internal/projectrunner/connection_retry_flow.go (100%) rename {cli => project-runner}/internal/projectrunner/connection_retry_test.go (100%) rename {cli => project-runner}/internal/projectrunner/control_play_mode_wait.go (100%) rename {cli => project-runner}/internal/projectrunner/control_play_mode_wait_test.go (100%) rename {cli => project-runner}/internal/projectrunner/debug_timing.go (100%) rename {cli => project-runner}/internal/projectrunner/debug_timing_test.go (100%) rename {cli => project-runner}/internal/projectrunner/dynamic_code_file.go (100%) rename {cli => project-runner}/internal/projectrunner/dynamic_code_file_test.go (100%) rename {cli => project-runner}/internal/projectrunner/dynamic_code_wait.go (100%) rename {cli => project-runner}/internal/projectrunner/execution_errors.go (100%) rename {cli => project-runner}/internal/projectrunner/list_output.go (100%) rename {cli => project-runner}/internal/projectrunner/list_output_test.go (100%) rename {cli => project-runner}/internal/projectrunner/native_tool_settings.go (100%) rename {cli => project-runner}/internal/projectrunner/pause_point_errors.go (100%) rename {cli => project-runner}/internal/projectrunner/pause_point_logs.go (100%) rename {cli => project-runner}/internal/projectrunner/pause_point_wait.go (100%) rename {cli => project-runner}/internal/projectrunner/pause_point_wait_test.go (100%) rename {cli => project-runner}/internal/projectrunner/projectrunner.go (100%) rename {cli => project-runner}/internal/projectrunner/run.go (100%) rename {cli => project-runner}/internal/projectrunner/run_test.go (100%) rename {cli => project-runner}/internal/projectrunner/runner_commands.go (100%) rename {cli => project-runner}/internal/projectrunner/runner_usage.go (100%) rename {cli => project-runner}/internal/projectrunner/runner_usage_test.go (100%) rename {cli => project-runner}/internal/projectrunner/tool_params.go (100%) rename {cli => project-runner}/internal/projectrunner/tools_test.go (100%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 12367d73cc..3cfdf13cca 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -27,7 +27,7 @@ jobs: - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version + go-version-file: .go-version cache: false - name: Test release automation shell helpers @@ -91,7 +91,7 @@ jobs: - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version + go-version-file: .go-version cache: false # Windows-only code paths (named pipe transport, winio deadlines, typed error @@ -133,10 +133,10 @@ jobs: test -d "Packages/src/Editor" || exit 1 test ! -d "Packages/src/Cli~" || exit 1 test -d "Packages/src/Editor/CliOnlyTools~" || exit 1 - test -d "cli" || exit 1 - test -f "cli/go.mod" || exit 1 - test -d "cli/cmd" || exit 1 - test -d "cli/internal" || exit 1 + test -d "project-runner" || exit 1 + test -f "project-runner/go.mod" || exit 1 + test -d "project-runner/cmd" || exit 1 + test -d "project-runner/internal" || exit 1 echo "✅ Unity Package structure is valid" diff --git a/.github/workflows/code-complexity.yml b/.github/workflows/code-complexity.yml index 5cbd39e0a6..6f193525be 100644 --- a/.github/workflows/code-complexity.yml +++ b/.github/workflows/code-complexity.yml @@ -4,9 +4,12 @@ on: pull_request: branches: [ main, v3-beta ] paths: - - 'cli/**/*.go' - - 'cli/.go-version' - - 'cli/.golangci-complexity.yml' + - 'common/**/*.go' + - 'dispatcher/**/*.go' + - 'project-runner/**/*.go' + - 'tools/release-automation/**/*.go' + - '.go-version' + - '.golangci-complexity.yml' - 'Packages/src/**/*.cs' - 'tools/UnityCliLoop.CodeComplexity/**' - 'tests/UnityCliLoop.CodeComplexity.Tests/**' @@ -29,7 +32,7 @@ jobs: - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version + go-version-file: .go-version cache: false - name: Setup .NET @@ -54,7 +57,7 @@ jobs: if: always() run: | mkdir -p artifacts - (cd cli && golangci-lint run --config .golangci-complexity.yml --issues-exit-code=0 --show-stats=false --output.json.path ../artifacts/go-code-complexity.json ./...) + (cd project-runner && golangci-lint run --config ../.golangci-complexity.yml --issues-exit-code=0 --show-stats=false --output.json.path ../artifacts/go-code-complexity.json ./...) dotnet run --project tools/UnityCliLoop.CodeComplexity/UnityCliLoop.CodeComplexity.csproj -- \ --root . \ --max-complexity 15 \ diff --git a/.github/workflows/dispatcher-publish.yml b/.github/workflows/dispatcher-publish.yml index 388cbe13b8..5555ff4340 100644 --- a/.github/workflows/dispatcher-publish.yml +++ b/.github/workflows/dispatcher-publish.yml @@ -70,8 +70,8 @@ jobs: if: steps.release.outputs.publish == 'true' || steps.release.outputs.release == 'true' uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version - cache-dependency-path: cli/**/go.sum + go-version-file: .go-version + cache-dependency-path: '**/go.sum' - name: Install golangci-lint if: steps.release.outputs.publish == 'true' @@ -95,7 +95,7 @@ jobs: if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run != 'true' uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 with: - subject-path: cli/dist/dispatcher-release/* + subject-path: dist/dispatcher-release/* - name: Verify dispatcher asset attestations if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run != 'true' @@ -106,7 +106,7 @@ jobs: run: | set -eu - for asset_path in cli/dist/dispatcher-release/*; do + for asset_path in dist/dispatcher-release/*; do gh attestation verify "${asset_path}" \ --repo "${GITHUB_REPOSITORY}" \ --signer-workflow "${SIGNER_WORKFLOW}" @@ -117,7 +117,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: dispatcher-release-assets-${{ steps.release.outputs.tag }} - path: cli/dist/dispatcher-release/* + path: dist/dispatcher-release/* if-no-files-found: error - name: Create or reuse draft dispatcher release @@ -194,7 +194,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ steps.release.outputs.tag }} run: | - gh release upload "${RELEASE_TAG}" cli/dist/dispatcher-release/* --clobber + gh release upload "${RELEASE_TAG}" dist/dispatcher-release/* --clobber - name: Verify remote dispatcher release assets if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run != 'true' @@ -246,4 +246,4 @@ jobs: - name: Dry run summary if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run == 'true' run: | - ls -lh cli/dist/dispatcher-release + ls -lh dist/dispatcher-release diff --git a/.github/workflows/native-cli-publish.yml b/.github/workflows/native-cli-publish.yml index cc9e8f393e..b62d499dc8 100644 --- a/.github/workflows/native-cli-publish.yml +++ b/.github/workflows/native-cli-publish.yml @@ -75,8 +75,8 @@ jobs: if: steps.release.outputs.publish == 'true' || steps.release.outputs.release == 'true' uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version - cache-dependency-path: cli/**/go.sum + go-version-file: .go-version + cache-dependency-path: '**/go.sum' - name: Install golangci-lint if: steps.release.outputs.publish == 'true' @@ -100,7 +100,7 @@ jobs: if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run != 'true' uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 with: - subject-path: cli/dist/release/* + subject-path: dist/release/* - name: Verify native CLI asset attestations if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run != 'true' @@ -111,7 +111,7 @@ jobs: run: | set -eu - for asset_path in cli/dist/release/*; do + for asset_path in dist/release/*; do gh attestation verify "${asset_path}" \ --repo "${GITHUB_REPOSITORY}" \ --signer-workflow "${SIGNER_WORKFLOW}" @@ -122,7 +122,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: native-cli-release-assets-${{ steps.release.outputs.tag }} - path: cli/dist/release/* + path: dist/release/* if-no-files-found: error - name: Write release notes @@ -151,7 +151,7 @@ jobs: exit 1 } } - ' cli/CHANGELOG.md > "${NOTES_FILE}" + ' project-runner/CHANGELOG.md > "${NOTES_FILE}" echo "RELEASE_NOTES_FILE=${NOTES_FILE}" >> "$GITHUB_ENV" @@ -212,7 +212,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ steps.release.outputs.tag }} run: | - gh release upload "${RELEASE_TAG}" cli/dist/release/* --clobber + gh release upload "${RELEASE_TAG}" dist/release/* --clobber - name: Verify remote release assets if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run != 'true' @@ -281,4 +281,4 @@ jobs: - name: Dry run summary if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run == 'true' run: | - ls -lh cli/dist/release + ls -lh dist/release diff --git a/.github/workflows/protocol-minimum-version-warning.yml b/.github/workflows/protocol-minimum-version-warning.yml index db632f965d..162aa7d23c 100644 --- a/.github/workflows/protocol-minimum-version-warning.yml +++ b/.github/workflows/protocol-minimum-version-warning.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version + go-version-file: .go-version cache: false - name: Fetch pull request merge diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 8ba90ec342..4d2f10f0d0 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -74,8 +74,8 @@ jobs: - name: Setup Go for package release sync uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version - cache-dependency-path: cli/**/go.sum + go-version-file: .go-version + cache-dependency-path: '**/go.sum' - name: 🏷️ Sync release-please package releases id: package_release_sync @@ -108,8 +108,8 @@ jobs: if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true' uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version - cache-dependency-path: cli/**/go.sum + go-version-file: .go-version + cache-dependency-path: '**/go.sum' - name: Dispatch release PR checks if: steps.target.outputs.branch == 'v3-beta' && steps.release_commit.outputs.skip != 'true' && steps.package_release_sync.outputs.ready != 'false' && steps.release.outputs.prs_created == 'true' diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 0cced8c378..d8f276d909 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -6,21 +6,27 @@ on: paths: - 'Packages/src/**/*.cs' - 'Assets/**/*.cs' - - 'cli/**/*.go' - - 'cli/.golangci.yml' - - 'cli/.go-version' - - 'cli/**/go.mod' - - 'cli/**/go.sum' + - 'common/**/*.go' + - 'dispatcher/**/*.go' + - 'project-runner/**/*.go' + - 'tools/release-automation/**/*.go' + - '.golangci.yml' + - '.go-version' + - '**/go.mod' + - '**/go.sum' pull_request: branches: [ main, v3-beta ] paths: - 'Packages/src/**/*.cs' - 'Assets/**/*.cs' - - 'cli/**/*.go' - - 'cli/.golangci.yml' - - 'cli/.go-version' - - 'cli/**/go.mod' - - 'cli/**/go.sum' + - 'common/**/*.go' + - 'dispatcher/**/*.go' + - 'project-runner/**/*.go' + - 'tools/release-automation/**/*.go' + - '.golangci.yml' + - '.go-version' + - '**/go.mod' + - '**/go.sum' workflow_dispatch: jobs: @@ -114,7 +120,7 @@ jobs: - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version-file: cli/.go-version + go-version-file: .go-version cache: false - name: Install golangci-lint @@ -132,4 +138,6 @@ jobs: - name: Run govulncheck run: | - (cd cli && govulncheck ./...) + for module_dir in common dispatcher project-runner tools/release-automation; do + (cd "$module_dir" && govulncheck ./...) + done diff --git a/.gitignore b/.gitignore index ae8e364345..27334bae0c 100644 --- a/.gitignore +++ b/.gitignore @@ -112,12 +112,7 @@ yarn-error.log* **/[Oo]bj/ # Native Go CLI build and release outputs -/cli/dist/ -!cli/Core~/dist/darwin-arm64/uloop-core -!cli/Core~/dist/darwin-amd64/uloop-core -!cli/Core~/dist/windows-amd64/uloop-core.exe -cli/Dispatcher~/dist/ -cli/Dispatcher~/release/ +/dist/ # Environment configuration files .env diff --git a/cli/.go-version b/.go-version similarity index 100% rename from cli/.go-version rename to .go-version diff --git a/cli/.golangci-complexity.yml b/.golangci-complexity.yml similarity index 100% rename from cli/.golangci-complexity.yml rename to .golangci-complexity.yml diff --git a/cli/.golangci.yml b/.golangci.yml similarity index 100% rename from cli/.golangci.yml rename to .golangci.yml diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 25e9db2ef6..8e43add019 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,4 @@ { "Packages/src": "3.0.0-beta.48", - "cli": "3.0.0-beta.45" + "project-runner": "3.0.0-beta.45" } diff --git a/cli/layout-contract.json b/cli/layout-contract.json deleted file mode 100644 index 894dc53cef..0000000000 --- a/cli/layout-contract.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "schemaVersion": 1, - "layout": { - "cliDir": "cli", - "distDir": "dist" - }, - "binaries": { - "cli": { - "unix": "uloop", - "windows": "uloop.exe" - } - } -} diff --git a/go.work b/go.work index ff704f6bcf..71583884b6 100644 --- a/go.work +++ b/go.work @@ -1,7 +1,7 @@ go 1.26 use ( - ./cli + ./project-runner ./common ./dispatcher ./tools/release-automation diff --git a/layout-contract.json b/layout-contract.json new file mode 100644 index 0000000000..3ad525967a --- /dev/null +++ b/layout-contract.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 2, + "layout": { + "modules": { + "common": "common", + "dispatcher": "dispatcher", + "projectRunner": "project-runner", + "releaseAutomation": "tools/release-automation" + }, + "distDir": "dist" + }, + "binaries": { + "dispatcher": { + "unix": "uloop", + "windows": "uloop.exe" + }, + "projectRunner": { + "unix": "uloop-project-runner", + "windows": "uloop-project-runner.exe" + } + } +} diff --git a/cli/CHANGELOG.md b/project-runner/CHANGELOG.md similarity index 100% rename from cli/CHANGELOG.md rename to project-runner/CHANGELOG.md diff --git a/cli/cmd/project-runner/main.go b/project-runner/cmd/project-runner/main.go similarity index 65% rename from cli/cmd/project-runner/main.go rename to project-runner/cmd/project-runner/main.go index 3ebca6633d..b93c2132c9 100644 --- a/cli/cmd/project-runner/main.go +++ b/project-runner/cmd/project-runner/main.go @@ -4,7 +4,7 @@ import ( "context" "os" - "github.com/hatayama/unity-cli-loop/cli/internal/projectrunner" + "github.com/hatayama/unity-cli-loop/project-runner/internal/projectrunner" ) func main() { diff --git a/cli/go.mod b/project-runner/go.mod similarity index 82% rename from cli/go.mod rename to project-runner/go.mod index 156c5ee482..b6be3b3a2f 100644 --- a/cli/go.mod +++ b/project-runner/go.mod @@ -1,4 +1,4 @@ -module github.com/hatayama/unity-cli-loop/cli +module github.com/hatayama/unity-cli-loop/project-runner go 1.26 diff --git a/cli/go.sum b/project-runner/go.sum similarity index 100% rename from cli/go.sum rename to project-runner/go.sum diff --git a/cli/internal/architecture/architecture_test.go b/project-runner/internal/architecture/architecture_test.go similarity index 75% rename from cli/internal/architecture/architecture_test.go rename to project-runner/internal/architecture/architecture_test.go index d1bcc49631..4cc35a6f7c 100644 --- a/cli/internal/architecture/architecture_test.go +++ b/project-runner/internal/architecture/architecture_test.go @@ -12,9 +12,9 @@ import ( ) const ( - cliModulePath = "github.com/hatayama/unity-cli-loop/cli" - dispatcherModulePath = "github.com/hatayama/unity-cli-loop/dispatcher" - maxProductionFileLines = 500 + projectRunnerModulePath = "github.com/hatayama/unity-cli-loop/project-runner" + dispatcherModulePath = "github.com/hatayama/unity-cli-loop/dispatcher" + maxProductionFileLines = 500 ) type goPackage struct { @@ -29,15 +29,23 @@ type layoutContract struct { } type layoutSection struct { - CliDir string `json:"cliDir"` - DistDir string `json:"distDir"` + Modules layoutModules `json:"modules"` + DistDir string `json:"distDir"` +} + +type layoutModules struct { + Common string `json:"common"` + Dispatcher string `json:"dispatcher"` + ProjectRunner string `json:"projectRunner"` + ReleaseAutomation string `json:"releaseAutomation"` } type binariesLayout struct { - Cli cliBinaryNames `json:"cli"` + Dispatcher binaryNames `json:"dispatcher"` + ProjectRunner binaryNames `json:"projectRunner"` } -type cliBinaryNames struct { +type binaryNames struct { Unix string `json:"unix"` Windows string `json:"windows"` } @@ -50,13 +58,13 @@ func TestCliFeaturePackagesDoNotImportOrchestrationLayer(t *testing.T) { moduleRoot := findModuleRoot(t) packages := listPackages(t, moduleRoot) orchestrationPackagePrefixes := []string{ - cliModulePath + "/internal/dispatcher", - cliModulePath + "/internal/projectrunner", - cliModulePath + "/internal/clicore", + projectRunnerModulePath + "/internal/dispatcher", + projectRunnerModulePath + "/internal/projectrunner", + projectRunnerModulePath + "/internal/clicore", } for _, goPackage := range packages { if hasAnyPackagePrefix(goPackage.ImportPath, orchestrationPackagePrefixes) || - strings.HasPrefix(goPackage.ImportPath, cliModulePath+"/cmd/") { + strings.HasPrefix(goPackage.ImportPath, projectRunnerModulePath+"/cmd/") { continue } for _, importedPath := range goPackage.Imports { @@ -78,23 +86,23 @@ func hasAnyPackagePrefix(importPath string, packagePrefixes []string) bool { return false } -// Tests that CLI internal packages stay inside explicit runtime boundaries. +// Tests that project runner internal packages stay inside explicit runtime boundaries. func TestCliInternalPackagesStayInsideExplicitBoundaries(t *testing.T) { moduleRoot := findModuleRoot(t) packages := listPackages(t, moduleRoot) boundaryPrefixes := []string{} for _, boundary := range []string{"automation", "clicore", "dispatcher", "install", "project", "projectrunner", "skills", "tools", "uninstall", "unityipc", "update", "version"} { - boundaryPrefixes = append(boundaryPrefixes, cliModulePath+"/internal/"+boundary) + boundaryPrefixes = append(boundaryPrefixes, projectRunnerModulePath+"/internal/"+boundary) } for _, goPackage := range packages { - if !strings.HasPrefix(goPackage.ImportPath, cliModulePath+"/internal/") { + if !strings.HasPrefix(goPackage.ImportPath, projectRunnerModulePath+"/internal/") { continue } - if goPackage.ImportPath == cliModulePath+"/internal/architecture" { + if goPackage.ImportPath == projectRunnerModulePath+"/internal/architecture" { continue } if !hasAnyPackagePrefix(goPackage.ImportPath, boundaryPrefixes) { - t.Fatalf("CLI internal package must live under an explicit runtime boundary: %s", goPackage.ImportPath) + t.Fatalf("project runner internal package must live under an explicit runtime boundary: %s", goPackage.ImportPath) } } } @@ -107,13 +115,13 @@ func TestDispatcherCommandOnlyDependsOnDispatcherEntrypoint(t *testing.T) { // Tests that the project runner command only enters the project runner package. func TestProjectRunnerCommandOnlyDependsOnProjectRunnerEntrypoint(t *testing.T) { - assertCommandOnlyDependsOnInternalEntrypoint(t, findModuleRoot(t), cliModulePath, "./cmd/project-runner", cliModulePath+"/internal/projectrunner") + assertCommandOnlyDependsOnInternalEntrypoint(t, findModuleRoot(t), projectRunnerModulePath, "./cmd/project-runner", projectRunnerModulePath+"/internal/projectrunner") } -// Tests that the dispatcher binary, now in its own module, does not transitively pull in the CLI module's project runner package. +// Tests that the dispatcher binary, now in its own module, does not transitively pull in the project runner module's project runner package. func TestDispatcherBinaryDoesNotTransitivelyDependOnProjectRunner(t *testing.T) { dispatcherModuleDir := filepath.Join(findRepositoryRoot(t, findModuleRoot(t)), "dispatcher") - assertBinaryDoesNotTransitivelyDependOn(t, dispatcherModuleDir, "./cmd/dispatcher", cliModulePath+"/internal/projectrunner") + assertBinaryDoesNotTransitivelyDependOn(t, dispatcherModuleDir, "./cmd/dispatcher", projectRunnerModulePath+"/internal/projectrunner") } // Tests that the project runner binary does not transitively pull in the dispatcher module's dispatcher package. @@ -164,23 +172,45 @@ func assertCommandOnlyDependsOnInternalEntrypoint(t *testing.T, moduleDir string } } -// Tests that the parent CLI layout manifest matches repository paths used by tooling. +// Tests that the repository-root layout manifest matches repository paths used by tooling. func TestLayoutContractMatchesRepositoryPaths(t *testing.T) { moduleRoot := findModuleRoot(t) repositoryRoot := findRepositoryRoot(t, moduleRoot) - contract := readLayoutContract(t, filepath.Join(moduleRoot, "layout-contract.json")) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) - if contract.SchemaVersion != 1 { + if contract.SchemaVersion != 2 { t.Fatalf("layout contract schema version mismatch: %d", contract.SchemaVersion) } - assertDirectoryName(t, moduleRoot, contract.Layout.CliDir) + // Every module directory named in the contract must exist under the repo root. + moduleDirs := map[string]string{ + "common": contract.Layout.Modules.Common, + "dispatcher": contract.Layout.Modules.Dispatcher, + "projectRunner": contract.Layout.Modules.ProjectRunner, + "releaseAutomation": contract.Layout.Modules.ReleaseAutomation, + } + for name, moduleDir := range moduleDirs { + if moduleDir == "" { + t.Fatalf("layout contract module %s is empty", name) + } + assertPathExists(t, filepath.Join(repositoryRoot, moduleDir)) + } + // The project runner module root must be named according to the contract. + assertDirectoryName(t, moduleRoot, contract.Layout.Modules.ProjectRunner) assertPathExists(t, filepath.Join(moduleRoot, "cmd")) assertPathExists(t, filepath.Join(moduleRoot, "internal")) assertPathDoesNotExist(t, filepath.Join(moduleRoot, "Core~")) assertPathDoesNotExist(t, filepath.Join(moduleRoot, "Dispatcher~")) assertPathDoesNotExist(t, filepath.Join(moduleRoot, "Shared~")) - assertTextContains(t, filepath.Join(repositoryRoot, "scripts", "build-go-cli.sh"), packagePath(contract, "")) - assertTextContains(t, filepath.Join(repositoryRoot, "scripts", "verify-go-cli-dist.sh"), filepath.ToSlash(filepath.Join(packagePath(contract, contract.Layout.DistDir), "darwin-arm64", contract.Binaries.Cli.Unix))) + // build-go-cli.sh must reference the project runner module directory. + assertTextContains(t, filepath.Join(repositoryRoot, "scripts", "build-go-cli.sh"), contract.Layout.Modules.ProjectRunner) + // verify-go-cli-dist.sh must reference both binaries in the repo-root dist tree. + distDir := contract.Layout.DistDir + assertTextContains(t, + filepath.Join(repositoryRoot, "scripts", "verify-go-cli-dist.sh"), + filepath.ToSlash(filepath.Join(distDir, "darwin-arm64", contract.Binaries.Dispatcher.Unix))) + assertTextContains(t, + filepath.Join(repositoryRoot, "scripts", "verify-go-cli-dist.sh"), + filepath.ToSlash(filepath.Join(distDir, "darwin-arm64", contract.Binaries.ProjectRunner.Unix))) } // Tests that production files stay small enough to keep each file focused on one responsibility. @@ -255,10 +285,6 @@ func readLayoutContract(t *testing.T, path string) layoutContract { return contract } -func packagePath(contract layoutContract, childDir string) string { - return filepath.ToSlash(filepath.Join(contract.Layout.CliDir, childDir)) -} - func assertDirectoryName(t *testing.T, path string, expectedName string) { t.Helper() if filepath.Base(path) != expectedName { diff --git a/cli/internal/architecture/github_actions_security_test.go b/project-runner/internal/architecture/github_actions_security_test.go similarity index 100% rename from cli/internal/architecture/github_actions_security_test.go rename to project-runner/internal/architecture/github_actions_security_test.go diff --git a/cli/internal/projectrunner/compile_wait.go b/project-runner/internal/projectrunner/compile_wait.go similarity index 100% rename from cli/internal/projectrunner/compile_wait.go rename to project-runner/internal/projectrunner/compile_wait.go diff --git a/cli/internal/projectrunner/compile_wait_test.go b/project-runner/internal/projectrunner/compile_wait_test.go similarity index 100% rename from cli/internal/projectrunner/compile_wait_test.go rename to project-runner/internal/projectrunner/compile_wait_test.go diff --git a/cli/internal/projectrunner/connection_retry.go b/project-runner/internal/projectrunner/connection_retry.go similarity index 100% rename from cli/internal/projectrunner/connection_retry.go rename to project-runner/internal/projectrunner/connection_retry.go diff --git a/cli/internal/projectrunner/connection_retry_flow.go b/project-runner/internal/projectrunner/connection_retry_flow.go similarity index 100% rename from cli/internal/projectrunner/connection_retry_flow.go rename to project-runner/internal/projectrunner/connection_retry_flow.go diff --git a/cli/internal/projectrunner/connection_retry_test.go b/project-runner/internal/projectrunner/connection_retry_test.go similarity index 100% rename from cli/internal/projectrunner/connection_retry_test.go rename to project-runner/internal/projectrunner/connection_retry_test.go diff --git a/cli/internal/projectrunner/control_play_mode_wait.go b/project-runner/internal/projectrunner/control_play_mode_wait.go similarity index 100% rename from cli/internal/projectrunner/control_play_mode_wait.go rename to project-runner/internal/projectrunner/control_play_mode_wait.go diff --git a/cli/internal/projectrunner/control_play_mode_wait_test.go b/project-runner/internal/projectrunner/control_play_mode_wait_test.go similarity index 100% rename from cli/internal/projectrunner/control_play_mode_wait_test.go rename to project-runner/internal/projectrunner/control_play_mode_wait_test.go diff --git a/cli/internal/projectrunner/debug_timing.go b/project-runner/internal/projectrunner/debug_timing.go similarity index 100% rename from cli/internal/projectrunner/debug_timing.go rename to project-runner/internal/projectrunner/debug_timing.go diff --git a/cli/internal/projectrunner/debug_timing_test.go b/project-runner/internal/projectrunner/debug_timing_test.go similarity index 100% rename from cli/internal/projectrunner/debug_timing_test.go rename to project-runner/internal/projectrunner/debug_timing_test.go diff --git a/cli/internal/projectrunner/dynamic_code_file.go b/project-runner/internal/projectrunner/dynamic_code_file.go similarity index 100% rename from cli/internal/projectrunner/dynamic_code_file.go rename to project-runner/internal/projectrunner/dynamic_code_file.go diff --git a/cli/internal/projectrunner/dynamic_code_file_test.go b/project-runner/internal/projectrunner/dynamic_code_file_test.go similarity index 100% rename from cli/internal/projectrunner/dynamic_code_file_test.go rename to project-runner/internal/projectrunner/dynamic_code_file_test.go diff --git a/cli/internal/projectrunner/dynamic_code_wait.go b/project-runner/internal/projectrunner/dynamic_code_wait.go similarity index 100% rename from cli/internal/projectrunner/dynamic_code_wait.go rename to project-runner/internal/projectrunner/dynamic_code_wait.go diff --git a/cli/internal/projectrunner/execution_errors.go b/project-runner/internal/projectrunner/execution_errors.go similarity index 100% rename from cli/internal/projectrunner/execution_errors.go rename to project-runner/internal/projectrunner/execution_errors.go diff --git a/cli/internal/projectrunner/list_output.go b/project-runner/internal/projectrunner/list_output.go similarity index 100% rename from cli/internal/projectrunner/list_output.go rename to project-runner/internal/projectrunner/list_output.go diff --git a/cli/internal/projectrunner/list_output_test.go b/project-runner/internal/projectrunner/list_output_test.go similarity index 100% rename from cli/internal/projectrunner/list_output_test.go rename to project-runner/internal/projectrunner/list_output_test.go diff --git a/cli/internal/projectrunner/native_tool_settings.go b/project-runner/internal/projectrunner/native_tool_settings.go similarity index 100% rename from cli/internal/projectrunner/native_tool_settings.go rename to project-runner/internal/projectrunner/native_tool_settings.go diff --git a/cli/internal/projectrunner/pause_point_errors.go b/project-runner/internal/projectrunner/pause_point_errors.go similarity index 100% rename from cli/internal/projectrunner/pause_point_errors.go rename to project-runner/internal/projectrunner/pause_point_errors.go diff --git a/cli/internal/projectrunner/pause_point_logs.go b/project-runner/internal/projectrunner/pause_point_logs.go similarity index 100% rename from cli/internal/projectrunner/pause_point_logs.go rename to project-runner/internal/projectrunner/pause_point_logs.go diff --git a/cli/internal/projectrunner/pause_point_wait.go b/project-runner/internal/projectrunner/pause_point_wait.go similarity index 100% rename from cli/internal/projectrunner/pause_point_wait.go rename to project-runner/internal/projectrunner/pause_point_wait.go diff --git a/cli/internal/projectrunner/pause_point_wait_test.go b/project-runner/internal/projectrunner/pause_point_wait_test.go similarity index 100% rename from cli/internal/projectrunner/pause_point_wait_test.go rename to project-runner/internal/projectrunner/pause_point_wait_test.go diff --git a/cli/internal/projectrunner/projectrunner.go b/project-runner/internal/projectrunner/projectrunner.go similarity index 100% rename from cli/internal/projectrunner/projectrunner.go rename to project-runner/internal/projectrunner/projectrunner.go diff --git a/cli/internal/projectrunner/run.go b/project-runner/internal/projectrunner/run.go similarity index 100% rename from cli/internal/projectrunner/run.go rename to project-runner/internal/projectrunner/run.go diff --git a/cli/internal/projectrunner/run_test.go b/project-runner/internal/projectrunner/run_test.go similarity index 100% rename from cli/internal/projectrunner/run_test.go rename to project-runner/internal/projectrunner/run_test.go diff --git a/cli/internal/projectrunner/runner_commands.go b/project-runner/internal/projectrunner/runner_commands.go similarity index 100% rename from cli/internal/projectrunner/runner_commands.go rename to project-runner/internal/projectrunner/runner_commands.go diff --git a/cli/internal/projectrunner/runner_usage.go b/project-runner/internal/projectrunner/runner_usage.go similarity index 100% rename from cli/internal/projectrunner/runner_usage.go rename to project-runner/internal/projectrunner/runner_usage.go diff --git a/cli/internal/projectrunner/runner_usage_test.go b/project-runner/internal/projectrunner/runner_usage_test.go similarity index 100% rename from cli/internal/projectrunner/runner_usage_test.go rename to project-runner/internal/projectrunner/runner_usage_test.go diff --git a/cli/internal/projectrunner/tool_params.go b/project-runner/internal/projectrunner/tool_params.go similarity index 100% rename from cli/internal/projectrunner/tool_params.go rename to project-runner/internal/projectrunner/tool_params.go diff --git a/cli/internal/projectrunner/tools_test.go b/project-runner/internal/projectrunner/tools_test.go similarity index 100% rename from cli/internal/projectrunner/tools_test.go rename to project-runner/internal/projectrunner/tools_test.go diff --git a/release-please-config.json b/release-please-config.json index 8b425e5ad1..8456d30d7a 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -28,7 +28,7 @@ } ] }, - "cli": { + "project-runner": { "component": "uloop-project-runner", "release-type": "go", "versioning": "prerelease", diff --git a/scripts/build-go-cli.sh b/scripts/build-go-cli.sh index f906503719..a1686a3c77 100755 --- a/scripts/build-go-cli.sh +++ b/scripts/build-go-cli.sh @@ -2,7 +2,7 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) -CLI_DIR="$ROOT_DIR/cli" +RUNNER_DIR="$ROOT_DIR/project-runner" DISPATCHER_DIR="$ROOT_DIR/dispatcher" . "$ROOT_DIR/scripts/go-cli-toolchain.sh" @@ -20,7 +20,7 @@ build_binary() { extension=".exe" fi - output_dir="$CLI_DIR/dist/$os-$arch" + output_dir="$ROOT_DIR/dist/$os-$arch" mkdir -p "$output_dir" ( @@ -30,8 +30,8 @@ build_binary() { } build_binary darwin arm64 uloop "$DISPATCHER_DIR" ./cmd/dispatcher -build_binary darwin arm64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner +build_binary darwin arm64 uloop-project-runner "$RUNNER_DIR" ./cmd/project-runner build_binary darwin amd64 uloop "$DISPATCHER_DIR" ./cmd/dispatcher -build_binary darwin amd64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner +build_binary darwin amd64 uloop-project-runner "$RUNNER_DIR" ./cmd/project-runner build_binary windows amd64 uloop "$DISPATCHER_DIR" ./cmd/dispatcher -build_binary windows amd64 uloop-project-runner "$CLI_DIR" ./cmd/project-runner +build_binary windows amd64 uloop-project-runner "$RUNNER_DIR" ./cmd/project-runner diff --git a/scripts/check-build-link-go-cli.sh b/scripts/check-build-link-go-cli.sh index 450456bb9f..4eb3cb6993 100755 --- a/scripts/check-build-link-go-cli.sh +++ b/scripts/check-build-link-go-cli.sh @@ -77,13 +77,13 @@ arch=$(uname -m) case "$os:$arch" in Darwin:arm64 | Darwin:aarch64) - cli_path="$ROOT_DIR/cli/dist/darwin-arm64/uloop" + cli_path="$ROOT_DIR/dist/darwin-arm64/uloop" ;; Darwin:x86_64 | Darwin:amd64) - cli_path="$ROOT_DIR/cli/dist/darwin-amd64/uloop" + cli_path="$ROOT_DIR/dist/darwin-amd64/uloop" ;; MINGW*:x86_64 | MINGW*:amd64 | MSYS*:x86_64 | MSYS*:amd64 | CYGWIN*:x86_64 | CYGWIN*:amd64 | Windows_NT:x86_64 | Windows_NT:amd64) - cli_path="$ROOT_DIR/cli/dist/windows-amd64/uloop.exe" + cli_path="$ROOT_DIR/dist/windows-amd64/uloop.exe" global_command_name="uloop.exe" ;; esac diff --git a/scripts/check-code-complexity.sh b/scripts/check-code-complexity.sh index e2a6a6c6b0..630311d372 100755 --- a/scripts/check-code-complexity.sh +++ b/scripts/check-code-complexity.sh @@ -4,7 +4,7 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) MAX_COMPLEXITY=${CODE_COMPLEXITY_MAX_COMPLEXITY:-15} FAIL_ON_EXCEEDED=$(printf '%s' "${CODE_COMPLEXITY_FAIL_ON_EXCEEDED:-false}" | tr '[:upper:]' '[:lower:]') -GO_CONFIG="$ROOT_DIR/cli/.golangci-complexity.yml" +GO_CONFIG="$ROOT_DIR/.golangci-complexity.yml" TEMP_GO_CONFIG= GO_STATUS=0 @@ -19,7 +19,7 @@ cleanup() { trap cleanup 0 1 2 15 if [ "$MAX_COMPLEXITY" != "15" ]; then - TEMP_GO_CONFIG="$ROOT_DIR/cli/.golangci-complexity.$$.yml" + TEMP_GO_CONFIG="$ROOT_DIR/.golangci-complexity.$$.yml" awk -v max_complexity="$MAX_COMPLEXITY" ' $1 == "max-complexity:" { print " max-complexity: " max_complexity @@ -34,7 +34,7 @@ fi echo "=== Go complexity (cyclop, max ${MAX_COMPLEXITY}) ===" ( - cd "$ROOT_DIR/cli" + cd "$ROOT_DIR/project-runner" golangci-lint run --config "$GO_CONFIG" ./... ) || GO_STATUS=$? diff --git a/scripts/check-go-cli-source.sh b/scripts/check-go-cli-source.sh index 740e134f8c..6058db960d 100755 --- a/scripts/check-go-cli-source.sh +++ b/scripts/check-go-cli-source.sh @@ -2,7 +2,7 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) -CLI_DIR="$ROOT_DIR/cli" +RUNNER_DIR="$ROOT_DIR/project-runner" . "$ROOT_DIR/scripts/go-cli-toolchain.sh" require_go_cli_toolchain "$ROOT_DIR" @@ -28,9 +28,9 @@ run_module_checks() { print "./" substr($0, length(root) + 2) } ') - golangci-lint fmt --config "$CLI_DIR/.golangci.yml" --diff + golangci-lint fmt --config "$ROOT_DIR/.golangci.yml" --diff go vet $packages - golangci-lint run --config "$CLI_DIR/.golangci.yml" $packages + golangci-lint run --config "$ROOT_DIR/.golangci.yml" $packages go test $packages ) } @@ -38,4 +38,4 @@ run_module_checks() { run_module_checks "$ROOT_DIR/common" run_module_checks "$ROOT_DIR/dispatcher" run_module_checks "$ROOT_DIR/tools/release-automation" -run_module_checks "$CLI_DIR" +run_module_checks "$RUNNER_DIR" diff --git a/scripts/go-cli-toolchain.sh b/scripts/go-cli-toolchain.sh index 8cd5b9032c..5f1ef7bfa8 100644 --- a/scripts/go-cli-toolchain.sh +++ b/scripts/go-cli-toolchain.sh @@ -3,7 +3,7 @@ set -eu require_go_cli_toolchain() { root_dir="$1" - version_file="$root_dir/cli/.go-version" + version_file="$root_dir/.go-version" if [ ! -f "$version_file" ]; then echo "Go CLI toolchain version file is missing: $version_file" >&2 diff --git a/scripts/package-dispatcher.sh b/scripts/package-dispatcher.sh index f45ae14bc8..12399db392 100755 --- a/scripts/package-dispatcher.sh +++ b/scripts/package-dispatcher.sh @@ -2,9 +2,8 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) -CLI_DIR="$ROOT_DIR/cli" -DIST_DIR="$CLI_DIR/dist" -RELEASE_DIR="$CLI_DIR/dist/dispatcher-release" +DIST_DIR="$ROOT_DIR/dist" +RELEASE_DIR="$DIST_DIR/dispatcher-release" rm -rf "$RELEASE_DIR" mkdir -p "$RELEASE_DIR" diff --git a/scripts/package-go-cli.sh b/scripts/package-go-cli.sh index d9af1d411e..0016b6af64 100755 --- a/scripts/package-go-cli.sh +++ b/scripts/package-go-cli.sh @@ -2,8 +2,7 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) -CLI_DIR="$ROOT_DIR/cli" -DIST_DIR="$CLI_DIR/dist" +DIST_DIR="$ROOT_DIR/dist" RELEASE_DIR="$DIST_DIR/release" rm -rf "$RELEASE_DIR" diff --git a/scripts/refresh-neighbor-game-skills-macos.sh b/scripts/refresh-neighbor-game-skills-macos.sh index e84b8e0f54..222f8039f6 100644 --- a/scripts/refresh-neighbor-game-skills-macos.sh +++ b/scripts/refresh-neighbor-game-skills-macos.sh @@ -199,7 +199,7 @@ parse_args() { has_local_uloop() { candidate=$1 - [ -x "$candidate/cli/dist/darwin-arm64/uloop" ] + [ -x "$candidate/dist/darwin-arm64/uloop" ] } resolve_uloop_root() { @@ -404,7 +404,7 @@ resolve_uloop_root discover_projects assert_project_count -uloop_bin="$uloop_root/cli/dist/darwin-arm64/uloop" +uloop_bin="$uloop_root/dist/darwin-arm64/uloop" command -v launch-unity >/dev/null 2>&1 || fail "launch-unity is not available on PATH" log "Using uloop: $uloop_bin" diff --git a/scripts/refresh-neighbor-game-skills-windows.sh b/scripts/refresh-neighbor-game-skills-windows.sh index 7725bc7a91..f8d00b0ef6 100644 --- a/scripts/refresh-neighbor-game-skills-windows.sh +++ b/scripts/refresh-neighbor-game-skills-windows.sh @@ -239,10 +239,10 @@ uloop_binary_for_root() { if [ "$(uname -m)" = "x86_64" ] || [ "$(uname -m)" = "amd64" ]; then arch=amd64 fi - printf '%s\n' "$candidate/cli/dist/darwin-$arch/uloop" + printf '%s\n' "$candidate/dist/darwin-$arch/uloop" ;; windows|wsl) - printf '%s\n' "$candidate/cli/dist/windows-amd64/uloop.exe" + printf '%s\n' "$candidate/dist/windows-amd64/uloop.exe" ;; *) fail "unsupported host OS for local uloop binary: $host_os" diff --git a/scripts/resolve-native-cli-release-target.sh b/scripts/resolve-native-cli-release-target.sh index 98b3a326d9..b11d3f703b 100755 --- a/scripts/resolve-native-cli-release-target.sh +++ b/scripts/resolve-native-cli-release-target.sh @@ -11,15 +11,13 @@ INPUT_RELEASE_TAG=${INPUT_RELEASE_TAG:-} INPUT_DRY_RUN=${INPUT_DRY_RUN:-false} RELEASE_DATA="" CLI_RELEASE_INPUT_PATHS=" -cli/.go-version -cli/layout-contract.json -cli/contract.go -cli/contract.json -cli/contract_test.go -cli/cmd -cli/go.mod -cli/go.sum -cli/internal +.go-version +layout-contract.json +common +project-runner/cmd +project-runner/go.mod +project-runner/go.sum +project-runner/internal scripts/build-go-cli.sh scripts/go-cli-toolchain.sh scripts/package-go-cli.sh @@ -149,10 +147,10 @@ cli_release_inputs_changed() { release_commit_updates_cli_version() { commit_sha=$1 version=$2 - expected_manifest_entry="\"cli\": \"$version\"" + expected_manifest_entry="\"project-runner\": \"$version\"" expected_changelog_heading="## [$version]" - commit_diff=$(git show --format= "$commit_sha" -- .release-please-manifest.json cli/CHANGELOG.md 2>/dev/null || true) + commit_diff=$(git show --format= "$commit_sha" -- .release-please-manifest.json project-runner/CHANGELOG.md 2>/dev/null || true) printf '%s\n' "$commit_diff" \ | awk -v manifest_entry="$expected_manifest_entry" -v changelog_heading="$expected_changelog_heading" ' substr($0, 1, 1) == "+" && (index($0, manifest_entry) > 0 || index($0, changelog_heading) > 0) { @@ -251,7 +249,7 @@ release_commit_sha_for_version() { done } -VERSION=$(jq -r '.["cli"]' .release-please-manifest.json) +VERSION=$(jq -r '.["project-runner"]' .release-please-manifest.json) if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then echo "Could not resolve project runner release version from .release-please-manifest.json." >&2 exit 1 diff --git a/scripts/run-posix-e2e.sh b/scripts/run-posix-e2e.sh index b684addce7..86e5c2aaeb 100755 --- a/scripts/run-posix-e2e.sh +++ b/scripts/run-posix-e2e.sh @@ -93,13 +93,13 @@ default_uloop_path() { Darwin) machine=$(uname -m) if [ "$machine" = "arm64" ] || [ "$machine" = "aarch64" ]; then - printf '%s\n' "$ROOT_DIR/cli/dist/darwin-arm64/uloop" + printf '%s\n' "$ROOT_DIR/dist/darwin-arm64/uloop" else - printf '%s\n' "$ROOT_DIR/cli/dist/darwin-amd64/uloop" + printf '%s\n' "$ROOT_DIR/dist/darwin-amd64/uloop" fi ;; MINGW*|MSYS*|CYGWIN*) - printf '%s\n' "$ROOT_DIR/cli/dist/windows-amd64/uloop.exe" + printf '%s\n' "$ROOT_DIR/dist/windows-amd64/uloop.exe" ;; *) printf '%s\n' "" diff --git a/scripts/sync-release-please-package-releases.sh b/scripts/sync-release-please-package-releases.sh index 26c164c1ba..53005dc320 100755 --- a/scripts/sync-release-please-package-releases.sh +++ b/scripts/sync-release-please-package-releases.sh @@ -7,7 +7,7 @@ ROOT_DIR=${ULOOP_REPO_ROOT:-$(CDPATH= cd "$(dirname "$0")/.." && pwd)} SCRIPT_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) CONFIG="$ROOT_DIR/release-please-config.json" MANIFEST="$ROOT_DIR/.release-please-manifest.json" -CLI_PACKAGE_PATH="cli" +CLI_PACKAGE_PATH="project-runner" UNITY_PACKAGE_CLI_PIN_FILE="Packages/src/project-runner-pin.json" REPO_FULL_NAME=${GITHUB_REPOSITORY:-hatayama/unity-cli-loop} TMP_DIR=$(mktemp -d) diff --git a/scripts/test-go-cli-toolchain.sh b/scripts/test-go-cli-toolchain.sh index 566aa84462..08aed92a9b 100755 --- a/scripts/test-go-cli-toolchain.sh +++ b/scripts/test-go-cli-toolchain.sh @@ -38,8 +38,8 @@ EOF_GO create_repo() { version=$1 repo_dir="$TMP_DIR/repo-$version" - mkdir -p "$repo_dir/cli" - printf '%s\n' "$version" > "$repo_dir/cli/.go-version" + mkdir -p "$repo_dir" + printf '%s\n' "$version" > "$repo_dir/.go-version" printf '%s\n' "$repo_dir" } diff --git a/scripts/test-native-cli-publish-workflow.sh b/scripts/test-native-cli-publish-workflow.sh index 6a71190e73..31e53632d7 100755 --- a/scripts/test-native-cli-publish-workflow.sh +++ b/scripts/test-native-cli-publish-workflow.sh @@ -48,7 +48,7 @@ test_release_assets_are_attested() { assert_contains "$WORKFLOW" " - name: Attest native CLI release assets" assert_contains "$WORKFLOW" " if: steps.release.outputs.publish == 'true' && steps.release.outputs.dry_run != 'true'" assert_contains "$WORKFLOW" " uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26" - assert_contains "$WORKFLOW" " subject-path: cli/dist/release/*" + assert_contains "$WORKFLOW" " subject-path: dist/release/*" assert_before "$WORKFLOW" " - name: Verify packaged release assets" " - name: Attest native CLI release assets" assert_before "$WORKFLOW" " - name: Attest native CLI release assets" " - name: Upload native CLI assets" } @@ -56,7 +56,7 @@ test_release_assets_are_attested() { test_release_asset_attestations_are_verified() { assert_contains "$WORKFLOW" " - name: Verify native CLI asset attestations" assert_contains "$WORKFLOW" ' SIGNER_WORKFLOW: ${{ github.repository }}/.github/workflows/native-cli-publish.yml' - assert_contains "$WORKFLOW" ' for asset_path in cli/dist/release/*; do' + assert_contains "$WORKFLOW" ' for asset_path in dist/release/*; do' assert_contains "$WORKFLOW" ' gh attestation verify "${asset_path}" \' assert_contains "$WORKFLOW" ' --repo "${GITHUB_REPOSITORY}" \' assert_contains "$WORKFLOW" ' --signer-workflow "${SIGNER_WORKFLOW}"' diff --git a/scripts/test-release-please-config.sh b/scripts/test-release-please-config.sh index 3b69587986..ccd00d2c08 100755 --- a/scripts/test-release-please-config.sh +++ b/scripts/test-release-please-config.sh @@ -119,7 +119,7 @@ assert_json_value '.["pull-request-header"] | contains("Dispatcher releases")' ' # Package roots are the release boundary: the repository root must not be a # package, otherwise unrelated root-level changes leak into unity-package releases. -assert_json_value '.packages | keys | sort | join(",")' 'Packages/src,cli' +assert_json_value '.packages | keys | sort | join(",")' 'Packages/src,project-runner' assert_json_value '.packages["Packages/src"].["changelog-path"]' 'CHANGELOG.md' assert_json_value '.packages["Packages/src"].component' 'unity-package' @@ -133,20 +133,20 @@ assert_json_value '.packages["Packages/src"].["extra-files"][1].jsonpath' '$.pac assert_json_value '.packages["Packages/src"].["extra-files"][2].path' '/.uloop/project-runner-pin.json' assert_json_value '.packages["Packages/src"].["extra-files"][2].jsonpath' '$.packageVersion' -assert_json_value '.packages["cli"].component' 'uloop-project-runner' -assert_json_value '.packages["cli"].["include-component-in-tag"]' 'true' -assert_json_value '.packages["cli"].["changelog-path"]' 'CHANGELOG.md' +assert_json_value '.packages["project-runner"].component' 'uloop-project-runner' +assert_json_value '.packages["project-runner"].["include-component-in-tag"]' 'true' +assert_json_value '.packages["project-runner"].["changelog-path"]' 'CHANGELOG.md' # The release automation cmd and internal packages that used to live under cli -# moved to tools/release-automation, so there is nothing left under cli to -# exclude from uloop-project-runner releases. -assert_json_value '.packages["cli"] | has("exclude-paths")' 'false' -assert_json_value '.packages["cli"].["extra-files"] | length' '4' -assert_json_value '.packages["cli"].["extra-files"][0].path' '/common/tools/default-tools.json' -assert_json_value '.packages["cli"].["extra-files"][1].path' '/common/clicontract/contract.json' -assert_json_value '.packages["cli"].["extra-files"][2].path' '/Packages/src/project-runner-pin.json' -assert_json_value '.packages["cli"].["extra-files"][2].jsonpath' '$.projectRunnerVersion' -assert_json_value '.packages["cli"].["extra-files"][3].path' '/.uloop/project-runner-pin.json' -assert_json_value '.packages["cli"].["extra-files"][3].jsonpath' '$.projectRunnerVersion' +# moved to tools/release-automation, so there is nothing left under project-runner +# to exclude from uloop-project-runner releases. +assert_json_value '.packages["project-runner"] | has("exclude-paths")' 'false' +assert_json_value '.packages["project-runner"].["extra-files"] | length' '4' +assert_json_value '.packages["project-runner"].["extra-files"][0].path' '/common/tools/default-tools.json' +assert_json_value '.packages["project-runner"].["extra-files"][1].path' '/common/clicontract/contract.json' +assert_json_value '.packages["project-runner"].["extra-files"][2].path' '/Packages/src/project-runner-pin.json' +assert_json_value '.packages["project-runner"].["extra-files"][2].jsonpath' '$.projectRunnerVersion' +assert_json_value '.packages["project-runner"].["extra-files"][3].path' '/.uloop/project-runner-pin.json' +assert_json_value '.packages["project-runner"].["extra-files"][3].jsonpath' '$.projectRunnerVersion' assert_file_contains "$RELEASE_WORKFLOW" 'id: package_release_sync' assert_file_contains "$RELEASE_WORKFLOW" "steps.package_release_sync.outputs.ready != 'false'" @@ -163,7 +163,7 @@ assert_file_order "$RELEASE_WORKFLOW" ' - name: Setup Go for package releas assert_file_order "$RELEASE_WORKFLOW" ' - name: Setup Go for release PR automation' ' - name: Dispatch release PR checks' assert_manifest_semver '.["Packages/src"]' -assert_manifest_semver '.["cli"]' +assert_manifest_semver '.["project-runner"]' # The old repository-root package key must not linger after the boundary move. # has(".") detects the key even when its value is null, which a plain diff --git a/scripts/test-resolve-native-cli-release-target.sh b/scripts/test-resolve-native-cli-release-target.sh index 0526c23b11..182837f9a0 100755 --- a/scripts/test-resolve-native-cli-release-target.sh +++ b/scripts/test-resolve-native-cli-release-target.sh @@ -23,8 +23,8 @@ set -eu emit_cli_release_diff() { version=$1 printf '%s\n' 'diff --git a/.release-please-manifest.json b/.release-please-manifest.json' - printf '+ "cli": "%s"\n' "$version" - printf '%s\n' 'diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md' + printf '+ "project-runner": "%s"\n' "$version" + printf '%s\n' 'diff --git a/project-runner/CHANGELOG.md b/project-runner/CHANGELOG.md' printf '+## [%s]\n' "$version" } @@ -142,7 +142,7 @@ write_manifest() { version=$1 cat > .release-please-manifest.json < release-please-config.json < .release-please-manifest.json < cli/CHANGELOG.md < project-runner/CHANGELOG.md <&2 diff --git a/scripts/verify-dispatcher-release-assets.sh b/scripts/verify-dispatcher-release-assets.sh index 495f741040..7e4ea935b0 100755 --- a/scripts/verify-dispatcher-release-assets.sh +++ b/scripts/verify-dispatcher-release-assets.sh @@ -18,7 +18,7 @@ if [ "${1:-}" = "--list" ]; then exit 0 fi -RELEASE_DIR="${1:-$ROOT_DIR/cli/dist/dispatcher-release}" +RELEASE_DIR="${1:-$ROOT_DIR/dist/dispatcher-release}" fail() { echo "$1" >&2 diff --git a/scripts/verify-go-cli-dist.sh b/scripts/verify-go-cli-dist.sh index 431389d4d6..7abcf40b2a 100755 --- a/scripts/verify-go-cli-dist.sh +++ b/scripts/verify-go-cli-dist.sh @@ -4,12 +4,12 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) DIST_FILES=" -cli/dist/darwin-arm64/uloop -cli/dist/darwin-arm64/uloop-project-runner -cli/dist/darwin-amd64/uloop -cli/dist/darwin-amd64/uloop-project-runner -cli/dist/windows-amd64/uloop.exe -cli/dist/windows-amd64/uloop-project-runner.exe +dist/darwin-arm64/uloop +dist/darwin-arm64/uloop-project-runner +dist/darwin-amd64/uloop +dist/darwin-amd64/uloop-project-runner +dist/windows-amd64/uloop.exe +dist/windows-amd64/uloop-project-runner.exe " "$ROOT_DIR/scripts/build-go-cli.sh" diff --git a/scripts/verify-native-cli-release-assets.sh b/scripts/verify-native-cli-release-assets.sh index d7f118d883..dde91afdbb 100755 --- a/scripts/verify-native-cli-release-assets.sh +++ b/scripts/verify-native-cli-release-assets.sh @@ -16,7 +16,7 @@ if [ "${1:-}" = "--list" ]; then exit 0 fi -RELEASE_DIR="${1:-$ROOT_DIR/cli/dist/release}" +RELEASE_DIR="${1:-$ROOT_DIR/dist/release}" fail() { echo "$1" >&2 diff --git a/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go b/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go index 45ec478b32..d6e7b04a74 100644 --- a/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go +++ b/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go @@ -9,7 +9,7 @@ import ( // Verifies project-runner-only changes do not require a dispatcher release bump. func TestDispatcherVersionBumpGuardPassesWhenDispatcherInputsAreUnchanged(t *testing.T) { result := AnalyzeDispatcherVersionBumpGuard( - []string{"cli/internal/projectrunner/compile_wait.go"}, + []string{"project-runner/internal/projectrunner/compile_wait.go"}, DispatcherVersionBumpValues{ HasContract: true, DispatcherVersion: "1.0.0", diff --git a/tools/release-automation/internal/automation/ipc_protocol_reminder.go b/tools/release-automation/internal/automation/ipc_protocol_reminder.go index 21234acf7d..d241adad0d 100644 --- a/tools/release-automation/internal/automation/ipc_protocol_reminder.go +++ b/tools/release-automation/internal/automation/ipc_protocol_reminder.go @@ -13,7 +13,7 @@ import ( var ipcProtocolReminderPatterns = []string{ "common/clicontract/contract.json", "common/clicontract/contract.go", - "cli/layout-contract.json", + "layout-contract.json", "common/unityipc/**", "common/tools/**", "Packages/src/Editor/CompositionRoot/UnityCliLoopFirstPartyServerLifecycleBinding.cs", From d7101bbb5f5483cee9626dcef182213916082dc3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 00:11:55 +0900 Subject: [PATCH 10/18] Fix the recovery smoke default uloop path for the repo-root dist tree The directory split moved built binaries from cli/dist to the repo-root dist tree, but defaultUloopPath still joined the legacy cli segment, so running the smoke without --uloop-path or ULOOP_BIN always failed with a missing-binary error. The string never matched the cli/dist residual grep because filepath.Join splits it into separate segments. --- scripts/smoke-cli-recovery-readiness.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke-cli-recovery-readiness.go b/scripts/smoke-cli-recovery-readiness.go index 47efd51901..d805fb4124 100644 --- a/scripts/smoke-cli-recovery-readiness.go +++ b/scripts/smoke-cli-recovery-readiness.go @@ -128,9 +128,9 @@ func defaultUloopPath() (string, error) { if runtime.GOARCH == "arm64" { arch = "darwin-arm64" } - return filepath.Join(repoRoot, "cli", "dist", arch, "uloop"), nil + return filepath.Join(repoRoot, "dist", arch, "uloop"), nil case "windows": - return filepath.Join(repoRoot, "cli", "dist", "windows-amd64", "uloop.exe"), nil + return filepath.Join(repoRoot, "dist", "windows-amd64", "uloop.exe"), nil default: return "", nil } From f39e2b49c6ffbdac6ba52ca04c0c94cb9878268f Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 00:12:10 +0900 Subject: [PATCH 11/18] Scan all four Go modules in the code complexity checks The complexity job triggers on changes in every module, but after the split both the script and the workflow only linted project-runner, so complexity in common, dispatcher, and tools/release-automation went silently unmeasured. Before the split the single cli module covered all of that code. The workflow writes one JSON artifact per module, and the script keeps a fatal golangci-lint status from being masked by a later module that only reports findings. --- .github/workflows/code-complexity.yml | 5 ++++- scripts/check-code-complexity.sh | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/code-complexity.yml b/.github/workflows/code-complexity.yml index 6f193525be..01e954e925 100644 --- a/.github/workflows/code-complexity.yml +++ b/.github/workflows/code-complexity.yml @@ -57,7 +57,10 @@ jobs: if: always() run: | mkdir -p artifacts - (cd project-runner && golangci-lint run --config ../.golangci-complexity.yml --issues-exit-code=0 --show-stats=false --output.json.path ../artifacts/go-code-complexity.json ./...) + for module_dir in common dispatcher project-runner tools/release-automation; do + artifact_name="go-code-complexity-$(printf '%s' "$module_dir" | tr '/' '-').json" + (cd "$module_dir" && golangci-lint run --config "$GITHUB_WORKSPACE/.golangci-complexity.yml" --issues-exit-code=0 --show-stats=false --output.json.path "$GITHUB_WORKSPACE/artifacts/$artifact_name" ./...) + done dotnet run --project tools/UnityCliLoop.CodeComplexity/UnityCliLoop.CodeComplexity.csproj -- \ --root . \ --max-complexity 15 \ diff --git a/scripts/check-code-complexity.sh b/scripts/check-code-complexity.sh index 630311d372..87266219df 100755 --- a/scripts/check-code-complexity.sh +++ b/scripts/check-code-complexity.sh @@ -33,10 +33,19 @@ if [ "$MAX_COMPLEXITY" != "15" ]; then fi echo "=== Go complexity (cyclop, max ${MAX_COMPLEXITY}) ===" -( - cd "$ROOT_DIR/project-runner" - golangci-lint run --config "$GO_CONFIG" ./... -) || GO_STATUS=$? +for module_dir in common dispatcher project-runner tools/release-automation; do + ( + cd "$ROOT_DIR/$module_dir" + golangci-lint run --config "$GO_CONFIG" ./... + ) || { + module_status=$? + # Keep a fatal status (anything but the findings exit code 1) from being + # masked by a later module that only reports findings. + if [ "$GO_STATUS" -eq 0 ] || [ "$module_status" -ne 1 ]; then + GO_STATUS=$module_status + fi + } +done echo "" echo "=== C# complexity (CA1502, max ${MAX_COMPLEXITY}) ===" From 5c4e863d5b2ee2c1367c6a0fd3e2a0681c1c8028 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 00:12:19 +0900 Subject: [PATCH 12/18] Collapse the duplicated legacy contract fallback into one helper runnerContractFileAtRef and dispatcherContractFileAtRef implemented the same try-primary-then-legacy-path branching with only the file constants differing. A single contractFileAtRefWithLegacyFallback keeps the two readers from drifting when the fallback behavior changes. --- .../dispatcher_minimum_version_guard.go | 12 +--------- .../protocol_minimum_version_guard.go | 23 ++++++++++++++----- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tools/release-automation/internal/automation/dispatcher_minimum_version_guard.go b/tools/release-automation/internal/automation/dispatcher_minimum_version_guard.go index fdd3084ca1..a2c3969300 100644 --- a/tools/release-automation/internal/automation/dispatcher_minimum_version_guard.go +++ b/tools/release-automation/internal/automation/dispatcher_minimum_version_guard.go @@ -275,18 +275,8 @@ func verifyDispatcherMinimumVersionAtRef( } // dispatcherContractFileAtRef reads the dispatcher release contract at a git ref. -// Dispatcher releases published before the cli/ directory split still provide the -// contract at the pre-split path, so this falls back to it when the new path is -// missing at the given ref. func dispatcherContractFileAtRef(ctx context.Context, repoRoot string, ref string) (string, error) { - content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, dispatcherContractFile) - if err == nil { - return content, nil - } - if !isMissingFileAtRefError(err, dispatcherContractFile) { - return "", err - } - return protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, legacyDispatcherContractFile) + return contractFileAtRefWithLegacyFallback(ctx, repoRoot, ref, dispatcherContractFile, legacyDispatcherContractFile) } func verifyCurrentDispatcherMinimumVersion(values dispatcherMinimumVersionValues) error { diff --git a/tools/release-automation/internal/automation/protocol_minimum_version_guard.go b/tools/release-automation/internal/automation/protocol_minimum_version_guard.go index 448c80e9be..8106dc8fb3 100644 --- a/tools/release-automation/internal/automation/protocol_minimum_version_guard.go +++ b/tools/release-automation/internal/automation/protocol_minimum_version_guard.go @@ -266,18 +266,29 @@ func runnerContractMissingAtReleaseMessage(releaseTag string) string { } // runnerContractFileAtRef reads the CLI/runner IPC contract file at a git ref. -// Release tags published before the cli/ directory split still provide the -// contract at the pre-split path, so this falls back to it when the new path -// is missing at the given ref. func runnerContractFileAtRef(ctx context.Context, repoRoot string, ref string) (string, error) { - content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, cliContractFile) + return contractFileAtRefWithLegacyFallback(ctx, repoRoot, ref, cliContractFile, legacyRunnerContractFile) +} + +// contractFileAtRefWithLegacyFallback reads a release contract at a git ref. +// Release tags published before the cli/ directory split still provide their +// contracts at the pre-split paths, so this falls back to the legacy path when +// the primary path is missing at the given ref. +func contractFileAtRefWithLegacyFallback( + ctx context.Context, + repoRoot string, + ref string, + primaryFile string, + legacyFile string, +) (string, error) { + content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, primaryFile) if err == nil { return content, nil } - if !isMissingFileAtRefError(err, cliContractFile) { + if !isMissingFileAtRefError(err, primaryFile) { return "", err } - return protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, legacyRunnerContractFile) + return protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, legacyFile) } // isMissingFileAtRefError reports whether err came from `git show ref:file` From 3b7d1ee98bf0415652a91fb3d582bc2415f3b389 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 00:49:40 +0900 Subject: [PATCH 13/18] Extract contract-file-at-ref helpers into a dedicated file The architecture test now applies the 500-line production file cap to every module, which surfaced protocol_minimum_version_guard.go at 523 lines. The git contract reading layer (legacy-path fallback and command execution) is a distinct concern also consumed by the dispatcher guard, so move it to its own file instead of raising the cap. --- .../automation/contract_file_at_ref.go | 85 +++++++++++++++++++ .../protocol_minimum_version_guard.go | 77 ----------------- 2 files changed, 85 insertions(+), 77 deletions(-) create mode 100644 tools/release-automation/internal/automation/contract_file_at_ref.go diff --git a/tools/release-automation/internal/automation/contract_file_at_ref.go b/tools/release-automation/internal/automation/contract_file_at_ref.go new file mode 100644 index 0000000000..d44b552223 --- /dev/null +++ b/tools/release-automation/internal/automation/contract_file_at_ref.go @@ -0,0 +1,85 @@ +package automation + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "path/filepath" + "strings" +) + +func runnerContractMissingAtReleaseMessage(releaseTag string) string { + return fmt.Sprintf("project runner release %s does not provide %s or %s", releaseTag, cliContractFile, legacyRunnerContractFile) +} + +// runnerContractFileAtRef reads the CLI/runner IPC contract file at a git ref. +func runnerContractFileAtRef(ctx context.Context, repoRoot string, ref string) (string, error) { + return contractFileAtRefWithLegacyFallback(ctx, repoRoot, ref, cliContractFile, legacyRunnerContractFile) +} + +// contractFileAtRefWithLegacyFallback reads a release contract at a git ref. +// Release tags published before the cli/ directory split still provide their +// contracts at the pre-split paths, so this falls back to the legacy path when +// the primary path is missing at the given ref. +func contractFileAtRefWithLegacyFallback( + ctx context.Context, + repoRoot string, + ref string, + primaryFile string, + legacyFile string, +) (string, error) { + content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, primaryFile) + if err == nil { + return content, nil + } + if !isMissingFileAtRefError(err, primaryFile) { + return "", err + } + return protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, legacyFile) +} + +// isMissingFileAtRefError reports whether err came from `git show ref:file` +// failing because file does not exist at ref, as opposed to any other git +// failure (auth, network, etc.) that must not be silently swallowed. +func isMissingFileAtRefError(err error, file string) bool { + message := err.Error() + quotedPath := "'" + file + "'" + return strings.Contains(message, "path "+quotedPath+" exists on disk, but not in") || + strings.Contains(message, "Path "+quotedPath+" does not exist in") +} + +func protocolMinimumVersionFileAtRef( + ctx context.Context, + repoRoot string, + ref string, + file string, +) (string, error) { + return runProtocolMinimumVersionOutput( + ctx, + repoRoot, + "git", + "-C", + repoRoot, + "show", + ref+":"+file) +} + +func runProtocolMinimumVersionOutput( + ctx context.Context, + workDir string, + name string, + args ...string, +) (string, error) { + command := exec.CommandContext(ctx, name, args...) + command.Dir = filepath.Clean(workDir) + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + command.Stdout = &stdout + command.Stderr = &stderr + err := command.Run() + if err != nil { + return "", fmt.Errorf("%s %s failed: %w\n%s%s", name, strings.Join(args, " "), err, stderr.String(), stdout.String()) + } + return stdout.String(), nil +} diff --git a/tools/release-automation/internal/automation/protocol_minimum_version_guard.go b/tools/release-automation/internal/automation/protocol_minimum_version_guard.go index 8106dc8fb3..f310080869 100644 --- a/tools/release-automation/internal/automation/protocol_minimum_version_guard.go +++ b/tools/release-automation/internal/automation/protocol_minimum_version_guard.go @@ -1,14 +1,12 @@ package automation import ( - "bytes" "context" "encoding/json" "errors" "fmt" "io" "os" - "os/exec" "path/filepath" "strconv" "strings" @@ -261,46 +259,6 @@ func verifyMinimumCliReleaseProtocolAtRef( return verifyMinimumCliReleaseIsPublished(ctx, repoRoot, release) } -func runnerContractMissingAtReleaseMessage(releaseTag string) string { - return fmt.Sprintf("project runner release %s does not provide %s or %s", releaseTag, cliContractFile, legacyRunnerContractFile) -} - -// runnerContractFileAtRef reads the CLI/runner IPC contract file at a git ref. -func runnerContractFileAtRef(ctx context.Context, repoRoot string, ref string) (string, error) { - return contractFileAtRefWithLegacyFallback(ctx, repoRoot, ref, cliContractFile, legacyRunnerContractFile) -} - -// contractFileAtRefWithLegacyFallback reads a release contract at a git ref. -// Release tags published before the cli/ directory split still provide their -// contracts at the pre-split paths, so this falls back to the legacy path when -// the primary path is missing at the given ref. -func contractFileAtRefWithLegacyFallback( - ctx context.Context, - repoRoot string, - ref string, - primaryFile string, - legacyFile string, -) (string, error) { - content, err := protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, primaryFile) - if err == nil { - return content, nil - } - if !isMissingFileAtRefError(err, primaryFile) { - return "", err - } - return protocolMinimumVersionFileAtRef(ctx, repoRoot, ref, legacyFile) -} - -// isMissingFileAtRefError reports whether err came from `git show ref:file` -// failing because file does not exist at ref, as opposed to any other git -// failure (auth, network, etc.) that must not be silently swallowed. -func isMissingFileAtRefError(err error, file string) bool { - message := err.Error() - quotedPath := "'" + file + "'" - return strings.Contains(message, "path "+quotedPath+" exists on disk, but not in") || - strings.Contains(message, "Path "+quotedPath+" does not exist in") -} - func minimumCliReleaseProtocolFile(ctx context.Context, repoRoot string, ref string) ([]byte, error) { if ref == "" { return os.ReadFile(filepath.Join(repoRoot, protocolMinimumVersionFile)) @@ -482,41 +440,6 @@ func protocolMinimumProjectRunnerVersionAtRef( return contract.ProjectRunnerVersion, nil } -func protocolMinimumVersionFileAtRef( - ctx context.Context, - repoRoot string, - ref string, - file string, -) (string, error) { - return runProtocolMinimumVersionOutput( - ctx, - repoRoot, - "git", - "-C", - repoRoot, - "show", - ref+":"+file) -} - -func runProtocolMinimumVersionOutput( - ctx context.Context, - workDir string, - name string, - args ...string, -) (string, error) { - command := exec.CommandContext(ctx, name, args...) - command.Dir = filepath.Clean(workDir) - stdout := bytes.Buffer{} - stderr := bytes.Buffer{} - command.Stdout = &stdout - command.Stderr = &stderr - err := command.Run() - if err != nil { - return "", fmt.Errorf("%s %s failed: %w\n%s%s", name, strings.Join(args, " "), err, stderr.String(), stdout.String()) - } - return stdout.String(), nil -} - func writeProtocolMinimumVersionLine(writer io.Writer, values ...any) { // CI status output failures cannot be recovered after the command outcome is known. _, _ = fmt.Fprintln(writer, values...) From d1af0fa1b333f2126616c1814a5334347bfc958a Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 00:49:54 +0900 Subject: [PATCH 14/18] Move architecture tests to release-automation and validate all modules After the module split, the architecture test could only see the project-runner module from its old location, leaving stale never-match boundary lists. Relocate it to the release-automation module and anchor everything at the repository root so it enforces the whole-repo layout: - the pre-split top-level cli/ directory must not reappear - module dependency directions: common requires no repo module, the other three require only common (acceptance criterion 7) - go directive alignment across go.mod files, go.work, and .go-version - production file size cap and internal boundary lists now cover all four modules; common must never grow internal packages - layout-contract v2 checks now include the Windows binary names, and module enumeration is cross-checked against go.work, check scripts, and the code-complexity workflow Each new guard was proven to fail via fake violation injection. --- .../architecture/architecture_test.go | 366 ----------- .../architecture/architecture_test.go | 574 ++++++++++++++++++ .../github_actions_security_test.go | 6 +- 3 files changed, 577 insertions(+), 369 deletions(-) delete mode 100644 project-runner/internal/architecture/architecture_test.go create mode 100644 tools/release-automation/internal/architecture/architecture_test.go rename {project-runner => tools/release-automation}/internal/architecture/github_actions_security_test.go (98%) diff --git a/project-runner/internal/architecture/architecture_test.go b/project-runner/internal/architecture/architecture_test.go deleted file mode 100644 index 4cc35a6f7c..0000000000 --- a/project-runner/internal/architecture/architecture_test.go +++ /dev/null @@ -1,366 +0,0 @@ -package architecture - -import ( - "encoding/json" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" -) - -const ( - projectRunnerModulePath = "github.com/hatayama/unity-cli-loop/project-runner" - dispatcherModulePath = "github.com/hatayama/unity-cli-loop/dispatcher" - maxProductionFileLines = 500 -) - -type goPackage struct { - ImportPath string - Imports []string -} - -type layoutContract struct { - SchemaVersion int `json:"schemaVersion"` - Layout layoutSection `json:"layout"` - Binaries binariesLayout `json:"binaries"` -} - -type layoutSection struct { - Modules layoutModules `json:"modules"` - DistDir string `json:"distDir"` -} - -type layoutModules struct { - Common string `json:"common"` - Dispatcher string `json:"dispatcher"` - ProjectRunner string `json:"projectRunner"` - ReleaseAutomation string `json:"releaseAutomation"` -} - -type binariesLayout struct { - Dispatcher binaryNames `json:"dispatcher"` - ProjectRunner binaryNames `json:"projectRunner"` -} - -type binaryNames struct { - Unix string `json:"unix"` - Windows string `json:"windows"` -} - -// Tests that every package outside the CLI orchestration layer (dispatcher, -// project runner, shared CLI core) and cmd/ stays free of orchestration -// imports. Skipping only known orchestration packages keeps future packages -// covered by default instead of requiring a hand-maintained feature list. -func TestCliFeaturePackagesDoNotImportOrchestrationLayer(t *testing.T) { - moduleRoot := findModuleRoot(t) - packages := listPackages(t, moduleRoot) - orchestrationPackagePrefixes := []string{ - projectRunnerModulePath + "/internal/dispatcher", - projectRunnerModulePath + "/internal/projectrunner", - projectRunnerModulePath + "/internal/clicore", - } - for _, goPackage := range packages { - if hasAnyPackagePrefix(goPackage.ImportPath, orchestrationPackagePrefixes) || - strings.HasPrefix(goPackage.ImportPath, projectRunnerModulePath+"/cmd/") { - continue - } - for _, importedPath := range goPackage.Imports { - if hasAnyPackagePrefix(importedPath, orchestrationPackagePrefixes) { - t.Fatalf("feature package %s must not import CLI orchestration package %s", goPackage.ImportPath, importedPath) - } - } - } -} - -// hasAnyPackagePrefix reports whether importPath is packagePrefix itself or a subpackage of it, -// avoiding false positives such as "/internal/project" matching "/internal/projectrunner". -func hasAnyPackagePrefix(importPath string, packagePrefixes []string) bool { - for _, packagePrefix := range packagePrefixes { - if importPath == packagePrefix || strings.HasPrefix(importPath, packagePrefix+"/") { - return true - } - } - return false -} - -// Tests that project runner internal packages stay inside explicit runtime boundaries. -func TestCliInternalPackagesStayInsideExplicitBoundaries(t *testing.T) { - moduleRoot := findModuleRoot(t) - packages := listPackages(t, moduleRoot) - boundaryPrefixes := []string{} - for _, boundary := range []string{"automation", "clicore", "dispatcher", "install", "project", "projectrunner", "skills", "tools", "uninstall", "unityipc", "update", "version"} { - boundaryPrefixes = append(boundaryPrefixes, projectRunnerModulePath+"/internal/"+boundary) - } - for _, goPackage := range packages { - if !strings.HasPrefix(goPackage.ImportPath, projectRunnerModulePath+"/internal/") { - continue - } - if goPackage.ImportPath == projectRunnerModulePath+"/internal/architecture" { - continue - } - if !hasAnyPackagePrefix(goPackage.ImportPath, boundaryPrefixes) { - t.Fatalf("project runner internal package must live under an explicit runtime boundary: %s", goPackage.ImportPath) - } - } -} - -// Tests that the dispatcher command, now in its own module, only enters the dispatcher package. -func TestDispatcherCommandOnlyDependsOnDispatcherEntrypoint(t *testing.T) { - dispatcherModuleDir := filepath.Join(findRepositoryRoot(t, findModuleRoot(t)), "dispatcher") - assertCommandOnlyDependsOnInternalEntrypoint(t, dispatcherModuleDir, dispatcherModulePath, "./cmd/dispatcher", dispatcherModulePath+"/internal/dispatcher") -} - -// Tests that the project runner command only enters the project runner package. -func TestProjectRunnerCommandOnlyDependsOnProjectRunnerEntrypoint(t *testing.T) { - assertCommandOnlyDependsOnInternalEntrypoint(t, findModuleRoot(t), projectRunnerModulePath, "./cmd/project-runner", projectRunnerModulePath+"/internal/projectrunner") -} - -// Tests that the dispatcher binary, now in its own module, does not transitively pull in the project runner module's project runner package. -func TestDispatcherBinaryDoesNotTransitivelyDependOnProjectRunner(t *testing.T) { - dispatcherModuleDir := filepath.Join(findRepositoryRoot(t, findModuleRoot(t)), "dispatcher") - assertBinaryDoesNotTransitivelyDependOn(t, dispatcherModuleDir, "./cmd/dispatcher", projectRunnerModulePath+"/internal/projectrunner") -} - -// Tests that the project runner binary does not transitively pull in the dispatcher module's dispatcher package. -func TestProjectRunnerBinaryDoesNotTransitivelyDependOnDispatcher(t *testing.T) { - assertBinaryDoesNotTransitivelyDependOn(t, findModuleRoot(t), "./cmd/project-runner", dispatcherModulePath+"/internal/dispatcher") -} - -func assertBinaryDoesNotTransitivelyDependOn(t *testing.T, moduleDir string, commandPath string, forbiddenPackage string) { - t.Helper() - command := exec.Command("go", "list", "-deps", commandPath) - command.Dir = moduleDir - output, err := command.Output() - if err != nil { - t.Fatalf("go list -deps failed: %v", err) - } - for _, dependency := range strings.Split(strings.TrimSpace(string(output)), "\n") { - if dependency == forbiddenPackage { - t.Fatalf("%s must not transitively depend on %s", commandPath, forbiddenPackage) - } - } -} - -func assertCommandOnlyDependsOnInternalEntrypoint(t *testing.T, moduleDir string, modulePath string, commandPath string, expectedEntrypoint string) { - t.Helper() - command := exec.Command("go", "list", "-json", commandPath) - command.Dir = moduleDir - output, err := command.Output() - if err != nil { - t.Fatalf("go list failed: %v", err) - } - - var commandPackage goPackage - if err := json.Unmarshal(output, &commandPackage); err != nil { - t.Fatalf("failed to decode command package: %v", err) - } - for _, dependency := range commandPackage.Imports { - for _, removedModule := range []string{"/cli/Dispatcher", "/cli/Core", "/cli/Shared"} { - if strings.Contains(dependency, removedModule) { - t.Fatalf("CLI command must not depend on removed split module package %s", dependency) - } - } - if !strings.HasPrefix(dependency, modulePath+"/internal/") { - continue - } - if dependency != expectedEntrypoint { - t.Fatalf("%s must enter internal code through %s, got %s", commandPath, expectedEntrypoint, dependency) - } - } -} - -// Tests that the repository-root layout manifest matches repository paths used by tooling. -func TestLayoutContractMatchesRepositoryPaths(t *testing.T) { - moduleRoot := findModuleRoot(t) - repositoryRoot := findRepositoryRoot(t, moduleRoot) - contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) - - if contract.SchemaVersion != 2 { - t.Fatalf("layout contract schema version mismatch: %d", contract.SchemaVersion) - } - // Every module directory named in the contract must exist under the repo root. - moduleDirs := map[string]string{ - "common": contract.Layout.Modules.Common, - "dispatcher": contract.Layout.Modules.Dispatcher, - "projectRunner": contract.Layout.Modules.ProjectRunner, - "releaseAutomation": contract.Layout.Modules.ReleaseAutomation, - } - for name, moduleDir := range moduleDirs { - if moduleDir == "" { - t.Fatalf("layout contract module %s is empty", name) - } - assertPathExists(t, filepath.Join(repositoryRoot, moduleDir)) - } - // The project runner module root must be named according to the contract. - assertDirectoryName(t, moduleRoot, contract.Layout.Modules.ProjectRunner) - assertPathExists(t, filepath.Join(moduleRoot, "cmd")) - assertPathExists(t, filepath.Join(moduleRoot, "internal")) - assertPathDoesNotExist(t, filepath.Join(moduleRoot, "Core~")) - assertPathDoesNotExist(t, filepath.Join(moduleRoot, "Dispatcher~")) - assertPathDoesNotExist(t, filepath.Join(moduleRoot, "Shared~")) - // build-go-cli.sh must reference the project runner module directory. - assertTextContains(t, filepath.Join(repositoryRoot, "scripts", "build-go-cli.sh"), contract.Layout.Modules.ProjectRunner) - // verify-go-cli-dist.sh must reference both binaries in the repo-root dist tree. - distDir := contract.Layout.DistDir - assertTextContains(t, - filepath.Join(repositoryRoot, "scripts", "verify-go-cli-dist.sh"), - filepath.ToSlash(filepath.Join(distDir, "darwin-arm64", contract.Binaries.Dispatcher.Unix))) - assertTextContains(t, - filepath.Join(repositoryRoot, "scripts", "verify-go-cli-dist.sh"), - filepath.ToSlash(filepath.Join(distDir, "darwin-arm64", contract.Binaries.ProjectRunner.Unix))) -} - -// Tests that production files stay small enough to keep each file focused on one responsibility. -func TestProductionGoFilesStayFocused(t *testing.T) { - moduleRoot := findModuleRoot(t) - err := filepath.WalkDir(moduleRoot, func(path string, entry os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if entry.IsDir() { - if entry.Name() == "dist" { - return filepath.SkipDir - } - return nil - } - if !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") { - return nil - } - lineCount, err := countLines(path) - if err != nil { - return err - } - if lineCount > maxProductionFileLines { - relativePath, err := filepath.Rel(moduleRoot, path) - if err != nil { - return err - } - return fmt.Errorf("%s has %d lines; split files above %d lines", relativePath, lineCount, maxProductionFileLines) - } - return nil - }) - if err != nil { - t.Fatal(err) - } -} - -func listPackages(t *testing.T, moduleRoot string) []goPackage { - t.Helper() - command := exec.Command("go", "list", "-json", "./...") - command.Dir = moduleRoot - output, err := command.Output() - if err != nil { - t.Fatalf("go list failed: %v", err) - } - - decoder := json.NewDecoder(strings.NewReader(string(output))) - packages := []goPackage{} - for { - var goPackage goPackage - err := decoder.Decode(&goPackage) - if err == io.EOF { - break - } - if err != nil { - t.Fatalf("failed to decode go list output: %v", err) - } - packages = append(packages, goPackage) - } - return packages -} - -func readLayoutContract(t *testing.T, path string) layoutContract { - t.Helper() - content, err := os.ReadFile(path) - if err != nil { - t.Fatalf("failed to read layout contract: %v", err) - } - var contract layoutContract - if err := json.Unmarshal(content, &contract); err != nil { - t.Fatalf("failed to parse layout contract: %v", err) - } - return contract -} - -func assertDirectoryName(t *testing.T, path string, expectedName string) { - t.Helper() - if filepath.Base(path) != expectedName { - t.Fatalf("directory name mismatch: %s", path) - } -} - -func assertPathExists(t *testing.T, path string) { - t.Helper() - if _, err := os.Stat(path); err != nil { - t.Fatalf("expected path to exist: %s", path) - } -} - -func assertPathDoesNotExist(t *testing.T, path string) { - t.Helper() - if _, err := os.Stat(path); err == nil { - t.Fatalf("expected path not to exist: %s", path) - } -} - -func assertTextContains(t *testing.T, path string, expected string) { - t.Helper() - content, err := os.ReadFile(path) - if err != nil { - t.Fatalf("failed to read %s: %v", path, err) - } - if !strings.Contains(string(content), expected) { - t.Fatalf("%s must contain %s", path, expected) - } -} - -func findModuleRoot(t *testing.T) string { - t.Helper() - currentPath, err := os.Getwd() - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - for { - if _, err := os.Stat(filepath.Join(currentPath, "go.mod")); err == nil { - return currentPath - } - parentPath := filepath.Dir(currentPath) - if parentPath == currentPath { - t.Fatal("go.mod not found") - } - currentPath = parentPath - } -} - -func findRepositoryRoot(t *testing.T, startPath string) string { - t.Helper() - currentPath := startPath - for { - if _, err := os.Stat(filepath.Join(currentPath, ".git")); err == nil { - return currentPath - } - parentPath := filepath.Dir(currentPath) - if parentPath == currentPath { - t.Fatal(".git not found") - } - currentPath = parentPath - } -} - -func countLines(path string) (int, error) { - content, err := os.ReadFile(path) - if err != nil { - return 0, err - } - if len(content) == 0 { - return 0, nil - } - lineCount := strings.Count(string(content), "\n") - if !strings.HasSuffix(string(content), "\n") { - lineCount++ - } - return lineCount, nil -} diff --git a/tools/release-automation/internal/architecture/architecture_test.go b/tools/release-automation/internal/architecture/architecture_test.go new file mode 100644 index 0000000000..aa2f8d7a09 --- /dev/null +++ b/tools/release-automation/internal/architecture/architecture_test.go @@ -0,0 +1,574 @@ +package architecture + +import ( + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "testing" +) + +const ( + repositoryModulePrefix = "github.com/hatayama/unity-cli-loop/" + commonModulePath = repositoryModulePrefix + "common" + dispatcherModulePath = repositoryModulePrefix + "dispatcher" + projectRunnerModulePath = repositoryModulePrefix + "project-runner" + releaseAutomationModulePath = repositoryModulePrefix + "tools/release-automation" + maxProductionFileLines = 500 +) + +// goPackage mirrors the subset of `go list -json` output that these tests read. +type goPackage struct { + ImportPath string + Imports []string +} + +// goModEditJSON mirrors the subset of `go mod edit -json` output that these tests read. +type goModEditJSON struct { + Module goModEditModule `json:"Module"` + Go string `json:"Go"` + Require []goModEditRequire `json:"Require"` +} + +type goModEditModule struct { + Path string `json:"Path"` +} + +type goModEditRequire struct { + Path string `json:"Path"` +} + +// goWorkEditJSON mirrors the subset of `go work edit -json` output that these tests read. +type goWorkEditJSON struct { + Go string `json:"Go"` + Use []goWorkEditUse `json:"Use"` +} + +type goWorkEditUse struct { + DiskPath string `json:"DiskPath"` +} + +type layoutContract struct { + SchemaVersion int `json:"schemaVersion"` + Layout layoutSection `json:"layout"` + Binaries binariesLayout `json:"binaries"` +} + +type layoutSection struct { + Modules layoutModules `json:"modules"` + DistDir string `json:"distDir"` +} + +type layoutModules struct { + Common string `json:"common"` + Dispatcher string `json:"dispatcher"` + ProjectRunner string `json:"projectRunner"` + ReleaseAutomation string `json:"releaseAutomation"` +} + +type binariesLayout struct { + Dispatcher binaryNames `json:"dispatcher"` + ProjectRunner binaryNames `json:"projectRunner"` +} + +type binaryNames struct { + Unix string `json:"unix"` + Windows string `json:"windows"` +} + +// Tests that the pre-split top-level `cli/` directory no longer exists at the repo root. +func TestNoTopLevelCliDirectory(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + cliPath := filepath.Join(repositoryRoot, "cli") + if _, err := os.Stat(cliPath); err == nil { + t.Fatalf("pre-split top-level directory must not exist: %s", cliPath) + } +} + +// Tests that every module's require directives respect the repo's dependency direction: +// common depends on nothing else in the repo; the other three modules only depend on common. +func TestModuleDependencyDirections(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + + commonDir := filepath.Join(repositoryRoot, contract.Layout.Modules.Common) + commonRequires := readGoModEdit(t, commonDir).Require + for _, require := range commonRequires { + if strings.HasPrefix(require.Path, repositoryModulePrefix) { + t.Fatalf("common module must not require any other repo module, got %s", require.Path) + } + } + + dependentModuleDirs := []string{ + filepath.Join(repositoryRoot, contract.Layout.Modules.Dispatcher), + filepath.Join(repositoryRoot, contract.Layout.Modules.ProjectRunner), + filepath.Join(repositoryRoot, contract.Layout.Modules.ReleaseAutomation), + } + for _, moduleDir := range dependentModuleDirs { + requires := readGoModEdit(t, moduleDir).Require + for _, require := range requires { + if !strings.HasPrefix(require.Path, repositoryModulePrefix) { + continue + } + if require.Path != commonModulePath { + t.Fatalf("module %s may only depend on %s among repo modules, got %s", moduleDir, commonModulePath, require.Path) + } + } + } +} + +// Tests that the Go toolchain directive stays identical across every go.mod, go.work, and .go-version. +func TestGoToolchainSingleSourceOfTruth(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + + workDirective := readGoWorkEdit(t, repositoryRoot).Go + if workDirective == "" { + t.Fatalf("go.work must declare a go directive") + } + + for _, moduleDir := range allModuleDirs(repositoryRoot, contract) { + moduleDirective := readGoModEdit(t, moduleDir).Go + if moduleDirective != workDirective { + t.Fatalf("go directive mismatch: %s has %q, go.work has %q", moduleDir, moduleDirective, workDirective) + } + } + + goVersionRaw, err := os.ReadFile(filepath.Join(repositoryRoot, ".go-version")) + if err != nil { + t.Fatalf("failed to read .go-version: %v", err) + } + goVersion := strings.TrimSpace(string(goVersionRaw)) + if goVersion != workDirective && !strings.HasPrefix(goVersion, workDirective+".") { + t.Fatalf(".go-version %q must equal or start with %q followed by '.'", goVersion, workDirective) + } +} + +// Tests that the dispatcher command only enters dispatcher-internal code through the dispatcher entrypoint. +func TestDispatcherCommandOnlyDependsOnDispatcherEntrypoint(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + dispatcherDir := filepath.Join(repositoryRoot, contract.Layout.Modules.Dispatcher) + assertCommandOnlyDependsOnInternalEntrypoint(t, dispatcherDir, dispatcherModulePath, "./cmd/dispatcher", dispatcherModulePath+"/internal/dispatcher") +} + +// Tests that the project runner command only enters project-runner-internal code through the projectrunner entrypoint. +func TestProjectRunnerCommandOnlyDependsOnProjectRunnerEntrypoint(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + projectRunnerDir := filepath.Join(repositoryRoot, contract.Layout.Modules.ProjectRunner) + assertCommandOnlyDependsOnInternalEntrypoint(t, projectRunnerDir, projectRunnerModulePath, "./cmd/project-runner", projectRunnerModulePath+"/internal/projectrunner") +} + +// Tests that the dispatcher binary does not transitively pull in the project runner's projectrunner package. +func TestDispatcherBinaryDoesNotTransitivelyDependOnProjectRunner(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + dispatcherDir := filepath.Join(repositoryRoot, contract.Layout.Modules.Dispatcher) + assertBinaryDoesNotTransitivelyDependOn(t, dispatcherDir, "./cmd/dispatcher", projectRunnerModulePath+"/internal/projectrunner") +} + +// Tests that the project runner binary does not transitively pull in the dispatcher module's dispatcher package. +func TestProjectRunnerBinaryDoesNotTransitivelyDependOnDispatcher(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + projectRunnerDir := filepath.Join(repositoryRoot, contract.Layout.Modules.ProjectRunner) + assertBinaryDoesNotTransitivelyDependOn(t, projectRunnerDir, "./cmd/project-runner", dispatcherModulePath+"/internal/dispatcher") +} + +// Tests that production Go files across every module stay small enough to keep each file focused on one responsibility. +func TestProductionGoFilesStayFocused(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + + for _, moduleDir := range allModuleDirs(repositoryRoot, contract) { + walkErr := filepath.WalkDir(moduleDir, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + if entry.Name() == "dist" { + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") { + return nil + } + lineCount, err := countLines(path) + if err != nil { + return err + } + if lineCount > maxProductionFileLines { + relativePath, err := filepath.Rel(repositoryRoot, path) + if err != nil { + return err + } + return fmt.Errorf("%s has %d lines; split files above %d lines", relativePath, lineCount, maxProductionFileLines) + } + return nil + }) + if walkErr != nil { + t.Fatal(walkErr) + } + } +} + +// Tests that every package in the common module (except clicore itself) stays free of clicore imports. +func TestCommonPackagesDoNotImportClicore(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + commonDir := filepath.Join(repositoryRoot, contract.Layout.Modules.Common) + + clicorePackage := commonModulePath + "/clicore" + for _, goPackage := range listPackages(t, commonDir) { + if goPackage.ImportPath == clicorePackage || strings.HasPrefix(goPackage.ImportPath, clicorePackage+"/") { + continue + } + for _, importedPath := range goPackage.Imports { + if importedPath == clicorePackage || strings.HasPrefix(importedPath, clicorePackage+"/") { + t.Fatalf("common package %s must not import %s", goPackage.ImportPath, importedPath) + } + } + } +} + +// Tests that every module's internal packages sit under the explicit boundary list allowed for that module. +func TestInternalBoundariesPerModule(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + + type moduleBoundary struct { + moduleDir string + modulePath string + allowed []string + } + moduleBoundaries := []moduleBoundary{ + { + // The common module publishes shared packages only; it must never grow internal packages. + moduleDir: filepath.Join(repositoryRoot, contract.Layout.Modules.Common), + modulePath: commonModulePath, + allowed: []string{}, + }, + { + moduleDir: filepath.Join(repositoryRoot, contract.Layout.Modules.ProjectRunner), + modulePath: projectRunnerModulePath, + allowed: []string{"projectrunner"}, + }, + { + moduleDir: filepath.Join(repositoryRoot, contract.Layout.Modules.Dispatcher), + modulePath: dispatcherModulePath, + allowed: []string{"dispatcher", "install", "uninstall", "update"}, + }, + { + moduleDir: filepath.Join(repositoryRoot, contract.Layout.Modules.ReleaseAutomation), + modulePath: releaseAutomationModulePath, + allowed: []string{"automation", "architecture"}, + }, + } + + for _, boundary := range moduleBoundaries { + allowedPrefixes := []string{} + for _, name := range boundary.allowed { + allowedPrefixes = append(allowedPrefixes, boundary.modulePath+"/internal/"+name) + } + internalPrefix := boundary.modulePath + "/internal/" + for _, goPackage := range listPackages(t, boundary.moduleDir) { + if !strings.HasPrefix(goPackage.ImportPath, internalPrefix) { + continue + } + if !hasAnyPackagePrefix(goPackage.ImportPath, allowedPrefixes) { + t.Fatalf("%s internal package must live under one of %v, got %s", boundary.modulePath, boundary.allowed, goPackage.ImportPath) + } + } + } +} + +// Tests that the layout contract matches repository paths used by tooling and downstream scripts. +func TestLayoutContractMatchesRepositoryPaths(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + + if contract.SchemaVersion != 2 { + t.Fatalf("layout contract schema version mismatch: %d", contract.SchemaVersion) + } + moduleDirs := map[string]string{ + "common": contract.Layout.Modules.Common, + "dispatcher": contract.Layout.Modules.Dispatcher, + "projectRunner": contract.Layout.Modules.ProjectRunner, + "releaseAutomation": contract.Layout.Modules.ReleaseAutomation, + } + for name, moduleDir := range moduleDirs { + if moduleDir == "" { + t.Fatalf("layout contract module %s is empty", name) + } + assertPathExists(t, filepath.Join(repositoryRoot, moduleDir)) + } + projectRunnerDir := filepath.Join(repositoryRoot, contract.Layout.Modules.ProjectRunner) + assertPathExists(t, filepath.Join(projectRunnerDir, "cmd")) + assertPathExists(t, filepath.Join(projectRunnerDir, "internal")) + assertPathDoesNotExist(t, filepath.Join(projectRunnerDir, "Core~")) + assertPathDoesNotExist(t, filepath.Join(projectRunnerDir, "Dispatcher~")) + assertPathDoesNotExist(t, filepath.Join(projectRunnerDir, "Shared~")) + + assertTextContains(t, filepath.Join(repositoryRoot, "scripts", "build-go-cli.sh"), contract.Layout.Modules.ProjectRunner) + + distDir := contract.Layout.DistDir + verifyScript := filepath.Join(repositoryRoot, "scripts", "verify-go-cli-dist.sh") + assertTextContains(t, verifyScript, filepath.ToSlash(filepath.Join(distDir, "darwin-arm64", contract.Binaries.Dispatcher.Unix))) + assertTextContains(t, verifyScript, filepath.ToSlash(filepath.Join(distDir, "darwin-arm64", contract.Binaries.ProjectRunner.Unix))) + assertTextContains(t, verifyScript, filepath.ToSlash(filepath.Join(distDir, "windows-amd64", contract.Binaries.Dispatcher.Windows))) + assertTextContains(t, verifyScript, filepath.ToSlash(filepath.Join(distDir, "windows-amd64", contract.Binaries.ProjectRunner.Windows))) +} + +// Tests that the set of module dirs in the layout contract equals the go.work `use` set +// and that each module dir string is referenced from the scripts and workflows that drive multi-module CI. +func TestModuleEnumerationConsistency(t *testing.T) { + repositoryRoot := findRepositoryRoot(t) + contract := readLayoutContract(t, filepath.Join(repositoryRoot, "layout-contract.json")) + + contractModuleDirs := []string{ + contract.Layout.Modules.Common, + contract.Layout.Modules.Dispatcher, + contract.Layout.Modules.ProjectRunner, + contract.Layout.Modules.ReleaseAutomation, + } + sort.Strings(contractModuleDirs) + + workEdit := readGoWorkEdit(t, repositoryRoot) + workModuleDirs := []string{} + for _, use := range workEdit.Use { + workModuleDirs = append(workModuleDirs, normalizeWorkDiskPath(use.DiskPath)) + } + sort.Strings(workModuleDirs) + + if !stringSlicesEqual(contractModuleDirs, workModuleDirs) { + t.Fatalf("layout contract modules %v must equal go.work use entries %v", contractModuleDirs, workModuleDirs) + } + + scriptsToCheck := []string{ + filepath.Join(repositoryRoot, "scripts", "check-go-cli-source.sh"), + filepath.Join(repositoryRoot, "scripts", "check-code-complexity.sh"), + filepath.Join(repositoryRoot, ".github", "workflows", "code-complexity.yml"), + } + for _, moduleDir := range contractModuleDirs { + for _, scriptPath := range scriptsToCheck { + assertTextContains(t, scriptPath, moduleDir) + } + } +} + +// assertBinaryDoesNotTransitivelyDependOn runs `go list -deps` for the given command +// and fails when forbiddenPackage appears in its transitive dependency list. +func assertBinaryDoesNotTransitivelyDependOn(t *testing.T, moduleDir string, commandPath string, forbiddenPackage string) { + t.Helper() + command := exec.Command("go", "list", "-deps", commandPath) + command.Dir = moduleDir + output, err := command.Output() + if err != nil { + t.Fatalf("go list -deps failed: %v", err) + } + for _, dependency := range strings.Split(strings.TrimSpace(string(output)), "\n") { + if dependency == forbiddenPackage { + t.Fatalf("%s must not transitively depend on %s", commandPath, forbiddenPackage) + } + } +} + +// assertCommandOnlyDependsOnInternalEntrypoint fails when the command's direct imports include +// any internal package other than expectedEntrypoint from its own module. +func assertCommandOnlyDependsOnInternalEntrypoint(t *testing.T, moduleDir string, modulePath string, commandPath string, expectedEntrypoint string) { + t.Helper() + command := exec.Command("go", "list", "-json", commandPath) + command.Dir = moduleDir + output, err := command.Output() + if err != nil { + t.Fatalf("go list failed: %v", err) + } + + var commandPackage goPackage + if err := json.Unmarshal(output, &commandPackage); err != nil { + t.Fatalf("failed to decode command package: %v", err) + } + for _, dependency := range commandPackage.Imports { + if !strings.HasPrefix(dependency, modulePath+"/internal/") { + continue + } + if dependency != expectedEntrypoint { + t.Fatalf("%s must enter internal code through %s, got %s", commandPath, expectedEntrypoint, dependency) + } + } +} + +// hasAnyPackagePrefix reports whether importPath is packagePrefix itself or a subpackage of it. +func hasAnyPackagePrefix(importPath string, packagePrefixes []string) bool { + for _, packagePrefix := range packagePrefixes { + if importPath == packagePrefix || strings.HasPrefix(importPath, packagePrefix+"/") { + return true + } + } + return false +} + +// allModuleDirs returns the absolute directory paths of every repo module in a stable order. +func allModuleDirs(repositoryRoot string, contract layoutContract) []string { + return []string{ + filepath.Join(repositoryRoot, contract.Layout.Modules.Common), + filepath.Join(repositoryRoot, contract.Layout.Modules.Dispatcher), + filepath.Join(repositoryRoot, contract.Layout.Modules.ProjectRunner), + filepath.Join(repositoryRoot, contract.Layout.Modules.ReleaseAutomation), + } +} + +// listPackages runs `go list -json ./...` inside moduleDir and decodes each package object. +func listPackages(t *testing.T, moduleDir string) []goPackage { + t.Helper() + command := exec.Command("go", "list", "-json", "./...") + command.Dir = moduleDir + output, err := command.Output() + if err != nil { + t.Fatalf("go list failed in %s: %v", moduleDir, err) + } + + decoder := json.NewDecoder(strings.NewReader(string(output))) + packages := []goPackage{} + for { + var goPackage goPackage + err := decoder.Decode(&goPackage) + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("failed to decode go list output: %v", err) + } + packages = append(packages, goPackage) + } + return packages +} + +// readGoModEdit runs `go mod edit -json` inside moduleDir and decodes the result. +func readGoModEdit(t *testing.T, moduleDir string) goModEditJSON { + t.Helper() + command := exec.Command("go", "mod", "edit", "-json") + command.Dir = moduleDir + output, err := command.Output() + if err != nil { + t.Fatalf("go mod edit -json failed in %s: %v", moduleDir, err) + } + var result goModEditJSON + if err := json.Unmarshal(output, &result); err != nil { + t.Fatalf("failed to decode go mod edit output for %s: %v", moduleDir, err) + } + return result +} + +// readGoWorkEdit runs `go work edit -json` inside the repository root and decodes the result. +func readGoWorkEdit(t *testing.T, repositoryRoot string) goWorkEditJSON { + t.Helper() + command := exec.Command("go", "work", "edit", "-json") + command.Dir = repositoryRoot + output, err := command.Output() + if err != nil { + t.Fatalf("go work edit -json failed: %v", err) + } + var result goWorkEditJSON + if err := json.Unmarshal(output, &result); err != nil { + t.Fatalf("failed to decode go work edit output: %v", err) + } + return result +} + +// normalizeWorkDiskPath strips the leading "./" that `go work` emits so paths compare against layout contract values. +func normalizeWorkDiskPath(diskPath string) string { + return strings.TrimPrefix(diskPath, "./") +} + +// stringSlicesEqual reports whether two sorted string slices are element-wise equal. +func stringSlicesEqual(left []string, right []string) bool { + if len(left) != len(right) { + return false + } + for index := range left { + if left[index] != right[index] { + return false + } + } + return true +} + +func readLayoutContract(t *testing.T, path string) layoutContract { + t.Helper() + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read layout contract: %v", err) + } + var contract layoutContract + if err := json.Unmarshal(content, &contract); err != nil { + t.Fatalf("failed to parse layout contract: %v", err) + } + return contract +} + +func assertPathExists(t *testing.T, path string) { + t.Helper() + if _, err := os.Stat(path); err != nil { + t.Fatalf("expected path to exist: %s", path) + } +} + +func assertPathDoesNotExist(t *testing.T, path string) { + t.Helper() + if _, err := os.Stat(path); err == nil { + t.Fatalf("expected path not to exist: %s", path) + } +} + +func assertTextContains(t *testing.T, path string, expected string) { + t.Helper() + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read %s: %v", path, err) + } + if !strings.Contains(string(content), expected) { + t.Fatalf("%s must contain %s", path, expected) + } +} + +// findRepositoryRoot walks up from the current working directory until it finds a `.git` entry. +// This works regardless of which module the test is running in. +func findRepositoryRoot(t *testing.T) string { + t.Helper() + currentPath, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + for { + if _, err := os.Stat(filepath.Join(currentPath, ".git")); err == nil { + return currentPath + } + parentPath := filepath.Dir(currentPath) + if parentPath == currentPath { + t.Fatal(".git not found while walking up from CWD") + } + currentPath = parentPath + } +} + +func countLines(path string) (int, error) { + content, err := os.ReadFile(path) + if err != nil { + return 0, err + } + if len(content) == 0 { + return 0, nil + } + lineCount := strings.Count(string(content), "\n") + if !strings.HasSuffix(string(content), "\n") { + lineCount++ + } + return lineCount, nil +} diff --git a/project-runner/internal/architecture/github_actions_security_test.go b/tools/release-automation/internal/architecture/github_actions_security_test.go similarity index 98% rename from project-runner/internal/architecture/github_actions_security_test.go rename to tools/release-automation/internal/architecture/github_actions_security_test.go index a3f589b621..803761fb6f 100644 --- a/project-runner/internal/architecture/github_actions_security_test.go +++ b/tools/release-automation/internal/architecture/github_actions_security_test.go @@ -14,7 +14,7 @@ var githubActionCommitRefPattern = regexp.MustCompile(`^[0-9a-fA-F]{40}$`) // Tests that remote GitHub Actions are pinned to immutable commit SHAs. func TestWorkflowActionsUseCommitPins(t *testing.T) { - repositoryRoot := findRepositoryRoot(t, findModuleRoot(t)) + repositoryRoot := findRepositoryRoot(t) violations := []string{} for _, workflowPath := range workflowFilePaths(t, repositoryRoot) { lines := readWorkflowLines(t, workflowPath) @@ -37,7 +37,7 @@ func TestWorkflowActionsUseCommitPins(t *testing.T) { // Tests that setup-go does not use cache in pull request workflows. func TestPullRequestWorkflowsDisableSetupGoCache(t *testing.T) { - repositoryRoot := findRepositoryRoot(t, findModuleRoot(t)) + repositoryRoot := findRepositoryRoot(t) violations := []string{} for _, workflowPath := range workflowFilePaths(t, repositoryRoot) { lines := readWorkflowLines(t, workflowPath) @@ -63,7 +63,7 @@ func TestPullRequestWorkflowsDisableSetupGoCache(t *testing.T) { // Tests that pull request workflow cache actions are guarded behind trusted Unity secrets. func TestPullRequestWorkflowCacheActionsRequireTrustedUnitySecrets(t *testing.T) { - repositoryRoot := findRepositoryRoot(t, findModuleRoot(t)) + repositoryRoot := findRepositoryRoot(t) violations := []string{} for _, workflowPath := range workflowFilePaths(t, repositoryRoot) { lines := readWorkflowLines(t, workflowPath) From 87e8a61df3ca6fc42124563c6067ba2ed8abc57f Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 00:54:03 +0900 Subject: [PATCH 15/18] Update docs and review config to the split module paths The module split moved every path these documents referenced: the IPC contract now lives at common/clicontract/contract.json, the dispatcher contract under dispatcher/, the bump guard under tools/release-automation, and development binaries under the repo-root dist/. The Unity-side constant is now MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION. Refresh AGENTS.md (CLAUDE.md is a symlink to it), README, the CodeRabbit IPC instructions, and the simplification plan's verification commands so agents and reviewers stop being pointed at pre-split paths. --- .coderabbit.yaml | 4 +- AGENTS.md | 40 +++++++++---------- README.md | 4 +- .../native-cli-simplification-plan.md | 4 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 2564399544..d730823d22 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -35,9 +35,9 @@ reviews: 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`. + 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, `common/unityipc/**`, `Packages/src/Editor/Infrastructure/Api/**`, `common/clicontract/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. + 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 `common/clicontract/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. `projectRunnerVersion` and `default-tools.json` `version` are release-please managed release metadata. If a PR bumps `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION`, require a matching update to `CliConstants.MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` after the corresponding published project runner release tag is available, because setup must install a project runner release that advertises the required protocol. chat: diff --git a/AGENTS.md b/AGENTS.md index 2346420506..d5d03382a5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ Every test method must have a short comment that states what behavior the test v 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). +- Go side: `protocolVersion` in `common/clicontract/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. @@ -33,14 +33,14 @@ 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. +- `common/clicontract/contract.json` `projectRunnerVersion` and `common/tools/default-tools.json` + `version` are stamped by release-please only. Never edit them by hand in a feature PR. +- `CliConstants.MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` is the release that setup installs. It + must always point at a published project runner release. - When a protocol bump changes `CliConstants.REQUIRED_CLI_PROTOCOL_VERSION`, prepare the matching - CLI release tag first, then update `CliConstants.MINIMUM_REQUIRED_CLI_VERSION` in the same PR. - PR CI fails, and the PR warning comment stays open, until the minimum CLI release advances to a - published CLI release that advertises the required protocol. + project runner release tag first, then update `CliConstants.MINIMUM_REQUIRED_PROJECT_RUNNER_VERSION` + in the same PR. PR CI fails, and the PR warning comment stays open, until the minimum project + runner release advances to a published release that advertises the required protocol. - Runtime protocol mismatch guidance must use the unpinned CLI update path for older clients and tell newer clients to align the package and CLI releases. @@ -56,20 +56,20 @@ Shell scripts are acceptable only as thin wrappers or simple command sequences. ## Dispatcher Release Inputs -The global dispatcher has its own release version in `cli/dispatcher-contract.json`. +The global dispatcher has its own release version in `dispatcher/dispatcher-contract.json`. When changing dispatcher release inputs, update `dispatcherVersion` in the same PR. Pull request CI runs `check-dispatcher-version-bump` and fails if dispatcher inputs changed without a dispatcher version increase. The authoritative dispatcher release input list is `dispatcherReleaseInputPatterns` in -`cli/internal/automation/dispatcher_version_bump_guard.go`. +`tools/release-automation/internal/automation/dispatcher_version_bump_guard.go`. Dispatcher release inputs include these non-exhaustive examples: -- `cli/cmd/dispatcher/main.go` -- `cli/dispatcher-contract.json` -- `cli/internal/dispatcher/*.go` -- `cli/internal/clicore/*.go` -- `cli/internal/update/*.go` +- `dispatcher/cmd/dispatcher/main.go` +- `dispatcher/dispatcher-contract.json` +- `dispatcher/internal/dispatcher/*.go` +- `common/clicore/*.go` +- `dispatcher/internal/update/*.go` - `scripts/install.ps1` Do not bump `dispatcherContractVersion` unless the dispatcher contract itself changes. @@ -87,7 +87,7 @@ Before changing scripts, skill files, generated-file synchronization, path handl - Normalize relative paths at API boundaries. Do not compare raw path strings that may contain `/` on one side and `\` on another. Convert separators before storing, comparing, deleting, or syncing generated files. - Prefer forward slashes in JSON `file:` paths and other cross-platform config values. Use escaped backslashes only when the target format explicitly requires them. - Validate Windows-facing PowerShell with both `pwsh` and Windows PowerShell when practical, especially for multiline arguments, here-strings, UTF-8 files, and native executable calls. -- When validating this checkout on Windows, use the repo-local native binary (`cli/dist/windows-amd64/uloop.exe`) instead of a `PATH`-resolved `uloop`. If a bash validation command cannot see the expected Go toolchain on Windows, retry through a login shell such as `bash -lc`. +- When validating this checkout on Windows, use the repo-local native binary (`dist/windows-amd64/uloop.exe`) instead of a `PATH`-resolved `uloop`. If a bash validation command cannot see the expected Go toolchain on Windows, retry through a login shell such as `bash -lc`. - Add or update a regression test whenever a fix depends on encoding, line endings, or separator normalization. A passing macOS test alone is not enough for these cases. ## Dead Code Scanner @@ -117,19 +117,19 @@ Interpret scanner output conservatively: When running `uloop` commands for this project during CLI development, do not use the `uloop` command resolved from `PATH`. Run this checkout's built development binary directly so validation uses the code under review: ```bash -cli/dist/darwin-arm64/uloop compile --project-path "$(git rev-parse --show-toplevel)" +dist/darwin-arm64/uloop compile --project-path "$(git rev-parse --show-toplevel)" ``` Before running a command with `--project-path`, confirm that the path is the intended Unity project for the current task. Do not copy a sibling checkout path from another repository or prior session. When intentionally validating a different Unity project, use an explicit placeholder in notes and replace it at execution time: ```bash -cli/dist/darwin-arm64/uloop compile --project-path +dist/darwin-arm64/uloop compile --project-path ``` If CLI source changes affect the command behavior you are validating, rebuild the development binary before running it. -When changing Go CLI source files under `cli`, run `scripts/check-go-cli.sh`. -Use `scripts/build-go-cli.sh` when you need to refresh local development binaries under `cli/dist`; generated binaries are ignored and must not be committed. +When changing Go source files under any of the Go modules (`common`, `dispatcher`, `project-runner`, `tools/release-automation`), run `scripts/check-go-cli.sh`. +Use `scripts/build-go-cli.sh` when you need to refresh local development binaries under `dist`; generated binaries are ignored and must not be committed. This script is the local equivalent of the Go CLI CI validation: it runs formatting checks, vet, lint, tests, rebuilds the built native binaries, and verifies that required platform binaries exist. ## Unity Freeze Prevention diff --git a/README.md b/README.md index f52f3f8adf..2f8bd842f3 100644 --- a/README.md +++ b/README.md @@ -596,9 +596,9 @@ Run the native Go CLI checks before changing files under `cli`: scripts/check-go-cli.sh ``` -The check script verifies formatting with `goimports` and `gofumpt`, runs `go vet ./...`, runs `golangci-lint`, and then runs `go test ./...` for the Go CLI module. Install `golangci-lint` first if it is not available on your `PATH`. +The check script verifies formatting with `goimports` and `gofumpt`, runs `go vet ./...`, runs `golangci-lint`, and then runs `go test ./...` for every Go module in the repository. Install `golangci-lint` first if it is not available on your `PATH`. -Use the existing build script when you need to refresh local development binaries under `cli/dist`: +Use the existing build script when you need to refresh local development binaries under `dist`: ```bash scripts/build-go-cli.sh diff --git a/docs/architecture/native-cli-simplification-plan.md b/docs/architecture/native-cli-simplification-plan.md index 75100f64ee..dbe145b916 100644 --- a/docs/architecture/native-cli-simplification-plan.md +++ b/docs/architecture/native-cli-simplification-plan.md @@ -64,11 +64,11 @@ cli ```bash scripts/check-go-cli.sh -cli/dist/darwin-arm64/uloop compile --wait-for-domain-reload +dist/darwin-arm64/uloop compile --wait-for-domain-reload ``` skill discovery や CLI-only skill source path を動かした場合は、`ToolSkillSynchronizerTests` の targeted EditMode test も実行します。 ```bash -cli/dist/darwin-arm64/uloop run-tests --test-mode EditMode --filter-type regex --filter-value ToolSkillSynchronizerTests +dist/darwin-arm64/uloop run-tests --test-mode EditMode --filter-type regex --filter-value ToolSkillSynchronizerTests ``` From b7caf4968dbce3e88341ab9d37c230889bfa5754 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 01:11:54 +0900 Subject: [PATCH 16/18] Treat lowercase does-not-exist git errors as missing files Current git (measured on 2.50.1) reports a file absent from both the ref and the working tree as "fatal: path '...' does not exist in ...", but the classifier only matched the capitalized "Path" form from older git versions. In environments without the file on disk (bare or sparse CI checkouts), the legacy contract fallback and the initial-introduction detection would therefore fail instead of falling back. --- .../automation/contract_file_at_ref.go | 3 ++ .../automation/contract_file_at_ref_test.go | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tools/release-automation/internal/automation/contract_file_at_ref_test.go diff --git a/tools/release-automation/internal/automation/contract_file_at_ref.go b/tools/release-automation/internal/automation/contract_file_at_ref.go index d44b552223..6d0fa48771 100644 --- a/tools/release-automation/internal/automation/contract_file_at_ref.go +++ b/tools/release-automation/internal/automation/contract_file_at_ref.go @@ -42,10 +42,13 @@ func contractFileAtRefWithLegacyFallback( // isMissingFileAtRefError reports whether err came from `git show ref:file` // failing because file does not exist at ref, as opposed to any other git // failure (auth, network, etc.) that must not be silently swallowed. +// Current git prints the lowercase "path ..." forms; the capitalized +// "Path ... does not exist in" form is kept for older git versions. func isMissingFileAtRefError(err error, file string) bool { message := err.Error() quotedPath := "'" + file + "'" return strings.Contains(message, "path "+quotedPath+" exists on disk, but not in") || + strings.Contains(message, "path "+quotedPath+" does not exist in") || strings.Contains(message, "Path "+quotedPath+" does not exist in") } diff --git a/tools/release-automation/internal/automation/contract_file_at_ref_test.go b/tools/release-automation/internal/automation/contract_file_at_ref_test.go new file mode 100644 index 0000000000..9959db7928 --- /dev/null +++ b/tools/release-automation/internal/automation/contract_file_at_ref_test.go @@ -0,0 +1,46 @@ +package automation + +import ( + "errors" + "testing" +) + +// Verifies the lowercase form git currently emits when the file exists nowhere is treated as missing. +func TestIsMissingFileAtRefErrorMatchesLowercaseDoesNotExist(t *testing.T) { + err := errors.New("git show failed: fatal: path 'common/clicontract/contract.json' does not exist in 'uloop-project-runner-v3.0.0-beta.40'") + if !isMissingFileAtRefError(err, "common/clicontract/contract.json") { + t.Fatal("expected lowercase does-not-exist git error to be treated as a missing file") + } +} + +// Verifies the capitalized does-not-exist form emitted by older git versions stays covered. +func TestIsMissingFileAtRefErrorMatchesCapitalizedDoesNotExist(t *testing.T) { + err := errors.New("git show failed: fatal: Path 'common/clicontract/contract.json' does not exist in 'uloop-project-runner-v3.0.0-beta.40'") + if !isMissingFileAtRefError(err, "common/clicontract/contract.json") { + t.Fatal("expected capitalized does-not-exist git error to be treated as a missing file") + } +} + +// Verifies the exists-on-disk-but-not-in-ref form is treated as missing at the ref. +func TestIsMissingFileAtRefErrorMatchesExistsOnDiskForm(t *testing.T) { + err := errors.New("git show failed: fatal: path 'common/clicontract/contract.json' exists on disk, but not in 'uloop-project-runner-v3.0.0-beta.40'") + if !isMissingFileAtRefError(err, "common/clicontract/contract.json") { + t.Fatal("expected exists-on-disk git error to be treated as a missing file") + } +} + +// Verifies unrelated git failures are never classified as a missing file, so they propagate. +func TestIsMissingFileAtRefErrorRejectsUnrelatedErrors(t *testing.T) { + err := errors.New("git show failed: fatal: unable to read tree") + if isMissingFileAtRefError(err, "common/clicontract/contract.json") { + t.Fatal("expected unrelated git error to propagate instead of being treated as missing") + } +} + +// Verifies a missing-file error for a different path is not attributed to the requested file. +func TestIsMissingFileAtRefErrorRejectsOtherFilePaths(t *testing.T) { + err := errors.New("git show failed: fatal: path 'other/file.json' does not exist in 'HEAD'") + if isMissingFileAtRefError(err, "common/clicontract/contract.json") { + t.Fatal("expected missing-file error for another path to propagate") + } +} From 6d60dc862b24c8d7c5fdd27221eb8877c3b3873c Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 01:11:54 +0900 Subject: [PATCH 17/18] Cover every common package in the dispatcher release input patterns The dispatcher binary depends on all seven common packages (verified via go list -deps), not just clicore, so changes to common/project or common/version could previously ship without the required dispatcherVersion bump. --- .../dispatcher_version_bump_guard.go | 4 +++- .../dispatcher_version_bump_guard_test.go | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go b/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go index 062defb09d..0283049377 100644 --- a/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go +++ b/tools/release-automation/internal/automation/dispatcher_version_bump_guard.go @@ -17,7 +17,9 @@ var dispatcherReleaseInputPatterns = []string{ "dispatcher/cmd/dispatcher/main.go", "dispatcher/contract.go", dispatcherContractFile, - "common/clicore/*.go", + // The dispatcher binary depends on every common package (clicontract, clicore, + // project, skills, tools, unityipc, version), so the whole module is a release input. + "common/*/*.go", "dispatcher/internal/dispatcher/*.go", "dispatcher/internal/install/*.go", "dispatcher/internal/uninstall/*.go", diff --git a/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go b/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go index d6e7b04a74..084fc34325 100644 --- a/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go +++ b/tools/release-automation/internal/automation/dispatcher_version_bump_guard_test.go @@ -109,6 +109,26 @@ func TestDispatcherVersionBumpGuardCoversClicorePackage(t *testing.T) { } } +// Verifies every common package the dispatcher binary depends on is covered, not just clicore. +func TestDispatcherVersionBumpGuardCoversOtherCommonPackages(t *testing.T) { + result := AnalyzeDispatcherVersionBumpGuard( + []string{"common/version/compare.go"}, + DispatcherVersionBumpValues{ + HasContract: true, + DispatcherVersion: "1.0.0", + DispatcherContractVersion: 1, + }, + DispatcherVersionBumpValues{ + HasContract: true, + DispatcherVersion: "1.0.0", + DispatcherContractVersion: 1, + }) + + if !dispatcherVersionBumpGuardNeedsAction(result) { + t.Fatal("expected common package changes without a version increase to fail") + } +} + // Verifies the first dispatcher contract can be introduced without comparing against a missing base. func TestDispatcherVersionBumpGuardAcceptsInitialDispatcherContract(t *testing.T) { result := AnalyzeDispatcherVersionBumpGuard( From 35e46287a63f771358c0a631e3bd58cbd84d9ec5 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 3 Jul 2026 01:11:54 +0900 Subject: [PATCH 18/18] Include go.work files in the native CLI release input paths The workspace files wire module resolution for the release build, so changing them can change the built binaries without touching any path the release-input detection previously watched. --- scripts/resolve-native-cli-release-target.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/resolve-native-cli-release-target.sh b/scripts/resolve-native-cli-release-target.sh index b11d3f703b..36f1a9ebae 100755 --- a/scripts/resolve-native-cli-release-target.sh +++ b/scripts/resolve-native-cli-release-target.sh @@ -12,6 +12,8 @@ INPUT_DRY_RUN=${INPUT_DRY_RUN:-false} RELEASE_DATA="" CLI_RELEASE_INPUT_PATHS=" .go-version +go.work +go.work.sum layout-contract.json common project-runner/cmd