Skip to content

Fix: GitHub Actions Environment Variables Not Available Between Steps

FixDevs ·

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 steps

Or 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 deprecated

Or 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 correctly

Why 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 export instead of GITHUB_ENVexport sets a variable only for the current shell process. To pass variables to subsequent steps, write to the $GITHUB_ENV file.
  • Using deprecated ::set-output — the ::set-output workflow command was deprecated in 2022 and disabled in 2023. Use $GITHUB_OUTPUT instead.
  • Step output not referenced correctly — outputs must be referenced with ${{ steps.<step-id>.outputs.<name> }} and the step must have an id.
  • Job output not declared in outputs block — job-level outputs must be explicitly declared in the job’s outputs block to be available to dependent jobs.
  • Multi-line values not escaped — multi-line env var values written to GITHUB_ENV require 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=value works within the same step’s shell but is completely invisible to subsequent steps. Always use >> $GITHUB_ENV for 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-bucket

Never 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 env

Fix 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: bash

Verify 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 exactly

For related GitHub Actions issues, see Fix: GitHub Actions Cache Not Working and Fix: GitHub Actions if Condition Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles