Passing Data Between Jobs Without the YAML Bash Mess
How GitHub Actions outputs actually chain together between jobs, why GITHUB_ENV is the simpler tool for steps in the same job, and why I reach for a file over both when I can.
needs solves the big version of sharing data between jobs: an entire built artifact, handed from build to deploy. Sometimes what you need to pass along isn't a file. It's a single value: a version string, a build ID, a flag.
GitHub Actions has a mechanism for that too. It's also the fussiest corner of the syntax, worth understanding exactly so you know when to reach for something simpler instead.
The chain a job output actually goes through
Getting one small value out of a job and into the next one takes four separate pieces, each named to match the one before it.
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.info.outputs.value }}
steps:
- id: info
run: echo "value=1.2.3" >> "$GITHUB_OUTPUT"
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Deploying version ${{ needs.build.outputs.version }}"Four places have to agree on the same name: the step's id, the key you echo into $GITHUB_OUTPUT, the job's outputs block, and the needs.<job>.outputs.<key> reference downstream. Miss one, and the value just silently comes through empty. Nothing errors.
Under the hood, echo "value=1.2.3" >> "$GITHUB_OUTPUT" is writing a line into a file GitHub Actions already created for you and is quietly reading back afterward. It's the exact same trick as sourcing a .env file into your shell, GitHub Actions just does it for you and never shows you the file.
The simpler tool for the smaller case
That whole chain only exists because outputs moves a value between jobs, and jobs don't share anything by default.
If you only need to move a value between steps in the same job, there's a shortcut: $GITHUB_ENV.
steps:
- run: echo "VERSION=1.2.3" >> "$GITHUB_ENV"
- run: echo "Deploying $VERSION"Write a line to $GITHUB_ENV once, and every step after it in that job has that environment variable set automatically. No step id, no outputs block, no needs reference. Same underlying trick, one job's worth of scope instead of the whole workflow's.
Why I still reach for a file over either one
Here's the honest version: I use the outputs chain as rarely as I can get away with.
Every one of those four names has to match exactly, in four different places, in a language that doesn't tell you when one of them is wrong. A typo here doesn't fail loudly. It just produces an empty string three steps later, and now you're debugging a value that was never wrong, just never arrived.
The alternative is almost embarrassingly simple: if a job needs to hand a value to the next job, write that value into a small file and include it in the artifact you're already uploading.
- run: echo "1.2.3" > version.txt
- uses: actions/upload-artifact@v4
with:
name: dist
path: |
dist/
version.txtThe deploy job downloads the same artifact it was already downloading, reads version.txt, and moves on. One file, one place the value lives, no four-name chain to keep in sync. It's a smaller mental model precisely because it reuses a tool you're already using for something else, instead of introducing a second, separate mechanism just for small values.
This is the same reasoning behind testing the actual built artifact instead of a local rebuild: a rebuilt copy, however careful, always carries a little of the environment that built it. A file that traveled with the artifact carries nothing but the value itself.
The Essentials
- Passing a value between jobs takes four matching names: a step
id,$GITHUB_OUTPUT, a job'soutputsblock, andneeds.<job>.outputs.<key>. Miss one and the value arrives silently empty, nothing errors. $GITHUB_ENVis the simpler tool, but only within one job. One line, noid, nooutputsblock, every later step in that job just has the variable.- A file written into the artifact you're already uploading beats both for passing values between jobs. One place the value lives, reusing a mechanism you already have instead of adding a second one.
Further Reading and Watching
Keep reading