Orchestrating Shell Execution in GitHub Actions

GitHub Actions serves as a robust automation engine within the GitHub ecosystem, designed to streamline development workflows by automating repetitive tasks such as testing, building, and deploying code. At the heart of this automation lies the ability to execute custom logic, primarily through shell scripts. While GitHub Actions provides predefined actions for common tasks, the true power of the platform is unlocked when developers utilize shell scripts—specifically Bash on Linux and macOS, and PowerShell on Windows—to tailor workflows to precise requirements. Understanding the mechanics of shell execution, from default environment behaviors to advanced script consolidation techniques, is essential for creating efficient, maintainable, and scalable CI/CD pipelines.

Default Shells and Runner Environments

When a workflow step utilizes the run keyword, it initiates a new process and shell within the runner environment. A runner is a server equipped with the GitHub Actions runner application, functioning similarly to hosted agents in other CI/CD platforms like Azure DevOps. These runners listen for available jobs, execute them one at a time, and report progress, logs, and results back to GitHub. GitHub provides hosted runners based on Ubuntu Linux, Microsoft Windows, and macOS, where each job operates within a fresh virtual environment. For organizations requiring specific hardware configurations or operating systems not available on hosted runners, self-hosted runners offer an alternative solution.

The shell used to execute commands depends heavily on the runner environment's operating system. GitHub Actions determines the default shell based on the runs-on value specified in the job configuration. On non-Windows platforms, such as Ubuntu Linux and macOS, the default shell is bash, with sh serving as a fallback. Conversely, on Windows platforms, the default shell is pwsh, which refers to PowerShell Core. If a self-hosted Windows runner lacks PowerShell Core, the system falls back to PowerShell Desktop. When specifying bash on a Windows runner, GitHub Actions utilizes the Bash shell included with Git for Windows.

Platform Default Shell Description
Windows pwsh PowerShell Core is the default. GitHub appends the .ps1 extension to script names. If PowerShell Core is missing on a self-hosted runner, PowerShell Desktop is used.
Non-Windows bash The default shell for Linux and macOS. Falls back to sh if necessary. On Windows, this invokes the Bash shell from Git for Windows.

Configuring Shell Defaults

While the runner environment dictates the default shell, GitHub Actions allows users to override these defaults at various levels of the workflow configuration. This flexibility is crucial when a workflow involves jobs running on different operating systems or when specific steps require a different shell language than the job's default. Default settings can be defined at the workflow level or the job level. When multiple default settings with the same name are defined, GitHub resolves the conflict by using the most specific setting. For instance, a default setting defined within a specific job will override a default setting defined at the workflow level.

Consider a scenario where a workflow includes a job running on windows-latest. By default, commands will execute in PowerShell Core. However, if a developer wishes to enforce PowerShell Core explicitly or switch to a different shell for specific steps, they can define the shell property under the defaults key. The following configuration demonstrates setting the default shell to pwsh for a job named name-of-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 this example, the run keyword executes the multi-line command write-output "Hello World". Since write-output is a PowerShell command, it runs successfully because the job is configured to use pwsh as the shell. Each line within a multi-line run block executes in the same shell, preserving environment state and variables across lines.

Executing Custom Bash Scripts

Bash scripts serve as collections of commands that outline the precise course of action for GitHub Actions. They provide developers with the freedom to customize actions to meet exact specifications, facilitating task automation and simplifying complex development processes. A common use case involves compiling code, executing tests, or launching applications through a single, cohesive script. To implement this, developers create a repository on GitHub and store their Bash script, such as bash.sh, within the repository structure. The workflow file, typically named blank.yml or similar, is then configured to invoke this script.

The following example illustrates a basic workflow named "Bash Script" that triggers manually via workflow_dispatch. The workflow checks out the code and then executes the external Bash script:

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 run: bash bash.sh command explicitly calls the Bash interpreter to execute the script file. This approach allows developers to maintain complex logic in separate script files, keeping the YAML workflow configuration clean and readable. The workflow dispatch trigger ensures that the script only runs when manually initiated, providing control over when automation tasks are performed.

