Repository: nektos/act (language: go)

Issue:
Manage special bash options when no shell is defined in a workflow
### Bug report info

```plain text
act version:            0.2.66
GOOS:                   linux
GOARCH:                 amd64
NumCPU:                 4
Docker host:            DOCKER_HOST environment variable is not set
Sockets found:
        /var/run/docker.sock
Config files:           
        /home/derek/.config/act/actrc:
                -P ubuntu-latest=catthehacker/ubuntu:act-latest
                -P ubuntu-22.04=catthehacker/ubuntu:act-22.04
                -P ubuntu-20.04=catthehacker/ubuntu:act-20.04
                -P ubuntu-18.04=catthehacker/ubuntu:act-18.04
Build info:
        Go version:            go1.23.0
        Module path:           command-line-arguments
        Main version:          
        Main path:             
        Main checksum:         
        Build settings:
                -buildmode:           exe
                -compiler:            gc
                -ldflags:             -X main.version=0.2.66
                DefaultGODEBUG:       asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1
                CGO_ENABLED:          1
                CGO_CFLAGS:           
                CGO_CPPFLAGS:         
                CGO_CXXFLAGS:         
                CGO_LDFLAGS:          
                GOARCH:               amd64
                GOOS:                 linux
                GOAMD64:              v1
Docker Engine:
        Engine version:        27.2.1
        Engine runtime:        runc
        Cgroup version:        1
        Cgroup driver:         cgroupfs
        Storage driver:        overlay2
        Registry URI:          https://index.docker.io/v1/
        OS:                    Linux Mint 20.3
        OS type:               linux
        OS version:            20.3
        OS arch:               x86_64
        OS kernel:             5.4.0-193-generic
        OS CPU:                4
        OS memory:             15880 MB
        Security options:
                name=apparmor
                name=seccomp,profile=builtin
```


### Command used with act

```sh
act -v -P ubuntu-latest=localhost:5000/act-ubuntu-mvn:latest --remote-name github -j check-dependencies  workflowdispatch
```


### Describe issue

The job fails when executing this bash expression on a file that does not contain any dependency which is updatable:

`nb_updatable_dependencies=$(cat updatable.txt | grep . | grep -v '^No dependencies' | wc -l)`

It fails because bash is called with :

`-o pipefail`

This option is not used in github actions.

This issue was already fixed : https://github.com/nektos/act/issues/528
But the "-o pipefail" option came back in this PR: https://github.com/nektos/act/pull/575

### Link to GitHub repository

https://github.com/sebastien-perpignane/cardgame

### Workflow content

```yml
name: "Check dependencies"

on:
  schedule:
    - cron: '35 1 * * *'

  workflow_dispatch:

jobs:
  check-dependencies:
    uses: sebastien-perpignane/my-workflows/.github/workflows/check-dependencies.yml@main
    with:
      java-version: '21'
      distribution: 'temurin'
```


### Relevant log output

```sh
[check-dependencies/Check maven dependency updates/check-dependency-updates]   🐳  docker exec cmd=[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/2] user= workdir=
[check-dependencies/Check maven dependency updates/check-dependency-updates] [DEBUG] Exec command '[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/2]'
[check-dependencies/Check maven dependency updates/check-dependency-updates] [DEBUG] Working directory '/home/derek/workspace/cardgame'
| [INFO] Scanning for projects...
| [INFO] 
| [INFO] -------------------< sebastien.perpignane:cardgame >--------------------
| [INFO] Building cardgame 0.0.1-SNAPSHOT
| [INFO]   from pom.xml
| [INFO] --------------------------------[ jar ]---------------------------------
| [INFO] 
| [INFO] --- versions:2.17.1:display-dependency-updates (default-cli) @ cardgame ---
| [INFO] No dependencies in Dependencies have newer versions.
| [INFO] 
| [INFO] No dependencies in Plugin Dependencies have newer versions.
| [INFO] 
| [INFO] ------------------------------------------------------------------------
| [INFO] BUILD SUCCESS
| [INFO] ------------------------------------------------------------------------
| [INFO] Total time:  1.957 s
| [INFO] Finished at: 2024-09-10T20:07:03Z
| [INFO] ------------------------------------------------------------------------
[check-dependencies/Check maven dependency updates/check-dependency-updates]   ❌  Failure - Main Check dependencies
[check-dependencies/Check maven dependency updates/check-dependency-updates] exitcode '1': failure
```


