Fix: GitHub Actions Environment Variables Not Available Between Steps
Quick Answer
How to fix GitHub Actions env vars and outputs not persisting between steps — GITHUB_ENV, GITHUB_OUTPUT, job outputs, and why echo >> $GITHUB_ENV is required.
The Error
An environment variable set in one step is undefined in the next:
steps:
- name: Set version
run: export VERSION=1.2.3
- name: Use version
run: echo "Version is $VERSION"
# Output: "Version is " ← Empty — export doesn't persist between stepsOr a step output isn’t available in a later step:
steps:
- name: Get commit hash
id: commit
run: echo "::set-output name=hash::$(git rev-parse --short HEAD)"
- name: Use hash
run: echo "Hash is ${{ steps.commit.outputs.hash }}"
# Output: "Hash is " ← Empty — ::set-output is deprecatedOr a job output isn’t accessible in a dependent job:
jobs:
build:
outputs:
version: ${{ steps.get-version.outputs.version }}
deploy:
needs: build
steps:
- run: echo "${{ needs.build.outputs.version }}"
# Output: "" ← Empty — output not wired correctlyWhy This Happens
Each step in a GitHub Actions workflow runs in its own shell process. Shell environment variables (set with export or VAR=value) are not inherited by subsequent steps — they only exist for the duration of the step that set them:
- Using
exportinstead ofGITHUB_ENV—exportsets a variable only for the current shell process. To pass variables to subsequent steps, write to the$GITHUB_ENVfile. - Using deprecated
::set-output— the::set-outputworkflow command was deprecated in 2022 and disabled in 2023. Use$GITHUB_OUTPUTinstead. - Step output not referenced correctly — outputs must be referenced with
${{ steps.<step-id>.outputs.<name> }}and the step must have anid. - Job output not declared in
outputsblock — job-level outputs must be explicitly declared in the job’soutputsblock to be available to dependent jobs. - Multi-line values not escaped — multi-line env var values written to
GITHUB_ENVrequire the heredoc syntax. Writing a newline character directly corrupts the file format.
Fix 1: Use GITHUB_ENV for Environment Variables
To share environment variables between steps, write to the $GITHUB_ENV file using the NAME=VALUE format:
steps:
- name: Set version
run: echo "VERSION=1.2.3" >> $GITHUB_ENV
- name: Use version
run: echo "Version is $VERSION"
# Output: "Version is 1.2.3" ✓Set multiple variables:
steps:
- name: Set build variables
run: |
echo "VERSION=1.2.3" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +%Y-%m-%d)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Use variables
run: |
echo "Version: $VERSION"
echo "Date: $BUILD_DATE"
echo "SHA: $COMMIT_SHA"Set a variable from command output:
steps:
- name: Get package version
run: echo "PKG_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Print version
run: echo "Package version: $PKG_VERSION"Set a multi-line variable (requires heredoc syntax):
steps:
- name: Set multi-line variable
run: |
EOF_MARKER=$(openssl rand -hex 8)
echo "CHANGELOG<<$EOF_MARKER" >> $GITHUB_ENV
echo "Line 1 of changelog" >> $GITHUB_ENV
echo "Line 2 of changelog" >> $GITHUB_ENV
echo "$EOF_MARKER" >> $GITHUB_ENV
- name: Use multi-line variable
run: echo "$CHANGELOG"Common Mistake: Writing
export VAR=valueworks within the same step’s shell but is completely invisible to subsequent steps. Always use>> $GITHUB_ENVfor cross-step variables.
Fix 2: Use GITHUB_OUTPUT for Step Outputs
The ::set-output syntax was disabled in May 2023. Use $GITHUB_OUTPUT instead:
# Old way (deprecated and disabled)
- name: Get hash
id: commit
run: echo "::set-output name=hash::$(git rev-parse --short HEAD)"
# New way — write to $GITHUB_OUTPUT
- name: Get hash
id: commit
run: echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Use hash
run: echo "Commit hash: ${{ steps.commit.outputs.hash }}"Step outputs require an id on the step:
steps:
- name: This step has no id
run: echo "result=hello" >> $GITHUB_OUTPUT
# Cannot reference this output — no id to reference it by
- name: This step has an id
id: my-step # Required for output referencing
run: echo "result=hello" >> $GITHUB_OUTPUT
- name: Reference the output
run: echo "${{ steps.my-step.outputs.result }}"
# Output: hello ✓Multiple outputs from one step:
- name: Build info
id: build
run: |
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
echo "timestamp=$(date -u +%Y%m%dT%H%M%SZ)" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Use build info
run: |
echo "Version: ${{ steps.build.outputs.version }}"
echo "Timestamp: ${{ steps.build.outputs.timestamp }}"
echo "SHA: ${{ steps.build.outputs.sha }}"Fix 3: Fix Job-Level Outputs
To pass data between jobs (not just steps), you must declare job outputs explicitly and wire them to step outputs:
jobs:
build:
runs-on: ubuntu-latest
# Declare job outputs — maps job output names to step output expressions
outputs:
version: ${{ steps.get-version.outputs.version }}
artifact-name: ${{ steps.set-artifact.outputs.name }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: get-version # id required — referenced in outputs above
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Set artifact name
id: set-artifact
run: echo "name=myapp-${{ github.sha }}" >> $GITHUB_OUTPUT
deploy:
runs-on: ubuntu-latest
needs: build # Must declare dependency to access outputs
steps:
- name: Deploy
run: |
echo "Deploying version: ${{ needs.build.outputs.version }}"
echo "Artifact: ${{ needs.build.outputs.artifact-name }}"Common mistake — forgetting needs:
# Wrong — deploy doesn't declare dependency on build
deploy:
runs-on: ubuntu-latest
steps:
- run: echo "${{ needs.build.outputs.version }}"
# Output: "" — needs.build is not available without needs: build# Correct
deploy:
runs-on: ubuntu-latest
needs: build # Declares dependency AND makes outputs available
steps:
- run: echo "${{ needs.build.outputs.version }}"
# Output: "1.2.3" ✓Fix 4: Use env: for Step-Scoped Variables
For variables used only within a single step or job, use the env: key — it’s cleaner than writing to $GITHUB_ENV:
# Job-level env — available to all steps in the job
jobs:
build:
runs-on: ubuntu-latest
env:
NODE_ENV: production
API_URL: https://api.example.com
steps:
- name: Build
run: echo "Building for $NODE_ENV at $API_URL"
# Step-level env — overrides job-level for this step only
- name: Test
env:
NODE_ENV: test # Overrides job-level NODE_ENV for this step
run: echo "Testing in $NODE_ENV" # Output: "Testing in test"Workflow-level env (available to all jobs):
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: docker build -t $REGISTRY/$IMAGE_NAME .Fix 5: Pass Secrets and Env Vars Securely
Secrets set in GitHub repository settings are not automatically available as env vars. You must explicitly map them:
steps:
- name: Deploy
env:
# Map GitHub secret to an environment variable for this step
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: aws s3 sync ./dist s3://my-bucketNever write secrets to $GITHUB_ENV — values written to $GITHUB_ENV appear in logs and are accessible to subsequent steps. GitHub masks known secret values in logs, but avoid unnecessary exposure:
# Avoid — passes secret through GITHUB_ENV unnecessarily
- run: echo "MY_SECRET=${{ secrets.MY_SECRET }}" >> $GITHUB_ENV
# Better — pass secret directly to the step that needs it via env:
- name: Use secret
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
run: ./deploy.sh # Script reads $MY_SECRET from envFix 6: Debug Variable Values
When values are empty or unexpected, add debug steps to inspect the state:
steps:
- name: Debug environment
run: |
echo "All env vars:"
env | sort | grep -v SECRET | grep -v TOKEN # Filter sensitive values
echo "GITHUB_ENV contents:"
cat $GITHUB_ENV
echo "GITHUB_OUTPUT contents:"
cat $GITHUB_OUTPUT || echo "(empty)"
- name: Debug specific variable
run: |
echo "VERSION='$VERSION'"
echo "Empty check: ${VERSION:-(not set)}"Enable debug logging for the entire workflow:
In your repository, go to Settings → Secrets → Add a secret named ACTIONS_STEP_DEBUG with value true. This enables verbose logging for all steps.
Or use ACTIONS_RUNNER_DEBUG=true for runner-level debugging.
Print the GitHub context to understand what’s available:
- name: Dump contexts
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
STEPS_CONTEXT: ${{ toJson(steps) }}
NEEDS_CONTEXT: ${{ toJson(needs) }}
run: |
echo "$GITHUB_CONTEXT"
echo "$STEPS_CONTEXT"
echo "$NEEDS_CONTEXT"Still Not Working?
Check if the variable name contains special characters. Variable names in $GITHUB_ENV must be alphanumeric with underscores. Hyphens and dots are not allowed.
Check for Windows line endings. If your runner is Windows, use >> with PowerShell syntax:
# Windows runner
- name: Set variable (Windows)
run: echo "VERSION=1.2.3" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
shell: pwsh
# Or use bash on Windows runner
- name: Set variable (bash on Windows)
run: echo "VERSION=1.2.3" >> $GITHUB_ENV
shell: bashVerify the step id matches the reference exactly:
# id defined as "get-version" (with hyphen)
- name: Get version
id: get-version
run: echo "version=1.2.3" >> $GITHUB_OUTPUT
# Must reference with the same id — hyphens in expressions need no escaping
- run: echo "${{ steps.get-version.outputs.version }}"
# ^^^^^^^^^^^^ Must match the id exactlyFor related GitHub Actions issues, see Fix: GitHub Actions Cache Not Working and Fix: GitHub Actions if Condition Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: GitHub Actions if Condition Not Working (Steps and Jobs Being Skipped or Always Running)
How to fix GitHub Actions if conditions that don't evaluate correctly — why steps are skipped or always run, how to use context expressions, fix boolean checks, and handle job outputs in conditions.
Fix: GitHub Actions Cache Not Working (Cache Miss on Every Run)
How to fix GitHub Actions cache not restoring — why actions/cache always misses, how to construct correct cache keys, debug cache hits and misses, and optimize caching for npm, pip, and Gradle.
Fix: AWS ECR Authentication Failed (docker login and push Errors)
How to fix AWS ECR authentication errors — no basic auth credentials, token expired, permission denied on push, and how to authenticate correctly from CI/CD pipelines and local development.
Fix: GitHub Actions permission denied (EACCES, 403, or Permission to X denied)
How to fix GitHub Actions permission denied errors caused by GITHUB_TOKEN permissions, checkout issues, artifact access, npm/pip cache, and Docker socket access.