Let's Build Your First Campaign Together with our Lead Generation Expert

How to Use GitHub Actions Environment Variables

Table of Contents

You set up a GitHub Actions workflow. It runs. Then it breaks — because a hardcoded token expired, a path was wrong across environments, or a secret leaked into your logs.

Sound familiar?

Environment variables are what separates a brittle workflow from a bulletproof one. They let you pass dynamic values into your pipeline without touching your code, reuse the same workflow across different environments, and keep sensitive credentials out of your repository.

This guide covers everything you need — from the basics of declaring variables to managing secrets, setting scopes, and avoiding the mistakes that silently break pipelines in production.

What Are Environment Variables in GitHub Actions?

Environment variables in GitHub Actions are key-value pairs you inject into your workflow at runtime. They work just like environment variables in any shell — your workflow steps can read them, branch on them, and pass them along to scripts and tools.

GitHub provides two categories:

  • Default environment variables — automatically set by GitHub for every run (things like GITHUB_SHA, GITHUB_REF, GITHUB_ACTOR)
  • Custom environment variables — defined by you, either in your YAML file or in your repository/organization settings

According to GitHub’s own documentation, over 73 million developers use GitHub — and CI/CD automation via Actions is one of the platform’s most widely adopted features. Getting your variable management right isn’t optional. It’s foundational.

Why Environment Variables Matter More Than You Think

Hardcoded values are a ticking clock. A 2023 GitGuardian report found that over 10 million secrets were exposed in public GitHub repositories in a single year — API keys, tokens, and credentials committed directly into code.

Environment variables don’t just make your workflows cleaner. They:

  • Eliminate hardcoded credentials that expose your infrastructure
  • Make workflows reusable across dev, staging, and production environments
  • Centralize configuration so one change propagates everywhere
  • Enable dynamic behavior based on branch, tag, or event type

Developers who adopt proper secrets management practices reduce credential-related incidents by as much as 80%, according to findings from the 2023 State of DevSecOps report.

 

Default Environment Variables GitHub Provides Automatically

Before you create a single custom variable, GitHub already gives your workflow a rich set of defaults. These are available in every step of every job.

Here are the most commonly used ones:

Variable

What It Contains

GITHUB_SHA

The commit SHA that triggered the workflow

GITHUB_REF

The branch or tag ref (e.g., refs/heads/main)

GITHUB_ACTOR

The username that triggered the workflow

GITHUB_REPOSITORY

The owner/repo name

GITHUB_EVENT_NAME

The name of the triggering event (push, pull_request, etc.)

GITHUB_WORKSPACE

The path to the checked-out repo on the runner

GITHUB_RUN_ID

The unique run identifier

GITHUB_RUN_NUMBER

The sequential run count for the workflow

You reference these in your YAML like this:

steps:

  – name: Print the triggering commit

    run: echo “Triggered by commit ${{ env.GITHUB_SHA }}”

 

Or via shell syntax in run steps:

echo “Branch: $GITHUB_REF”

 

These defaults are read-only. You cannot overwrite them.

How to Set Custom Environment Variables

At the Workflow Level

Variables set at the top-level env key are available to every job and every step in the workflow.

name: Deploy App

 

env:

  NODE_ENV: production

  APP_PORT: 3000

 

jobs:

  build:

    runs-on: ubuntu-latest

    steps:

      – name: Show environment

        run: echo “Running in $NODE_ENV on port $APP_PORT”

 

Use this level for values that every part of your pipeline needs.

At the Job Level

Variables defined inside a job’s env block are scoped to that job only.

jobs:

  test:

    runs-on: ubuntu-latest

    env:

      TEST_DB_URL: postgres://localhost:5432/testdb

    steps:

      – name: Run tests

        run: npm test

 

Other jobs in the same workflow won’t have access to TEST_DB_URL.

At the Step Level

Variables scoped to a single step are the most targeted approach — great for isolating environment-specific behavior.

steps:

  – name: Run migration

    env:

      MIGRATION_MODE: safe

    run: ./migrate.sh

 

This is also useful when different steps in the same job need different values for the same variable name.

Setting Variables Dynamically at Runtime

You can write to $GITHUB_ENV to make a variable available to all subsequent steps in the same job.

steps:

  – name: Set dynamic version

    run: echo “APP_VERSION=$(git describe –tags –abbrev=0)” >> $GITHUB_ENV

 

  – name: Use the version

    run: echo “Deploying version $APP_VERSION”

 

This is one of the most powerful patterns in GitHub Actions. It lets you compute values (from git tags, timestamps, API responses) mid-pipeline and pass them forward.

How to Use Secrets as Environment Variables

Secrets are encrypted environment variables stored at the repository, environment, or organization level. They’re the right tool for API keys, tokens, passwords, and anything that must never appear in your logs.

Storing Secrets

Go to your repository → SettingsSecrets and variablesActions → click New repository secret.

Referencing Secrets in a Workflow

steps:

  – name: Deploy to production

    env:

      API_TOKEN: ${{ secrets.API_TOKEN }}

      DATABASE_URL: ${{ secrets.DATABASE_URL }}

    run: ./deploy.sh

 

