diff --git a/internal/cli/root.go b/internal/cli/root.go index f243fb2..4018c61 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -60,6 +60,9 @@ var rootCmd = &cobra.Command{ if cmd.CommandPath() == "flashduty update" { return nil } + if update.IsManagedByRunner() { + return nil + } if !isTerminalFn(int(os.Stderr.Fd())) { return nil } diff --git a/internal/cli/root_managed_test.go b/internal/cli/root_managed_test.go new file mode 100644 index 0000000..4a3b543 --- /dev/null +++ b/internal/cli/root_managed_test.go @@ -0,0 +1,42 @@ +package cli + +import ( + "runtime" + "testing" + + "github.com/flashcatcloud/flashduty-cli/internal/update" +) + +func TestRootSkipsAutoUpdateWhenManagedByRunner(t *testing.T) { + saveAndResetGlobals(t) + tmp := t.TempDir() + t.Setenv("HOME", tmp) + if runtime.GOOS == "windows" { + t.Setenv("USERPROFILE", tmp) + } + t.Setenv("CI", "") + t.Setenv("GITHUB_ACTIONS", "") + t.Setenv("JENKINS_URL", "") + t.Setenv("GITLAB_CI", "") + t.Setenv("FLASHDUTY_NO_UPDATE_CHECK", "") + t.Setenv("FLASHDUTY_MANAGED_BY_RUNNER", "1") + + origIsTerminal := isTerminalFn + isTerminalFn = func(int) bool { return true } + t.Cleanup(func() { isTerminalFn = origIsTerminal }) + + called := false + origCheck := checkForUpdateAutoFn + checkForUpdateAutoFn = func(string) (*update.CheckResult, error) { + called = true + return &update.CheckResult{}, nil + } + t.Cleanup(func() { checkForUpdateAutoFn = origCheck }) + + if _, err := execCommand("version"); err != nil { + t.Fatalf("version command should run in runner-managed mode: %v", err) + } + if called { + t.Fatal("runner-managed CLI must not check for updates") + } +} diff --git a/internal/cli/update.go b/internal/cli/update.go index 921aaef..9597540 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -18,6 +18,10 @@ func newUpdateCmd() *cobra.Command { Use: "update", Short: "Update flashduty to the latest version", RunE: func(cmd *cobra.Command, _ []string) error { + if update.IsManagedByRunner() { + return fmt.Errorf("flashduty is managed by flashduty-runner; upgrade the runner instead") + } + w := cmd.OutOrStdout() _, _ = fmt.Fprintf(w, "Current version: %s\n", versionStr) _, _ = fmt.Fprintf(w, "Checking for updates...\n") diff --git a/internal/cli/update_managed_test.go b/internal/cli/update_managed_test.go new file mode 100644 index 0000000..78e081c --- /dev/null +++ b/internal/cli/update_managed_test.go @@ -0,0 +1,29 @@ +package cli + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestUpdateRejectsRunnerManagedCLIWithoutChecking(t *testing.T) { + saveAndResetGlobals(t) + t.Setenv("FLASHDUTY_MANAGED_BY_RUNNER", "1") + + checked := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + checked = true + _, _ = w.Write([]byte("v0.0.0\n")) + })) + t.Cleanup(server.Close) + t.Setenv("FLASHDUTY_UPDATE_BASE_URL", server.URL) + + _, err := execCommand("update", "--check") + if err == nil || !strings.Contains(err.Error(), "managed by flashduty-runner") { + t.Fatalf("expected runner-managed update rejection, got %v", err) + } + if checked { + t.Fatal("runner-managed CLI must reject update before checking for releases") + } +} diff --git a/internal/update/managed.go b/internal/update/managed.go new file mode 100644 index 0000000..77dad63 --- /dev/null +++ b/internal/update/managed.go @@ -0,0 +1,10 @@ +package update + +import "os" + +// IsManagedByRunner reports whether flashduty-runner owns this CLI binary. +// The marker is intentionally exact so a user environment cannot +// accidentally disable standalone CLI updates with a truthy-looking value. +func IsManagedByRunner() bool { + return os.Getenv("FLASHDUTY_MANAGED_BY_RUNNER") == "1" +} diff --git a/internal/update/managed_test.go b/internal/update/managed_test.go new file mode 100644 index 0000000..bb7073f --- /dev/null +++ b/internal/update/managed_test.go @@ -0,0 +1,15 @@ +package update + +import "testing" + +func TestIsManagedByRunner(t *testing.T) { + t.Setenv("FLASHDUTY_MANAGED_BY_RUNNER", "1") + if !IsManagedByRunner() { + t.Fatal("runner-managed CLI was not detected") + } + + t.Setenv("FLASHDUTY_MANAGED_BY_RUNNER", "true") + if IsManagedByRunner() { + t.Fatal("only the runner's explicit marker value should enable managed mode") + } +}