Capturing multiline output and exit status in a Github Action with bash

You can capture output from a Github Action by echoing a variable name to the file specified by the "$GITHUB_OUTPUT" variable:

echo "foobar='here is some data'" | tee -a "$GITHUB_OUTPUT"

This is a bit trickier with multiline output. You can use the bash EOF syntax for multiline strings:

FOOBAR<<EOF
here
is
some
multiline
data
EOF

The easiest way to use bash multiline strings in a Github Action is to use a bash command group with curly braces.

To be able to experiment with this locally, match the Github Action bash settings and allow piping to a file specified in the "$GITHUB_OUTPUT" bash variable:

set -e -o pipefail  # match Github Action bash settings
export GITHUB_OUTPUT=/dev/null  # allow piping to "$GITHUB_OUTPUT"
{
  echo 'FOOBAR<<EOF'
  for i in {1..5}; do echo $i; done
  echo 'EOF'
} | tee -a "$GITHUB_OUTPUT"

We’re using | tee -a instead of >> so that we can see the output in the terminal history, which is useful for debugging.

Capturing multiline stderr output in bash in a Github Action

What about if you also want the stderr output from the command? The easiest way to also capture stderr is to redirect it to stdout so you get both together. Because Github Actions have set -e -o pipefail by default, we need to run set +e in the Github Action so that it continues even after a non-zero exit status.

set +e  # Continue even after a non-zero exit status

For example if we want to capture the multiline stderr from the isort Python linter:

set +e

{
  echo 'FOOBAR<<EOF'
  isort . --check 2>&1
  echo 'EOF'
} | tee -a "$GITHUB_OUTPUT"

Keeping exit status and multiline output in bash in a Github Action

What about if you also want to keep the exit status from the command? In the example with isort above, the exit status (accessible as $?) is going to be 0, because the exit status of the command group as a whole comes from the final echo:

set +e

{
  false  # exit status 1
  echo "anything"  # exit status 0
} | tee -a "$GITHUB_OUTPUT"

echo $?  # 0

We can capture the exit status of the command into a local shell variable first and then access it later. However, because it’s a local shell variable, it would be local to the command group, which would make it inaccessible outside the command group. We’ll have to run multiple echo commands to use EOF for the multiline output and still be able to access the local shell variable with the exit status afterwards.

set +e

echo "COMMAND_OUT<<EOF" | tee "$GITHUB_OUTPUT"
isort . --check 2>&1 | tee -a "$GITHUB_OUTPUT" ; COMMAND_STATUS=$?
( echo ; echo "EOF" ) | tee -a "$GITHUB_OUTPUT"
echo $COMMAND_STATUS

This lets us capture the multiline stderr output from isort as a Github Action output called COMMAND_OUT, and then use the exit status from isort as necessary. If you want the Github Action step to use the exit status from isort, then you can exit $COMMAND_STATUS at the end of the bash script in that step:

set +e

echo "COMMAND_OUT<<EOF" | tee "$GITHUB_OUTPUT"
isort . --check 2>&1 | tee -a "$GITHUB_OUTPUT" ; COMMAND_STATUS=$?
( echo ; echo "EOF" ) | tee -a "$GITHUB_OUTPUT"
echo $COMMAND_STATUS
exit $COMMAND_STATUS

See also: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#multiline-strings


Tech mentioned