GitHub automatically redacts secrets from log output — you’ll see *** instead of the actual value. However, be aware that encoding tricks (base64, hex) can sometimes expose secrets if your scripts echo transformed values.

What Secrets Cannot Do

  • You cannot read a secret’s value through the GitHub UI after saving it
  • Secrets are not passed to workflows triggered by pull requests from forks (a critical security boundary)
  • Secret names are case-insensitive and can only contain alphanumeric characters and underscores

A 2022 study by researchers at North Carolina State University found that over 100,000 GitHub Actions workflows had at least one potential secret exposure risk — primarily from improper handling of fork-triggered events.

Environment-Level Variables and Secrets

GitHub Actions supports deployment environments (like staging, production) that carry their own variables and secrets, plus protection rules like required reviewers.

jobs:

  deploy:

    runs-on: ubuntu-latest

    environment: production

    steps:

      – name: Deploy

        env:

          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

        run: ./deploy-prod.sh

 

By assigning the job to the production environment, it automatically pulls secrets and variables scoped to that environment. If the environment has protection rules (manual approval, wait timers), those are enforced before the job runs.

This pattern is how mature engineering teams manage multi-environment deployments — the same workflow YAML deploys to dev automatically but requires a human approval gate for production.

Repository Variables vs. Secrets — When to Use Which

GitHub also supports non-secret configuration variables (available since 2023) stored at the repository or organization level. Unlike secrets, these are visible in the UI and in logs.

steps:

  – name: Use a config variable

    run: echo “Region is ${{ vars.DEPLOY_REGION }}”

 

Use variables (vars.*) for:

  • Non-sensitive configuration (region names, feature flags, version numbers)
  • Values your team needs to read and audit

Use secrets (secrets.*) for:

  • API keys and tokens
  • Passwords and private keys
  • Anything that would cause a security incident if leaked

According to GitHub’s 2023 Octoverse report, Actions workflows now run over 2 billion minutes per month across all repositories — which means misconfigured variable handling affects real pipelines at enormous scale.

Passing Variables Between Jobs

Jobs run on isolated runners and don’t share environment state. To pass a value from one job to another, use job outputs.

jobs:

  build:

    runs-on: ubuntu-latest

    outputs:

      version: ${{ steps.get-version.outputs.version }}

    steps:

      – name: Get version

        id: get-version

        run: echo “version=$(cat VERSION)” >> $GITHUB_OUTPUT

 

  deploy:

    needs: build

    runs-on: ubuntu-latest

    steps:

      – name: Use version from build job

        run: echo “Deploying version ${{ needs.build.outputs.version }}”

 

Key things to note:

  • The step must write to $GITHUB_OUTPUT (the old ::set-output command was deprecated in 2022)
  • The job must declare outputs mapping
  • Downstream jobs reference outputs via needs.<job-id>.outputs.<name>

Using Matrix Variables to Multiply Jobs

The strategy.matrix pattern lets you run the same job with different variable values in parallel — a common pattern for testing across Node.js versions, OS platforms, or database configurations.

jobs:

  test:

    runs-on: ${{ matrix.os }}

    strategy:

      matrix:

        os: [ubuntu-latest, windows-latest, macos-latest]

        node: [16, 18, 20]

    steps:

      – uses: actions/setup-node@v4

        with:

          node-version: ${{ matrix.node }}

      – run: npm test

 

This single job definition spawns 9 parallel jobs (3 OS × 3 Node versions). Matrix values are accessible as ${{ matrix.os }} and ${{ matrix.node }} throughout the job.

GitHub Actions supports up to 256 jobs generated from a single matrix — enough to cover virtually any multi-environment testing scenario.

Common Mistakes That Break Pipelines

Mistake: Using set-output Instead of $GITHUB_OUTPUT

The ::set-output command was deprecated in September 2022 and stopped working in 2023. Any workflow still using it will silently fail to pass values.

Wrong:

echo “::set-output name=version::1.2.3”

 

Right:

echo “version=1.2.3” >> $GITHUB_OUTPUT

 

Mistake: Printing Secrets to Logs

Even though GitHub redacts known secrets, transformed values can slip through.

# This can expose a secret if base64 encoding is logged

run: echo “Encoded: $(echo ${{ secrets.TOKEN }} | base64)”

 

Never echo, print, or log secret values — even in “debug” steps.

Mistake: Assuming Fork PRs Have Secret Access

Workflows triggered by pull_request events from forks do not have access to repository secrets. Use pull_request_target with caution (it runs in the base repo context, which introduces its own risks).

Mistake: Overwriting GitHub’s Default Variables

You cannot and should not try to override variables like GITHUB_SHA or GITHUB_TOKEN. GitHub sets these immutably for security and integrity reasons.

Mistake: Scope Confusion

Setting a variable at the workflow level and expecting it to be unavailable in a specific job doesn’t work — workflow-level variables cascade down. If you need job isolation, set variables at the job level.

Best Practices for Production-Grade Variable Management

Use the narrowest scope possible. If a variable is only needed in one step, declare it at the step level — not the workflow level.

