6d2a663551eab77e3a84edbfb2b7923d5e11e65a

Author
Solaris-star <67425364+Solaris-star@users.noreply.github.com>
Committer
GitHub <noreply@github.com>
Date

Message

fix(hooks): propagate custom hook failures (#910)

Diff

  1diff --git a/cmd/soft/hook/hook.go b/cmd/soft/hook/hook.go
  2index a16cad6d6728886df7de26be436170b28b6ec350..d56ebc90ab02333933fc972538d4ce0acbf529ad 100644
  3--- a/cmd/soft/hook/hook.go
  4+++ b/cmd/soft/hook/hook.go
  5@@ -117,11 +117,9 @@ var (
  6 		}
  7 
  8 		// Custom hooks
  9-		if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
 10-			// If the custom hook is executable, run it
 11-			if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
 12-				logger.Error("failed to run custom hook", "err", err)
 13-			}
 14+		if err := runCustomHook(ctx, customHookPath, &buf, stdout, stderr, args...); err != nil {
 15+			logger.Error("failed to run custom hook", "err", err)
 16+			return err
 17 		}
 18 
 19 		return nil
 20@@ -170,3 +168,12 @@ func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer,
 21 	cmd.Stderr = err
 22 	return cmd.Run()
 23 }
 24+
 25+func runCustomHook(ctx context.Context, path string, in io.Reader, out io.Writer, err io.Writer, args ...string) error {
 26+	stat, statErr := os.Stat(path)
 27+	if statErr != nil || stat.IsDir() || stat.Mode()&0o111 == 0 {
 28+		return nil
 29+	}
 30+
 31+	return runCommand(ctx, in, out, err, path, args...)
 32+}
 33diff --git a/cmd/soft/hook/hook_test.go b/cmd/soft/hook/hook_test.go
 34new file mode 100644
 35index 0000000000000000000000000000000000000000..77c427c3350abf66ea254862141dabe4c4c87095
 36--- /dev/null
 37+++ b/cmd/soft/hook/hook_test.go
 38@@ -0,0 +1,69 @@
 39+package hook
 40+
 41+import (
 42+	"context"
 43+	"errors"
 44+	"io"
 45+	"os"
 46+	"os/exec"
 47+	"path/filepath"
 48+	"testing"
 49+)
 50+
 51+func TestRunCustomHookPropagatesExitError(t *testing.T) {
 52+	executable, err := os.Executable()
 53+	if err != nil {
 54+		t.Fatal(err)
 55+	}
 56+	info, err := os.Stat(executable)
 57+	if err != nil {
 58+		t.Fatal(err)
 59+	}
 60+	if info.Mode()&0o111 == 0 {
 61+		t.Skip("test binary is not executable on this platform")
 62+	}
 63+
 64+	t.Setenv("SOFT_SERVE_TEST_HELPER", "1")
 65+	t.Setenv("SOFT_SERVE_TEST_HELPER_EXIT", "1")
 66+	err = runCustomHook(
 67+		context.Background(),
 68+		executable,
 69+		io.Reader(nil),
 70+		io.Discard,
 71+		io.Discard,
 72+		"-test.run=TestCustomHookHelperProcess",
 73+	)
 74+	if err == nil {
 75+		t.Fatal("runCustomHook returned nil for a failing custom hook")
 76+	}
 77+
 78+	var exitErr *exec.ExitError
 79+	if !errors.As(err, &exitErr) {
 80+		t.Fatalf("runCustomHook error = %T %v, want exec.ExitError", err, err)
 81+	}
 82+}
 83+
 84+func TestRunCustomHookIgnoresMissingOrNonExecutableHook(t *testing.T) {
 85+	missing := filepath.Join(t.TempDir(), "missing-hook")
 86+	if err := runCustomHook(context.Background(), missing, nil, io.Discard, io.Discard); err != nil {
 87+		t.Fatalf("runCustomHook(missing) error = %v", err)
 88+	}
 89+
 90+	nonExecutable := filepath.Join(t.TempDir(), "non-executable-hook")
 91+	if err := os.WriteFile(nonExecutable, []byte("exit 1\n"), 0o600); err != nil {
 92+		t.Fatal(err)
 93+	}
 94+	if err := runCustomHook(context.Background(), nonExecutable, nil, io.Discard, io.Discard); err != nil {
 95+		t.Fatalf("runCustomHook(non-executable) error = %v", err)
 96+	}
 97+}
 98+
 99+func TestCustomHookHelperProcess(t *testing.T) {
100+	if os.Getenv("SOFT_SERVE_TEST_HELPER") != "1" {
101+		return
102+	}
103+	if os.Getenv("SOFT_SERVE_TEST_HELPER_EXIT") == "1" {
104+		os.Exit(1)
105+	}
106+	os.Exit(0)
107+}