diff --git a/runatlantis.io/docs/post-workflow-hooks.md b/runatlantis.io/docs/post-workflow-hooks.md index ac4341b71e..f980ac112a 100644 --- a/runatlantis.io/docs/post-workflow-hooks.md +++ b/runatlantis.io/docs/post-workflow-hooks.md @@ -6,14 +6,12 @@ workflows](custom-workflows.html#custom-run-command) in that they are run outside of Atlantis commands. Which means they do not surface their output back to the PR as a comment. -Post workflow hooks also only allow `run` and `description` commands. - [[toc]] ## Usage Post workflow hooks can only be specified in the Server-Side Repo Config under -`repos` key. +the `repos` key. ## Use Cases @@ -45,6 +43,25 @@ repos: # ... ``` +## Customizing the Shell + +By default, the commands will be run using the 'sh' shell with an argument of '-c'. This +can be customized using the `shell` and `shellArgs` keys. + +Example: + +```yaml +repos: + - id: /.*/ + post_workflow_hooks: + - run: | + echo 'atlantis.yaml config:' + cat atlantis.yaml + description: atlantis.yaml report + shell: bash + shellArgs: -cv +``` + ## Reference ### Custom `run` Command @@ -60,6 +77,8 @@ command](custom-workflows.html#custom-run-command). | ----------- | ------ | ------- | -------- | --------------------- | | run | string | none | no | Run a custom command | | description | string | none | no | Post hook description | +| shell | string | 'sh' | no | The shell to use for running the command | +| shellArgs | string | '-c' | no | The shell arguments to use for running the command | ::: tip Notes * `run` commands are executed with the following environment variables: diff --git a/runatlantis.io/docs/pre-workflow-hooks.md b/runatlantis.io/docs/pre-workflow-hooks.md index 1dde7c3a8e..b4eb852bf3 100644 --- a/runatlantis.io/docs/pre-workflow-hooks.md +++ b/runatlantis.io/docs/pre-workflow-hooks.md @@ -4,27 +4,29 @@ Pre workflow hooks can be defined to run scripts before default or custom workflows are executed. Pre workflow hooks differ from [custom workflows](custom-workflows.html#custom-run-command) in several ways. -1. Pre workflow hooks do not require for repository configuration to be +1. Pre workflow hooks do not require the repository configuration to be present. This can be utilized to [dynamically generate repo configs](pre-workflow-hooks.html#dynamic-repo-config-generation). 2. Pre workflow hooks are run outside of Atlantis commands. Which means they do not surface their output back to the PR as a comment. -3. Pre workflow hooks only allow `run` and `description` commands. [[toc]] ## Usage -Pre workflow hooks can only be specified in the Server-Side Repo Config under -`repos` key. + +Pre workflow hooks can only be specified in the Server-Side Repo Config under the +`repos` key. ::: tip Note `pre-workflow-hooks` do not prevent Atlantis from executing its workflows(`plan`, `apply`) even if a `run` command exits with an error. ::: ## Use Cases + ### Dynamic Repo Config Generation -If you want generate your `atlantis.yaml` before Atlantis can parse it. You -can add a `run` command to `pre_workflow_hooks`. Your Repo config will be generated -right before Atlantis can parse it. + +To generate the repo `atlantis.yaml` before Atlantis can parse it, +add a `run` command to `pre_workflow_hooks`. Your Repo config will be generated +right before Atlantis parses it. ```yaml repos: @@ -33,17 +35,43 @@ repos: - run: ./repo-config-generator.sh description: Generating configs ``` + +## Customizing the Shell + +By default, the command will be run using the 'sh' shell with an argument of '-c'. This +can be customized using the `shell` and `shellArgs` keys. + +Example: + +```yaml +repos: + - id: /.*/ + pre_workflow_hooks: + - run: | + echo "generating atlantis.yaml" + terragrunt-atlantis-config generate --output atlantis.yaml --autoplan --parallel + description: Generating atlantis.yaml + shell: bash + shellArgs: -cv +``` + ## Reference + ### Custom `run` Command -This is very similar to [custom workflow run -command](custom-workflows.html#custom-run-command). + +This is very similar to the [custom workflow run +command](custom-workflows.html#custom-run-command). + ```yaml - run: custom-command ``` + | Key | Type | Default | Required | Description | | ----------- | ------ | ------- | -------- | -------------------- | | run | string | none | no | Run a custom command | | description | string | none | no | Pre hook description | +| shell | string | 'sh' | no | The shell to use for running the command | +| shellArgs | string | '-c' | no | The shell arguments to use for running the command | ::: tip Notes * `run` commands are executed with the following environment variables: @@ -64,4 +92,3 @@ command](custom-workflows.html#custom-run-command). * `COMMAND_NAME` - The name of the command that is being executed, i.e. `plan`, `apply` etc. * `OUTPUT_STATUS_FILE` - An output file to customize the success or failure status. ex. `echo 'failure' > $OUTPUT_STATUS_FILE`. ::: - diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index d489c1079c..9a2d9a43f0 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -610,9 +610,11 @@ func TestGitHubWorkflow(t *testing.T) { expNumHooks := len(c.Comments) + 1 - c.ExpParseFailedCount // Let's verify the pre-workflow hook was called for each comment including the pull request opened event - mockPreWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](), Eq("some dummy command"), Any[string]()) + mockPreWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](), + Eq("some dummy command"), Any[string](), Any[string](), Any[string]()) // Let's verify the post-workflow hook was called for each comment including the pull request opened event - mockPostWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](), Eq("some post dummy command"), Any[string]()) + mockPostWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](), + Eq("some post dummy command"), Any[string](), Any[string](), Any[string]()) // Now we're ready to verify Atlantis made all the comments back (or // replies) that we expect. We expect each plan to have 1 comment, @@ -794,7 +796,8 @@ func TestSimpleWorkflow_terraformLockFile(t *testing.T) { } // Let's verify the pre-workflow hook was called for each comment including the pull request opened event - mockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(Any[models.WorkflowHookCommandContext](), Eq("some dummy command"), Any[string]()) + mockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(Any[models.WorkflowHookCommandContext](), + Eq("some dummy command"), Any[string](), Any[string](), Any[string]()) // Now we're ready to verify Atlantis made all the comments back (or // replies) that we expect. We expect each plan to have 1 comment, diff --git a/server/core/config/raw/workflow_step.go b/server/core/config/raw/workflow_step.go index b825f2a827..16a4268b05 100644 --- a/server/core/config/raw/workflow_step.go +++ b/server/core/config/raw/workflow_step.go @@ -76,6 +76,8 @@ func (s WorkflowHook) ToValid() *valid.WorkflowHook { StepName: RunStepName, RunCommand: s.StringVal["run"], StepDescription: s.StringVal["description"], + Shell: s.StringVal["shell"], + ShellArgs: s.StringVal["shellArgs"], } } diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index 614c71d7e0..a62625db92 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -109,6 +109,8 @@ type WorkflowHook struct { StepName string RunCommand string StepDescription string + Shell string + ShellArgs string } // DefaultApplyStage is the Atlantis default apply stage. diff --git a/server/core/runtime/mocks/mock_post_workflows_hook_runner.go b/server/core/runtime/mocks/mock_post_workflows_hook_runner.go index 1eadfe762d..2674bf3e0e 100644 --- a/server/core/runtime/mocks/mock_post_workflows_hook_runner.go +++ b/server/core/runtime/mocks/mock_post_workflows_hook_runner.go @@ -25,11 +25,11 @@ func NewMockPostWorkflowHookRunner(options ...pegomock.Option) *MockPostWorkflow func (mock *MockPostWorkflowHookRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockPostWorkflowHookRunner) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error) { +func (mock *MockPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockPostWorkflowHookRunner().") } - params := []pegomock.Param{ctx, command, path} + params := []pegomock.Param{ctx, command, shell, shellArgs, path} result := pegomock.GetGenericMockFrom(mock).Invoke("Run", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 string var ret1 string @@ -85,8 +85,8 @@ type VerifierMockPostWorkflowHookRunner struct { timeout time.Duration } -func (verifier *VerifierMockPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) *MockPostWorkflowHookRunner_Run_OngoingVerification { - params := []pegomock.Param{ctx, command, path} +func (verifier *VerifierMockPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) *MockPostWorkflowHookRunner_Run_OngoingVerification { + params := []pegomock.Param{ctx, command, shell, shellArgs, path} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout) return &MockPostWorkflowHookRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -96,12 +96,12 @@ type MockPostWorkflowHookRunner_Run_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, string) { - ctx, command, path := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], command[len(command)-1], path[len(path)-1] +func (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, string, string, string) { + ctx, command, shell, shellArgs, path := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], command[len(command)-1], shell[len(shell)-1], shellArgs[len(shellArgs)-1], path[len(path)-1] } -func (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []string) { +func (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []string, _param3 []string, _param4 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.WorkflowHookCommandContext, len(c.methodInvocations)) @@ -116,6 +116,14 @@ func (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArgum for u, param := range params[2] { _param2[u] = param.(string) } + _param3 = make([]string, len(c.methodInvocations)) + for u, param := range params[3] { + _param3[u] = param.(string) + } + _param4 = make([]string, len(c.methodInvocations)) + for u, param := range params[4] { + _param4[u] = param.(string) + } } return } diff --git a/server/core/runtime/mocks/mock_pre_workflows_hook_runner.go b/server/core/runtime/mocks/mock_pre_workflows_hook_runner.go index 09d4435d98..c9894c5cdd 100644 --- a/server/core/runtime/mocks/mock_pre_workflows_hook_runner.go +++ b/server/core/runtime/mocks/mock_pre_workflows_hook_runner.go @@ -25,11 +25,11 @@ func NewMockPreWorkflowHookRunner(options ...pegomock.Option) *MockPreWorkflowHo func (mock *MockPreWorkflowHookRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockPreWorkflowHookRunner) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error) { +func (mock *MockPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockPreWorkflowHookRunner().") } - params := []pegomock.Param{ctx, command, path} + params := []pegomock.Param{ctx, command, shell, shellArgs, path} result := pegomock.GetGenericMockFrom(mock).Invoke("Run", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 string var ret1 string @@ -85,8 +85,8 @@ type VerifierMockPreWorkflowHookRunner struct { timeout time.Duration } -func (verifier *VerifierMockPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) *MockPreWorkflowHookRunner_Run_OngoingVerification { - params := []pegomock.Param{ctx, command, path} +func (verifier *VerifierMockPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) *MockPreWorkflowHookRunner_Run_OngoingVerification { + params := []pegomock.Param{ctx, command, shell, shellArgs, path} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout) return &MockPreWorkflowHookRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -96,12 +96,12 @@ type MockPreWorkflowHookRunner_Run_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, string) { - ctx, command, path := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], command[len(command)-1], path[len(path)-1] +func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, string, string, string) { + ctx, command, shell, shellArgs, path := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], command[len(command)-1], shell[len(shell)-1], shellArgs[len(shellArgs)-1], path[len(path)-1] } -func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []string) { +func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []string, _param3 []string, _param4 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.WorkflowHookCommandContext, len(c.methodInvocations)) @@ -116,6 +116,14 @@ func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArgume for u, param := range params[2] { _param2[u] = param.(string) } + _param3 = make([]string, len(c.methodInvocations)) + for u, param := range params[3] { + _param3[u] = param.(string) + } + _param4 = make([]string, len(c.methodInvocations)) + for u, param := range params[4] { + _param4[u] = param.(string) + } } return } diff --git a/server/core/runtime/post_workflow_hook_runner.go b/server/core/runtime/post_workflow_hook_runner.go index 1835d50bb7..d2e1f8cbb4 100644 --- a/server/core/runtime/post_workflow_hook_runner.go +++ b/server/core/runtime/post_workflow_hook_runner.go @@ -13,17 +13,18 @@ import ( //go:generate pegomock generate --package mocks -o mocks/mock_post_workflows_hook_runner.go PostWorkflowHookRunner type PostWorkflowHookRunner interface { - Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error) + Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) } type DefaultPostWorkflowHookRunner struct { OutputHandler jobs.ProjectCommandOutputHandler } -func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error) { +func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) { outputFilePath := filepath.Join(path, "OUTPUT_STATUS_FILE") - cmd := exec.Command("sh", "-c", command) // #nosec + shellArgsSlice := append(strings.Split(shellArgs, " "), command) + cmd := exec.Command(shell, shellArgsSlice...) // #nosec cmd.Dir = path baseEnvVars := os.Environ() @@ -58,7 +59,7 @@ func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContex wh.OutputHandler.SendWorkflowHook(ctx, "\n", true) if err != nil { - err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out) + err = fmt.Errorf("%s: running %q in %q: \n%s", err, shell+" "+shellArgs+" "+command, path, out) ctx.Log.Debug("error: %s", err) return string(out), "", err } @@ -70,12 +71,12 @@ func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContex var customStatusErr error customStatusOut, customStatusErr = os.ReadFile(outputFilePath) if customStatusErr != nil { - err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out) + err = fmt.Errorf("%s: running %q in %q: \n%s", err, shell+" "+shellArgs+" "+command, path, out) ctx.Log.Debug("error: %s", err) return string(out), "", err } } - ctx.Log.Info("successfully ran %q in %q", command, path) - return outString, strings.Trim(string(customStatusOut), "\n"), nil + ctx.Log.Info("successfully ran %q in %q", shell+" "+shellArgs+" "+command, path) + return string(out), strings.Trim(string(customStatusOut), "\n"), nil } diff --git a/server/core/runtime/post_workflow_hook_runner_test.go b/server/core/runtime/post_workflow_hook_runner_test.go index d3ab1f5d1b..f4db6fcf93 100644 --- a/server/core/runtime/post_workflow_hook_runner_test.go +++ b/server/core/runtime/post_workflow_hook_runner_test.go @@ -15,72 +15,122 @@ import ( ) func TestPostWorkflowHookRunner_Run(t *testing.T) { + + defaultShell := "sh" + defaultShellArgs := "-c" + cases := []struct { Command string + Shell string + ShellArgs string ExpOut string ExpErr string ExpDescription string }{ { Command: "", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "", ExpErr: "", ExpDescription: "", }, { Command: "echo hi", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "hi\r\n", ExpErr: "", ExpDescription: "", }, { Command: `printf \'your main.tf file does not provide default region.\\ncheck\'`, + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: `'your`, ExpErr: "", ExpDescription: "", }, { Command: `printf 'your main.tf file does not provide default region.\ncheck'`, + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "your main.tf file does not provide default region.\r\ncheck", ExpErr: "", ExpDescription: "", }, { Command: "echo 'a", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "sh: 1: Syntax error: Unterminated quoted string\r\n", - ExpErr: "exit status 2: running \"echo 'a\" in", + ExpErr: "exit status 2: running \"sh -c echo 'a\" in", ExpDescription: "", }, { Command: "echo hi >> file && cat file", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "hi\r\n", ExpErr: "", ExpDescription: "", }, { Command: "lkjlkj", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "sh: 1: lkjlkj: not found\r\n", - ExpErr: "exit status 127: running \"lkjlkj\" in", + ExpErr: "exit status 127: running \"sh -c lkjlkj\" in", ExpDescription: "", }, { Command: "echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_url=$PULL_URL pull_author=$PULL_AUTHOR", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=main pull_num=2 pull_url=https://github.com/runatlantis/atlantis/pull/2 pull_author=acme\r\n", ExpErr: "", ExpDescription: "", }, { Command: "echo user_name=$USER_NAME", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "user_name=acme-user\r\n", ExpErr: "", ExpDescription: "", }, { Command: "echo something > $OUTPUT_STATUS_FILE", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "", ExpErr: "", ExpDescription: "something", }, + { + Command: "echo shell test 1", + Shell: "bash", + ShellArgs: defaultShellArgs, + ExpOut: "shell test 1\r\n", + ExpErr: "", + ExpDescription: "", + }, + { + Command: "echo shell test 2", + Shell: defaultShell, + ShellArgs: "-cx", + ExpOut: "+ echo shell test 2\r\nshell test 2\r\n", + ExpErr: "", + ExpDescription: "", + }, + { + Command: "echo shell test 3", + Shell: "bash", + ShellArgs: "-cv", + ExpOut: "echo shell test 3\r\nshell test 3\r\n", + ExpErr: "", + ExpDescription: "", + }, } for _, c := range cases { @@ -124,7 +174,7 @@ func TestPostWorkflowHookRunner_Run(t *testing.T) { Log: logger, CommandName: "plan", } - _, desc, err := r.Run(ctx, c.Command, tmpDir) + _, desc, err := r.Run(ctx, c.Command, c.Shell, c.ShellArgs, tmpDir) if c.ExpErr != "" { ErrContains(t, c.ExpErr, err) } else { diff --git a/server/core/runtime/pre_workflow_hook_runner.go b/server/core/runtime/pre_workflow_hook_runner.go index c80542c5e1..9e30e8eb27 100644 --- a/server/core/runtime/pre_workflow_hook_runner.go +++ b/server/core/runtime/pre_workflow_hook_runner.go @@ -13,17 +13,18 @@ import ( //go:generate pegomock generate --package mocks -o mocks/mock_pre_workflows_hook_runner.go PreWorkflowHookRunner type PreWorkflowHookRunner interface { - Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error) + Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) } type DefaultPreWorkflowHookRunner struct { OutputHandler jobs.ProjectCommandOutputHandler } -func (wh DefaultPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error) { +func (wh DefaultPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) { outputFilePath := filepath.Join(path, "OUTPUT_STATUS_FILE") - cmd := exec.Command("sh", "-c", command) // #nosec + shellArgsSlice := append(strings.Split(shellArgs, " "), command) + cmd := exec.Command(shell, shellArgsSlice...) // #nosec cmd.Dir = path baseEnvVars := os.Environ() @@ -58,7 +59,7 @@ func (wh DefaultPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext wh.OutputHandler.SendWorkflowHook(ctx, "\n", true) if err != nil { - err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out) + err = fmt.Errorf("%s: running %q in %q: \n%s", err, shell+" "+shellArgs+" "+command, path, out) ctx.Log.Debug("error: %s", err) return string(out), "", err } @@ -70,12 +71,12 @@ func (wh DefaultPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext var customStatusErr error customStatusOut, customStatusErr = os.ReadFile(outputFilePath) if customStatusErr != nil { - err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out) + err = fmt.Errorf("%s: running %q in %q: \n%s", err, shell+" "+shellArgs+" "+command, path, out) ctx.Log.Debug("error: %s", err) return string(out), "", err } } - ctx.Log.Info("successfully ran %q in %q", command, path) - return outString, strings.Trim(string(customStatusOut), "\n"), nil + ctx.Log.Info("successfully ran %q in %q", shell+" "+shellArgs+" "+command, path) + return string(out), strings.Trim(string(customStatusOut), "\n"), nil } diff --git a/server/core/runtime/pre_workflow_hook_runner_test.go b/server/core/runtime/pre_workflow_hook_runner_test.go index ad608954cc..e51d49e58e 100644 --- a/server/core/runtime/pre_workflow_hook_runner_test.go +++ b/server/core/runtime/pre_workflow_hook_runner_test.go @@ -15,72 +15,122 @@ import ( ) func TestPreWorkflowHookRunner_Run(t *testing.T) { + + defaultShell := "sh" + defaultShellArgs := "-c" + cases := []struct { Command string + Shell string + ShellArgs string ExpOut string ExpErr string ExpDescription string }{ { Command: "", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "", ExpErr: "", ExpDescription: "", }, { Command: "echo hi", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "hi\r\n", ExpErr: "", ExpDescription: "", }, { Command: `printf \'your main.tf file does not provide default region.\\ncheck\'`, + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: `'your`, ExpErr: "", ExpDescription: "", }, { Command: `printf 'your main.tf file does not provide default region.\ncheck'`, + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "your main.tf file does not provide default region.\r\ncheck", ExpErr: "", ExpDescription: "", }, { Command: "echo 'a", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "sh: 1: Syntax error: Unterminated quoted string\r\n", - ExpErr: "exit status 2: running \"echo 'a\" in", + ExpErr: "exit status 2: running \"sh -c echo 'a\" in", ExpDescription: "", }, { Command: "echo hi >> file && cat file", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "hi\r\n", ExpErr: "", ExpDescription: "", }, { Command: "lkjlkj", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "sh: 1: lkjlkj: not found\r\n", - ExpErr: "exit status 127: running \"lkjlkj\" in", + ExpErr: "exit status 127: running \"sh -c lkjlkj\" in", ExpDescription: "", }, { Command: "echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_url=$PULL_URL pull_author=$PULL_AUTHOR", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=main pull_num=2 pull_url=https://github.com/runatlantis/atlantis/pull/2 pull_author=acme\r\n", ExpErr: "", ExpDescription: "", }, { Command: "echo user_name=$USER_NAME", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "user_name=acme-user\r\n", ExpErr: "", ExpDescription: "", }, { Command: "echo something > $OUTPUT_STATUS_FILE", + Shell: defaultShell, + ShellArgs: defaultShellArgs, ExpOut: "", ExpErr: "", ExpDescription: "something", }, + { + Command: "echo shell test 1", + Shell: "bash", + ShellArgs: defaultShellArgs, + ExpOut: "shell test 1\r\n", + ExpErr: "", + ExpDescription: "", + }, + { + Command: "echo shell test 2", + Shell: defaultShell, + ShellArgs: "-cx", + ExpOut: "+ echo shell test 2\r\nshell test 2\r\n", + ExpErr: "", + ExpDescription: "", + }, + { + Command: "echo shell test 3", + Shell: "bash", + ShellArgs: "-cv", + ExpOut: "echo shell test 3\r\nshell test 3\r\n", + ExpErr: "", + ExpDescription: "", + }, } for _, c := range cases { @@ -124,7 +174,7 @@ func TestPreWorkflowHookRunner_Run(t *testing.T) { Log: logger, CommandName: "plan", } - _, desc, err := r.Run(ctx, c.Command, tmpDir) + _, desc, err := r.Run(ctx, c.Command, c.Shell, c.ShellArgs, tmpDir) if c.ExpErr != "" { ErrContains(t, c.ExpErr, err) } else { diff --git a/server/events/post_workflow_hooks_command_runner.go b/server/events/post_workflow_hooks_command_runner.go index 12f7e9e1b8..45b44b9947 100644 --- a/server/events/post_workflow_hooks_command_runner.go +++ b/server/events/post_workflow_hooks_command_runner.go @@ -109,6 +109,16 @@ func (w *DefaultPostWorkflowHooksCommandRunner) runHooks( } ctx.HookID = uuid.NewString() + shell := hook.Shell + if shell == "" { + ctx.Log.Debug("Setting shell to default: %q", shell) + shell = "sh" + } + shellArgs := hook.ShellArgs + if shellArgs == "" { + ctx.Log.Debug("Setting shellArgs to default: %q", shellArgs) + shellArgs = "-c" + } url, err := w.Router.GenerateProjectWorkflowHookURL(ctx.HookID) if err != nil { return err @@ -118,7 +128,7 @@ func (w *DefaultPostWorkflowHooksCommandRunner) runHooks( ctx.Log.Warn("unable to update post workflow hook status: %s", err) } - _, runtimeDesc, err := w.PostWorkflowHookRunner.Run(ctx, hook.RunCommand, repoDir) + _, runtimeDesc, err := w.PostWorkflowHookRunner.Run(ctx, hook.RunCommand, shell, shellArgs, repoDir) if err != nil { if err := w.CommitStatusUpdater.UpdatePostWorkflowHook(ctx.Pull, models.FailedCommitStatus, hookDescription, runtimeDesc, url); err != nil { diff --git a/server/events/post_workflow_hooks_command_runner_test.go b/server/events/post_workflow_hooks_command_runner_test.go index c812ef6708..b0f8edc2b3 100644 --- a/server/events/post_workflow_hooks_command_runner_test.go +++ b/server/events/post_workflow_hooks_command_runner_test.go @@ -57,11 +57,33 @@ func TestRunPostHooks_Clone(t *testing.T) { Log: log, } + defaultShell := "sh" + defaultShellArgs := "-c" + testHook := valid.WorkflowHook{ StepName: "test", RunCommand: "some command", } + testHookWithShell := valid.WorkflowHook{ + StepName: "test1", + RunCommand: "echo test1", + Shell: "bash", + } + + testHookWithShellArgs := valid.WorkflowHook{ + StepName: "test2", + RunCommand: "echo test2", + ShellArgs: "-ce", + } + + testHookWithShellandShellArgs := valid.WorkflowHook{ + StepName: "test3", + RunCommand: "echo test3", + Shell: "bash", + ShellArgs: "-ce", + } + repoDir := "path/to/repo" result := "some result" runtimeDesc := "" @@ -104,11 +126,14 @@ func TestRunPostHooks_Clone(t *testing.T) { When(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) When(postWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) - When(whPostWorkflowHookRunner.Run(pCtx, testHook.RunCommand, repoDir)).ThenReturn(result, runtimeDesc, nil) + When(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Any[string](), + Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + err := postWh.RunPostHooks(ctx, cmd) Ok(t, err) - whPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir)) + whPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) Assert(t, *unlockCalled == true, "unlock function called") }) t.Run("success hooks not in cfg", func(t *testing.T) { @@ -136,7 +161,8 @@ func TestRunPostHooks_Clone(t *testing.T) { Ok(t, err) - whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(pCtx, testHook.RunCommand, repoDir) + whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), + Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) postWhWorkingDirLocker.VerifyWasCalled(Never()).TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, "path") postWhWorkingDir.VerifyWasCalled(Never()).Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace) }) @@ -162,7 +188,8 @@ func TestRunPostHooks_Clone(t *testing.T) { Assert(t, err != nil, "error not nil") postWhWorkingDir.VerifyWasCalled(Never()).Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace) - whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(pCtx, testHook.RunCommand, repoDir) + whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), + Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) }) t.Run("error cloning", func(t *testing.T) { @@ -193,7 +220,8 @@ func TestRunPostHooks_Clone(t *testing.T) { Assert(t, err != nil, "error not nil") - whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(pCtx, testHook.RunCommand, repoDir) + whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), + Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) Assert(t, *unlockCalled == true, "unlock function called") }) @@ -220,7 +248,8 @@ func TestRunPostHooks_Clone(t *testing.T) { When(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) When(postWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) - When(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir))).ThenReturn(result, runtimeDesc, errors.New("some error")) + When(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), + Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, errors.New("some error")) err := postWh.RunPostHooks(ctx, cmd) @@ -259,12 +288,117 @@ func TestRunPostHooks_Clone(t *testing.T) { When(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) When(postWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) - When(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + When(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), + Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + + err := postWh.RunPostHooks(ctx, cmd) + + Ok(t, err) + whPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) + Assert(t, *unlockCalled == true, "unlock function called") + }) + + t.Run("shell passed to webhooks", func(t *testing.T) { + postWorkflowHooksSetup(t) + + var unlockCalled = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: testdata.GithubRepo.ID(), + PostWorkflowHooks: []*valid.WorkflowHook{ + &testHookWithShell, + }, + }, + }, + } + + postWh.GlobalCfg = globalCfg + + When(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) + When(postWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithShell.RunCommand), + Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + + err := postWh.RunPostHooks(ctx, cmd) + + Ok(t, err) + whPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHookWithShell.RunCommand), Eq(testHookWithShell.Shell), Eq(defaultShellArgs), Eq(repoDir)) + Assert(t, *unlockCalled == true, "unlock function called") + }) + + t.Run("shellArgs passed to webhooks", func(t *testing.T) { + postWorkflowHooksSetup(t) + + var unlockCalled = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: testdata.GithubRepo.ID(), + PostWorkflowHooks: []*valid.WorkflowHook{ + &testHookWithShellArgs, + }, + }, + }, + } + + postWh.GlobalCfg = globalCfg + + When(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) + When(postWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), + Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) err := postWh.RunPostHooks(ctx, cmd) Ok(t, err) - whPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir)) + whPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHookWithShellArgs.RunCommand), Eq(defaultShell), Eq(testHookWithShellArgs.ShellArgs), Eq(repoDir)) Assert(t, *unlockCalled == true, "unlock function called") }) + + t.Run("Shell and ShellArgs passed to webhooks", func(t *testing.T) { + postWorkflowHooksSetup(t) + + var unlockCalled = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: testdata.GithubRepo.ID(), + PostWorkflowHooks: []*valid.WorkflowHook{ + &testHookWithShellandShellArgs, + }, + }, + }, + } + + postWh.GlobalCfg = globalCfg + + When(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) + When(postWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), + Eq(testHookWithShellandShellArgs.RunCommand), Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + + err := postWh.RunPostHooks(ctx, cmd) + + Ok(t, err) + whPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHookWithShellandShellArgs.RunCommand), Eq(testHookWithShellandShellArgs.Shell), Eq(testHookWithShellandShellArgs.ShellArgs), Eq(repoDir)) + Assert(t, *unlockCalled == true, "unlock function called") + }) + } diff --git a/server/events/pre_workflow_hooks_command_runner.go b/server/events/pre_workflow_hooks_command_runner.go index 1cc2b4daba..ce1aa51198 100644 --- a/server/events/pre_workflow_hooks_command_runner.go +++ b/server/events/pre_workflow_hooks_command_runner.go @@ -106,6 +106,16 @@ func (w *DefaultPreWorkflowHooksCommandRunner) runHooks( } ctx.HookID = uuid.NewString() + shell := hook.Shell + if shell == "" { + ctx.Log.Debug("Setting shell to default: %q", shell) + shell = "sh" + } + shellArgs := hook.ShellArgs + if shellArgs == "" { + ctx.Log.Debug("Setting shellArgs to default: %q", shellArgs) + shellArgs = "-c" + } url, err := w.Router.GenerateProjectWorkflowHookURL(ctx.HookID) if err != nil { return err @@ -116,7 +126,7 @@ func (w *DefaultPreWorkflowHooksCommandRunner) runHooks( return err } - _, runtimeDesc, err := w.PreWorkflowHookRunner.Run(ctx, hook.RunCommand, repoDir) + _, runtimeDesc, err := w.PreWorkflowHookRunner.Run(ctx, hook.RunCommand, shell, shellArgs, repoDir) if err != nil { if err := w.CommitStatusUpdater.UpdatePreWorkflowHook(ctx.Pull, models.FailedCommitStatus, hookDescription, runtimeDesc, url); err != nil { diff --git a/server/events/pre_workflow_hooks_command_runner_test.go b/server/events/pre_workflow_hooks_command_runner_test.go index e9660db6e2..f261dd80c4 100644 --- a/server/events/pre_workflow_hooks_command_runner_test.go +++ b/server/events/pre_workflow_hooks_command_runner_test.go @@ -60,11 +60,33 @@ func TestRunPreHooks_Clone(t *testing.T) { Log: log, } + defaultShell := "sh" + defaultShellArgs := "-c" + testHook := valid.WorkflowHook{ StepName: "test", RunCommand: "some command", } + testHookWithShell := valid.WorkflowHook{ + StepName: "test1", + RunCommand: "echo test1", + Shell: "bash", + } + + testHookWithShellArgs := valid.WorkflowHook{ + StepName: "test2", + RunCommand: "echo test2", + ShellArgs: "-ce", + } + + testHookWithShellandShellArgs := valid.WorkflowHook{ + StepName: "test3", + RunCommand: "echo test3", + Shell: "bash", + ShellArgs: "-ce", + } + repoDir := "path/to/repo" result := "some result" runtimeDesc := "" @@ -106,12 +128,14 @@ func TestRunPreHooks_Clone(t *testing.T) { When(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) When(preWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) - When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), + Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) err := preWh.RunPreHooks(ctx, cmd) Ok(t, err) - whPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir)) + whPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) Assert(t, *unlockCalled == true, "unlock function called") }) @@ -140,7 +164,7 @@ func TestRunPreHooks_Clone(t *testing.T) { Ok(t, err) - whPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir)) + whPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) preWhWorkingDirLocker.VerifyWasCalled(Never()).TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, "") preWhWorkingDir.VerifyWasCalled(Never()).Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace) }) @@ -167,7 +191,7 @@ func TestRunPreHooks_Clone(t *testing.T) { Assert(t, err != nil, "error not nil") preWhWorkingDir.VerifyWasCalled(Never()).Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace) - whPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir)) + whPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) }) t.Run("error cloning", func(t *testing.T) { @@ -198,7 +222,7 @@ func TestRunPreHooks_Clone(t *testing.T) { Assert(t, err != nil, "error not nil") - whPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir)) + whPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) Assert(t, *unlockCalled == true, "unlock function called") }) @@ -225,7 +249,8 @@ func TestRunPreHooks_Clone(t *testing.T) { When(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) When(preWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) - When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir))).ThenReturn(result, runtimeDesc, errors.New("some error")) + When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), + Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, errors.New("some error")) err := preWh.RunPreHooks(ctx, cmd) @@ -264,12 +289,115 @@ func TestRunPreHooks_Clone(t *testing.T) { When(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) When(preWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) - When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + + err := preWh.RunPreHooks(ctx, cmd) + + Ok(t, err) + whPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir)) + Assert(t, *unlockCalled == true, "unlock function called") + }) + + t.Run("shell passed to webhooks", func(t *testing.T) { + preWorkflowHooksSetup(t) + + var unlockCalled = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: testdata.GithubRepo.ID(), + PreWorkflowHooks: []*valid.WorkflowHook{ + &testHookWithShell, + }, + }, + }, + } + + preWh.GlobalCfg = globalCfg + + When(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) + When(preWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithShell.RunCommand), + Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + + err := preWh.RunPreHooks(ctx, cmd) + + Ok(t, err) + whPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHookWithShell.RunCommand), Eq(testHookWithShell.Shell), Eq(defaultShellArgs), Eq(repoDir)) + Assert(t, *unlockCalled == true, "unlock function called") + }) + + t.Run("shellArgs passed to webhooks", func(t *testing.T) { + preWorkflowHooksSetup(t) + + var unlockCalled = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: testdata.GithubRepo.ID(), + PreWorkflowHooks: []*valid.WorkflowHook{ + &testHookWithShellArgs, + }, + }, + }, + } + + preWh.GlobalCfg = globalCfg + + When(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) + When(preWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), + Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) + + err := preWh.RunPreHooks(ctx, cmd) + + Ok(t, err) + whPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHookWithShellArgs.RunCommand), Eq(defaultShell), Eq(testHookWithShellArgs.ShellArgs), Eq(repoDir)) + Assert(t, *unlockCalled == true, "unlock function called") + }) + + t.Run("Shell and ShellArgs passed to webhooks", func(t *testing.T) { + preWorkflowHooksSetup(t) + + var unlockCalled = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: testdata.GithubRepo.ID(), + PreWorkflowHooks: []*valid.WorkflowHook{ + &testHookWithShellandShellArgs, + }, + }, + }, + } + + preWh.GlobalCfg = globalCfg + + When(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir)).ThenReturn(unlockFn, nil) + When(preWhWorkingDir.Clone(log, testdata.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), + Eq(testHookWithShellandShellArgs.RunCommand), Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil) err := preWh.RunPreHooks(ctx, cmd) Ok(t, err) - whPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Eq(repoDir)) + whPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), + Eq(testHookWithShellandShellArgs.RunCommand), Eq(testHookWithShellandShellArgs.Shell), + Eq(testHookWithShellandShellArgs.ShellArgs), Eq(repoDir)) Assert(t, *unlockCalled == true, "unlock function called") }) }