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[*].shell
|
||||||
- [ ] jobs.<job_id>.steps[*].with
|
- [ ] jobs.<job_id>.steps[*].with
|
||||||
- [ ] jobs.<job_id>.steps[*].env
|
- [ ] 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
|
- [ ] jobs.<job_id>.steps[*].timeout-minutes
|
||||||
|
|
||||||
## Behaviours
|
## Behaviours
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
Info log.Logger
|
Info log.Logger
|
||||||
Error log.Logger
|
Error log.Logger
|
||||||
|
Warn log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
var Log Logger
|
var Log Logger
|
||||||
|
@ -23,6 +24,7 @@ var Log Logger
|
||||||
func ConfigureLogger() {
|
func ConfigureLogger() {
|
||||||
Log = Logger{
|
Log = Logger{
|
||||||
Info: *log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime),
|
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),
|
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...)
|
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
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type MockCall struct {
|
type MockCall struct {
|
||||||
fname string
|
fname string
|
||||||
args []string
|
args []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockDriver struct {
|
type MockDriver struct {
|
||||||
calls map[string][]MockCall
|
calls map[string][]MockCall
|
||||||
mockResult *CommandResult
|
mockedCalls map[string]map[string]CommandResult
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMockDriver() MockDriver {
|
func NewMockDriver() MockDriver {
|
||||||
return 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 {
|
func (d *MockDriver) Pull(uri string) error {
|
||||||
if _, init := d.calls["Pull"]; !init {
|
if _, init := d.calls["Pull"]; !init {
|
||||||
d.calls["Pull"] = []MockCall{}
|
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}})
|
d.calls["Exec"] = append(d.calls["Exec"], MockCall{fname: "Exec", args: []string{containerName, command, cwd}})
|
||||||
|
|
||||||
if d.mockResult != nil {
|
if _, mocked := d.mockedCalls["Exec"][strings.Join([]string{containerName, command, cwd}, " ")]; mocked {
|
||||||
return *d.mockResult
|
return d.mockedCalls["Exec"][strings.Join([]string{containerName, command, cwd}, " ")]
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandResult{}
|
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)
|
stepError = r.RunCommandInContainer(containerId, step.Run, stepCwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if stepError != nil {
|
if stepError != nil && !step.ContinueOnError {
|
||||||
return stepError
|
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) {
|
func TestRunnerRunCommandInContainerReturnsErrorFromDriver(t *testing.T) {
|
||||||
mockDriver := NewMockDriver()
|
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{
|
runner := Runner{
|
||||||
Driver: &mockDriver,
|
Driver: &mockDriver,
|
||||||
|
@ -29,7 +29,7 @@ func TestRunnerRunCommandInContainerReturnsErrorFromDriver(t *testing.T) {
|
||||||
|
|
||||||
func TestRunnerRunCommandInContainerReturnsErrorIfCommandExitCodeNonzero(t *testing.T) {
|
func TestRunnerRunCommandInContainerReturnsErrorIfCommandExitCodeNonzero(t *testing.T) {
|
||||||
mockDriver := NewMockDriver()
|
mockDriver := NewMockDriver()
|
||||||
mockDriver.WithCommandResult(&CommandResult{ExitCode: 1, Error: nil})
|
mockDriver.WithMockedCall("Exec", CommandResult{ExitCode: 1, Error: nil}, "test-container", "test-command", ".")
|
||||||
|
|
||||||
runner := Runner{
|
runner := Runner{
|
||||||
Driver: &mockDriver,
|
Driver: &mockDriver,
|
||||||
|
@ -44,7 +44,7 @@ func TestRunnerRunCommandInContainerReturnsErrorIfCommandExitCodeNonzero(t *test
|
||||||
|
|
||||||
func TestRunJobInContainerSchedulesStoppingContainers(t *testing.T) {
|
func TestRunJobInContainerSchedulesStoppingContainers(t *testing.T) {
|
||||||
mockDriver := NewMockDriver()
|
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{})
|
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 {
|
type Step struct {
|
||||||
Run string `yaml:"run"`
|
Run string `yaml:"run"`
|
||||||
WorkingDirectory string `yaml:"working-directory"`
|
WorkingDirectory string `yaml:"working-directory"`
|
||||||
|
ContinueOnError bool `yaml:"continue-on-error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Step) Validate() []error {
|
func (s Step) Validate() []error {
|
||||||
|
|
Loading…
Reference in a new issue