// Package process runs child commands for ReleaseKit's internal adapters. package process import ( "context" "errors" "fmt" "io" "os" "os/exec" "sort" "strings" ) // Invocation describes one child process. UnsetEnv removes inherited values // before Env overlays values for this child only. type Invocation struct { Args []string Dir string Env map[string]string UnsetEnv []string Stdin io.Reader 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 command.Stdin = invocation.Stdin if len(invocation.Env) != 0 || len(invocation.UnsetEnv) != 0 { command.Env = environmentEntries(invocation.Env, invocation.UnsetEnv) } 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(overlay map[string]string, unset []string) []string { environment := make(map[string]string, len(os.Environ())+len(overlay)) for _, entry := range os.Environ() { key, value, ok := strings.Cut(entry, "=") if ok { environment[key] = value } } for _, key := range unset { delete(environment, key) } for key, value := range overlay { environment[key] = value } 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 }