This article was last updated on January 10, 2025, to include advanced techniques for managing job outputs in GitHub Actions, such as handling outputs in matrix jobs, processing large output files efficiently, and managing special characters in outputs, along with simplified explanations to improve clarity.
TL;DR
Job outputs in GitHub Actions let you pass data between jobs in your workflow. Think of them like passing notes between coworkers - one job writes down information that another job needs later.
Key points:
- Use the
outputs
field to define what data to share - Reference outputs using
needs.job_id.outputs.output_name
- Always treat outputs as strings
- Limited to 1MB per job
Introductionโ
After spending hours and hours debugging GitHub Actions workflows, I can say one of the trickiest parts has got to be sharing information between jobs. I still remember this one day when I needed to pass a dynamically generated version number from a build job to a deploy job. What seemed like an easy task turned into hours of head scratching until I discovered job outputs.
Let me share what I've learned about job outputs, with real examples from my own experience maintaining CI/CD pipelines.
Steps we'll cover:
- Introduction
- Understanding GitHub Actions Job Outputs
- Interactive GitHub Actions Output Explorer
- How to Define and Use Job Outputs in GitHub Actions
- Real-World Examples of GitHub Actions Output
- Common GitHub Actions Output Pitfalls and Solutions
- Best Practices for GitHub Actions Job Outputs
- GitHub Actions Job Outputs FAQ
- Using Outputs in Matrix Jobs
- Conclusion and Next Steps
Understanding GitHub Actions Job Outputsโ
Think of job outputs like a relay race. Each runner needs to pass the baton onto the next runner. In GitHub Actions, outputs are the official way to pass that baton between jobs.
Interactive GitHub Actions Output Explorerโ
Try this interactive tool to learn more about how job outputs work:
Job Outputs Explorer
Job 1: Set Output
Job 2: Read Output
Waiting for Job 1 to complete...
This interactive demo shows how job outputs work in GitHub Actions. Enter a value in Job 1, and see how it gets passed to Job 2 using the outputs syntax.
How to Define and Use Job Outputs in GitHub Actionsโ
Let me show you the basic pattern I use for job outputs:
jobs:
job1:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- id: get_version
run: echo "version=1.0.0" >> "$GITHUB_OUTPUT"
job2:
needs: job1
runs-on: ubuntu-latest
steps:
- run: echo "Using version ${{ needs.job1.outputs.version }}"
Now, let me break it down the way I would explain it to a friend:
- The first job (
job1
) says, "I'm going to share something called 'version." - A step in that job writes the actual value.
- Secondary job (
job2
) states "I need job1 to run first." - Then it can read the version using the
needs
context.
Real-World Examples of GitHub Actions Outputโ
Let me now share a couple of real scenarios where job outputs saved my day. These are real examples, simplified from some of the projects I have worked on.
Example 1: Passing Dynamical Version Numbers between Jobsโ
A few months ago, I was working on the automation of our mobile app releases. We needed to generate a unique version number for each build that would include both the date and the git commit-so we could track which version of the code was in each build.
Common GitHub Actions Output Pitfalls and Solutionsโ
After having broken my fair share of workflows, here are some of the most common problems and their solutions:
Processing Large Output files in GitHub Actionโ
When trying to pass large files between jobs, you might try something like this:
โ Problem: This will fail - output is too large!
jobs:
job1:
runs-on: ubuntu-latest
outputs:
log: ${{ steps.get_logs.outputs.content }}
steps:
- id: get_logs
run: |
echo "content=$(cat huge_log.txt)" >> "$GITHUB_OUTPUT"
โ Solution: Use artifacts instead:
jobs:
job1:
runs-on: ubuntu-latest
steps:
- uses: actions/upload-artifact@v3
with:
name: build-logs
path: huge_log.txt
job2:
needs: job1
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: build-logs
Handling Special Characters in Job Outputsโ
When trying to pass JSON or strings containing special characters:
โ Problem: This will fail due to special characters
jobs:
job1:
runs-on: ubuntu-latest
outputs:
data: ${{ steps.get_data.outputs.content }}
steps:
- id: get_data
run: |
echo "content={"key": "value"}" >> "$GITHUB_OUTPUT"
โ Solution: Use base64 encoding:
jobs:
job1:
runs-on: ubuntu-latest
outputs:
data: ${{ steps.get_data.outputs.content }}
steps:
- id: get_data
run: |
# Encode JSON as base64
content=$(echo '{"key": "value"}' | base64)
echo "content=$content" >> "$GITHUB_OUTPUT"
job2:
needs: job1
runs-on: ubuntu-latest
steps:
- run: |
# Decode base64 back to JSON
echo "${{ needs.job1.outputs.data }}" | base64 -d
Avoiding Race Conditions by Using Job Dependenciesโ
One common error is not specifying any job dependencies.
โ Problem: Jobs might run in the wrong order
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.value }}
steps:
- id: get_version
run: echo "value=1.0.0" >> "$GITHUB_OUTPUT"
deploy:
runs-on: ubuntu-latest
# Missing 'needs' - might run before build!
steps:
- run: echo "Deploying ${{ build.outputs.version }}"
Always use needs
to specify dependencies:
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.value }}
steps:
- id: get_version
run: echo "value=1.0.0" >> "$GITHUB_OUTPUT"
deploy:
needs: build # Explicitly wait for build job
runs-on: ubuntu-latest
steps:
- run: echo "Deploying ${{ needs.build.outputs.version }}"
These examples show the most common pitfalls one may experience with job outputs. Remember:
- Use artifacts for large files instead of outputs
- base64 encode complex data
- Use
needs
to always specify job dependencies - Double-check your output variable names and step IDs
Best Practices for GitHub Actions Job Outputsโ
In my experience with maintaining dozens of workflows, the best approach is to:
Keep outputs small
- Use artifacts for large files
- Pass only what's necessary
Use meaningful names
build_version
is better thanoutput1
- Document what each output means
Gracefully handle failures
- Set default values where possible
- Add error checking
Test outputs individually
- Echo for debugging output
- Use small test workflows
GitHub Actions Job Outputs FAQโ
Q: Can I pass complex JSON as an output?
A: Yep! But you'd have to escape it, of course. I just use toJSON()
:
echo "json=$(echo $my_json | jq -c '.' | sed 's/"/\\"/g')" >> "$GITHUB_OUTPUT"
Q: What's the limit on output size?
A: 1MB per job. I learned this the hard way trying to pass an entire log file!
Q: Is it possible to use outputs in reusable workflows?
A: Yes, but they must be explicitly defined inside the workflow call.
Using Outputs in Matrix Jobsโ
Matrix jobs on GitHub Actions allow one to run several jobs concurrently, each with different parameters. Sometimes, there is a requirement to share outputs among them or utilize outputs in jobs that depend on the matrix jobs.
Here's how you can use outputs in matrix jobs with a simple example.
Example: Passing Outputs from a Matrix Jobโ
Let's assume that there is a build job for different versions of a tool, which runs after the build is complete. It also provides a path to the built artifact as an output, which can then be used by another job to deploy the artifact.
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
version: [1.0, 2.0, 3.0] # Different versions for the matrix
outputs:
artifact-path: ${{ steps.build.outputs.path }} # Define an output for the job
steps:
- name: Check out code
uses: actions/checkout@v3
- id: build
run: echo "path=/artifacts/version-${{ matrix.version }}" >> "$GITHUB_OUTPUT"
# Simulate the creation of an artifact and save the path to output.
deploy:
needs: build # Waits for the 'build' job to complete
runs-on: ubuntu-latest
steps:
- name: Deploy artifact
run: echo "Deploying artifact from ${{ needs.build.outputs.artifact-path }}"
Explanationโ
Matrix in Build Job:
- The build job runs several times, once for each version. For example: 1.0, 2.0, 3.0.
- Each run produces an artifact path as an output.
Outputs Field:
- The
artifact-path
is specified as a job output in the outputs field of the build job. - The output value is written using the
$GITHUB_OUTPUT
environment variable.
Usage of the Outputs: During Deploy Job:
- The deploy job depends on the build job (
needs: build
). - It uses the
artifact-path
output to deploy the artifact.
Key Pointsโ
- Outputs from matrix jobs are often used in downstream jobs.
- Outputs should be given meaningful names, such as
artifact-path
. - Always test the workflow for correct values passed between jobs. This is useful when you have different builds or environments and want to dynamically process their output in other jobs.
Conclusion and Next Stepsโ
GitHub Actions job outputs are like a well-run relay race: when done right, they make your workflows smooth and efficient. Keep it simple, follow the patterns I shared above, and you'll be passing data between jobs in no time.
Remember: outputs shall be strings, keep them short, and always test your workflows. And if you need to monitor how your workflows are performing, check out CICube for detailed insights. Happy automating!