From 5893dae60f98946a678151103ae9610af207e9c3 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Fri, 26 Jun 2026 22:18:09 -0300 Subject: [PATCH] Add support for run.shell task Implements the RunTask.shell process defined by the Serverless Workflow DSL. Adds a shell runner that evaluates the command, environment and arguments (as runtime expressions), executes it via `sh -c`, and returns output according to the `return` policy (stdout, stderr, code, all, none). - Add `return` to RunTaskConfiguration with oneof validation - Wire RunTask into the task runner dispatch - Support both ordered array and map forms for shell arguments via the RunArguments model (array form preserves order) - Set the command environment via cmd.Env instead of mutating the parent process global environment Closes #246 Signed-off-by: Matheus Cruz --- impl/task_runner_do.go | 2 + impl/task_runner_run.go | 69 ++++++ impl/task_runner_run_shell.go | 178 +++++++++++++++ impl/task_runner_run_shell_test.go | 202 ++++++++++++++++++ impl/testdata/run_shell_echo.yaml | 25 +++ .../run_shell_echo_env_no_awaiting.yaml | 27 +++ impl/testdata/run_shell_echo_jq.yaml | 25 +++ impl/testdata/run_shell_echo_none.yaml | 25 +++ .../testdata/run_shell_echo_not_awaiting.yaml | 27 +++ impl/testdata/run_shell_echo_with_args.yaml | 28 +++ .../run_shell_echo_with_args_only_key.yaml | 31 +++ impl/testdata/run_shell_echo_with_env.yaml | 28 +++ impl/testdata/run_shell_exitcode.yaml | 25 +++ impl/testdata/run_shell_ls_stderr.yaml | 25 +++ impl/testdata/run_shell_missing_command.yaml | 27 +++ impl/testdata/run_shell_touch_cat.yaml | 25 +++ .../run_shell_with_args_key_value_jq.yaml | 28 +++ model/task_run.go | 5 + 18 files changed, 802 insertions(+) create mode 100644 impl/task_runner_run.go create mode 100644 impl/task_runner_run_shell.go create mode 100644 impl/task_runner_run_shell_test.go create mode 100644 impl/testdata/run_shell_echo.yaml create mode 100644 impl/testdata/run_shell_echo_env_no_awaiting.yaml create mode 100644 impl/testdata/run_shell_echo_jq.yaml create mode 100644 impl/testdata/run_shell_echo_none.yaml create mode 100644 impl/testdata/run_shell_echo_not_awaiting.yaml create mode 100644 impl/testdata/run_shell_echo_with_args.yaml create mode 100644 impl/testdata/run_shell_echo_with_args_only_key.yaml create mode 100644 impl/testdata/run_shell_echo_with_env.yaml create mode 100644 impl/testdata/run_shell_exitcode.yaml create mode 100644 impl/testdata/run_shell_ls_stderr.yaml create mode 100644 impl/testdata/run_shell_missing_command.yaml create mode 100644 impl/testdata/run_shell_touch_cat.yaml create mode 100644 impl/testdata/run_shell_with_args_key_value_jq.yaml diff --git a/impl/task_runner_do.go b/impl/task_runner_do.go index 23d7645..83a9670 100644 --- a/impl/task_runner_do.go +++ b/impl/task_runner_do.go @@ -42,6 +42,8 @@ func NewTaskRunner(taskName string, task model.Task, workflowDef *model.Workflow return NewForkTaskRunner(taskName, t, workflowDef) case *model.WaitTask: return NewWaitTaskRunner(taskName, t) + case *model.RunTask: + return NewRunTaskRunner(taskName, t) default: return nil, fmt.Errorf("unsupported task type '%T' for task '%s'", t, taskName) } diff --git a/impl/task_runner_run.go b/impl/task_runner_run.go new file mode 100644 index 0000000..bcb5c91 --- /dev/null +++ b/impl/task_runner_run.go @@ -0,0 +1,69 @@ +// Copyright 2025 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package impl + +import ( + "fmt" + + "github.com/serverlessworkflow/sdk-go/v3/model" +) + +type RunTaskRunner struct { + Task *model.RunTask + TaskName string +} + +func (d *RunTaskRunner) GetTaskName() string { + return d.TaskName +} + +func NewRunTaskRunner(taskName string, task *model.RunTask) (*RunTaskRunner, error) { + + if task == nil { + return nil, model.NewErrValidation(fmt.Errorf("no set configuration provided for RunTask %s", taskName), taskName) + } + + return &RunTaskRunner{ + Task: task, + TaskName: taskName, + }, nil +} + +func (d *RunTaskRunner) Run(input interface{}, taskSupport TaskSupport) (output interface{}, err error) { + + if d.Task.Run.Shell != nil { + shellTask := NewRunTaskShell() + return shellTask.RunTask(d, input, taskSupport) + } + + return nil, fmt.Errorf("no set configuration provided for RunTask %s", d.TaskName) + +} + +// ProcessResult Describes the result of a process. +type ProcessResult struct { + Stdout string + Stderr string + Code int +} + +// NewProcessResult creates a new ProcessResult instance. +func NewProcessResult(stdout, stderr string, code int) *ProcessResult { + return &ProcessResult{ + Stdout: stdout, + Stderr: stderr, + Code: code, + } +} diff --git a/impl/task_runner_run_shell.go b/impl/task_runner_run_shell.go new file mode 100644 index 0000000..d2f5e0f --- /dev/null +++ b/impl/task_runner_run_shell.go @@ -0,0 +1,178 @@ +// Copyright 2025 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package impl + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/serverlessworkflow/sdk-go/v3/impl/expr" + "github.com/serverlessworkflow/sdk-go/v3/model" +) + +// RunTaskShell defines the shell configuration for RunTask. +// It implements the RunTask.shell definition. +type RunTaskShell struct { +} + +// NewRunTaskShell creates a new RunTaskShell instance. +func NewRunTaskShell() *RunTaskShell { + return &RunTaskShell{} +} + +func (shellTask *RunTaskShell) RunTask(r *RunTaskRunner, input interface{}, taskSupport TaskSupport) (interface{}, error) { + await := r.Task.Run.Await + shell := r.Task.Run.Shell + + if shell == nil { + return nil, model.NewErrValidation(fmt.Errorf("no shell configuration provided for RunTask %s", r.TaskName), r.TaskName) + } + + if shell.Command == "" { + return nil, model.NewErrValidation(fmt.Errorf("no command provided for RunTask shell: %s", r.TaskName), r.TaskName) + } + + // Build the environment for the command without mutating the parent process' + // global environment. + env := os.Environ() + for key, value := range shell.Environment { + evaluated, evalErr := expr.TraverseAndEvaluate(value, input, taskSupport.GetContext()) + if evalErr != nil { + return nil, model.NewErrRuntime(fmt.Errorf("error evaluating environment variable value for RunTask shell: %s", r.TaskName), r.TaskName) + } + env = append(env, fmt.Sprintf("%s=%s", key, fmt.Sprint(evaluated))) + } + + evaluated, err := expr.TraverseAndEvaluate(shell.Command, input, taskSupport.GetContext()) + if err != nil { + return nil, model.NewErrRuntime(fmt.Errorf("error evaluating command for RunTask shell: %s", r.TaskName), r.TaskName) + } + cmdEvaluated := fmt.Sprint(evaluated) + + args, err := shellTask.buildArguments(r, shell.Arguments, input, taskSupport) + if err != nil { + return nil, err + } + + var fullCmd strings.Builder + fullCmd.WriteString(cmdEvaluated) + for _, arg := range args { + fullCmd.WriteString(" ") + fullCmd.WriteString(arg) + } + + newCmd := func() *exec.Cmd { + cmd := exec.Command("sh", "-c", fullCmd.String()) + cmd.Env = env + return cmd + } + + // When the task is explicitly not awaited, fire and forget. + if await != nil && !*await { + go func() { + cmd := newCmd() + _ = cmd.Start() + _ = cmd.Wait() + }() + return input, nil + } + + cmd := newCmd() + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err = cmd.Run() + exitCode := 0 + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + exitCode = exitErr.ExitCode() + } + } else if cmd.ProcessState != nil { + exitCode = cmd.ProcessState.ExitCode() + } + + stdoutStr := strings.TrimSpace(stdout.String()) + stderrStr := strings.TrimSpace(stderr.String()) + + switch r.Task.Run.Return { + case "all": + return NewProcessResult(stdoutStr, stderrStr, exitCode), nil + case "stderr": + return stderrStr, nil + case "code": + return exitCode, nil + case "none": + return nil, nil + default: + return stdoutStr, nil + } +} + +// buildArguments resolves the shell arguments into an ordered list of strings. +// Arguments may be provided either as an ordered array (preferred when order +// matters) or as a key/value map (order is not guaranteed by Go map iteration). +func (shellTask *RunTaskShell) buildArguments(r *RunTaskRunner, arguments *model.RunArguments, input interface{}, taskSupport TaskSupport) ([]string, error) { + if arguments == nil { + return nil, nil + } + + eval := func(value interface{}) (string, error) { + evaluated, evalErr := expr.TraverseAndEvaluate(value, input, taskSupport.GetContext()) + if evalErr != nil { + return "", model.NewErrRuntime(fmt.Errorf("error evaluating argument for RunTask shell: %s", r.TaskName), r.TaskName) + } + return fmt.Sprint(evaluated), nil + } + + if slice := arguments.AsSlice(); slice != nil { + args := make([]string, 0, len(slice)) + for _, value := range slice { + arg, err := eval(value) + if err != nil { + return nil, err + } + args = append(args, arg) + } + return args, nil + } + + if m := arguments.AsMap(); m != nil { + args := make([]string, 0, len(m)) + for key, value := range m { + keyStr, err := eval(key) + if err != nil { + return nil, err + } + if value != nil { + valueStr, err := eval(value) + if err != nil { + return nil, err + } + args = append(args, fmt.Sprintf("%s=%s", keyStr, valueStr)) + } else { + args = append(args, keyStr) + } + } + return args, nil + } + + return nil, nil +} diff --git a/impl/task_runner_run_shell_test.go b/impl/task_runner_run_shell_test.go new file mode 100644 index 0000000..f97cc60 --- /dev/null +++ b/impl/task_runner_run_shell_test.go @@ -0,0 +1,202 @@ +// Copyright 2025 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package impl + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/serverlessworkflow/sdk-go/v3/parser" + "github.com/stretchr/testify/assert" +) + +func TestRunShellWithTestData(t *testing.T) { + + t.Run("Simple with echo", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo.yaml" + + input := map[string]interface{}{} + output, err := runWorkflow(t, workflowPath, input, nil) + + processResult := output.(*ProcessResult) + + assert.NotNilf(t, output, "output should not be nil") + assert.Equal(t, "Hello, anonymous", processResult.Stdout) + assert.Equal(t, "", processResult.Stderr) + assert.Equal(t, 0, processResult.Code) + assert.NoError(t, err) + }) + + t.Run("Simple echo looking exit code", func(t *testing.T) { + workflowPath := "./testdata/run_shell_exitcode.yaml" + input := map[string]interface{}{} + output, err := runWorkflow(t, workflowPath, input, nil) + assert.NoError(t, err) + // `ls` of a nonexistent directory exits non-zero; the exact code is + // platform-dependent (2 on GNU/Linux, 1 on macOS), so assert non-zero. + assert.NotZero(t, output.(int)) + }) + + t.Run("JQ expression in command with 'all' return", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_jq.yaml" + input := map[string]interface{}{ + "user": map[string]interface{}{ + "name": "Matheus Cruz", + }, + } + output, err := runWorkflow(t, workflowPath, input, nil) + + processResult := output.(*ProcessResult) + assert.NoError(t, err) + assert.Equal(t, "", processResult.Stderr) + assert.Equal(t, "Hello, Matheus Cruz", processResult.Stdout) + assert.Equal(t, 0, processResult.Code) + }) + + t.Run("Simple echo with 'none' return", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_none.yaml" + input := map[string]interface{}{} + output, err := runWorkflow(t, workflowPath, input, nil) + + assert.NoError(t, err) + assert.Nil(t, output) + }) + + t.Run("Simple echo with env and await as 'false'", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_env_no_awaiting.yaml" + input := map[string]interface{}{ + "full_name": "John Doe", + } + output, err := runWorkflow(t, workflowPath, input, nil) + + assert.NoError(t, err) + assert.Equal(t, output, input) + }) + + t.Run("Simple echo not awaiting, function should returns immediately", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_not_awaiting.yaml" + input := map[string]interface{}{ + "full_name": "John Doe", + } + output, err := runWorkflow(t, workflowPath, input, nil) + + assert.NoError(t, err) + assert.Equal(t, output, input) + }) + + t.Run("Simple 'ls' command getting output as stderr", func(t *testing.T) { + workflowPath := "./testdata/run_shell_ls_stderr.yaml" + input := map[string]interface{}{} + + output, err := runWorkflow(t, workflowPath, input, nil) + + assert.NoError(t, err) + assert.True(t, strings.Contains(output.(string), "ls:")) + }) + + t.Run("Simple echo with args using JQ expression", func(t *testing.T) { + workflowPath := "./testdata/run_shell_with_args_key_value_jq.yaml" + input := map[string]interface{}{ + "user": "Alice", + "passwordKey": "--password", + } + + output, err := runWorkflow(t, workflowPath, input, nil) + + processResult := output.(*ProcessResult) + + // Arguments are provided as a map, so iteration order is not guaranteed; + // assert each evaluated argument independently. + assert.NoError(t, err) + assert.True(t, strings.Contains(processResult.Stdout, "--user=Alice")) + assert.True(t, strings.Contains(processResult.Stdout, "--password=serverless")) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Simple echo with args", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_with_args.yaml" + input := map[string]interface{}{} + + output, err := runWorkflow(t, workflowPath, input, nil) + + processResult := output.(*ProcessResult) + + // Arguments are provided as an ordered array, so the order is preserved. + assert.NoError(t, err) + assert.Equal(t, "--user=john --password=doe", processResult.Stdout) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Simple echo with args using only key", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_with_args_only_key.yaml" + input := map[string]interface{}{ + "firstName": "Mary", + "lastName": "Jane", + } + + output, err := runWorkflow(t, workflowPath, input, nil) + + processResult := output.(*ProcessResult) + + // Arguments are provided as an ordered array, so the order is preserved. + assert.NoError(t, err) + assert.Equal(t, "Hello Mary Jane from args!", processResult.Stdout) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Simple echo with env and JQ", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_with_env.yaml" + input := map[string]interface{}{ + "lastName": "Doe", + } + + output, err := runWorkflow(t, workflowPath, input, nil) + + processResult := output.(*ProcessResult) + + assert.NoError(t, err) + assert.True(t, strings.Contains(processResult.Stdout, "Hello John Doe from env!")) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Execute touch and cat command", func(t *testing.T) { + workflowPath := "./testdata/run_shell_touch_cat.yaml" + input := map[string]interface{}{} + + output, err := runWorkflow(t, workflowPath, input, nil) + + processResult := output.(*ProcessResult) + + assert.NoError(t, err) + assert.Equal(t, "hello world", strings.TrimSpace(processResult.Stdout)) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Missing command fails validation", func(t *testing.T) { + workflowPath := "./testdata/run_shell_missing_command.yaml" + yamlBytes, err := os.ReadFile(filepath.Clean(workflowPath)) + assert.NoError(t, err) + + _, err = parser.FromYAMLSource(yamlBytes) + assert.Error(t, err) + }) +} diff --git a/impl/testdata/run_shell_echo.yaml b/impl/testdata/run_shell_echo.yaml new file mode 100644 index 0000000..b2c70cb --- /dev/null +++ b/impl/testdata/run_shell_echo.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: 'echo "Hello, anonymous"' + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_env_no_awaiting.yaml b/impl/testdata/run_shell_echo_env_no_awaiting.yaml new file mode 100644 index 0000000..757d701 --- /dev/null +++ b/impl/testdata/run_shell_echo_env_no_awaiting.yaml @@ -0,0 +1,27 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: echo "hello world not awaiting ($FULL_NAME)" > /tmp/hello-world.txt && cat /tmp/hello-world.txt + environment: + FULL_NAME: ${.full_name} + await: false \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_jq.yaml b/impl/testdata/run_shell_echo_jq.yaml new file mode 100644 index 0000000..706e713 --- /dev/null +++ b/impl/testdata/run_shell_echo_jq.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: ${ "echo Hello, \(.user.name)" } + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_none.yaml b/impl/testdata/run_shell_echo_none.yaml new file mode 100644 index 0000000..9cf72d3 --- /dev/null +++ b/impl/testdata/run_shell_echo_none.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: 'echo "Serverless Workflow"' + return: none \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_not_awaiting.yaml b/impl/testdata/run_shell_echo_not_awaiting.yaml new file mode 100644 index 0000000..721446f --- /dev/null +++ b/impl/testdata/run_shell_echo_not_awaiting.yaml @@ -0,0 +1,27 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: echo "hello world not awaiting ($FULL_NAME)" > /tmp/hello.txt && cat /tmp/hello.txt + environment: + FULL_NAME: ${.full_name} + await: false \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_with_args.yaml b/impl/testdata/run_shell_echo_with_args.yaml new file mode 100644 index 0000000..aa4739b --- /dev/null +++ b/impl/testdata/run_shell_echo_with_args.yaml @@ -0,0 +1,28 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + arguments: + - '--user=john' + - '--password=doe' + command: echo + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_with_args_only_key.yaml b/impl/testdata/run_shell_echo_with_args_only_key.yaml new file mode 100644 index 0000000..90b03a5 --- /dev/null +++ b/impl/testdata/run_shell_echo_with_args_only_key.yaml @@ -0,0 +1,31 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + arguments: + - 'Hello' + - '${.firstName}' + - '${.lastName}' + - from + - 'args!' + command: echo + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_with_env.yaml b/impl/testdata/run_shell_echo_with_env.yaml new file mode 100644 index 0000000..5ad6516 --- /dev/null +++ b/impl/testdata/run_shell_echo_with_env.yaml @@ -0,0 +1,28 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: echo "Hello $FIRST_NAME $LAST_NAME from env!" + environment: + FIRST_NAME: John + LAST_NAME: ${.lastName} + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_exitcode.yaml b/impl/testdata/run_shell_exitcode.yaml new file mode 100644 index 0000000..1c2fd54 --- /dev/null +++ b/impl/testdata/run_shell_exitcode.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: 'ls /nonexistent_directory' + return: code \ No newline at end of file diff --git a/impl/testdata/run_shell_ls_stderr.yaml b/impl/testdata/run_shell_ls_stderr.yaml new file mode 100644 index 0000000..a776ebc --- /dev/null +++ b/impl/testdata/run_shell_ls_stderr.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: 'ls /nonexistent_directory' + return: stderr \ No newline at end of file diff --git a/impl/testdata/run_shell_missing_command.yaml b/impl/testdata/run_shell_missing_command.yaml new file mode 100644 index 0000000..92f0b81 --- /dev/null +++ b/impl/testdata/run_shell_missing_command.yaml @@ -0,0 +1,27 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - missingShellCommand: + run: + shell: + command: '' + environment: + FIRST_NAME: John + LAST_NAME: ${.lastName} \ No newline at end of file diff --git a/impl/testdata/run_shell_touch_cat.yaml b/impl/testdata/run_shell_touch_cat.yaml new file mode 100644 index 0000000..65796f9 --- /dev/null +++ b/impl/testdata/run_shell_touch_cat.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: echo "hello world" > /tmp/hello.txt && cat /tmp/hello.txt + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_with_args_key_value_jq.yaml b/impl/testdata/run_shell_with_args_key_value_jq.yaml new file mode 100644 index 0000000..938bc58 --- /dev/null +++ b/impl/testdata/run_shell_with_args_key_value_jq.yaml @@ -0,0 +1,28 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + arguments: + '--user': '${.user}' + '${.passwordKey}': 'serverless' + command: echo + return: all \ No newline at end of file diff --git a/model/task_run.go b/model/task_run.go index bbdc00d..9f9a700 100644 --- a/model/task_run.go +++ b/model/task_run.go @@ -35,6 +35,7 @@ type RunTaskConfiguration struct { Script *Script `json:"script,omitempty"` Shell *Shell `json:"shell,omitempty"` Workflow *RunWorkflow `json:"workflow,omitempty"` + Return string `json:"return,omitempty" validate:"omitempty,oneof=stdout stderr code all none"` } type Container struct { @@ -85,6 +86,7 @@ func (rtc *RunTaskConfiguration) UnmarshalJSON(data []byte) error { Script *Script `json:"script"` Shell *Shell `json:"shell"` Workflow *RunWorkflow `json:"workflow"` + Return string `json:"return"` }{} if err := json.Unmarshal(data, &temp); err != nil { @@ -116,6 +118,7 @@ func (rtc *RunTaskConfiguration) UnmarshalJSON(data []byte) error { } rtc.Await = temp.Await + rtc.Return = temp.Return return nil } @@ -127,12 +130,14 @@ func (rtc *RunTaskConfiguration) MarshalJSON() ([]byte, error) { Script *Script `json:"script,omitempty"` Shell *Shell `json:"shell,omitempty"` Workflow *RunWorkflow `json:"workflow,omitempty"` + Return string `json:"return,omitempty"` }{ Await: rtc.Await, Container: rtc.Container, Script: rtc.Script, Shell: rtc.Shell, Workflow: rtc.Workflow, + Return: rtc.Return, } return json.Marshal(temp)