package exec import ( "bufio" "bytes" "context" "os" "os/exec" "path/filepath" "runtime" "strings" "time" "github.com/mrhid6/vantage/agent/internal/grpc/pb" ) // RunStep writes the script to a temp file, provides a WORKFLOW_ENV file for // the script to append KEY=value output to, executes it under the requested // interpreter, and returns captured output plus parsed output env. func RunStep(cmd *pb.RunStepCmd) *pb.StepResult { res := &pb.StepResult{CommandId: "", OutputEnv: map[string]string{}} dir, err := os.MkdirTemp("", "vantage-step-") if err != nil { res.ExitCode = 1 res.Stderr = "create temp dir: " + err.Error() return res } defer os.RemoveAll(dir) envFile := filepath.Join(dir, "workflow_env") if err := os.WriteFile(envFile, nil, 0600); err != nil { res.ExitCode = 1 res.Stderr = "create env file: " + err.Error() return res } var scriptPath string var c *exec.Cmd timeout := time.Duration(cmd.TimeoutSeconds) * time.Second if timeout <= 0 { timeout = 30 * time.Minute } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() switch cmd.Interpreter { case "powershell": scriptPath = filepath.Join(dir, "step.ps1") if err := os.WriteFile(scriptPath, []byte(cmd.Script), 0600); err != nil { res.ExitCode = 1 res.Stderr = err.Error() return res } shell := "pwsh" if runtime.GOOS == "windows" { if _, err := exec.LookPath("pwsh"); err != nil { shell = "powershell.exe" } } c = exec.CommandContext(ctx, shell, "-NoProfile", "-NonInteractive", "-File", scriptPath) default: // "bash" scriptPath = filepath.Join(dir, "step.sh") if err := os.WriteFile(scriptPath, []byte(cmd.Script), 0700); err != nil { res.ExitCode = 1 res.Stderr = err.Error() return res } c = exec.CommandContext(ctx, "bash", scriptPath) } c.Env = append(os.Environ(), "WORKFLOW_ENV="+envFile) for k, v := range cmd.Env { c.Env = append(c.Env, k+"="+v) } var stdout, stderr bytes.Buffer c.Stdout = &stdout c.Stderr = &stderr runErr := c.Run() res.Stdout = stdout.String() res.Stderr = stderr.String() if ctx.Err() == context.DeadlineExceeded { res.ExitCode = 124 res.Stderr += "\n[vantage] step timed out" } else if ee, ok := runErr.(*exec.ExitError); ok { res.ExitCode = ee.ExitCode() } else if runErr != nil { res.ExitCode = 1 res.Stderr += "\n[vantage] " + runErr.Error() } res.OutputEnv = parseEnvFile(envFile) return res } // parseEnvFile reads KEY=value lines (last write wins). Blank lines and lines // without '=' are ignored. func parseEnvFile(path string) map[string]string { out := map[string]string{} f, err := os.Open(path) if err != nil { return out } defer f.Close() sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) for sc.Scan() { line := sc.Text() i := strings.IndexByte(line, '=') if i <= 0 { continue } out[line[:i]] = line[i+1:] } return out }