diff --git a/WORKFLOW_SUPPORT.md b/WORKFLOW_SUPPORT.md index 48ca16d..6e13386 100644 --- a/WORKFLOW_SUPPORT.md +++ b/WORKFLOW_SUPPORT.md @@ -22,7 +22,7 @@ syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for - [ ] jobs..outputs - [x] jobs..env - [x] jobs..defaults - - [ ] jobs..run.shell + - [x] jobs..run.shell - [x] jobs..run.working-directory - [ ] jobs..timeout-minutes - [ ] jobs..strategy @@ -40,7 +40,7 @@ syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for - [ ] jobs..steps[*].uses - [x] jobs..steps[*].run - [x] jobs..steps[*].working-directory - - [ ] jobs..steps[*].shell + - [x] jobs..steps[*].shell - [ ] jobs..steps[*].with - [x] jobs..steps[*].env - [X] jobs..steps[*].continue-on-error diff --git a/internal/driver/driver.go b/internal/driver/driver.go index 12336b8..1f6fd96 100644 --- a/internal/driver/driver.go +++ b/internal/driver/driver.go @@ -26,12 +26,15 @@ type CommandOptions struct { // Note: In the case of container drivers, these mappings are applied within the // container. Env map[string]string + // Shell to use when running the command. + Shell string } func NewCommandOptions() CommandOptions { return CommandOptions{ - Cwd: ".", - Env: map[string]string{}, + Cwd: ".", + Shell: "bash", + Env: map[string]string{}, } } diff --git a/internal/driver/podman_driver.go b/internal/driver/podman_driver.go index 58794c7..b57da3e 100644 --- a/internal/driver/podman_driver.go +++ b/internal/driver/podman_driver.go @@ -95,7 +95,7 @@ func (d PodmanDriver) Exec(containerId string, command string, options CommandOp commandArgs = append(commandArgs, envArgs...) commandArgs = append(commandArgs, containerId, - "bash", + options.Shell, "-c", command, ) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 12b8f36..5bf92b5 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -104,8 +104,8 @@ func (r Runner) runJob(jobContext context.Context, jobTracker *TaskTracker, jobW // // If the command raises an error while in the container or fails to run // the command at all, an error is returned, otherwise nil. -func (r *Runner) RunCommandInContainer(containerId string, command string, stepCwd string, stepEnv map[string]string) error { - result := r.Driver.Exec(containerId, command, driver.CommandOptions{Cwd: stepCwd, Env: stepEnv}) +func (r *Runner) RunCommandInContainer(containerId string, command string, options driver.CommandOptions) error { + result := r.Driver.Exec(containerId, command, options) if result.Error != nil { return result.Error @@ -133,15 +133,22 @@ func (r *Runner) RunJobInContainer(imageUri string, containerId string, jobConte logger.Info("Started %s", containerId) for stepIndex, step := range job.Steps { - stepCwd := jobContext.Value("workflow").(workflow.Workflow).GetWorkingDirectory(job.Name, stepIndex) - stepEnv := jobContext.Value("workflow").(workflow.Workflow).GetEnv(job.Name, stepIndex) + workflow := jobContext.Value("workflow").(workflow.Workflow) + stepCwd := workflow.GetWorkingDirectory(job.Name, stepIndex) + stepEnv := workflow.GetEnv(job.Name, stepIndex) + stepShell := workflow.GetShell(job.Name, stepIndex) logger.Info("Run: %s", step.Run) logger.Info("Using working directory %s", stepCwd) var stepError error if step.Run != "" { - stepError = r.RunCommandInContainer(containerId, step.Run, stepCwd, stepEnv) + commandOptions := driver.CommandOptions{ + Cwd: stepCwd, + Env: stepEnv, + Shell: stepShell, + } + stepError = r.RunCommandInContainer(containerId, step.Run, commandOptions) } if stepError != nil && !step.ContinueOnError { diff --git a/internal/runner/runner_flow_test.go b/internal/runner/runner_flow_test.go index ed04a3c..9918fe2 100644 --- a/internal/runner/runner_flow_test.go +++ b/internal/runner/runner_flow_test.go @@ -22,7 +22,7 @@ func TestRunnerRunCommandInContainerReturnsErrorFromDriver(t *testing.T) { Driver: &mockDriver, } - err := runner.RunCommandInContainer("test-container", "test-command", ".", map[string]string{}) + err := runner.RunCommandInContainer("test-container", "test-command", driver.NewCommandOptions()) if err == nil { t.Errorf("Expected error, got nil.") @@ -37,7 +37,7 @@ func TestRunnerRunCommandInContainerReturnsErrorIfCommandExitCodeNonzero(t *test Driver: &mockDriver, } - err := runner.RunCommandInContainer("test-container", "test-command", ".", map[string]string{}) + err := runner.RunCommandInContainer("test-container", "test-command", driver.NewCommandOptions()) if err == nil { t.Errorf("Expected error, got nil.") diff --git a/internal/workflow/models.go b/internal/workflow/models.go index 41ff373..9e62cc5 100644 --- a/internal/workflow/models.go +++ b/internal/workflow/models.go @@ -17,6 +17,7 @@ type Job struct { Defaults struct { Run struct { WorkingDirectory string `yaml:"working-directory"` + Shell string `yaml:"shell"` } `yaml:"run"` } `yaml:"defaults"` } @@ -44,6 +45,7 @@ type Step struct { WorkingDirectory string `yaml:"working-directory"` ContinueOnError bool `yaml:"continue-on-error"` Env map[string]string `yaml:"env"` + Shell string `yaml:"shell"` } func (s Step) Validate() []error { @@ -99,6 +101,26 @@ func (w Workflow) GetEnv(jobName string, stepIndex int) map[string]string { return finalEnv } +// Returns the shell defined for the given job's step. +// Overriding values are considered in the order: step, job, global. +// +// If the shell is undefined in all cases, "bash" is used. +func (w Workflow) GetShell(jobName string, stepIndex int) string { + var shell string + + if stepShell := w.Jobs[jobName].Steps[stepIndex].Shell; stepShell != "" { + shell = stepShell + } else if jobShell := w.Jobs[jobName].Defaults.Run.Shell; jobShell != "" { + shell = jobShell + } + + if shell == "" { + return "bash" + } + + return shell +} + func (w Workflow) Validate() []error { validationErrors := []error{} diff --git a/internal/workflow/models_test.go b/internal/workflow/models_test.go index 8326d89..3ff9606 100644 --- a/internal/workflow/models_test.go +++ b/internal/workflow/models_test.go @@ -206,3 +206,60 @@ func TestStepEnvOverwritesJobEnv(t *testing.T) { t.Errorf("Unexpected env: %#v", env) } } + +func TestStepShellOverridesAllOtherShellValues(t *testing.T) { + type TestCase struct { + sample string + name string + } + testCases := []TestCase{ + {sample: ` + jobs: + jobA: + defaults: + run: + shell: "job" + runs-on: default + steps: + - run: echo "test" + shell: "step" + `, name: "With job default"}, { + sample: ` + jobs: + jobA: + runs-on: default + steps: + - run: echo "test" + shell: "step" + `, name: "Without job default"}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + workflow, _ := FromYamlBytes([]byte(testCase.sample)) + + shell := workflow.GetShell("jobA", 0) + + if shell != "step" { + t.Errorf("Unexpected shell: %#v", shell) + } + }) + } +} + +func TestStepShellDefaultsToBash(t *testing.T) { + sample := ` + jobs: + jobA: + runs-on: default + steps: + - run: echo "test" +` + workflow, _ := FromYamlBytes([]byte(sample)) + + shell := workflow.GetShell("jobA", 0) + + if shell != "bash" { + t.Errorf("Unexpected shell: %#v", shell) + } +}