Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agent/agent_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type AgentConfiguration struct {
WriteJobLogsToStdout bool
LogFormat string
Shell string
HooksShell string
Profile string
RedactedVars []string
AcquireJob string
Expand Down
4 changes: 3 additions & 1 deletion agent/job_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ BUILDKITE_LOCAL_HOOKS_ENABLED
BUILDKITE_PLUGINS_ENABLED
BUILDKITE_REDACTED_VARS
BUILDKITE_SHELL
BUILDKITE_HOOKS_SHELL
BUILDKITE_SIGNAL_GRACE_PERIOD_SECONDS
BUILDKITE_SSH_KEYSCAN
BUILDKITE_STRICT_SINGLE_HOOKS
Expand Down Expand Up @@ -588,6 +589,7 @@ BUILDKITE_AGENT_JWKS_KEY_ID`
setEnv("BUILDKITE_GIT_MIRRORS_LOCK_TIMEOUT", strconv.Itoa(r.conf.AgentConfiguration.GitMirrorsLockTimeout))

setEnv("BUILDKITE_SHELL", r.conf.AgentConfiguration.Shell)
setEnv("BUILDKITE_HOOKS_SHELL", r.conf.AgentConfiguration.HooksShell)
setEnv("BUILDKITE_AGENT_EXPERIMENT", strings.Join(experiments.Enabled(ctx), ","))
setEnv("BUILDKITE_REDACTED_VARS", strings.Join(r.conf.AgentConfiguration.RedactedVars, ","))
setEnv("BUILDKITE_STRICT_SINGLE_HOOKS", fmt.Sprint(r.conf.AgentConfiguration.StrictSingleHooks))
Expand Down Expand Up @@ -751,7 +753,7 @@ func (r *JobRunner) executePreBootstrapHook(ctx context.Context, hook string) (b
environ.Set("BUILDKITE_AGENT_DEBUG", fmt.Sprint(r.conf.Debug))
environ.Set("BUILDKITE_AGENT_DEBUG_HTTP", fmt.Sprint(r.conf.DebugHTTP))

script, err := sh.Script(hook)
script, err := sh.Script(hook, r.conf.AgentConfiguration.HooksShell)
if err != nil {
r.agentLogger.Error("Finished pre-bootstrap hook %q: script not runnable: %v", hook, err)
return false, err
Expand Down
11 changes: 9 additions & 2 deletions clicommand/agent_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ type AgentStartConfig struct {
PluginsPath string `cli:"plugins-path" normalize:"filepath"`

Shell string `cli:"shell"`
HooksShell string `cli:"hooks-shell"`
BootstrapScript string `cli:"bootstrap-script" normalize:"commandpath"`
NoPTY bool `cli:"no-pty"`

Expand Down Expand Up @@ -293,7 +294,7 @@ func (asc AgentStartConfig) Features(ctx context.Context) []string {
}

func DefaultShell() string {
// https://github.com/golang/go/blob/master/src/go/build/syslist.go#L7
// https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go#L17
switch runtime.GOOS {
case "windows":
return `C:\Windows\System32\CMD.exe /S /C`
Expand Down Expand Up @@ -421,6 +422,11 @@ var AgentStartCommand = cli.Command{
Usage: "The shell command used to interpret build commands, e.g /bin/bash -e -c",
EnvVar: "BUILDKITE_SHELL",
},
cli.StringFlag{
Name: "hooks-shell",
Usage: "The shell command used to interpret hooks commands, e.g pwsh -Command",
EnvVar: "BUILDKITE_HOOKS_SHELL",
},
cli.StringFlag{
Name: "queue",
Usage: "The queue the agent will listen to for jobs. If not set, the agent will use the default queue. Overwrites the queue tag in the agent's tags",
Expand Down Expand Up @@ -1103,6 +1109,7 @@ var AgentStartCommand = cli.Command{
WriteJobLogsToStdout: cfg.WriteJobLogsToStdout,
LogFormat: cfg.LogFormat,
Shell: cfg.Shell,
HooksShell: cfg.HooksShell,
RedactedVars: cfg.RedactedVars,
AcquireJob: cfg.AcquireJob,
TracingBackend: cfg.TracingBackend,
Expand Down Expand Up @@ -1573,7 +1580,7 @@ func agentLifecycleHook(hookName string, log logger.Logger, cfg AgentStartConfig

// run hooks
for _, p = range hooks {
script, err := sh.Script(p)
script, err := sh.Script(p, cfg.HooksShell)
if err != nil {
log.Error("%q hook: %v", hookName, err)
return err
Expand Down
7 changes: 7 additions & 0 deletions clicommand/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type BootstrapConfig struct {
LogLevel string `cli:"log-level"`
Debug bool `cli:"debug"`
Shell string `cli:"shell"`
HooksShell string `cli:"hooks-shell"`
Experiments []string `cli:"experiment" normalize:"list"`
Phases []string `cli:"phases" normalize:"list"`
Profile string `cli:"profile"`
Expand Down Expand Up @@ -369,6 +370,11 @@ var BootstrapCommand = cli.Command{
EnvVar: "BUILDKITE_SHELL",
Value: DefaultShell(),
},
cli.StringFlag{
Name: "hooks-shell",
Usage: "The shell to use to interpret hooks commands",
EnvVar: "BUILDKITE_HOOKS_SHELL",
},
cli.StringSliceFlag{
Name: "phases",
Usage: "The specific phases to execute. The order they're defined is irrelevant.",
Expand Down Expand Up @@ -518,6 +524,7 @@ var BootstrapCommand = cli.Command{
RunInPty: runInPty,
SSHKeyscan: cfg.SSHKeyscan,
Shell: cfg.Shell,
HooksShell: cfg.HooksShell,
StrictSingleHooks: cfg.StrictSingleHooks,
Tag: cfg.Tag,
TracingBackend: cfg.TracingBackend,
Expand Down
1 change: 1 addition & 0 deletions env/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ var ProtectedEnv = map[string]struct{}{
"BUILDKITE_PLUGINS_ENABLED": {},
"BUILDKITE_PLUGINS_PATH": {},
"BUILDKITE_SHELL": {},
"BUILDKITE_HOOKS_SHELL": {},
"BUILDKITE_SSH_KEYSCAN": {},
}

Expand Down
1 change: 1 addition & 0 deletions env/environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ func TestProtectedEnv(t *testing.T) {
"BUILDKITE_PLUGINS_ENABLED",
"BUILDKITE_PLUGINS_PATH",
"BUILDKITE_SHELL",
"BUILDKITE_HOOKS_SHELL",
"BUILDKITE_SSH_KEYSCAN",
}

Expand Down
3 changes: 3 additions & 0 deletions internal/job/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ type ExecutorConfig struct {
// The shell used to execute commands
Shell string

// The shell used to execute agent hooks
HooksShell string

// Phases to execute, defaults to all phases
Phases []string

Expand Down
2 changes: 1 addition & 1 deletion internal/job/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ func (e *Executor) runWrappedShellScriptHook(ctx context.Context, hookName strin
// (which acquires open file descriptors of the parent process) and
// writing an executable (the script wrapper).
// See https://github.com/golang/go/issues/22315.
script, err := e.shell.Script(script.Path())
script, err := e.shell.Script(script.Path(), e.HooksShell)
if err != nil {
r.Break()
return err
Expand Down
3 changes: 2 additions & 1 deletion internal/job/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ func Find(root *os.Root, hookDir, name string) (string, error) {
exts = []string{".bat", ".cmd", ".ps1", ".exe"}
}
// always check for an extensionless file
exts = append(exts, "")
// PowerShell 7 is cross-platform
exts = append(exts, "", ".ps1")

// Check for a file named name+ext in hookDir.
for _, ext := range exts {
Expand Down
1 change: 1 addition & 0 deletions internal/job/integration/command_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func TestMultilineCommandRunUnderBatch(t *testing.T) {
env := []string{
"BUILDKITE_COMMAND=Setup.cmd\nset LLAMAS=COOL\nBuildProject.cmd",
`BUILDKITE_SHELL=C:\Windows\System32\CMD.exe /S /C`,
`BUILDKITE_HOOKS_SHELL=C:\Program Files\PowerShell\7\pwsh.exe`,
}

tester.RunAndCheck(t, env...)
Expand Down
22 changes: 20 additions & 2 deletions internal/shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func (s *Shell) Command(command string, args ...string) Command {
// executed directly, or some kind of intepreter is executed in order to
// interpret it (loosely: powershell.exe for .ps1 files, bash(.exe) for shell
// scripts without shebang lines).
func (s *Shell) Script(path string) (Command, error) {
func (s *Shell) Script(path string, commandOverride string) (Command, error) {
var command string
var args []string

Expand All @@ -310,6 +310,16 @@ func (s *Shell) Script(path string) (Command, error) {
isWindows := runtime.GOOS == "windows"
isPwsh := filepath.Ext(path) == ".ps1"

if commandOverride != "" {
// first element is the command, all others are args to which we append path
commandParts := strings.Fields(commandOverride)
return Command{
shell: s,
command: commandParts[0],
args: append(commandParts[1:], path),
}, nil
}

switch {
case isWindows && isSh:
if s.debug {
Expand All @@ -326,11 +336,19 @@ func (s *Shell) Script(path string) (Command, error) {

case isWindows && isPwsh:
if s.debug {
s.Commentf("Attempting to run %s with Powershell", path)
s.Commentf("Attempting to run %s with PowerShell", path)
}
command = "powershell.exe"
args = []string{"-file", path}

case !isWindows && isPwsh:
// If Pwsh on non-Windows platform, use cross-platform PowerShell 7
if s.debug {
s.Commentf("Attempting to run %s with PowerShell 7", path)
}
command = "pwsh"
args = []string{"-file", path}

case !isWindows && isSh:
// If the script contains a shebang line, it can be run directly,
// with the shebang line choosing the interpreter.
Expand Down