Do More with Workflow Commands for GitHub Actions

Janne Kemppainen |

If you’ve read my Gentle Introduction to GitHub Actions you should already have a good understanding of the GitHub Actions basics. Creating a workflow is usually quite simple as long as you can find suitable action implementations for your steps. In some cases you may need to write your own actions, but you can actually get quite far with workflow commands.

An important thing you need to remember when working with workflow commands is that all values are always passed around as strings.

What are workflow commands?

Workflow commands are a way to communicate with the GitHub Actions runner machine by printing specially formatted lines of text to the console, or by modifying a file. You can use them quite easily with shell commands, or even inside your custom actions.

For JavaScript actions the @actions/core library takes care of the proper message formatting for you. If your action calls an external script written in another language then you need to follow the correct formatting to interact with the runner.

Workflow commands allow you to share information between workflow steps, print messages with different severity levels to the log (debug, warning, error), set environment variables, and add to system path.

Commands that are printed to the console should have this special formatting:

::command-name parameter1=data1,parameter2=data2::value

Some commands omit the parameters, or even the value, depending on their function.

Note that your whole command must be on a single line, and characters that interfere with the parsing (comma, newline, etc.) need to be URL encoded. You can use the following table to implement replacement rules for the characters.

CharacterEncoded valueScope
%%25parameter, value
\r%0Dparameter, value
\n%0Aparameter, value

So in short, first encode possible percent signs, then carriage returns and newline characters. For the command parameters you also need to escape colons and commas.

For example, this

This text spans
multiple lines

should be encoded as

This text spans%0Amultiple lines

Correctly parsed commands won’t be visible on the workflow log.

Inputs and outputs

Inputs and outputs allow you to easily share information between steps. Inputs are defined in the with section of a step, and outputs become available for later steps if you define an id for the step with the output.

Outputs can be set with the set-output command. It needs an argument, name, to set the output name, and the actual value after the last two colons. In this workflow snippet the run command calls git show to fetch the commit author name and sets it as an output by printing the result as a workflow command.

- name: Get author name
  id: author
  run: echo "::set-output name=author::$(git show -s --format=%an)"

If I made a commit the step would print ::set-output name=author::Janne Kemppainen, which would then parsed as a workflow command by the runner environment. Because I’ve assigned an ID for the step I could access the value in other steps with a normal substitution ${{ }}.

Here’s a quick explanation of the actual command if you’re not that familiar with Bash. The echo command is used to print stuff to the console. The $(...) part evaluates the expression inside the brackets before the whole result is echoed, so the output of git show is placed as the value of the workflow command. You should be able to easily adjust this to your own use cases too as long as the command won’t print out multiple lines.

Inputs are defined in the with section of a step. You can access them in your action via environment variables that are formatted as INPUT_{NAME}. Remember to convert the name of the input to uppercase when you try to access it. So if a workflow calls your custom action like this:

- uses: myaccount/my-action@v1
    argument: Some value

then your action implementation needs to read an environment variable called INPUT_ARGUMENT, which would in this case contain “Some value”. How to actually do this depends on your programming language.

Note that run cannot be used with the with keyword, so this way of handling the input parameters doesn’t work when you run commands directly. Instead, you will need to use env to set an environment variable manually, or do a direct replacement on the command line:

name: Inputs and outputs


    runs-on: ubuntu-latest
      - uses: actions/checkout@v2
      - name: Get author name
        id: author
        run: echo "::set-output name=author::$(git show -s --format=%an)"
      - name: Print author name
        run: echo "${{ }}"
      - name: Print author name with an environment variable
        run: echo $AUTHOR_NAME
          AUTHOR_NAME: ${{ }}

There is a small distinction between these two ways. In the first version the output is directly replaced before running the command, as you can see from the image below. The second version uses an environment variable as an intermediate storage. Both versions still work essentially the same but use the second option if you don’t want to see the actual value in the command log (when you pass in an API key, for example).


