DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Vincent A. Cicirello
Vincent A. Cicirello

Posted on

How to Patch the Deprecated set-output in GitHub Workflows and in Container Actions

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

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}"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 }}"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Later steps access it in the same way as before with:

    - name: Use prior output
      run: echo "The count was ${{ steps.stepid.outputs.count }}"
Enter fullscreen mode Exit fullscreen mode

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/}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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:

GitHub logo cicirello / jacoco-badge-generator

Coverage badges, and pull request coverage checks, from JaCoCo reports in GitHub Actions

jacoco-badge-generator

cicirello/jacoco-badge-generator - Coverage badges, and pull request coverage checks, from JaCoCo reports in GitHub Actions

Check out all of our GitHub Actions: https://actions.cicirello.org/

About

GitHub Actions GitHub release (latest by date) Count of Action Users
Command-Line Utility PyPI PyPI Downloads/month PyPI Downloads/week
Build Status build CodeQL
Security Snyk security score
Source Info License GitHub top language
Support GitHub Sponsors Liberapay Ko-Fi

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):

GitHub logo cicirello / Chips-n-Salsa

A Java library of Customizable, Hybridizable, Iterative, Parallel, Stochastic, and Self-Adaptive Local Search Algorithms

Chips-n-Salsa - A Java library of customizable, hybridizable, iterative, parallel, stochastic, and self-adaptive local search algorithms

Chips-n-Salsa Mentioned in Awesome Machine Learning

Copyright (C) 2002-2022 Vincent A. Cicirello.

Website: https://chips-n-salsa.cicirello.org/

API documentation: https://chips-n-salsa.cicirello.org/api/

Publications About the Library DOI
Packages and Releases Maven Central GitHub release (latest by date) JitPack
Build Status build docs CodeQL
JaCoCo Test Coverage coverage branches coverage
Security Snyk security score Snyk Known Vulnerabilities
DOI DOI
License GitHub
Support GitHub Sponsors Liberapay Ko-Fi

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. For…

Where You Can Find Me

Follow me here on DEV:

Follow me on GitHub:

GitHub logo cicirello / cicirello

My GitHub Profile

Vincent A Cicirello

Vincent A. Cicirello

Sites where you can find me or my work
Web and social media Personal Website LinkedIn DEV Profile
Software development Github Maven Central PyPI Docker Hub
Publications Google Scholar ORCID DBLP ACM Digital Library IEEE Xplore ResearchGate arXiv

My bibliometrics

My 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:

Vincent A. Cicirello - Professor of Computer Science

Vincent A. Cicirello - Professor of Computer Science at Stockton University - is a researcher in artificial intelligence, evolutionary computation, swarm intelligence, and computational intelligence, with a Ph.D. in Robotics from Carnegie Mellon University. He is an ACM Senior Member, IEEE Senior Member, AAAI Life Member, EAI Distinguished Member, and SIAM Member.

favicon cicirello.org

Top comments (0)

Create an Account!

πŸ‘€ Just want to lurk?

That's fine, you can still create an account and turn on features like 🌚 dark mode.