### Additional information

Dockerfile to build the custom image I use (act-ubuntu-mvn image) -> https://gist.github.com/sebastien-perpignane/ade17b1922d35147d08cb08169a6a388

I submitted a PR as it is a very simple fix, even for someone who does not know Go, like me :)


Discussion / hints:
If you add `defaults.run.shell: bash` (short hand syntax, not valid yml) then pipefail will be used by GitHub Actions.

For your example is your suggestion not really correct, because they use the sh template (that only has -e) for bash if you don't tell the workflow to use bash on linux/mac (due to a bug?)

See here explicit bash has that flag https://github.com/actions/runner/blob/ddf41af7678a8bc585d9e6b8ab70d941cdb687b2/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L18
Hello @ChristopherHX 

Thanks a lot for your quick answer.

You're right.
I just changed my callable workflow with 
`shell: bash` 
and now it fails for the same reason when I run it in github.

Then, what would you think about running bash without pipefail in `act` when no explicit shell is declared on a step ?I guess it would make `act` better synchronized with the github actions behavior, right ?

Something like this :

```go
case "":
    shellCommand = "bash -e {0}"
case "sh":
    shellCommand = "sh -e {0}"
case "bash":
    shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
```

I'm just trying to find a solution to make act behavior as close as possible to github actions one. It will help me to convince my team mates to use act to win some time when implementing GitHub jobs ;)
Hi again,

It does not seem to be due to a bug as it is a documented behavior:

https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#defaultsrunshell

| Supported platform | shell parameter | Description | Command run internally |
|----|----|----|----|
| Linux / macOS | unspecified | The default shell on non-Windows platforms. Note that this runs a different command to when bash is specified explicitly. If bash is not found in the path, this is treated as sh. | bash -e {0} |
> It does not seem to be due to a bug as it is a documented behavior:

Or an old bug, then they merged a PR to document the current state. That wouldn't be the first time, where a fix would potential break existing workflows and then nothing is changed.

> Then, what would you think about running bash without pipefail in `act` when no explicit shell is declared on a step ?I guess it would make `act` better synchronized with the github actions behavior, right ?
> 
> Something like this :
> 
> ```go
> case "":
>     shellCommand = "bash -e {0}"
> case "sh":
>     shellCommand = "sh -e {0}"
> case "bash":
>     shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
> ```
> 
> I'm just trying to find a solution to make act behavior as close as possible to github actions one. It will help me to convince my team mates to use act to win some time when implementing GitHub jobs ;)

This looks like a good way to handle this and I give my :+1:, however I remember the code may have somewhere a if shell == "" then shell = "bash" that would escape your change.
> however I remember the code may have somewhere a if shell == "" then shell = "bash" that would escape your change.

I'll check this, thanks for the tip.

