Skip to content

Environment Variables


Every step in a workflow runs in its own shell process, and every one of those processes has environment variables available to it — some GitHub sets automatically, some you set yourself, and where you set them determines how far they reach. Getting the scoping right is the difference between a variable being available exactly where you need it and a workflow where half your steps mysteriously can’t see a value the other half can.

Default Environment Variables

GitHub injects a set of variables into every job automatically, without you declaring anything. A few of the most useful:

name: Default Env Demo
on: workflow_dispatch

jobs:
  show-defaults:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Repository: $GITHUB_REPOSITORY"
          echo "Ref: $GITHUB_REF"
          echo "SHA: $GITHUB_SHA"
          echo "Actor: $GITHUB_ACTOR"
          echo "Workspace: $GITHUB_WORKSPACE"
          echo "Run ID: $GITHUB_RUN_ID"

Try it:

mkdir gha-env-demo && cd gha-env-demo
git init
mkdir -p .github/workflows
cat > .github/workflows/default-env.yml << 'EOF'
name: Default Env Demo
on: workflow_dispatch

jobs:
  show-defaults:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Repository: $GITHUB_REPOSITORY"
          echo "Ref: $GITHUB_REF"
          echo "SHA: $GITHUB_SHA"
          echo "Actor: $GITHUB_ACTOR"
          echo "Workspace: $GITHUB_WORKSPACE"
          echo "Run ID: $GITHUB_RUN_ID"
EOF
git add .
git commit -m "Add default env demo workflow"
git branch -M main

Push and trigger it manually — the log will show real values pulled straight from GitHub’s own metadata about the run, with no configuration on your part.

Note

These are conventionally referenced two ways: as shell environment variables ($GITHUB_SHA, all-caps with underscores) inside a run: step, or through the github context (${{ github.sha }}, lowercase, dotted) inside expressions. Both point at the same underlying data — the context version gets its own full treatment shortly.

Custom Environment Variables: Workflow, Job, and Step Level

Beyond the defaults, you can define your own env block at three different levels, and they nest — a variable set at a broader scope is visible to everything narrower, and a narrower scope can override it.

name: Env Scoping Demo
on: workflow_dispatch

env:
  APP_NAME: devstuffs-demo

jobs:
  scoped:
    runs-on: ubuntu-latest
    env:
      STAGE: build
    steps:
      - name: Workflow and job level
        run: echo "App is $APP_NAME, stage is $STAGE"

      - name: Step level override
        env:
          STAGE: test
        run: echo "App is $APP_NAME, stage is now $STAGE"

Add this and run it:

cat > .github/workflows/env-scoping.yml << 'EOF'
name: Env Scoping Demo
on: workflow_dispatch

env:
  APP_NAME: devstuffs-demo

jobs:
  scoped:
    runs-on: ubuntu-latest
    env:
      STAGE: build
    steps:
      - name: Workflow and job level
        run: echo "App is $APP_NAME, stage is $STAGE"

      - name: Step level override
        env:
          STAGE: test
        run: echo "App is $APP_NAME, stage is now $STAGE"
EOF
git add .
git commit -m "Add env scoping demo"

Push and trigger it — the first step prints stage is build, the second prints stage is now test, while APP_NAME stays devstuffs-demo in both, since nothing overrides it. This is exactly how you’d expect variable shadowing to work in most programming languages: the closest, most specific definition wins.

Scope Summary

LevelDefined UnderVisible To
WorkflowTop-level env:Every job and step in the file
Jobjobs.<id>.env:Every step in that job only
Stepjobs.<id>.steps[].env:That single step only

A Common Trap: env Doesn’t Cross Jobs Automatically

Each job in a workflow runs on a fresh runner (unless configured otherwise), so a env variable set in one job is invisible to a different job — even one declared right below it in the same file.

jobs:
  first:
    runs-on: ubuntu-latest
    env:
      TOKEN: abc123
    steps:
      - run: echo "Visible here: $TOKEN"

  second:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Visible here too? $TOKEN"   # prints empty

The second job’s $TOKEN prints nothing — job-level env doesn’t leak sideways into other jobs. If you genuinely need to pass a value from one job to another, that’s a job-outputs problem, not an env problem, and it gets its own dedicated chapter.

Dynamically Setting Variables Mid-Job

A step can also set an environment variable for the rest of the job (not just itself) by writing to the special $GITHUB_ENV file.

steps:
  - name: Compute a value
    run: echo "BUILD_TIME=$(date -u +%Y%m%dT%H%M%S)" >> "$GITHUB_ENV"

  - name: Use it later
    run: echo "This build happened at $BUILD_TIME"

This is how you compute something at runtime — a timestamp, a git short SHA, a version string parsed from a file — and make it available to every subsequent step in the same job, without hardcoding it in the YAML.

How Real DevOps Engineers Use This

Workflow-level env is where teams put things that genuinely apply everywhere — a Python version string, an app name used in log messages, a default AWS region. Job-level env handles per-job configuration, like a STAGE=build versus STAGE=deploy distinction across different jobs in the same pipeline. Step-level env is reserved for the rare case where one specific command needs a variable that would be actively wrong anywhere else — for instance, temporarily setting AWS_PROFILE for a single AWS CLI call without letting it leak into unrelated steps.

The $GITHUB_ENV pattern is extremely common in real deploy pipelines for computing a Docker image tag or version string once, early in the job, and referencing it consistently through every later step — avoiding the situation where three different steps each recompute a timestamp and end up with three slightly different values.

Clean up:

cd .. && rm -rf gha-env-demo

Tip

If a variable “isn’t showing up,” check scope first. It’s almost always either defined in a different job, or defined at step level in a step that already finished.

Last updated on