GitHub Actions Set Output - Assure Parallel Job Access is Safe - github

I have configured my github actions which runs tests in parallel for different platforms. At the end of my tests I want the status to be saved to the outputs. Once all jobs complete I have another job that runs to send the results to a slack webhook.
I am having difficulty determining a method to save the output for multiple jobs and assuring there is no issues when they are running in parallel.
For example this is my code snippet
name: Test Notify
on:
push:
jobs:
build:
strategy:
matrix:
config:
- name: 'Ubuntu 18.04'
runner: 'ubuntu-18.04'
id: 'u18'
- name: 'Ubuntu 20.04'
runner: 'ubuntu-20.04'
fail-fast: false
runs-on: ${{ matrix.config.runner }}
outputs:
# Prefer to have one general output I can append to
global: ${{ steps.status.outputs.global }}
# I can output to separate outputs but I rather have a single one as shown above
u18: ${{ steps.status.outputs.u18 }}
u20: ${{ steps.status.outputs.u20 }}
steps:
- name: Test Failure u18
id: step1
if: ${{ matrix.config.id == 'u18' }}
run: |
exit 1
- name: Doing Step 2
id: step2
run: |
echo "DO NOTHING"
- name: Output Status
id: status
if: always()
env:
JOB_STATUS: "${{ job.status }}"
run: |
# This works, but is it safe since I have u18 and u20 running in parallel ?
echo "${{ matrix.config.id }}=$JOB_STATUS" >> $GITHUB_OUTPUT
# Is there a safe way to have a single status string that I add to, for example;
# echo "global=${{ github_output.global}}$JOB_STATUS" >> $GITHUB_OUTPUT
webhook:
needs: build
runs-on: 'ubuntu-20.04'
if: always()
steps:
- name: Send webhook update for all jobs
env:
JSON_RESULTS: "${{ toJSON(needs.build-and-test) }}"
run: |
# Will add code to properly send the information
echo $JSON_RESULTS

Currently, there is no easy way to reference all outputs of matrix jobs. Moreover combining it into a single output.
The issue is that only a single value is available for future jobs that need the strategy.matrix job’s output because even if the output is set by multiple matrix variations of the job, only one is retained.
For more detail, see the Community discussion.
TL;DR:
There are several workarounds:
defining separate outputs for a job with strategy.matrix by letting the job variations set different outputs
then process these outputs in a separate step that can provide single output for further steps
use artifacts to store the matrix jobs' outputs and then post-process it (discussioncomment-3814009)

Related

Sum expression in github action run-name

I am trying to set dynamic name to workflow run that has a number. The number should be calculated using an expression.
run-name: Perform Operation with ID [SOME_GITHUB_VAR + 1]
Is there a way to perform the above sum operation?
Thanks,
The run-name can include some expressions and can reference only the github and inputs contexts:
run-name: Deploy to ${{ inputs.deploy_target }} by #${{ github.actor }}
Unfortunately, GitHub Actions don't support math operations inside expressions. So, it's impossible to perform such a calculation for the value of the run-name option.
It's possible for the name value for a job step. You can add up these two numbers in a script and then set it as an environment variable.
Example:
name: 'Sum expression'
on:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: run number with offset
env:
NUM: ${{ github.run_number }}
run: |
echo "GITHUB_RUN_NUMBER_WITH_OFFSET=$(($NUM + 200))" >> $GITHUB_ENV
- name: 'Perform Operation with ID ${{ env.GITHUB_RUN_NUMBER_WITH_OFFSET }}'
run: echo "Success!"
Result:
For more details, read the Setting an environment variable article.
Reference: How to add two numbers.

GitHub Actions - Reuse outputs from other reusable workflows

I'm not sure if it's possible, but I'm attempting to use outputs from one reusable workflow, in another, that's part of the same caller workflow. For reference, please see the configs below:
Caller Workflow:
jobs:
call-workflow-calver:
uses: ./.github/workflows/called-workflow1.yaml
secrets: inherit
call-workflow-echo:
needs: call-workflow-calver
uses: ./.github/workflows/called-workflow2.yaml
secrets: inherit
Job for creating the CalVer tag (it outputs as $VERSION as part of the action)
Called Workflow 1:
...
jobs:
calver:
name: Create CalVer tag
...
steps:
- name: Calver tag
uses: StephaneBour/actions-calver#1.4.4
if: ${{ github.ref == 'refs/heads/main' }}
id: calVer
with:
date_format: "%Y-%m-%d"
release: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
...
Trying to use the CalVer $VERSION output from another caller workflow
Called Workflow 2:
...
- name: Echo
run: |
echo ${{needs.calver.outputs.VERSION}}
...
The basic concept is I'm trying to use outputs from one reusable workflow in another for setting CalVer versions in workflow 1, calling that in workflow 2 so I can set it as an image version. I will eventually use in a 3rd reusable workflow to deploy said image. If this is possible, that would be great!
Hopefully, this all makes a bit of sense, but if anything needs clarifying, please do let me know!
Many thanks in advance!
According to the official documentation, you can now declare outputs to reusable workflows.
These work just like job outputs and are available via needs.<reusable>.outputs.<output> format once you declare the output.
Example
1. Reusable workflow configuration:
name: Reusable workflow
on:
workflow_call:
# Map the workflow outputs to job outputs
outputs:
firstword:
description: "The first output string"
value: ${{ jobs.example_job.outputs.output1 }}
secondword:
description: "The second output string"
value: ${{ jobs.example_job.outputs.output2 }}
jobs:
example_job:
name: Generate output
runs-on: ubuntu-latest
# Map the job outputs to step outputs
outputs:
output1: ${{ steps.step1.outputs.firstword }}
output2: ${{ steps.step2.outputs.secondword }}
steps:
- id: step1
run: echo "::set-output name=firstword::hello"
- id: step2
run: echo "::set-output name=secondword::world"
2. Workflow using the reusable:
name: Call a reusable workflow and use its outputs
on:
workflow_dispatch:
jobs:
job1:
uses: octo-org/example-repo/.github/workflows/called-workflow.yml#v1
job2:
runs-on: ubuntu-latest
needs: job1
steps:
- run: echo ${{ needs.job1.outputs.firstword }} ${{ needs.job1.outputs.secondword }}
Note that if a reusable workflow that sets an output is executed with a matrix strategy, the output will be the output set by the last successful completing reusable workflow of the matrix which actually sets a value. That means if the last successful completing reusable workflow sets an empty string for its output, and the second last successful completing reusable workflow sets an actual value for its output, the output will contain the value of the second last completing reusable workflow.
I used this workflow as example here if you want to check the logs of the workflow run.

Using output from a previous job in a new one in a GitHub Action

