How to Use GitHub Actions Environment Variables
- Sophie Ricci
- Views : 28,543
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 → Settings → Secrets and variables → Actions → 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?
Can I use GitHub Actions environment variables to manage outbound lead generation or sales automation workflows?
Are GitHub Actions secrets truly secure?
Can I pass environment variables from one workflow to another?
We deliver 100–400+ qualified appointments in a year through tailored omnichannel strategies
- blog
- Sales Development
- How to Use GitHub Actions Environment Variables