Consolidating Commands into Shell Scripts

Running multiple commands as separate run steps can lead to cluttered and difficult-to-manage workflow files. When each command is split into its own step, the YAML file becomes lengthy, repetitive, and harder to maintain. Consolidating these commands into a single shell script offers significant advantages, including improved readability, reduced duplication, and easier reusability. This pattern is applicable to any multi-step process, such as testing, building, or deployment, allowing developers to swap commands within the script as needed.

To implement this, a file such as ascii-script.sh can be added to the root of the repository. This script contains all the necessary commands bundled together. After creating the script, it must be committed to the repository. The workflow is then refactored to invoke this single script instead of listing individual commands. This approach simplifies maintenance and makes the workflow logic more transparent.

However, executing external scripts introduces potential permission issues. If a script lacks execute permissions, the runner may return an exit code 126, indicating "Permission denied." To prevent this, developers must ensure the script is executable. This can be achieved by setting permissions locally using chmod +x ascii-script.sh before committing the file. Alternatively, permissions can be granted at runtime within the workflow by adding a step to update the file permissions before execution:

yaml - name: Grant execute permissions run: chmod +x ascii-script.sh - name: Run script run: ./ascii-script.sh

By committing and pushing these changes, the workflow restarts with the necessary permissions to execute the script successfully.

Managing Environment Variables and Files

GitHub Actions provides mechanisms to pass data between steps and store metadata using environment variables and files. These variables and files are accessible and editable via GitHub's default environment variables. When writing to these files, it is critical to use UTF-8 encoding to ensure proper processing of commands. Multiple commands can be written to the same file, separated by newlines.

Environment variables are set by appending values to specific files, such as $GITHUB_ENV. The following example demonstrates setting an environment variable MY_ENV_VAR and then using it in a subsequent step:

yaml name: Example Workflow for Environment Files on: push jobs: set_and_use_env_vars: runs-on: ubuntu-latest steps: - name: Set environment variable run: echo "MY_ENV_VAR=myValue" >> $GITHUB_ENV - name: Use environment variable run: | echo "The value of MY_ENV_VAR is $MY_ENV_VAR"

Environment variables are also useful for storing metadata such as build timestamps, commit SHAs, or artifact names. For instance, a build timestamp can be captured and used in later deployment steps:

yaml steps: - name: Store build timestamp run: echo "BUILD_TIME=$(date +'%T')" >> $GITHUB_ENV - name: Deploy using stored timestamp run: echo "Deploying at $BUILD_TIME"

Encoding considerations are particularly important when working with PowerShell. PowerShell versions 5.1 and below (specified as shell: powershell) do not use UTF-8 by default. To ensure compatibility, developers must explicitly specify UTF-8 encoding when writing to files like $env:GITHUB_PATH:

yaml jobs: legacy-powershell-example: runs-on: windows-latest steps: - shell: powershell run: | "mypath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

In contrast, PowerShell Core versions 6 and higher (specified as shell: pwsh) use UTF-8 by default, reducing the need for explicit encoding specifications in modern workflows.

Conclusion

Executing shell scripts in GitHub Actions is a foundational skill for DevOps engineers and developers seeking to automate complex workflows. By understanding the default shell behaviors across different runner environments, configuring shell defaults effectively, and consolidating commands into reusable scripts, teams can create robust and maintainable CI/CD pipelines. Furthermore, managing environment variables and file encoding ensures that data is passed correctly between steps, enabling dynamic and context-aware automation. Whether using Bash on Linux or PowerShell on Windows, the flexibility of shell execution allows GitHub Actions to adapt to a wide variety of development needs, streamlining the path from code commit to production deployment.

Sources

  1. KodeKloud Notes: Executing Shell Scripts in Workflow
  2. GeeksforGeeks: Run Bash Script in GitHub Actions
  3. Dev.to: GitHub Actions All the Shells
  4. GitHub Docs: Workflow Commands

Related Posts