Printing with different severity levels

When you print or echo something that does not match a workflow command it will appear in the workflow log as normal text. You can think of this as info level logging. For other severities you need to use workflow commands.

Remember to percent encode the newline characters when printing multiline messages!

Debug level messages contain information that you want to show only when you need to find an what’s causing an issue in a workflow. They become visible only after you have enabled debug logging. Sending debug messages is easy:

echo "::debug::This is a debug message"

Warning messages appear with yellow a yellow highlight and the text “warning”. Warnings are also shown on the annotations list on the job status page. You can create a warning message without any arguments, or include file details like shown below:

echo "::warning,line=1,col=7::Unused import 'os'"

Errors work in the exact same way but the highlight color is red.

echo "::error,line=5,col=4::Command failed"

This snippet uses all logging formats.

- name: Log levels
  run: |
    echo "::debug::This is a debug message"
    echo "This is an info message"
    echo "::warning::This is a warning"
    echo "::error::This is an error"    

And this is how they show up on the workflow log.


Log grouping

If you expect to log lots of content you should definitely know about the grouping functionality. The group and endgroup commands let you combine output in an expandable section so that you don’t have to scroll so much when you’re not interested in a particular part of the log output.

This example shows how it works in practice:

- name: Log grouping
  run: |
    echo "::group::Group name"
    echo "Log entry"
    echo "::warning::Warning message"
    echo "::endgroup::"
    echo "Outside the group"    

You can name the group by changing the value of the group command. The endgroup command doesn’t take any arguments but it ends the latest open group.

The group can be expanded when needed, it is hidden by default.


Mask sensitive data

When you use repository secrets GitHub will automatically mask their values out from the log if you try to print them. However, your action may also use sensitive data from other sources, for example in the form of temporary API keys.

Use the add-mask command if you want to be sure that you don’t leak sensitive information to the log output. You can mask a string value directly, or use an environment variable.

- name: Mask sensitive data
  run: |
    echo "::add-mask::password123"
    echo "My password is password123"
    echo "::add-mask::$SUPER_SECRET"
    echo "Here's a secret for you: $SUPER_SECRET"    
    SUPER_SECRET: The cake is a lie.

The secret data will be replaced with ***, preventing you from accidentally leaking it to the logs.


Note that because in this example I have explicitly written the secrets in the commands and the environment variables they are visible in the step information before the actual ouptut. Never commit sensitive data to the repository!

Add to system path

The PATH environment variable tells all the places where the computer tries to find executables when it only gets the executable name. If your step installs an executable in a non-standard location you will have to edit the system path to make it findable.

The $GITHUB_PATH environment variable holds the path to a file that you can use to add entries to the system path. The changes will become available for all later steps, but not the current one.

Assume that you copied a custom executable to /usr/local/myapp/myapp and want to be able to call myapp in later steps. You have to add /usr/local/myapp to the system path like this:

- name: Add to path
  run: |
        echo "/usr/local/myapp" >> $GITHUB_PATH

This will add your custom path to the end of the file that the action runner reads before starting to execute the step.

If you’re using another programming language you can read the filename from the environment variable and use the native file system functions to append new entries. Here’s a simple Python equivalent>

def add_path(path):
    with open(os.getenv("GITHUB_PATH"), "a") as github_path:
        print(path, file=github_path)

This one is taken from my Run Python Script action that lets you write Python code directly inside the workflow YAML file. You can check the action implementation to see those workflow commands in actual use, the implementation should be relatively easy to follow (the utility Python script is located in the dist directory).


The GitHub Actions workflow commands aren’t that difficult when you get used to how they are formatted, or which files you need to edit. Go and create some useful workflows with your newfound skills!

Discuss on Twitter

Subscribe to my newsletter

What’s new with PäksTech? Subscribe to receive occasional emails where I will sum up stuff that has happened at the blog and what may be coming next.

powered by TinyLetter | Privacy Policy