feat: add step continue-in-error support
This commit is contained in:
parent
c876477891
commit
76bd6eece1
7 changed files with 144 additions and 14 deletions
|
@ -43,7 +43,7 @@ syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for
|
|||
- [ ] jobs.<job_id>.steps[*].shell
|
||||
- [ ] jobs.<job_id>.steps[*].with
|
||||
- [ ] jobs.<job_id>.steps[*].env
|
||||
- [ ] jobs.<job_id>.steps[*].continue-on-error
|
||||
- [X] jobs.<job_id>.steps[*].continue-on-error
|
||||
- [ ] jobs.<job_id>.steps[*].timeout-minutes
|
||||
|
||||
## Behaviours
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
type Logger struct {
|
||||
Info log.Logger
|
||||
Error log.Logger
|
||||
Warn log.Logger
|
||||
}
|
||||
|
||||
var Log Logger
|
||||
|
@ -23,6 +24,7 @@ var Log Logger
|
|||
func ConfigureLogger() {
|
||||
Log = Logger{
|
||||
Info: *log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime),
|
||||
Warn: *log.New(os.Stdout, "[WARN] ", log.Ldate|log.Ltime),
|
||||
Error: *log.New(os.Stderr, "[ERROR] ", log.Ldate|log.Ltime),
|
||||
}
|
||||
}
|
||||
|
@ -42,3 +44,11 @@ func Error(message string, args ...any) {
|
|||
Log.Error.Printf(message, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Warn(message string, args ...any) {
|
||||
if len(args) == 0 {
|
||||
Log.Warn.Print(message)
|
||||
} else {
|
||||
Log.Warn.Printf(message, args...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MockCall struct {
|
||||
fname string
|
||||
args []string
|
||||
}
|
||||
|
||||
type MockDriver struct {
|
||||
calls map[string][]MockCall
|
||||
mockResult *CommandResult
|
||||
calls map[string][]MockCall
|
||||
mockedCalls map[string]map[string]CommandResult
|
||||
}
|
||||
|
||||
func NewMockDriver() MockDriver {
|
||||
return MockDriver{
|
||||
calls: map[string][]MockCall{},
|
||||
calls: map[string][]MockCall{},
|
||||
mockedCalls: map[string]map[string]CommandResult{},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MockDriver) WithCommandResult(c *CommandResult) {
|
||||
d.mockResult = c
|
||||
}
|
||||
|
||||
func (d *MockDriver) Pull(uri string) error {
|
||||
if _, init := d.calls["Pull"]; !init {
|
||||
d.calls["Pull"] = []MockCall{}
|
||||
|
@ -53,9 +54,22 @@ func (d *MockDriver) Exec(containerName string, command string, cwd string) Comm
|
|||
}
|
||||
d.calls["Exec"] = append(d.calls["Exec"], MockCall{fname: "Exec", args: []string{containerName, command, cwd}})
|
||||
|
||||
if d.mockResult != nil {
|
||||
return *d.mockResult
|
||||
if _, mocked := d.mockedCalls["Exec"][strings.Join([]string{containerName, command, cwd}, " ")]; mocked {
|
||||
return d.mockedCalls["Exec"][strings.Join([]string{containerName, command, cwd}, " ")]
|
||||
}
|
||||
|
||||
return CommandResult{}
|
||||
}
|
||||
|
||||
// Mocks a call to <fn> with arguments <args> to return <returnValue>.
|
||||
//
|
||||
// The mocked call is reused as long as it's defined within the mock driver.
|
||||
func (d *MockDriver) WithMockedCall(fn string, returnValue CommandResult, args ...string) {
|
||||
mockKey := strings.Join(args, " ")
|
||||
|
||||
if _, initialized := d.mockedCalls[fn]; !initialized {
|
||||
d.mockedCalls[fn] = map[string]CommandResult{}
|
||||
}
|
||||
|
||||
d.mockedCalls[fn][mockKey] = returnValue
|
||||
}
|
||||
|
|
|
@ -130,8 +130,10 @@ func (r *Runner) RunJobInContainer(imageUri string, containerId string, jobConte
|
|||
stepError = r.RunCommandInContainer(containerId, step.Run, stepCwd)
|
||||
}
|
||||
|
||||
if stepError != nil {
|
||||
if stepError != nil && !step.ContinueOnError {
|
||||
return stepError
|
||||
} else if stepError != nil {
|
||||
logger.Warn("Step errored by continued: %s", stepError)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ func init() {
|
|||
|
||||
func TestRunnerRunCommandInContainerReturnsErrorFromDriver(t *testing.T) {
|
||||
mockDriver := NewMockDriver()
|
||||
mockDriver.WithCommandResult(&CommandResult{ExitCode: 0, Error: errors.New("test")})
|
||||
mockDriver.WithMockedCall("Exec", CommandResult{ExitCode: 0, Error: errors.New("test")}, "test-container", "test-command", ".")
|
||||
|
||||
runner := Runner{
|
||||
Driver: &mockDriver,
|
||||
|
@ -29,7 +29,7 @@ func TestRunnerRunCommandInContainerReturnsErrorFromDriver(t *testing.T) {
|
|||
|
||||
func TestRunnerRunCommandInContainerReturnsErrorIfCommandExitCodeNonzero(t *testing.T) {
|
||||
mockDriver := NewMockDriver()
|
||||
mockDriver.WithCommandResult(&CommandResult{ExitCode: 1, Error: nil})
|
||||
mockDriver.WithMockedCall("Exec", CommandResult{ExitCode: 1, Error: nil}, "test-container", "test-command", ".")
|
||||
|
||||
runner := Runner{
|
||||
Driver: &mockDriver,
|
||||
|
@ -44,7 +44,7 @@ func TestRunnerRunCommandInContainerReturnsErrorIfCommandExitCodeNonzero(t *test
|
|||
|
||||
func TestRunJobInContainerSchedulesStoppingContainers(t *testing.T) {
|
||||
mockDriver := NewMockDriver()
|
||||
mockDriver.WithCommandResult(&CommandResult{ExitCode: 1, Error: nil})
|
||||
mockDriver.WithMockedCall("Exec", CommandResult{ExitCode: 1, Error: nil}, "test-container", "test-command", ".")
|
||||
|
||||
runner := NewRunner(&mockDriver, map[string]string{})
|
||||
|
||||
|
|
103
internal/runner/runner_job_test.go
Normal file
103
internal/runner/runner_job_test.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
workflow "courgette/internal/workflow"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJobStepSkipsErrorIfContinueOnErrorTruthy(t *testing.T) {
|
||||
workflowSample := `
|
||||
jobs:
|
||||
jobA:
|
||||
runs-on: test
|
||||
steps:
|
||||
- run: exit 1
|
||||
continue-on-error: true
|
||||
- run: echo "Continued!"
|
||||
`
|
||||
|
||||
workflow, _ := workflow.FromYamlBytes([]byte(workflowSample))
|
||||
mockDriver := NewMockDriver()
|
||||
|
||||
mockDriver.WithMockedCall("Exec", CommandResult{Error: errors.New("exit 1!"), ExitCode: 1}, "testContainer", "exit 1")
|
||||
runner := NewRunner(&mockDriver, map[string]string{"test": "test"})
|
||||
|
||||
jobContext := context.WithValue(context.Background(), "workflow", *workflow)
|
||||
jobContext = context.WithValue(jobContext, "currentJob", workflow.Jobs["jobA"])
|
||||
|
||||
runErr := runner.RunJobInContainer("testUri", "testContainer", jobContext)
|
||||
|
||||
if runErr != nil {
|
||||
t.Errorf("Did not expect error, got %+v", runErr)
|
||||
}
|
||||
|
||||
execCallCount := len(mockDriver.calls["Exec"])
|
||||
if execCallCount != 2 {
|
||||
t.Errorf("Expected 2 calls to Exec, got %d", execCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobStepExitsOnErrorIfContinueOnErrorFalsy(t *testing.T) {
|
||||
workflowSample := `
|
||||
jobs:
|
||||
jobA:
|
||||
runs-on: test
|
||||
steps:
|
||||
- run: exit 1
|
||||
continue-on-error: false
|
||||
- run: echo "Continued!"
|
||||
`
|
||||
|
||||
workflow, _ := workflow.FromYamlBytes([]byte(workflowSample))
|
||||
mockDriver := NewMockDriver()
|
||||
|
||||
mockDriver.WithMockedCall("Exec", CommandResult{Error: errors.New("exit 1!"), ExitCode: 1}, "testContainer", "exit 1", ".")
|
||||
runner := NewRunner(&mockDriver, map[string]string{"test": "test"})
|
||||
|
||||
jobContext := context.WithValue(context.Background(), "workflow", *workflow)
|
||||
jobContext = context.WithValue(jobContext, "currentJob", workflow.Jobs["jobA"])
|
||||
|
||||
runErr := runner.RunJobInContainer("testUri", "testContainer", jobContext)
|
||||
|
||||
if runErr == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
|
||||
execCallCount := len(mockDriver.calls["Exec"])
|
||||
if execCallCount != 1 {
|
||||
t.Errorf("Expected 1 calls to Exec, got %d", execCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobStepExitsOnErrorIfContinueOnErrorUndefined(t *testing.T) {
|
||||
workflowSample := `
|
||||
jobs:
|
||||
jobA:
|
||||
runs-on: test
|
||||
steps:
|
||||
- run: exit 1
|
||||
- run: echo "Continued!"
|
||||
`
|
||||
|
||||
workflow, _ := workflow.FromYamlBytes([]byte(workflowSample))
|
||||
mockDriver := NewMockDriver()
|
||||
|
||||
mockDriver.WithMockedCall("Exec", CommandResult{Error: errors.New("exit 1!"), ExitCode: 1}, "testContainer", "exit 1", ".")
|
||||
runner := NewRunner(&mockDriver, map[string]string{"test": "test"})
|
||||
|
||||
jobContext := context.WithValue(context.Background(), "workflow", *workflow)
|
||||
jobContext = context.WithValue(jobContext, "currentJob", workflow.Jobs["jobA"])
|
||||
|
||||
runErr := runner.RunJobInContainer("testUri", "testContainer", jobContext)
|
||||
|
||||
if runErr == nil {
|
||||
t.Error("Expect error, got nil")
|
||||
}
|
||||
|
||||
execCallCount := len(mockDriver.calls["Exec"])
|
||||
if execCallCount != 1 {
|
||||
t.Errorf("Expected 1 calls to Exec, got %d", execCallCount)
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ func (j Job) Validate() []error {
|
|||
type Step struct {
|
||||
Run string `yaml:"run"`
|
||||
WorkingDirectory string `yaml:"working-directory"`
|
||||
ContinueOnError bool `yaml:"continue-on-error"`
|
||||
}
|
||||
|
||||
func (s Step) Validate() []error {
|
||||
|
|
Loading…
Reference in a new issue