diff --git a/AGENTS.md b/AGENTS.md index ae55c1e10..d3c4dbcb9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,6 +54,28 @@ 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. + +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` +- `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. 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 } 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)