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 thebashshell 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.cmdextension to the generated temporary script.powershell: The Windows PowerShell Desktop version. GitHub appends the.ps1extension to the script name.python: Executes the provided script using thepythoncommand installed on the runner.
Technical Implementation of Overrides
The shell keyword can be applied at different granularities to manage the execution environment.
- Step-Level Override: The most specific override. Placing
shell: bashwithin a step ensures only that step uses the specified interpreter. - Job-Level Defaults: Using
jobs.<job_id>.defaults.run.shellallows a developer to set a uniform shell for all steps within a specific job. - 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-directoryensures 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
bashwith a fallback tosh. - Windows platforms default to
pwshas of October 2019. - Use
shell: cmdon Windows to avoid failures in legacy batch scripts. - Use
shell: bashexplicitly when running jobs inside containers to avoid the defaultshbehavior. - The
shell: pythonoption executes code as a Python script. - Custom shells require the
{0}placeholder to tell GitHub where to insert the temporary script file. - The
working-directoryparameter 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.