diff --git a/go.mod b/go.mod index 6cd4e7c..b93f167 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/temporalio/cloud-cli -go 1.25.3 +go 1.25.5 require ( github.com/dustin/go-humanize v1.0.1 diff --git a/mise.toml b/mise.toml index b057b1a..dd5b933 100644 --- a/mise.toml +++ b/mise.toml @@ -1,6 +1,6 @@ [tools] # Note: Keep in sync with `toolchain` directive in `go.mod`. -go = "1.25.3" +go = "1.25.5" # needed for go backends [settings] diff --git a/temporalcloudcli/commands.go b/temporalcloudcli/commands.go index 14bbd03..806925b 100644 --- a/temporalcloudcli/commands.go +++ b/temporalcloudcli/commands.go @@ -414,6 +414,8 @@ func (c *CloudCommand) initCommand(cctx *CommandContext) { // Unfortunately color is a global option, so we can set in pre-run but we // must unset in post-run origNoColor := color.NoColor + // AIDEV-NOTE: Store cancel function for command timeout context to prevent resource leak + var timeoutCancel context.CancelFunc c.Command.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { // Set command cctx.CurrentCommand = cmd @@ -430,7 +432,7 @@ func (c *CloudCommand) initCommand(cctx *CommandContext) { color.NoColor = c.Color.Value == "never" } - res := c.preRun(cctx) + res := c.preRun(cctx, &timeoutCancel) logCalls(cctx.Logger) @@ -443,6 +445,10 @@ func (c *CloudCommand) initCommand(cctx *CommandContext) { return res } c.Command.PersistentPostRun = func(*cobra.Command, []string) { + // AIDEV-NOTE: Clean up command timeout context to prevent resource leak + if timeoutCancel != nil { + timeoutCancel() + } color.NoColor = origNoColor } } @@ -459,7 +465,7 @@ func VersionString() string { return fmt.Sprintf("%s%s", Version, bi) } -func (c *CloudCommand) preRun(cctx *CommandContext) error { +func (c *CloudCommand) preRun(cctx *CommandContext, timeoutCancel *context.CancelFunc) error { // Set this command as the root cctx.RootCommand = c @@ -528,7 +534,8 @@ func (c *CloudCommand) preRun(cctx *CommandContext) error { } cctx.JSONShorthandPayloads = !c.NoJsonShorthandPayloads if c.CommandTimeout.Duration() > 0 { - cctx.Context, _ = context.WithTimeoutCause( + // AIDEV-NOTE: Store cancel function to prevent timeout goroutine leak + cctx.Context, *timeoutCancel = context.WithTimeoutCause( cctx.Context, c.CommandTimeout.Duration(), fmt.Errorf("command timed out after %v", c.CommandTimeout.Duration()), diff --git a/temporalcloudcli/commands.login.go b/temporalcloudcli/commands.login.go index babedae..690e7a9 100644 --- a/temporalcloudcli/commands.login.go +++ b/temporalcloudcli/commands.login.go @@ -45,7 +45,7 @@ func (c *CloudLoginCommand) run(cctx *CommandContext, _ []string) error { }); err != nil { return fmt.Errorf("failed to write config file: %w", err) } - fmt.Println("Login successful!") + cctx.Printer.Println("Login successful!") return nil } @@ -98,7 +98,7 @@ func (c *CloudLogoutCommand) run(cctx *CommandContext, _ []string) error { } logoutURL := domainURL.JoinPath("v2", "logout") - fmt.Printf("Opening browser to logout. If it doesn't open, visit: %s\n", logoutURL.String()) + cctx.Printer.Println(fmt.Sprintf("Opening browser to logout. If it doesn't open, visit: %s", logoutURL.String())) _ = browser.OpenURL(logoutURL.String()) return nil diff --git a/temporalcloudcli/oauth.go b/temporalcloudcli/oauth.go index 4a877ae..f3b3b6a 100644 --- a/temporalcloudcli/oauth.go +++ b/temporalcloudcli/oauth.go @@ -43,8 +43,10 @@ func Login(ctx context.Context, config *oauth2.Config, options ...oauth2.AuthCod } // Start HTTP server to handle callback. + // AIDEV-NOTE: serverErrCh captures server startup errors to prevent silent failures var once sync.Once resultCh := make(chan oauthCallbackResult, 1) + serverErrCh := make(chan error, 1) server := &http.Server{ Addr: url.Host, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -88,17 +90,24 @@ func Login(ctx context.Context, config *oauth2.Config, options ...oauth2.AuthCod }) }), } - go server.ListenAndServe() + // AIDEV-NOTE: Start server in goroutine and capture startup errors + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + serverErrCh <- fmt.Errorf("failed to start OAuth callback server: %w", err) + } + }() defer func() { shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() server.Shutdown(shutdownCtx) }() - // Wait for callback result or context cancellation. + // Wait for callback result, server error, or context cancellation. var result oauthCallbackResult select { case result = <-resultCh: + case err := <-serverErrCh: + return nil, err case <-ctx.Done(): return nil, ctx.Err() }