Rotate secrets regularly. GitHub doesn’t enforce expiry. Build it into your security calendar. The average credential that gets exposed stays active for over 20 days before being revoked, according to GitGuardian’s 2023 report.

Audit your secrets list. Remove secrets you no longer use. Stale credentials are a silent attack surface.

Use environments for multi-stage deployments. Environment-level variables and protection rules give you control without duplicating workflows.

Never commit .env files. Even with a .gitignore entry, one wrong command can expose them. Use GitHub’s secret storage for everything sensitive.

Document your variables. Use workflow comments or a README in your .github/workflows/ directory to describe what each variable does and where it comes from. Future teammates (and future you) will thank you.

A Real-World Workflow Putting It All Together

Here’s a realistic deployment workflow that uses multiple variable patterns:

name: Build and Deploy

 

on:

  push:

    branches: [main]

 

env:

  NODE_ENV: production

  APP_NAME: my-app

 

jobs:

  build:

    runs-on: ubuntu-latest

    outputs:

      image-tag: ${{ steps.tag.outputs.tag }}

    steps:

      – uses: actions/checkout@v4

 

      – name: Generate image tag

        id: tag

        run: echo “tag=${{ github.sha }}” >> $GITHUB_OUTPUT

 

      – name: Build Docker image

        env:

          REGISTRY: ${{ vars.CONTAINER_REGISTRY }}

        run: |

          docker build -t $REGISTRY/$APP_NAME:${{ steps.tag.outputs.tag }} .

 

      – name: Push image

        env:

          REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}

          REGISTRY: ${{ vars.CONTAINER_REGISTRY }}

        run: |

          echo $REGISTRY_TOKEN | docker login $REGISTRY -u _token –password-stdin

          docker push $REGISTRY/$APP_NAME:${{ steps.tag.outputs.tag }}

 

  deploy:

    needs: build

    runs-on: ubuntu-latest

    environment: production

    steps:

      – name: Deploy to production

        env:

          DEPLOY_HOST: ${{ vars.PROD_HOST }}

          DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

          IMAGE_TAG: ${{ needs.build.outputs.image-tag }}

        run: ./scripts/deploy.sh

 

This workflow demonstrates:

  • Workflow-level env for shared, non-sensitive config
  • Step-level env for secrets (scoped as tightly as possible)
  • $GITHUB_OUTPUT for passing values between jobs
  • vars.* for non-secret configuration variables
  • secrets.* for credentials
  • Environment-level protection for production deployments

Conclusion

Environment variables in GitHub Actions aren’t just a convenience feature — they’re the backbone of maintainable, secure, and scalable CI/CD pipelines.

Start with GitHub’s default variables to understand your context. Use env blocks to inject configuration at the right scope. Store everything sensitive in secrets. Use vars for non-sensitive config your team needs to read. Pass values between jobs with $GITHUB_OUTPUT. And always think about scope — the tighter, the safer.

The difference between a workflow that fails mysteriously and one that’s predictable and auditable often comes down to how deliberately you’ve managed your environment variables.

Get this right once, and you won’t have to debug credential errors at 2am.

🎯 Stop Losing Pipeline Time to Manual Outreach

Book meetings on autopilot while your team ships code While your CI/CD pipeline automates deployments, SalesSo automates your outbound — cold email, LinkedIn, and calling — so your pipeline never runs dry on qualified leads.

7-day Free Trial |No Credit Card Needed.

FAQs

What is the difference between env and vars in GitHub Actions?

env is used inline within your workflow YAML to set variables for steps or jobs. vars refers to repository-level or environment-level configuration variables you define in GitHub's settings UI — non-sensitive values visible to your team. secrets stores encrypted values that are hidden from logs and the UI. Use vars when the value is safe to display, secrets when it isn't, and env when you need to set values directly in the YAML.

Can I use GitHub Actions environment variables to manage outbound lead generation or sales automation workflows?

Absolutely — and this is where teams that build automation pipelines often look beyond code deployments. Just as GitHub Actions variables let you configure which environment to deploy to, outbound lead generation platforms like SalesSo use targeting, campaign design, and scaling systems to deliver consistent pipeline results. If your team is running manual prospecting alongside automated deployments, SalesSo's complete outbound strategy — covering cold email, LinkedIn, and cold calling — can run in parallel so your sales pipeline scales as fast as your product does. Book a strategy meeting to see how it works.

Are GitHub Actions secrets truly secure?

Secrets are encrypted at rest and in transit, and GitHub redacts known secret values from workflow logs. However, security also depends on how you use them — avoid echoing transformed values, restrict fork PR access, and rotate credentials regularly. No storage mechanism eliminates risk from misuse.

Can I pass environment variables from one workflow to another?

Not directly — workflows are isolated. For shared configuration across workflows, use repository-level vars and secrets (accessible from any workflow in the repo) or use artifacts and workflow dispatch events to chain workflows together.

We deliver 100–400+ qualified appointments in a year through tailored omnichannel strategies

What to Build a High-Converting B2B Sales Funnel from Scratch

Lead Generation Agency

Build a Full Lead Generation Engine in Just 30 Days Guaranteed