// Package process runs child commands for ReleaseKit's internal adapters. package process import ( "context" "errors" "fmt" "io" "os" "os/exec" "sort" ) // Invocation describes one child process. Environment values overlay the // current process environment for this child only. type Invocation struct { Args []string Dir string Env map[string]string Stdout io.Writer Stderr io.Writer } // Run starts the command in the requested directory and waits for it to exit. // Errors preserve the underlying exec error, including *exec.ExitError. func Run(ctx context.Context, invocation Invocation) error { if len(invocation.Args) == 0 || invocation.Args[0] == "" { return errors.New("process invocation requires a command") } command := exec.CommandContext(ctx, invocation.Args[0], invocation.Args[1:]...) command.Dir = invocation.Dir if len(invocation.Env) != 0 { command.Env = append(os.Environ(), environmentEntries(invocation.Env)...) } command.Stdout = invocation.Stdout command.Stderr = invocation.Stderr if err := command.Run(); err != nil { if contextErr := ctx.Err(); contextErr != nil { return fmt.Errorf("run %q: %w", invocation.Args[0], contextErr) } return fmt.Errorf("run %q: %w", invocation.Args[0], err) } return nil } func environmentEntries(environment map[string]string) []string { keys := make([]string, 0, len(environment)) for key := range environment { keys = append(keys, key) } sort.Strings(keys) entries := make([]string, 0, len(keys)) for _, key := range keys { entries = append(entries, key+"="+environment[key]) } return entries }