TL;DR: There is a new way for GitHub Container Actions, as well as workflow steps to produce outputs. This post explains how to patch container actions, including example Python functions, while maintaining backwards compatibility for non-upgraded self-hosted runners. Post also explains the new approach to workflow step outputs, using a workflow for a Java library deployment as an example.
Table of Contents: This post is organized as follows:
- Introduction
- Updated Workflow Step Outputs
- Example: Version Number for a Java Library Deployment
- Outputs from a Container Action Implemented in Python
- How to Enable Backwards Compatibility for Self-Hosted Runners
- Repositories Referenced in Examples
- Where You Can Find Me
Introduction
Within the last week or so, I started noticing deprecation warnings in the logs of my GitHub Actions workflow runs. At first, I assumed it was just a couple of my repositories, perhaps due to an action I was using. When I got around to investigating, I discovered the issue, which affected 21 repositories in different ways. GitHub recently deprecated the set-output
workflow command on October 11, which had been the way for workflow steps, as well as for container actions, to produce outputs that could be consumed by later steps of a workflow. That command will no longer work by the end of May 2023.
I use GitHub Actions to automate a variety of things in nearly all of my repositories, such as running a build and tests during pull-requests and pushes, deploying artifacts to Maven Central, etc for my Java libraries, or to PyPI for a couple Python projects, building my personal website with my custom static site generator, among a variety of other tasks. In addition to using GitHub Actions for workflow automation, I also develop and maintain a few Actions (all implemented in Python), including jacoco-badge-generator, user-statistician, javadoc-cleanup, and generate-sitemap.
All five GitHub Actions that I maintain were using the set-output
workflow command, mainly for reporting status, which means that all users of those Actions would have begun to see these deprecation notices (if they were to inspect the logs of their workflow runs). And one of those Actions has almost 1000 users (jacoco-badge-generator). And in another 16 of my repositories, I was using the set-output
workflow command in workflows as a way of passing information from one step to later steps. For example, in the deployment workflows for my Java libraries, I have a step that extracts the version from the release tag, setting an output with the version number that is then accessed by subsequent steps.
I patched all of these last week, starting with the five GitHub Actions that I maintain since those affect others. In this post, I explain my approach, including how I've maintained backwards compatibility for users of my actions that use self-hosted runners that still require set-output
for action outputs. For the cases where I was using set-output
in workflows, the fix is exactly as indicated in GitHub's blog post announcing the deprecation. However, I had to adapt this a bit for the five Actions that I maintain. The updated examples in the container action documentation assumes a shell script, but these actions are implemented in Python. Later in this post, I also include Python functions that implement GitHub's replacement for the deprecated set-output
. The first version of these strictly replaces the deprecated command. But then later I show my trick to maintain backwards compatibility.
Updated Workflow Step Outputs
I'm going to begin with the simpler case of patching workflows since that applies to more people, essentially anyone that uses GitHub Actions regardless of whether or not they develop and maintain any Actions. If you are more interested in how to patch the deprecated command in container actions that you maintain, jump to the section: Outputs from a Container Action Implemented in Python.
A step of a job in a GitHub Actions workflow can produce outputs that can later be consumed by other steps in the workflow.
Old Way
The old way that is now deprecated involved doing something like (this first one is almost directly from GitHub's blog post, but I've added a step id, which is needed to actually use the output later):
- name: Set output
id: stepid
run: echo "::set-output name={name}::{value}"
A more specific example is the following which would set an output named count
to 5
:
- name: Set output
id: stepid
run: echo "::set-output name=count::5"
Later steps of the workflow can then access the value of the output, provided the step has an id
as above, with the following:
- name: Use prior output
run: echo "The count was ${{ steps.stepid.outputs.count }}"
New Way
The replacement for the old set-output
workflow command involves appending to a file whose path is specified in an environment variable $GITHUB_OUTPUT
. The earlier example of setting an output named count
to 5
becomes the following:
- name: Set output
id: stepid
run: echo "count=5" >> $GITHUB_OUTPUT
Later steps access it in the same way as before with:
- name: Use prior output
run: echo "The count was ${{ steps.stepid.outputs.count }}"
Example: Version Number for a Java Library Deployment
In my deployment workflows for my Java libraries, I previously had a step that generated a Maven version string from the release tag by removing the v
from the release tag. That step looked like the following:
- name: Get the release version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
To deal with the deprecation of set-output
, I've updated it to the following:
- name: Get the release version
id: get_version
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
What do I use this for? Well there are a couple steps later in the workflow that use it. For example, one of the steps prior to the actual deployment injects the version for the release into the Maven pom.xml
with:
- name: Update package version
run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }}
And near the end of the workflow, after the actual deployment to Maven Central and GitHub Packages, I use the GitHub CLI to attach the jar files to the GitHub Release. Having the version available in this way makes it easy to determine the filename of the jar:
- name: Upload jar files to release as release assets
run: |
TAG=${GITHUB_REF/refs\/tags\//}
gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}.jar
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The complete workflow file that this example is derived from is maven-publish.yml.
Outputs from a Container Action Implemented in Python
There are two primary ways of implementing a GitHub Action: JavaScript Actions and Container Actions. The latter of which enables implementing Actions in any language via a Docker container. My language of choice for implementing GitHub Actions is Python. The purpose of most of these actions is to produce files (e.g., jacoco-badge-generator produces test coverage badges as SVGs, and generate-sitemap produces an XML sitemap) or to edit files in some way (e.g., javadoc-cleanup can insert canonical links and other user-defined elements into the head of javadoc pages). However, all of these also produce workflow step outputs. For example, generate-sitemap has outputs for the number of pages in the sitemap, and the number of pages excluded from the sitemap due to noindex
or robots.txt
exclusions; and jacoco-badge-generator has workflow step outputs for the coverage and branches coverage percentages if a user had some reason to use those in later steps of their workflow.
Here are three variations depending upon the specifics of what you want to do. All three require that we've imported os
with the following so that we can access environment variables:
import os
Case 1: One Output
The simplest case is if you have a single output for your Action. The following Python function accomplishes this. Notice that we'll need to open the relevant file for appending. If the Action is run outside of GitHub Actions, this function will do nothing since the relevant environment variable with the path to the relevant file for the outputs won't exist in that case.
def set_action_output(output_name, value) :
"""Sets the GitHub Action output.
Keyword arguments:
output_name - The name of the output
value - The value of the output
"""
if "GITHUB_OUTPUT" in os.environ :
with open(os.environ["GITHUB_OUTPUT"], "a") as f :
print("{0}={1}".format(output_name, value), file=f)
Case 2: Multiple Outputs
A couple of my GitHub Actions produce multiple outputs. Although I could use the above function and just call it multiple times, I instead use the following to avoid unnecessarily opening the environment file multiple times. In this version, the function is passed a Python dictionary with the names of the outputs as keys, along with the corresponding values.
def set_action_outputs(output_pairs) :
"""Sets the GitHub Action outputs.
Keyword arguments:
output_pairs - Dictionary of outputs with values
"""
if "GITHUB_OUTPUT" in os.environ :
with open(os.environ["GITHUB_OUTPUT"], "a") as f :
for key, value in output_pairs.items() :
print("{0}={1}".format(key, value), file=f)
Case 3: Multiple Outputs for a GitHub Action Also Usable as a CLI Tool
One of the GitHub Actions that I maintain, the jacoco-badge-generator, can also be used as a CLI tool outside of the Actions framework. Before GitHub deprecated the set-output
, the CLI mode would output the coverage percentages to the console in addition to generating the badges. That prior behavior was really a side-effect of GitHub's old approach to Action outputs (e.g., printing a specially formatted message to standard out). In revising to address the deprecation, I decided to keep a variation of that behavior. If running as a GitHub Action, the outputs are set via an approach much like the above example. But if running in CLI mode, it instead simply outputs them to the console. It uses a simple check of whether an environment variable named GITHUB_OUTPUT
exists to detect whether running as an Action.
Below is my first attempt at accomplishing this.
def set_action_outputs(output_pairs) :
"""Sets the GitHub Action outputs if running as a GitHub Action,
and otherwise logs these to terminal if running in CLI mode. Note
that if the CLI mode is used within a GitHub Actions
workflow, it will be treated the same as GitHub Actions mode.
Keyword arguments:
output_pairs - Dictionary of outputs with values
"""
if "GITHUB_OUTPUT" in os.environ :
with open(os.environ["GITHUB_OUTPUT"], "a") as f :
for key, value in output_pairs.items() :
print("{0}={1}".format(key, value), file=f)
else :
for key, value in output_pairs.items() :
print("{0}={1}".format(key, value))
The above seemed to work fine until I discovered via a report from a user of the Action that some self-hosted runners may not support the new approach to outputs yet. I should have noticed this since GitHub's blog post about the deprecation does explicitly state the following: "If you are using self-hosted runners make sure they are updated to version 2.297.0 or greater." In the next section, I explain my trick to maintain backwards compatibility for these users.
How to Enable Backwards Compatibility for Self-Hosted Runners
In the above section, you saw my initial fix. Those Python functions will suffice if you only want to support those using your Actions directly on GitHub. If they are using self-hosted runners, then whether the above will work depends upon whether your users have upgraded their runners to a version with the new environment file. If you want to avoid breaking things for those users of your Actions who haven't upgraded their runners, one option might be to delay patching and just deal with deprecation warnings. However, it is actually relatively straightforward to include backwards compatibility to support self-hosted runners.
Here is the approach I've ended up with for the jacoco-badge-generator. I really have three cases to support: (1) GitHub Actions with the new GITHUB_OUTPUT
environment file (when running on GitHub or newer self-hosted runners), (2) GitHub Actions on not-yet-upgraded self-hosted runners, and (3) those using the CLI mode of the utility such as on their local system. I added a parameter to my set_action_outputs
function to specify if in GitHub Actions mode, and then check for the GITHUB_OUTPUT
environment variable to determine support for the new approach. It falls back to the deprecated set-output
workflow command if GITHUB_OUTPUT
doesn't exist.
def set_action_outputs(output_pairs, gh_actions_mode) :
"""Sets the GitHub Action outputs if running as a GitHub Action,
and otherwise logs these to terminal if running in CLI mode. Note
that if the CLI mode is used within a GitHub Actions
workflow, it will be treated the same as GitHub Actions mode.
Keyword arguments:
output_pairs - Dictionary of outputs with values
gh_actions_mode - True if running as a GitHub Action, otherwise pass False
"""
if "GITHUB_OUTPUT" in os.environ :
with open(os.environ["GITHUB_OUTPUT"], "a") as f :
for key, value in output_pairs.items() :
print("{0}={1}".format(key, value), file=f)
else :
output_template = "::set-output name={0}::{1}" if gh_actions_mode else "{0}={1}"
for key, value in output_pairs.items() :
print(output_template.format(key, value))
If your GitHub Action is strictly a GitHub Action, then you can simplify the above. First, you won't need the parameter gh_actions_mode
. Simply assume that it is running within GitHub Actions, and use the existence of GITHUB_OUTPUT
to check if the new approach is supported on the runner. Fall-back to set-output
otherwise.
def set_action_outputs(output_pairs) :
"""Sets the GitHub Action outputs, with backwards compatibility for
self-hosted runners without a GITHUB_OUTPUT environment file.
Keyword arguments:
output_pairs - Dictionary of outputs with values
"""
if "GITHUB_OUTPUT" in os.environ :
with open(os.environ["GITHUB_OUTPUT"], "a") as f :
for key, value in output_pairs.items() :
print("{0}={1}".format(key, value), file=f)
else :
for key, value in output_pairs.items() :
print("::set-output name={0}::{1}".format(key, value))
And if your Action only has a single output, you can use something like:
def set_action_output(output_name, value) :
"""Sets the GitHub Action output, with backwards compatibility for
self-hosted runners without a GITHUB_OUTPUT environment file.
Keyword arguments:
output_name - The name of the output
value - The value of the output
"""
if "GITHUB_OUTPUT" in os.environ :
with open(os.environ["GITHUB_OUTPUT"], "a") as f :
print("{0}={1}".format(output_name, value), file=f)
else :
print("::set-output name={0}::{1}".format(output_name, value))
Repositories Referenced in Examples
The Python functions implementing the new approach to GitHub Action outputs, along with backwards compatible support for old runners, from this post are derived from a few different GitHub Actions that I maintain. The most elaborate example for the case of an Action that is designed to also be used outside of Actions as a CLI tool is from the following:
cicirello / jacoco-badge-generator
Coverage badges, and pull request coverage checks, from JaCoCo reports in GitHub Actions
jacoco-badge-generator
Check out all of our GitHub Actions: https://actions.cicirello.org/
About
The jacoco-badge-generator can be used in one of two ways: as a GitHub Action or as a command-line
utility (e.g., such as part of a local build script). The jacoco-badge-generator parses a jacoco.csv
from a JaCoCo coverage report, computes coverage percentages
from JaCoCo's Instructions and Branches counters, and
generates badges for one or both of these (user configurable) to provide an easy
to read visual summary of the code coverage of your test cases. The default behavior directly
generates the badges internally with no external calls, but the action also provides an option
to instead generate Shields JSON endpoints. It supports
both the basic case of a single jacoco.csv
, as well as multi-module projects in which
case the action can produce coverage badges from the combination of…
The workflow examples where I use step outputs to pass the release version to later steps is a technique I use in several Java library repositories. The one I specifically referenced in this post is the following (if you'd like to see the full workflow):
cicirello / Chips-n-Salsa
A Java library of Customizable, Hybridizable, Iterative, Parallel, Stochastic, and Self-Adaptive Local Search Algorithms
Copyright (C) 2002-2024 Vincent A. Cicirello.
Website: https://chips-n-salsa.cicirello.org/
API documentation: https://chips-n-salsa.cicirello.org/api/
How to Cite
If you use this library in your research, please cite the following paper:
Cicirello, V. A., (2020). Chips-n-Salsa: A Java Library of Customizable, Hybridizable, Iterative, Parallel, Stochastic, and Self-Adaptive Local Search Algorithms. Journal of Open Source Software, 5(52), 2448, https://doi.org/10.21105/joss.02448 .
Overview
Chips-n-Salsa is a Java library of customizable, hybridizable, iterative, parallel, stochastic, and self-adaptive local search algorithms. The library includes implementations of several stochastic local search algorithms, including simulated annealing, hill climbers, as well as constructive search algorithms such as stochastic sampling. Chips-n-Salsa now also includes genetic algorithms as well as evolutionary algorithms more generally. The library very extensively supports simulated annealing. It includes several classes for representing solutions to a variety of optimization problems…
Where You Can Find Me
Follow me here on DEV:
Follow me on GitHub:
Vincent A Cicirello
View My Detailed GitHub Activity
If you want to generate the equivalent to the above for your own GitHub profile, check out the cicirello/user-statistician GitHub Action.
Or visit my website:
Top comments (2)
How can this be replaced in javascript based action. I am getting lot of warnings.
According to GitHub's announcement of the deprecation, a JavaScript Action would need to update the version of their dependency on
@actions/core
.So if you are asking related to a GitHub Action that you maintain, then update that package. If it is an Action someone else maintains, check if there is a newer version available than the one you are using. And if not, maybe submit an issue in the repository of the Action.