GitHub Actions Shell Execution Environments

The orchestration of continuous integration and continuous deployment (CI/CD) pipelines within GitHub Actions relies heavily on the interpretation of commands provided in the run step. Central to this process is the "shell," the command-line interpreter that parses and executes the scripts defined in the workflow YAML. Understanding the default shell behavior is critical because the choice of shell dictates the syntax of environment variables, the behavior of pipes, and the availability of specific scripting features. By default, GitHub Actions selects a shell based on the runner's operating system, but this behavior can be overridden at the workflow level, the job level, or the individual step level to ensure cross-platform compatibility or to utilize specific language features.

Default Shell Mapping by Platform

The default shell assigned to a run step is not static; it varies based on the runs-on property of the job. When no shell is explicitly defined, GitHub Actions applies a platform-specific default.

Platform Default Shell Fallback/Details
Non-Windows (Linux/macOS) bash Falls back to sh if bash is not found in the path
Windows pwsh (PowerShell Core) Transitioned from Windows batch to PowerShell on 10/23/2019

On non-Windows platforms, such as ubuntu-latest or macos-latest, the system attempts to use bash. This is the preferred environment for most Linux-based workflows due to its advanced feature set compared to the POSIX-standard sh. However, if bash is absent from the runner's path, the system gracefully degrades to sh.

On Windows runners, the default shell is pwsh (PowerShell Core). This change was formalized on October 23, 2019, to modernize the automation experience on Windows. Before this date, the default was different, and users who relied on legacy Windows batch scripting were advised to explicitly specify cmd to avoid breaking their pipelines.

Explicit Shell Specification and Overrides

Users can override the default shell to gain finer control over the execution environment. This is particularly useful when a developer needs to run a specific script language across different operating systems or when using a runner that does not provide the desired default shell.

Available Shell Options

GitHub Actions supports a wide array of shells that can be explicitly invoked.

  • bash: The default for non-Windows platforms. When specified on Windows, GitHub uses the bash shell included with Git for Windows.
  • pwsh: PowerShell Core. This is the default for Windows but can be explicitly requested on Linux or macOS runners.
  • sh: The basic shell. It is the primary fallback for non-Windows platforms and can be explicitly requested.
  • cmd: The legacy Windows Command Prompt. When this is selected, GitHub appends the .cmd extension to the generated temporary script.
  • powershell: The Windows PowerShell Desktop version. GitHub appends the .ps1 extension to the script name.
  • python: Executes the provided script using the python command installed on the runner.

Technical Implementation of Overrides

The shell keyword can be applied at different granularities to manage the execution environment.

  1. Step-Level Override: The most specific override. Placing shell: bash within a step ensures only that step uses the specified interpreter.
  2. Job-Level Defaults: Using jobs.<job_id>.defaults.run.shell allows a developer to set a uniform shell for all steps within a specific job.
  3. Workflow-Level Defaults: Setting defaults at the top level of the workflow applies the shell to every job and step unless a more specific override exists.

GitHub follows a hierarchy of specificity: a step-level setting overrides a job-level setting, which in turn overrides a workflow-level setting.

Containerized Environments and Shell Discrepancies

A critical technical nuance exists when using the container property in GitHub Actions. While a standard ubuntu-latest runner defaults to bash, the behavior changes when the job is executed inside a Docker container.

The sh Default Issue

When a job is configured to run within a container (e.g., image: python:3.10-bullseye), the default shell often reverts to sh instead of bash. This occurs even if the underlying host runner is Ubuntu. This discrepancy is a known behavior where the runner interacts with the container's shell environment.

The impact of this is significant for developers who use bash-specific syntax (bashisms). If a script relies on [[ ]] for conditional testing or arrays, which are bash features but not supported in sh, the script will fail in a containerized environment unless shell: bash is explicitly declared.

Dockerfile Shell Configuration

It is important to note that specifying the shell within a Dockerfile using the SHELL instruction does not influence the GitHub Actions runner's default choice. Even if a Dockerfile contains:

dockerfile SHELL ["/bin/bash", "--login", "-c"] CMD [ "/bin/bash" ]

