GitHub Actions serves as a robust automation engine for software development workflows, enabling developers to automate testing, deployment, and alerting processes within their repositories. At the core of these automations are actions, which function as collections of scripting commands that dictate the precise course of action for the workflow. While pre-built actions are available, Bash scripts provide a critical layer of customization, allowing engineers to tailor actions to exact specifications. By leveraging Bash scripts, teams can compile code, execute complex test suites, or launch applications, thereby streamlining development processes and reducing manual overhead. The flexibility offered by shell scripting within GitHub Actions enables the automation of intricate procedures, transforming tedious tasks into efficient, repeatable workflows.
Understanding Runners and Shell Context
To effectively utilize shell scripts in GitHub Actions, it is essential to understand the underlying infrastructure, specifically the concept of runners. A runner is a server with the GitHub Actions runner application installed, functioning similarly to Azure DevOps-hosted agents. Runners can be hosted by GitHub or self-hosted to meet specific hardware or operating system requirements. GitHub-hosted runners are based on Ubuntu Linux, Microsoft Windows, and macOS, with each job running in a fresh virtual environment. Developers can inspect the software installed on these virtual machine images to determine available tools.
Within a workflow job, steps can either be pre-defined actions or shell commands. The run keyword initiates a new process and shell within the runner environment. A critical distinction in GitHub Actions is how multi-line commands are handled: when a multi-line command is provided under a single run step, each line executes within the same shell session. This continuity allows for variable persistence and complex logical flows within a single step.
The default shell language depends on the runner platform. On non-Windows platforms, the default shell is bash, with a fallback to sh. On Windows, the default is pwsh (PowerShell Core). If a self-hosted Windows runner lacks PowerShell Core, the system falls back to PowerShell Desktop. Understanding these defaults is crucial because specifying a shell explicitly can override the runner's default behavior, ensuring compatibility with specific script syntaxes.
| Platform | Default Shell | Description |
|---|---|---|
| Windows | pwsh | PowerShell Core. GitHub appends the .ps1 extension to script names. Falls back to PowerShell Desktop if Core is not installed. |
| non-Windows | bash | Default shell with a fallback to sh. On Windows, this utilizes the Bash shell included with Git for Windows. |
Configuring Default Shells and Overrides
While the runner platform dictates the default shell, developers often need to execute scripts in a specific environment regardless of the host OS. GitHub Actions allows for explicit shell specification using the shell keyword. This can be applied at the workflow level, the job level, or the individual step level. When multiple default settings are defined with the same name, GitHub uses the most specific setting; for instance, a default defined at the job level overrides a workflow-level default.
The defaults.run key provides a mechanism to set the default shell for all run steps within a workflow or a specific job. This reduces redundancy and ensures consistency across multiple steps.
- Setting a default shell for a job
yaml
name: my workflow
on: push
jobs:
name-of-job:
runs-on: windows-latest
defaults:
run:
shell: pwsh
steps:
- name: Hello world
run: |
write-output "Hello World"
In the above example, the runs-on: windows-latest context would normally default to pwsh, but explicitly defining it ensures clarity. If a developer needs to use Bash on a Windows runner, they can specify shell: bash, which will utilize the Bash shell provided by Git for Windows. Conversely, on a Linux runner, one might switch to PowerShell Core using shell: pwsh.
Explicit Shell Selection for Different Languages
GitHub Actions supports a variety of shells, allowing developers to write steps in their preferred language or tooling. This flexibility extends beyond Bash and PowerShell to include Python, Perl, and other command-line interpreters, provided they are installed on the runner.
- Running a script using Bash
yaml
steps:
- name: Display the path
run: echo $PATH
shell: bash
- Running a script using Windows Command Prompt
yaml
steps:
- name: Display the path
run: echo %PATH%
shell: cmd
- Running a script using PowerShell Core
yaml
steps:
- name: Display the path
run: write-output ${env:PATH}
shell: pwsh
- Using PowerShell Desktop
yaml
steps:
- name: Display the path
run: write-output ${env:PATH}
shell: powershell
- Running a Python script
yaml
steps:
- name: Display the environment variables and their values
run: |
import os
print(os.environ['PATH'])
shell: python
- Using a Custom Shell with Perl
yaml
steps:
- name: Display the environment variables and their values
run: |
print %ENV
shell: perl {0}
When using a custom shell, the shell value can be set to a template string. GitHub interprets the first whitespace-delimited word as the command and inserts the filename for the temporary script at {0}. This mechanism allows for the execution of any executable available on the runner, such as perl {0}, provided the binary is installed in the runner's environment.
Implementing Bash Scripts in Workflows
Creating a custom action with a Bash script involves a straightforward process that integrates user-defined scripts into the GitHub Actions YAML configuration. The workflow begins with the creation of a repository and the definition of an action file.
Step 1: Create the repository on GitHub.
Step 2: Create the action file under the workflows directory, typically named something like blank.yml.
Step 3: Store the Bash script in a file within the repository, such as bash.sh.
The YAML workflow file serves as the orchestration layer. It identifies the workflow by name, specifies the trigger events, and defines the jobs and steps.
yaml
name: Bash Script
on:
workflow_dispatch:
jobs:
bash-script:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run Bash script
run: bash bash.sh
In this configuration, the name field identifies the workflow, serving as a label for identification without affecting functionality. The on field specifies the trigger event; workflow_dispatch allows manual triggering. The job runs on ubuntu-latest, which defaults to Bash. The step run: bash bash.sh executes the custom script. This approach allows developers to encapsulate complex logic in standard Bash files, keeping the YAML configuration clean and modular.
Advanced Bash Techniques in GitHub Actions
Beyond simple script execution, Bash scripts within GitHub Actions can leverage advanced shell features for data manipulation and API interaction. A common use case involves extracting information from GitHub-provided environment variables or making calls to the GitHub GraphQL API.
Consider a scenario where a workflow needs to extract the owner and repository name from the GITHUB_REPOSITORY environment variable, which is formatted as owner/repository. This can be achieved efficiently using the Internal Field Separator (IFS) and the read command.
bash
IFS='/' read -r OWNER REPOSITORY <<< "$GITHUB_REPOSITORY"
This line splits the value of GITHUB_REPOSITORY by the / character and assigns the resulting parts to the variables OWNER and REPOSITORY. This technique is valuable for context-aware scripts that need to operate on specific repository metadata.
Another advanced pattern involves extracting specific data points from complex strings or API responses. For instance, extracting a branch name from a ref string using awk.
bash
HEADREFNAME=$(echo ${{ github.event.ref }} | awk -F'/' '{print $NF}')
Here, awk uses / as the field separator (-F'/') and prints the last field ($NF), effectively isolating the branch name from the full reference path.
When combined with API calls, these shell techniques enable powerful automation. A workflow might use curl to send a GraphQL query to the GitHub API, authenticate using a secret token, and then use jq to parse the JSON response.
bash
PR_ID=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-X POST \
-d "{\"query\": ... }" \
"$GITHUB_GRAPHQL_URL" \
| jq '.data.repository.pullRequests.nodes[].number' \
)
This sequence demonstrates how Bash scripts can integrate with external services and parse structured data, all within a single workflow step. The use of shell: bash ensures that these Linux-oriented commands execute correctly, regardless of the runner's default shell.
Conclusion
GitHub Actions provides a versatile platform for CI/CD automation, with shell scripting serving as the backbone for custom logic and task execution. By understanding the interplay between runners, default shells, and explicit shell overrides, developers can craft robust and portable workflows. Whether utilizing the default Bash environment on Linux, PowerShell on Windows, or custom interpreters like Python and Perl, the ability to specify the shell context ensures compatibility and precision. Advanced Bash techniques, such as variable splitting with IFS and data parsing with awk and jq, further enhance the utility of these scripts, enabling complex data extraction and API interactions. As organizations continue to adopt GitHub Actions, mastering these shell configurations becomes essential for optimizing development efficiency and automating intricate procedural workflows.