diff --git a/internal/temporalcli/commands.extension.go b/internal/temporalcli/commands.extension.go index 794242263..b00900810 100644 --- a/internal/temporalcli/commands.extension.go +++ b/internal/temporalcli/commands.extension.go @@ -25,6 +25,14 @@ var cliArgsToParseForExtension = map[string]bool{ "command-timeout": true, } +type ExtensionNonZeroExit struct { + *exec.ExitError +} + +func (err ExtensionNonZeroExit) Unwrap() error { + return err.ExitError +} + // tryExecuteExtension tries to execute an extension command if the command is not a built-in command. // It returns an error if the extension command fails, and a boolean indicating whether an extension was executed. func tryExecuteExtension(cctx *CommandContext, tcmd *TemporalCommand) (error, bool) { @@ -77,8 +85,8 @@ func tryExecuteExtension(cctx *CommandContext, tcmd *TemporalCommand) (error, bo if ctx.Err() != nil { return fmt.Errorf("program interrupted"), true } - if _, ok := err.(*exec.ExitError); ok { - return nil, true + if exitError, ok := err.(*exec.ExitError); ok { + return ExtensionNonZeroExit{exitError}, true } return fmt.Errorf("extension %s failed: %w", extPath, err), true } diff --git a/internal/temporalcli/commands.extension_test.go b/internal/temporalcli/commands.extension_test.go index 266a8fcd1..2590be0cc 100644 --- a/internal/temporalcli/commands.extension_test.go +++ b/internal/temporalcli/commands.extension_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/temporalio/cli/internal/temporalcli" "golang.org/x/tools/imports" ) @@ -232,7 +233,7 @@ func TestExtension_FailsOnNonExecutableCommand(t *testing.T) { h := newExtensionHarness(t) // Create file without execute permission. path := filepath.Join(h.binDir, "temporal-foo") - err := os.WriteFile(path, []byte("a text file"), 0644) + err := os.WriteFile(path, []byte("a text file"), 0o644) require.NoError(t, err) res := h.Execute("foo") @@ -248,7 +249,10 @@ func TestExtension_PassesThroughNonZeroExit(t *testing.T) { res := h.Execute("foo") assert.Equal(t, "Args: temporal-foo \n", res.Stdout.String()) - assert.NoError(t, res.Err) + var exitError temporalcli.ExtensionNonZeroExit + if assert.ErrorAs(t, res.Err, &exitError) { + assert.Equal(t, 42, exitError.ExitCode()) + } } func TestExtension_FailsOnCommandTimeout(t *testing.T) { @@ -306,7 +310,7 @@ func (h *extensionHarness) createExtension(name string, code ...string) string { // Write source file. srcPath := filepath.Join(h.binDir, name+".go") - require.NoError(h.t, os.WriteFile(srcPath, formatted, 0644)) + require.NoError(h.t, os.WriteFile(srcPath, formatted, 0o644)) // Build executable. binPath := filepath.Join(h.binDir, name) diff --git a/internal/temporalcli/commands.go b/internal/temporalcli/commands.go index ecb52515d..eaf08dbdf 100644 --- a/internal/temporalcli/commands.go +++ b/internal/temporalcli/commands.go @@ -145,6 +145,11 @@ func (c *CommandContext) preprocessOptions() error { if c.Err() != nil { err = fmt.Errorf("program interrupted") } + if exitError, ok := errors.AsType[ExtensionNonZeroExit](err); ok { + // An extension failed after being found and successfully started. Here we defer + // to its own error handling logic, and just copy the exit code through. + os.Exit(exitError.ExitCode()) + } fmt.Fprintf(c.Options.Stderr, "Error: %v\n", err) os.Exit(1) } @@ -524,7 +529,7 @@ var buildInfo string func VersionString() string { // To add build-time information to the version string, use // go build -ldflags "-X github.com/temporalio/cli/internal.buildInfo=" - var bi = buildInfo + bi := buildInfo if bi != "" { bi = fmt.Sprintf(", %s", bi) }