The GitHub Actions runner will still default to sh for the run steps unless the YAML configuration explicitly defines shell: bash. This means the Dockerfile configuration only affects the image build process and the default entry point, not the dynamic execution of steps defined in the workflow file.

Advanced Shell Configurations

For scenarios where the built-in shell options are insufficient, GitHub Actions allows for the definition of custom shells through template strings.

Custom Shell Templates

A custom shell can be defined by specifying a command and options, using the {0} placeholder. GitHub replaces {0} with the filename of the temporary script it creates to execute the run block.

The syntax follows this pattern: command […options] {0} [..more_options].

For example, to execute a step using perl, the configuration would be:

yaml - name: Display the environment variables and their values run: | print %ENV shell: perl {0}

In this scenario, the perl interpreter must be pre-installed on the runner image. The runner writes the run content to a file and then invokes perl to execute that file.

Working Directory Management

The shell's execution context is further modified by the working-directory parameter. By default, all shell commands are executed at the root of the repository tree.

The Role of working-directory

The working-directory option allows developers to specify the exact path where the shell should initialize and execute commands. This is an essential feature for monorepo architectures where different services or packages reside in separate subdirectories.

Technical implementation of the working directory can occur at three levels:

  • Step Level:
    ```yaml
  • name: Run build command in scripts directory
    run: npm run build
    working-directory: ./scripts
    `` In this instance,npm run buildis executed within the./scripts` directory rather than the root.

  • Job Level: Using jobs.<job_id>.defaults.run.working-directory ensures all steps in that job start in the specified path.

  • Workflow Level: Setting the working directory at the top level ensures consistency across all jobs.

Practical Shell Implementation Examples

Depending on the requirement, the syntax for accessing environment variables and executing commands changes based on the chosen shell.

Environment Variable Access by Shell

The following table demonstrates how to access the PATH environment variable across different shell configurations:

Shell Command Syntax Technical Note
bash echo $PATH Standard Unix-style variable expansion
cmd echo %PATH% Windows batch style percent-wrapped variables
pwsh write-output ${env:PATH} PowerShell provider syntax for environment variables
python import os; print(os.environ['PATH']) Python os module access

Cross-Platform Implementation Examples

To illustrate the flexibility of the shell keyword, consider a scenario where a user wants to run a PowerShell command on a Linux runner.

yaml jobs: name-of-job: runs-on: ubuntu-latest steps: - name: Hello world shell: pwsh run: | write-output "Hello World"

In this example, although the VM is ubuntu-latest, the shell: pwsh override forces the runner to use PowerShell Core, allowing the use of write-output instead of echo.

Summary of Technical Requirements and Behaviors

The following list outlines the critical requirements and behaviors associated with GitHub Actions shells:

  • Non-Windows platforms default to bash with a fallback to sh.
  • Windows platforms default to pwsh as of October 2019.
  • Use shell: cmd on Windows to avoid failures in legacy batch scripts.
  • Use shell: bash explicitly when running jobs inside containers to avoid the default sh behavior.
  • The shell: python option executes code as a Python script.
  • Custom shells require the {0} placeholder to tell GitHub where to insert the temporary script file.
  • The working-directory parameter can be defined globally, per job, or per step to manage execution paths in complex directory structures.
  • Job-level defaults override workflow-level defaults, and step-level settings override both.

Conclusion

The selection and management of the shell in GitHub Actions is a foundational aspect of pipeline stability. While the default behaviors provide a streamlined experience for most users, the nuances of containerized environments and cross-platform requirements necessitate a deep understanding of shell overrides. The shift toward pwsh on Windows and the propensity for containers to default to sh are the two most common areas where pipeline failures occur due to shell mismatch. By leveraging defaults.run and explicit shell declarations, developers can ensure that their automation is portable and that their scripts are executed by the intended interpreter, regardless of the underlying runner architecture.

Sources

  1. GitHub Actions All the Shells
  2. Actions Steps in Container defaults to sh shell instead of bash #1533
  3. GitHub Actions Working Directory
  4. GitHub Changelog: Default shell on Windows runners is changing to PowerShell

Related Posts