For (mainly) pedagogical reasons, I'm trying to run this workflow in GitHub actions:
name: "We 🎔 Perl"
on:
issues:
types: [opened, edited, milestoned]
jobs:
seasonal_greetings:
runs-on: windows-latest
steps:
- name: Maybe greet
id: maybe-greet
env:
HEY: "Hey you!"
GREETING: "Merry Xmas to you too!"
BODY: ${{ github.event.issue.body }}
run: |
$output=(perl -e 'print ($ENV{BODY} =~ /Merry/)?$ENV{GREETING}:$ENV{HEY};')
Write-Output "::set-output name=GREET::$output"
produce_comment:
name: Respond to issue
runs-on: ubuntu-latest
steps:
- name: Dump job context
env:
JOB_CONTEXT: ${{ jobs.maybe-greet.steps.id }}
run: echo "$JOB_CONTEXT"
I need two different jobs, since they use different context (operating systems), but I need to get the output of a step in the first job to the second job. I am trying with several combinations of the jobs context as found here but there does not seem to be any way to do that. Apparently, jobs is just the name of a YAML variable that does not really have a context, and the context job contains just the success or failure. Any idea?
Check the "GitHub Actions: New workflow features" from April 2020, which could help in your case (to reference step outputs from previous jobs)
Job outputs
You can specify a set of outputs that you want to pass to subsequent jobs and then access those values from your needs context.
See documentation:
jobs.<jobs_id>.outputs
A map of outputs for a job.
Job outputs are available to all downstream jobs that depend on this job.
For more information on defining job dependencies, see jobs.<job_id>.needs.
Job outputs are strings, and job outputs containing expressions are evaluated on the runner at the end of each job. Outputs containing secrets are redacted on the runner and not sent to GitHub Actions.
To use job outputs in a dependent job, you can use the needs context.
For more information, see "Context and expression syntax for GitHub Actions."
To use job outputs in a dependent job, you can use the needs context.
Example
jobs:
job1:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
output1: ${{ steps.step1.outputs.test }}
output2: ${{ steps.step2.outputs.test }}
steps:
- id: step1
run: echo "test=hello" >> $GITHUB_OUTPUT
- id: step2
run: echo "test=world" >> $GITHUB_OUTPUT
job2:
runs-on: ubuntu-latest
needs: job1
steps:
- run: echo ${{needs.job1.outputs.output1}} ${{needs.job1.outputs.output2}}
Note the use of $GITHUB_OUTPUT, instead of the older ::set-output now (Oct. 2022) deprecated.
To avoid untrusted logged data to use set-state and set-output workflow commands without the intention of the workflow author we have introduced a new set of environment files to manage state and output.
Jesse Adelman adds in the comments:
This seems to not work well for anything beyond a static string.
How, for example, would I take a multiline text output of step (say, I'm running a pytest or similar) and use that output in another job?
either write the multi-line text to a file (jschmitter's comment)
or base64-encode the output and then decode it in the next job (Nate Karasch's comment)
Update: It's now possible to set job outputs that can be used to transfer string values to downstream jobs. See this answer.
What follows is the original answer. These techniques might still be useful for some use cases.
Write the data to file and use actions/upload-artifact and actions/download-artifact. A bit awkward, but it works.
Create a repository dispatch event and send the data to a second workflow. I prefer this method personally, but the downside is that it needs a repo scoped PAT.
Here is an example of how the second way could work. It uses repository-dispatch action.
name: "We 🎔 Perl"
on:
issues:
types: [opened, edited, milestoned]
jobs:
seasonal_greetings:
runs-on: windows-latest
steps:
- name: Maybe greet
id: maybe-greet
env:
HEY: "Hey you!"
GREETING: "Merry Xmas to you too!"
BODY: ${{ github.event.issue.body }}
run: |
$output=(perl -e 'print ($ENV{BODY} =~ /Merry/)?$ENV{GREETING}:$ENV{HEY};')
Write-Output "::set-output name=GREET::$output"
- name: Repository Dispatch
uses: peter-evans/repository-dispatch#v1
with:
token: ${{ secrets.REPO_ACCESS_TOKEN }}
event-type: my-event
client-payload: '{"greet": "${{ steps.maybe-greet.outputs.GREET }}"}'
This triggers a repository dispatch workflow in the same repository.
name: Repository Dispatch
on:
repository_dispatch:
types: [my-event]
jobs:
myEvent:
runs-on: ubuntu-latest
steps:
- run: echo ${{ github.event.client_payload.greet }}
In my case I wanted to pass an entire build/artifact, not just a string:
name: Build something on Ubuntu then use it on MacOS
on:
workflow_dispatch:
# Allows for manual build trigger
jobs:
buildUbuntuProject:
name: Builds the project on Ubuntu (Put your stuff here)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout#v2
- uses: some/compile-action#v99
- uses: actions/upload-artifact#v2
# Upload the artifact so the MacOS runner do something with it
with:
name: CompiledProject
path: pathToCompiledProject
doSomethingOnMacOS:
name: Runs the program on MacOS or something
runs-on: macos-latest
needs: buildUbuntuProject # Needed so the job waits for the Ubuntu job to finish
steps:
- uses: actions/download-artifact#master
with:
name: CompiledProject
path: somewhereToPutItOnMacOSRunner
- run: ls somewhereToPutItOnMacOSRunner # See the artifact on the MacOS runner
It is possible to capture the entire output (and return code) of a command within a run step, which I've written up here to hopefully save someone else the headache. Fair warning, it requires a lot of shell trickery and a multiline run to ensure everything happens within a single shell instance.
In my case, I needed to invoke a script and capture the entirety of its stdout for use in a later step, as well as preserve its outcome for error checking:
# capture stdout from script
SCRIPT_OUTPUT=$(./do-something.sh)
# capture exit code as well
SCRIPT_RC=$?
# FYI, this would get stdout AND stderr
SCRIPT_ALL_OUTPUT=$(./do-something.sh 2>&1)
Since Github's job outputs only seem to be able to capture a single line of text, I also had to escape any newlines for the output:
echo "::set-output name=stdout::${SCRIPT_OUTPUT//$'\n'/\\n}"
Additionally, I needed to ultimately return the script's exit code to correctly indicate whether it failed. The whole shebang ends up looking like this:
- name: A run step with stdout as a captured output
id: myscript
run: |
# run in subshell, capturiing stdout to var
SCRIPT_OUTPUT=$(./do-something.sh)
# capture exit code too
SCRIPT_RC=$?
# print a single line output for github
echo "::set-output name=stdout::${SCRIPT_OUTPUT//$'\n'/\\n}"
# exit with the script status
exit $SCRIPT_RC
continue-on-error: true
- name: Add above outcome and output as an issue comment
uses: actions/github-script#v5
env:
STEP_OUTPUT: ${{ steps.myscript.outputs.stdout }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// indicates whather script succeeded or not
let comment = `Script finished with \`${{ steps.myscript.outcome }}\`\n`;
// adds stdout, unescaping newlines again to make it readable
comment += `<details><summary>Show Output</summary>
\`\`\`
${process.env.STEP_OUTPUT.replace(/\\n/g, '\n')}
\`\`\`
</details>`;
// add the whole damn thing as an issue comment
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
})
Edit: there is also an action to accomplish this with much less bootstrapping, which I only just found.
2022 October update: GitHub is deprecating set-output and recommends to use GITHUB_OUTPUT instead. The syntax for defining the outputs and referencing them in other steps, jobs.
An example from the docs:
- name: Set color
id: random-color-generator
run: echo "SELECTED_COLOR=green" >> $GITHUB_OUTPUT
- name: Get color
run: echo "The selected color is ${{ steps.random-color-generator.outputs.SELECTED_COLOR }}"

How do I get the output of a specific step in GitHub Actions?

I have this GitHub Actions workflow which runs tests, but now I am integrating slack notification in it. I want to get the output of the Run tests step and send it as a message in the slack step.
- name: Run tests
run: |
mix compile --warnings-as-errors
mix format --check-formatted
mix ecto.create
mix ecto.migrate
mix test
env:
MIX_ENV: test
PGHOST: localhost
PGUSER: postgres
- name: Slack Notification
uses: rtCamp/action-slack-notify#master
env:
SLACK_MESSAGE: Run tests output
SLACK_TITLE: CI Test Suite
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
You need to do 3 things:
Add an id to the step you want the output from
Create the outputs using the GITHUB_OUTPUT environment variable
Use the id and the output name in another step to get the outputs and then join them into one message for slack
- name: Run tests
run: |
echo "mix-compile--warnings-as-errors=$(mix compile --warnings-as-errors)\n" >> $GITHUB_OUTPUT
echo "mix-format--check-formatted=$(mix format --check-formatted)\n" >> $GITHUB_OUTPUT
echo "mix-ecto_create=$(mix ecto.create)\n" >> $GITHUB_OUTPUT
echo "mix-ecto_migrate=$(mix ecto.migrate)\n" >> $GITHUB_OUTPUT
echo "mix-test=$(mix test)\n" >> $GITHUB_OUTPUT
id: run_tests
env:
MIX_ENV: test
PGHOST: localhost
PGUSER: postgres
- name: Slack Notification
uses: rtCamp/action-slack-notify#v2
env:
SLACK_MESSAGE: ${{join(steps.run_tests.outputs.*, '\n')}}
SLACK_TITLE: CI Test Suite
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
See Metadata Syntax for outputs name description
The problem with the current accepted answer is that the result for the step will always be success since the test execution result is being masked by the echo command.
This modification to the last line should work in preserving the original exit status:
mix test 2>&1 | tee test.log
result_code=${PIPESTATUS[0]}
echo "::set-output name=mix-test::$(cat test.log)"
exit $result_code
I made an action with the same interface as run that stores stdout and stderr in output variables to maybe simplify some cases like this:
- name: Run tests
uses: mathiasvr/command-output#v1
id: tests
with:
run: |
mix compile --warnings-as-errors
mix format --check-formatted
mix ecto.create
mix ecto.migrate
mix test
- name: Slack Notification
uses: rtCamp/action-slack-notify#master
env:
SLACK_MESSAGE: ${{ steps.tests.outputs.stdout }}
SLACK_TITLE: CI Test Suite
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
I just wanted to add #smac89's solution was helpful but didn't quite work for me. I'm using a different Slack action (pullreminders/slack-action) to build more specific content. I found that I was getting single-quotes where each newline was, and my leading spaces on each line were also being truncated. After reading https://github.com/actions/toolkit/issues/403 and playing around, I found that in my case, I needed newlines to actually be escaped in the output (a literal \n), so I replaced \n characters with \\n. Then, I replaced regular space characters with a Unicode 'En Space' character.
Here's what worked:
Bash Run Step:
Tools/get-changed-fields.sh src/objects origin/${{ env.DIFF_BRANCH }} > changed-fields.out
output="$(cat changed-fields.out)"
output="${output//$'\n'/\\n}"
output="${output// / }" # replace regular space with 'En Space'
echo "::set-output name=changed-fields-output::$output"
Slack Notification Step:
- name: Changed Fields Slack Notification
if: ${{ success() && steps.summarize-changed-fields.outputs.changed-fields-output != '' && steps.changed-fields-cache.outputs.cache-hit != 'true' }}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
uses: pullreminders/slack-action#master
with:
args: '{\"channel\":\"${{ env.SUCCESS_SLACK_CHANNEL }}\",\"attachments\":[{\"color\":\"#36a64f\",\"title\":\"Changed Fields Report:\",\"author_name\":\"${{ github.workflow }} #${{ github.run_number }}: ${{ env.BRANCH }} -> ${{ env.TARGET_ORG }} (by: ${{ github.actor }})\",\"author_link\":\"${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}\",\"text\":\"```\n${{ steps.summarize-changed-fields.outputs.changed-fields-output }}\n```\"}]}'

How to fail a job in Github Actions?

I'm developing a Github actions workflow. This workflow runs on Linux, Mac, and Windows.
As part of the workflow, I have to check whether 2 environment variables are equal. If they don't - fail the job.
As described here, Github Actions support if: condition:
steps:
- run: # How can I make a cross-platform failure here?
if: ${{ envA }} != ${{ envB }}
How can I make the job fail if the above condition is true?
In the beginning, I thought of a script, but there must be a more elegant way to fail a job.
I'd do run: exit 1. That will simply exit with an exit code of 1, on all three platforms.
Proof that it's cross-platform: https://github.com/rmunn/Testing/runs/220188838 which runs the following workflow:
name: Test exiting on failure
on: [push]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout#v1
- name: Try to fail
run: exit 1
- name: Print message if we don't fail
run: echo Should not get here
(An earlier version of this answer recommended "/bin/false", but that would only work on Linux and macOS).
In 2021, there is perhaps a more graceful way to do this:
- name: A/B Check
if: ${{ envA }} != ${{ envB }}
uses: actions/github-script#v3
with:
script: |
core.setFailed('envA and envB are not equivalent!')
Here, we use the github-script action to provide a one liner script that will fail the job. The "A/B Check" step will only run if the condition in the if line is true, so the script will only run in that case, which is what we want.
The nice thing about this approach is that you will get nicely formatted output in the Actions UI in your repo, showing that the "A/B Check" step caused the failure, and why (i.e. "envA and envB are not equivalent").
Note that if you have additional steps in the job after this, and you do NOT want them to run if the A/B check fails, you'll want to use if: success() on them to prevent them from running in that case.
The Github workflow commands docs gives a hint on this.
Toolkit function
Equivalent workflow command
core.setFailed
Used as a shortcut for ::error and exit 1
Given that, you can do the following without using any external workflows.
steps:
- name: A/B Check
if: ${{ envA }} != ${{ envB }}
run: |
echo "::error file={name},line={line},endLine={endLine},title={title}::{message}"
exit 1