Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
13 changes: 10 additions & 3 deletions temporalcloudcli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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
}
}
Expand All @@ -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

Expand Down Expand Up @@ -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()),
Expand Down
4 changes: 2 additions & 2 deletions temporalcloudcli/commands.login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions temporalcloudcli/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
}
Expand Down