From 9210d958e682b806fc113031f30ff2cbe921842d Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Thu, 23 Jul 2026 09:27:28 +0800 Subject: [PATCH] fix(hooks): propagate custom hook failures --- cmd/soft/hook/hook.go | 17 +++++++--- cmd/soft/hook/hook_test.go | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 cmd/soft/hook/hook_test.go diff --git a/cmd/soft/hook/hook.go b/cmd/soft/hook/hook.go index a16cad6d6..d56ebc90a 100644 --- a/cmd/soft/hook/hook.go +++ b/cmd/soft/hook/hook.go @@ -117,11 +117,9 @@ var ( } // Custom hooks - if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 { - // If the custom hook is executable, run it - if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil { - logger.Error("failed to run custom hook", "err", err) - } + if err := runCustomHook(ctx, customHookPath, &buf, stdout, stderr, args...); err != nil { + logger.Error("failed to run custom hook", "err", err) + return err } return nil @@ -170,3 +168,12 @@ func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer, cmd.Stderr = err return cmd.Run() } + +func runCustomHook(ctx context.Context, path string, in io.Reader, out io.Writer, err io.Writer, args ...string) error { + stat, statErr := os.Stat(path) + if statErr != nil || stat.IsDir() || stat.Mode()&0o111 == 0 { + return nil + } + + return runCommand(ctx, in, out, err, path, args...) +} diff --git a/cmd/soft/hook/hook_test.go b/cmd/soft/hook/hook_test.go new file mode 100644 index 000000000..77c427c33 --- /dev/null +++ b/cmd/soft/hook/hook_test.go @@ -0,0 +1,69 @@ +package hook + +import ( + "context" + "errors" + "io" + "os" + "os/exec" + "path/filepath" + "testing" +) + +func TestRunCustomHookPropagatesExitError(t *testing.T) { + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + info, err := os.Stat(executable) + if err != nil { + t.Fatal(err) + } + if info.Mode()&0o111 == 0 { + t.Skip("test binary is not executable on this platform") + } + + t.Setenv("SOFT_SERVE_TEST_HELPER", "1") + t.Setenv("SOFT_SERVE_TEST_HELPER_EXIT", "1") + err = runCustomHook( + context.Background(), + executable, + io.Reader(nil), + io.Discard, + io.Discard, + "-test.run=TestCustomHookHelperProcess", + ) + if err == nil { + t.Fatal("runCustomHook returned nil for a failing custom hook") + } + + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) { + t.Fatalf("runCustomHook error = %T %v, want exec.ExitError", err, err) + } +} + +func TestRunCustomHookIgnoresMissingOrNonExecutableHook(t *testing.T) { + missing := filepath.Join(t.TempDir(), "missing-hook") + if err := runCustomHook(context.Background(), missing, nil, io.Discard, io.Discard); err != nil { + t.Fatalf("runCustomHook(missing) error = %v", err) + } + + nonExecutable := filepath.Join(t.TempDir(), "non-executable-hook") + if err := os.WriteFile(nonExecutable, []byte("exit 1\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := runCustomHook(context.Background(), nonExecutable, nil, io.Discard, io.Discard); err != nil { + t.Fatalf("runCustomHook(non-executable) error = %v", err) + } +} + +func TestCustomHookHelperProcess(t *testing.T) { + if os.Getenv("SOFT_SERVE_TEST_HELPER") != "1" { + return + } + if os.Getenv("SOFT_SERVE_TEST_HELPER_EXIT") == "1" { + os.Exit(1) + } + os.Exit(0) +}