It is common in a pipeline to have operational steps share data. Typically that’s in the form of an output from one step, and an input to another step. With GitHub Actions, this might be trickier than expected if you are working with multiline strings. Let’s take a look at a few points.
Single line output
When dealing with single line output, we can leverage the set-output
syntax for a job step:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
on:
push:
branch:
- '*'
jobs:
test_strings:
name: test strings
runs-on: ubuntu-latest
steps:
- name: create string
run: |
MY_STRING="hello world"
echo "::set-output name=content::$MY_STRING"
id: my_string
- name: display string
run: |
echo "The string is: ${{ steps.my_string.outputs.content }}"
To output this data, we echo the format string with ::set-output name=<output_name>::<output_content>
. And to consume this data as input we can reference it with ${{ steps.<step_id>.outputs.<output_name> }}
. This works as expected:
Multiline output (failed attempt)
With the multiline output, you might be tempted to try the following similar approach to single line strings:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
steps:
- name: create string
run: |
MY_STRING=$(cat << EOF
first line
second line
third line
EOF
)
echo "::set-output name=content::$MY_STRING"
id: my_string
- name: display string
run: |
echo "The string is: ${{ steps.my_string.outputs.content }}"
With this form, only the first line of the output would be transferred (which is very likely the undesired behavior):
That is because the set-output
notation only works on single line input. So how do we get around this behavior and transfer multiline output to different steps?
Option 1 - string substitution
One of the ways that we can circumvent this problem is to change this multiline string to a single line string, just like the first example. This solution was highlighted in this community post. We can escape a few characters on output that the runners will then expand on input:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
steps:
- name: create string
run: |
MY_STRING=$(cat << EOF
first line
second line
third line
EOF
)
MY_STRING="${MY_STRING//'%'/'%25'}"
MY_STRING="${MY_STRING//$'\n'/'%0A'}"
MY_STRING="${MY_STRING//$'\r'/'%0D'}"
echo "::set-output name=content::$MY_STRING"
id: my_string
- name: display string
run: |
echo "The string is: ${{ steps.my_string.outputs.content }}"
The part of this solution to focus on is that we’re substituting the %
, \n
, and \r
characters:
1
2
3
MY_STRING="${MY_STRING//'%'/'%25'}"
MY_STRING="${MY_STRING//$'\n'/'%0A'}"
MY_STRING="${MY_STRING//$'\r'/'%0D'}"
This is essentially turning this multiline string into a single line string with substitution. We get the desired data transfer:
Option 2 - environment variable
Another solution is to instead to pass the multiline string through an environment variable. The way to do that is through pushing the raw data through $GITHUB_ENV
. Take note here how literal we need to be:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
steps:
- name: create string
run: |
MY_STRING=$(cat << EOF
first line
second line
third line
EOF
)
echo "MY_STRING<<EOF" >> $GITHUB_ENV
echo "$MY_STRING" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
id: my_string
- name: display string
run: |
echo "The string is: ${{ env.MY_STRING }}"
With this approach we completely deviate from the set-output
notation and instead use environment variables. Here we want to focus on this:
1
2
3
echo "MY_STRING<<EOF" >> $GITHUB_ENV
echo "$MY_STRING" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
We’re constructing a here document and pushing it incrementally to $GITHUB_ENV
. By doing this, we are then able to reference this multiline string that is stored in the environment variable as input with ${{ env.<environment_variable> }}
(in this example it is ${{ env.MY_STRING }}
). The behavior is as desired:
Summary
Illustrated here are two ways you can approach passing multiline data between GitHub Actions steps. Using environment variables is more elegant in my opinion because it is much easier to remember than the string substitution (which would most likely be a copy/paste solution). Hopefully this blog post has helped clear up any confusion!