Fix patch:
diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go
index e66bf4ce949..57e8f57dc61 100644
--- a/pkg/model/workflow.go
+++ b/pkg/model/workflow.go
@@ -572,6 +572,8 @@ type Step struct {
 	Uses               string            `yaml:"uses"`
 	Run                string            `yaml:"run"`
 	WorkingDirectory   string            `yaml:"working-directory"`
+	// WorkflowShell is the shell really configured in the job, directly at step level or higher in defaults.run.shell
+	WorkflowShell      string            `yaml:"-"`
 	Shell              string            `yaml:"shell"`
 	Env                yaml.Node         `yaml:"env"`
 	With               map[string]string `yaml:"with"`
@@ -614,8 +616,14 @@ func (s *Step) ShellCommand() string {
 
 	//Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
 	switch s.Shell {
-	case "", "bash":
-		shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
+	case "":
+		shellCommand = "bash -e {0}"
+	case "bash":
+		if s.WorkflowShell == "" {
+			shellCommand = "bash -e {0}"
+		} else {
+			shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
+		}
 	case "pwsh":
 		shellCommand = "pwsh -command . '{0}'"
 	case "python":
diff --git a/pkg/runner/step_run.go b/pkg/runner/step_run.go
index 054ed702321..a905a20e1f6 100644
--- a/pkg/runner/step_run.go
+++ b/pkg/runner/step_run.go
@@ -166,16 +166,18 @@ func (sr *stepRun) setupShell(ctx context.Context) {
 	step := sr.Step
 
 	if step.Shell == "" {
-		step.Shell = rc.Run.Job().Defaults.Run.Shell
+		step.WorkflowShell = rc.Run.Job().Defaults.Run.Shell
+	} else {
+		step.WorkflowShell = step.Shell
 	}
 
-	step.Shell = rc.NewExpressionEvaluator(ctx).Interpolate(ctx, step.Shell)
+	step.WorkflowShell = rc.NewExpressionEvaluator(ctx).Interpolate(ctx, step.WorkflowShell)
 
-	if step.Shell == "" {
-		step.Shell = rc.Run.Workflow.Defaults.Run.Shell
+	if step.WorkflowShell == "" {
+		step.WorkflowShell = rc.Run.Workflow.Defaults.Run.Shell
 	}
 
-	if step.Shell == "" {
+	if step.WorkflowShell == "" {
 		if _, ok := rc.JobContainer.(*container.HostEnvironment); ok {
 			shellWithFallback := []string{"bash", "sh"}
 			// Don't use bash on windows by default, if not using a docker container
@@ -196,6 +198,8 @@ func (sr *stepRun) setupShell(ctx context.Context) {
 			// Currently only linux containers are supported, use sh by default like actions/runner
 			step.Shell = "sh"
 		}
+	} else {
+		step.Shell = step.WorkflowShell
 	}
 }
 


Test patch:
diff --git a/pkg/model/workflow_test.go b/pkg/model/workflow_test.go
index 03f29fd6967..92de8ca33fe 100644
--- a/pkg/model/workflow_test.go
+++ b/pkg/model/workflow_test.go
@@ -397,15 +397,18 @@ func TestReadWorkflow_Strategy(t *testing.T) {
 func TestStep_ShellCommand(t *testing.T) {
 	tests := []struct {
 		shell string
+		workflowShell string
 		want  string
 	}{
-		{"pwsh -v '. {0}'", "pwsh -v '. {0}'"},
-		{"pwsh", "pwsh -command . '{0}'"},
-		{"powershell", "powershell -command . '{0}'"},
+		{"pwsh -v '. {0}'", "", "pwsh -v '. {0}'"},
+		{"pwsh", "", "pwsh -command . '{0}'"},
+		{"powershell", "", "powershell -command . '{0}'"},
+		{"bash", "", "bash -e {0}"},
+		{"bash", "bash", "bash --noprofile --norc -e -o pipefail {0}"},
 	}
 	for _, tt := range tests {
 		t.Run(tt.shell, func(t *testing.T) {
-			got := (&Step{Shell: tt.shell}).ShellCommand()
+			got := (&Step{Shell: tt.shell, WorkflowShell: tt.workflowShell}).ShellCommand()
 			assert.Equal(t, got, tt.want)
 		})
 	}
diff --git a/pkg/runner/run_context_test.go b/pkg/runner/run_context_test.go
index 692b4eefa75..0656aad881c 100644
--- a/pkg/runner/run_context_test.go
+++ b/pkg/runner/run_context_test.go
@@ -387,15 +387,15 @@ func TestGetGitHubContext(t *testing.T) {
 		owner = o
 	}
 
-	assert.Equal(t, ghc.RunID, "1")
-	assert.Equal(t, ghc.RunNumber, "1")
-	assert.Equal(t, ghc.RetentionDays, "0")
-	assert.Equal(t, ghc.Actor, actor)
-	assert.Equal(t, ghc.Repository, repo)
-	assert.Equal(t, ghc.RepositoryOwner, owner)
-	assert.Equal(t, ghc.RunnerPerflog, "/dev/null")
-	assert.Equal(t, ghc.Token, rc.Config.Secrets["GITHUB_TOKEN"])
-	assert.Equal(t, ghc.Job, "job1")
+	assert.Equal(t, "1", ghc.RunID)
+	assert.Equal(t, "1", ghc.RunNumber)
+	assert.Equal(t, "0", ghc.RetentionDays)
+	assert.Equal(t, actor, ghc.Actor)
+	assert.Equal(t, repo, ghc.Repository)
+	assert.Equal(t, owner, ghc.RepositoryOwner)
+	assert.Equal(t, "/dev/null", ghc.RunnerPerflog)
+	assert.Equal(t, rc.Config.Secrets["GITHUB_TOKEN"], ghc.Token)
+	assert.Equal(t, "job1", ghc.Job)
 }
 
 func TestGetGithubContextRef(t *testing.T) {