feat: add support for job, step shell override

This commit is contained in:
Marc 2024-08-22 23:38:32 -04:00
parent 27715ecebb
commit 1aa62019cb
Signed by: marc
GPG key ID: 048E042F22B5DC79
7 changed files with 101 additions and 12 deletions

View file

@ -22,7 +22,7 @@ syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for
- [ ] jobs.<job_id>.outputs - [ ] jobs.<job_id>.outputs
- [x] jobs.<job_id>.env - [x] jobs.<job_id>.env
- [x] jobs.<job_id>.defaults - [x] jobs.<job_id>.defaults
- [ ] jobs.<job_id>.run.shell - [x] jobs.<job_id>.run.shell
- [x] jobs.<job_id>.run.working-directory - [x] jobs.<job_id>.run.working-directory
- [ ] jobs.<job_id>.timeout-minutes - [ ] jobs.<job_id>.timeout-minutes
- [ ] jobs.<job_id>.strategy - [ ] jobs.<job_id>.strategy
@ -40,7 +40,7 @@ syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for
- [ ] jobs.<job_id>.steps[*].uses - [ ] jobs.<job_id>.steps[*].uses
- [x] jobs.<job_id>.steps[*].run - [x] jobs.<job_id>.steps[*].run
- [x] jobs.<job_id>.steps[*].working-directory - [x] jobs.<job_id>.steps[*].working-directory
- [ ] jobs.<job_id>.steps[*].shell - [x] jobs.<job_id>.steps[*].shell
- [ ] jobs.<job_id>.steps[*].with - [ ] jobs.<job_id>.steps[*].with
- [x] jobs.<job_id>.steps[*].env - [x] jobs.<job_id>.steps[*].env
- [X] jobs.<job_id>.steps[*].continue-on-error - [X] jobs.<job_id>.steps[*].continue-on-error

View file

@ -26,12 +26,15 @@ type CommandOptions struct {
// Note: In the case of container drivers, these mappings are applied within the // Note: In the case of container drivers, these mappings are applied within the
// container. // container.
Env map[string]string Env map[string]string
// Shell to use when running the command.
Shell string
} }
func NewCommandOptions() CommandOptions { func NewCommandOptions() CommandOptions {
return CommandOptions{ return CommandOptions{
Cwd: ".", Cwd: ".",
Env: map[string]string{}, Shell: "bash",
Env: map[string]string{},
} }
} }

View file

@ -95,7 +95,7 @@ func (d PodmanDriver) Exec(containerId string, command string, options CommandOp
commandArgs = append(commandArgs, envArgs...) commandArgs = append(commandArgs, envArgs...)
commandArgs = append(commandArgs, containerId, commandArgs = append(commandArgs, containerId,
"bash", options.Shell,
"-c", "-c",
command, command,
) )

View file

@ -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 // If the command raises an error while in the container or fails to run
// the command at all, an error is returned, otherwise nil. // 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 { func (r *Runner) RunCommandInContainer(containerId string, command string, options driver.CommandOptions) error {
result := r.Driver.Exec(containerId, command, driver.CommandOptions{Cwd: stepCwd, Env: stepEnv}) result := r.Driver.Exec(containerId, command, options)
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
@ -133,15 +133,22 @@ func (r *Runner) RunJobInContainer(imageUri string, containerId string, jobConte
logger.Info("Started %s", containerId) logger.Info("Started %s", containerId)
for stepIndex, step := range job.Steps { for stepIndex, step := range job.Steps {
stepCwd := jobContext.Value("workflow").(workflow.Workflow).GetWorkingDirectory(job.Name, stepIndex) workflow := jobContext.Value("workflow").(workflow.Workflow)
stepEnv := jobContext.Value("workflow").(workflow.Workflow).GetEnv(job.Name, stepIndex) 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("Run: %s", step.Run)
logger.Info("Using working directory %s", stepCwd) logger.Info("Using working directory %s", stepCwd)
var stepError error var stepError error
if step.Run != "" { 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 { if stepError != nil && !step.ContinueOnError {

View file

@ -22,7 +22,7 @@ func TestRunnerRunCommandInContainerReturnsErrorFromDriver(t *testing.T) {
Driver: &mockDriver, Driver: &mockDriver,
} }
err := runner.RunCommandInContainer("test-container", "test-command", ".", map[string]string{}) err := runner.RunCommandInContainer("test-container", "test-command", driver.NewCommandOptions())
if err == nil { if err == nil {
t.Errorf("Expected error, got nil.") t.Errorf("Expected error, got nil.")
@ -37,7 +37,7 @@ func TestRunnerRunCommandInContainerReturnsErrorIfCommandExitCodeNonzero(t *test
Driver: &mockDriver, Driver: &mockDriver,
} }
err := runner.RunCommandInContainer("test-container", "test-command", ".", map[string]string{}) err := runner.RunCommandInContainer("test-container", "test-command", driver.NewCommandOptions())
if err == nil { if err == nil {
t.Errorf("Expected error, got nil.") t.Errorf("Expected error, got nil.")

View file

@ -17,6 +17,7 @@ type Job struct {
Defaults struct { Defaults struct {
Run struct { Run struct {
WorkingDirectory string `yaml:"working-directory"` WorkingDirectory string `yaml:"working-directory"`
Shell string `yaml:"shell"`
} `yaml:"run"` } `yaml:"run"`
} `yaml:"defaults"` } `yaml:"defaults"`
} }
@ -44,6 +45,7 @@ type Step struct {
WorkingDirectory string `yaml:"working-directory"` WorkingDirectory string `yaml:"working-directory"`
ContinueOnError bool `yaml:"continue-on-error"` ContinueOnError bool `yaml:"continue-on-error"`
Env map[string]string `yaml:"env"` Env map[string]string `yaml:"env"`
Shell string `yaml:"shell"`
} }
func (s Step) Validate() []error { func (s Step) Validate() []error {
@ -99,6 +101,26 @@ func (w Workflow) GetEnv(jobName string, stepIndex int) map[string]string {
return finalEnv 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 { func (w Workflow) Validate() []error {
validationErrors := []error{} validationErrors := []error{}

View file

@ -206,3 +206,60 @@ func TestStepEnvOverwritesJobEnv(t *testing.T) {
t.Errorf("Unexpected env: %#v", env) 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)
}
}