From 177854fbec93d42be9e7c6e287be1d4f322ebd55 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 30 Jun 2026 00:17:49 +0900 Subject: [PATCH 1/4] Throttle failed dispatcher self-update checks Record failed optional dispatcher update attempts so routine commands do not retry the installer on every invocation. Suppress PowerShell progress output when invoking the Windows installer because redirected streams do not reliably hide host progress UI. --- cli/internal/cli/dispatcher.go | 2 ++ cli/internal/cli/dispatcher_test.go | 26 ++++++++++++++++++++++---- cli/internal/cli/update_test.go | 10 ++++++++-- cli/internal/update/command.go | 2 +- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/cli/internal/cli/dispatcher.go b/cli/internal/cli/dispatcher.go index d92a507a9..dee4c7a36 100644 --- a/cli/internal/cli/dispatcher.go +++ b/cli/internal/cli/dispatcher.go @@ -162,6 +162,8 @@ func enforceDispatcherFreshness(ctx context.Context, pin dispatcherPin, stderr i return true, 1 } + // Why: optional update failures should not retry and redraw installer progress on every command. + markDispatcherSelfUpdateChecked() writeFormat(stderr, "warning: dispatcher self-update skipped: %v\n", err) return false, 0 } diff --git a/cli/internal/cli/dispatcher_test.go b/cli/internal/cli/dispatcher_test.go index ec96b817b..924f11210 100644 --- a/cli/internal/cli/dispatcher_test.go +++ b/cli/internal/cli/dispatcher_test.go @@ -229,8 +229,8 @@ func TestEnforceDispatcherFreshnessRequiresManualUpdateWhenSelfUpdateDisabled(t } } -func TestEnforceDispatcherFreshnessDoesNotMarkFailedOptionalUpdateChecked(t *testing.T) { - // Verifies transient optional update failures stay retryable on the next command. +func TestEnforceDispatcherFreshnessMarksFailedOptionalUpdateChecked(t *testing.T) { + // Verifies transient optional update failures are throttled until the next check interval. cacheRoot := t.TempDir() t.Setenv(dispatcherCacheDirEnvName, cacheRoot) @@ -238,7 +238,9 @@ func TestEnforceDispatcherFreshnessDoesNotMarkFailedOptionalUpdateChecked(t *tes defer func() { dispatcherRunUpdate = previousRunner }() + runnerCalls := 0 dispatcherRunUpdate = func(context.Context) error { + runnerCalls++ return errors.New("network unavailable") } @@ -255,8 +257,24 @@ func TestEnforceDispatcherFreshnessDoesNotMarkFailedOptionalUpdateChecked(t *tes t.Fatalf("freshness output mismatch: %s", stderr.String()) } statePath := filepath.Join(cacheRoot, dispatcherUpdateStateFileName) - if _, err := os.Stat(statePath); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("expected no update state after failed optional update, got err=%v", err) + if _, err := os.Stat(statePath); err != nil { + t.Fatalf("expected update state after failed optional update: %v", err) + } + + stderr.Reset() + handled, code = enforceDispatcherFreshness( + context.Background(), + dispatcherPin{MinimumDispatcherVersion: dispatcherVersion}, + &stderr) + + if handled || code != 0 { + t.Fatalf("second freshness result mismatch: handled=%t code=%d", handled, code) + } + if runnerCalls != 1 { + t.Fatalf("optional update should be throttled after failure, got %d calls", runnerCalls) + } + if stderr.Len() != 0 { + t.Fatalf("expected no throttled optional update output, got: %s", stderr.String()) } } diff --git a/cli/internal/cli/update_test.go b/cli/internal/cli/update_test.go index a6ac48177..5152b237a 100644 --- a/cli/internal/cli/update_test.go +++ b/cli/internal/cli/update_test.go @@ -40,7 +40,7 @@ func TestUpdateCommandForDarwinUsesDirectInstaller(t *testing.T) { } func TestUpdateCommandForWindowsUsesPowerShellInstaller(t *testing.T) { - // Verifies dispatcher update calls the same Windows installer script on the matching channel. + // Verifies dispatcher update calls the Windows installer script quietly on the matching channel. commandName, args, err := updateCommandForOS("windows") if err != nil { t.Fatalf("updateCommandForOS failed: %v", err) @@ -58,6 +58,9 @@ func TestUpdateCommandForWindowsUsesPowerShellInstaller(t *testing.T) { if !strings.Contains(joinedArgs, "$env:ULOOP_VERSION='"+expectedReleaseTag+"'") { t.Fatalf("installer version missing: %s", joinedArgs) } + if !strings.Contains(joinedArgs, "$ProgressPreference='SilentlyContinue'") { + t.Fatalf("installer progress suppression missing: %s", joinedArgs) + } if strings.Contains(joinedArgs, "npm") { t.Fatalf("update command still references npm: %s", joinedArgs) } @@ -127,7 +130,7 @@ func TestUpdateCommandForDarwinNormalizesProjectRunnerReleaseTag(t *testing.T) { } func TestUpdateCommandForWindowsUsesRequestedVersion(t *testing.T) { - // Verifies Windows dispatcher update can target the minimum release version requested by Unity. + // Verifies Windows dispatcher update can quietly target the minimum release version requested by Unity. commandName, args, err := updateCommandForOSWithOptions("windows", updateOptions{ targetVersion: "3.0.0", }) @@ -145,6 +148,9 @@ func TestUpdateCommandForWindowsUsesRequestedVersion(t *testing.T) { if !strings.Contains(joinedArgs, "$env:ULOOP_VERSION='dispatcher-v3.0.0'") { t.Fatalf("installer version missing: %s", joinedArgs) } + if !strings.Contains(joinedArgs, "$ProgressPreference='SilentlyContinue'") { + t.Fatalf("installer progress suppression missing: %s", joinedArgs) + } } func TestParseUpdateOptionsNormalizesVersionPrefix(t *testing.T) { diff --git a/cli/internal/update/command.go b/cli/internal/update/command.go index 3be110930..1e4990c22 100644 --- a/cli/internal/update/command.go +++ b/cli/internal/update/command.go @@ -37,7 +37,7 @@ func CommandForOS(goos string, options Options) (Command, error) { "-ExecutionPolicy", "Bypass", "-Command", - fmt.Sprintf("$env:ULOOP_VERSION=%s; irm %s | iex", shellQuote(updateSelector), shellQuote(scriptURL)), + fmt.Sprintf("$ProgressPreference='SilentlyContinue'; $env:ULOOP_VERSION=%s; irm %s | iex", shellQuote(updateSelector), shellQuote(scriptURL)), }}, nil default: return Command{}, errors.New(UnsupportedOSMessage) From e60858ae765fe03f975d71fd9b4d9441f47cf231 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 30 Jun 2026 00:37:29 +0900 Subject: [PATCH 2/4] Bump dispatcher version for update fixes Advance the dispatcher release version because this branch changes dispatcher release inputs. This satisfies the dispatcher version bump guard without changing the minimum dispatcher requirement for existing package installs. --- cli/dispatcher-contract.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/dispatcher-contract.json b/cli/dispatcher-contract.json index f1a164c23..754305f65 100644 --- a/cli/dispatcher-contract.json +++ b/cli/dispatcher-contract.json @@ -1,5 +1,5 @@ { "schemaVersion": 1, - "dispatcherVersion": "3.0.1-beta.6", + "dispatcherVersion": "3.0.1-beta.7", "dispatcherContractVersion": 1 } From 3f9d9d96fa8c746b321949cc1bb18e95bbdf3bdb Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 30 Jun 2026 00:43:33 +0900 Subject: [PATCH 3/4] Document dispatcher release version guard Explain that dispatcher release input changes must advance cli/dispatcher-contract.json dispatcherVersion so future work does not miss the CI guard. Clarify that dispatcherContractVersion and minimumDispatcherVersion should only move when their contracts require it. --- AGENTS.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ae55c1e10..803223921 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,6 +54,26 @@ These files are generated copies. Update the source skill definitions instead, t Write GitHub Actions and release automation logic in Go when it needs JSON parsing, workflow polling, state transitions, or non-trivial branching. 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`. +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. + +Dispatcher release inputs include: + +- `cli/cmd/dispatcher/main.go` +- `cli/dispatcher-contract.json` +- `cli/internal/cli/dispatcher*.go` +- `cli/internal/update/*.go` +- `scripts/install.ps1` + +Do not bump `dispatcherContractVersion` unless the dispatcher contract itself changes. +Do not raise Unity package `MINIMUM_REQUIRED_DISPATCHER_VERSION` or pin-file +`minimumDispatcherVersion` just because the dispatcher implementation changed; those should +advance only when the package must require a newly published dispatcher. + ## Windows Compatibility Guardrails Most day-to-day development happens on macOS, but this project must keep working on Windows. From eacaeed29e7f9ebeabea40b9ca2960c4fd6b29ef Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 30 Jun 2026 00:52:12 +0900 Subject: [PATCH 4/4] Clarify dispatcher release input examples Point contributors to the CI guard's authoritative dispatcher release input pattern list so the AGENTS guidance does not imply the example list is complete. --- AGENTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 803223921..d3c4dbcb9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,9 @@ When changing dispatcher release inputs, update `dispatcherVersion` in the same Pull request CI runs `check-dispatcher-version-bump` and fails if dispatcher inputs changed without a dispatcher version increase. -Dispatcher release inputs include: +The authoritative dispatcher release input list is `dispatcherReleaseInputPatterns` in +`cli/internal/automation/dispatcher_version_bump_guard.go`. +Dispatcher release inputs include these non-exhaustive examples: - `cli/cmd/dispatcher/main.go` - `cli/dispatcher-contract.json`