Skip to main content

How to Inject Environment Variables During Build in GitHub Actions

· 12 min read
Anand Raja
Senior Software Engineer

When deploying applications with GitHub Actions, you often need to inject environment variables during the build process. These variables might include API keys, database URLs, feature flags, or configuration settings. GitHub provides a secure way to manage and inject these values into your build pipeline using GitHub Variables and GitHub Secrets.

This comprehensive guide covers how to properly inject environment variables during the build process using GitHub Actions for different frameworks including React, Next.js, Vite, and Angular, with practical examples and best practices.


Quick Navigation

This guide is organized by framework. Jump to your specific framework:


Understanding GitHub Variables vs. Secrets

Before diving into implementation, it's important to understand the difference between GitHub Variables and Secrets:

GitHub Variables

  • Purpose: Store non-sensitive configuration data
  • Visibility: Visible in repository settings and workflow logs
  • Use cases: API endpoints, environment names, public configuration
  • Access: Available via ${{ vars.VARIABLE_NAME }}

GitHub Secrets

  • Purpose: Store sensitive data securely
  • Visibility: Encrypted and hidden in logs (shown as ***)
  • Use cases: API keys, passwords, tokens, private keys
  • Access: Available via ${{ secrets.SECRET_NAME }}

Setup Steps

1. Define Variables/Secrets in GitHub

  1. Go to SettingsSecrets and variablesActions
  2. Variables tab: Add non-sensitive config (API_ENDPOINT, environment names)
  3. Secrets tab: Add sensitive data (API_KEY, passwords)

2. Use in Workflow

Access in .github/workflows/*.yml:

  • Variables: ${{ vars.VARIABLE_NAME }}
  • Secrets: ${{ secrets.SECRET_NAME }}

Understanding YAML Syntax and Shell Commands

Before diving into framework-specific implementations, let's understand some common syntax you'll see in GitHub Actions workflows.

The Pipe Symbol | in YAML

The pipe symbol | after run: indicates a multi-line string in YAML. It preserves line breaks and allows you to write multiple shell commands.

Example:

- name: Run multiple commands
run: |
echo "First command"
echo "Second command"
npm install
npm run build

This is equivalent to running each command on a separate line in your terminal. Without |, you can only write a single-line command:

- name: Run single command
run: npm run build

Line Continuation with Backslash \

The backslash \ at the end of a line indicates line continuation in shell scripts. It tells the shell that the command continues on the next line, making long commands more readable.

Example:

- name: Build with inline environment variables
run: |
REACT_APP_API_KEY=${{ secrets.API_KEY }} \
REACT_APP_API_ENDPOINT=${{ vars.API_ENDPOINT }} \
REACT_APP_ENV=production \
npm run build

This is equivalent to a single line:

REACT_APP_API_KEY=secret REACT_APP_API_ENDPOINT=endpoint REACT_APP_ENV=production npm run build

Inline Environment Variable Syntax

You can pass environment variables inline with a command using the format:

VARIABLE_NAME=value command

Example:

MY_VAR='Pass123' npm run build

This runs npm run build with MY_VAR set to Pass123 for only that command. The variable is not available to other commands.

In GitHub Actions:

- name: Build with inline variable
run: MY_VAR='Pass123' npm run build

Comparison with env: block:

# Method 1: Inline (one command only)
- name: Build
run: MY_VAR='Pass123' npm run build

# Method 2: env block (available to all commands in this step)
- name: Build
run: |
echo $MY_VAR # Available here
npm run build # Available here
env:
MY_VAR: 'Pass123'

The sed Command Explained

sed stands for Stream Editor. It's a Unix utility used to find and replace text in files. In CI/CD, it's commonly used to inject environment variables into configuration files.

Basic syntax:

sed -i "s|OLD_TEXT|NEW_TEXT|g" filename

Breakdown:

  • sed - Stream Editor command
  • -i - In-place edit (modifies the file directly, doesn't create a new file)
  • s - Substitute command (find and replace)
  • | - Delimiter (separates parts; can be any character: /, |, #, etc.)
  • OLD_TEXT - Text to find
  • NEW_TEXT - Text to replace with
  • g - Global flag (replace all occurrences, not just the first)
  • filename - File to modify

Why use | instead of /?

When your text contains forward slashes (like URLs), using | as a delimiter is cleaner:

# Hard to read with / delimiter (need to escape slashes)
sed -i "s/API_URL_PLACEHOLDER/https:\/\/api.example.com/g" file.ts

# Easy to read with | delimiter (no escaping needed)
sed -i "s|API_URL_PLACEHOLDER|https://api.example.com|g" file.ts

Practical Example in GitHub Actions:

File before sed:

// environment.prod.ts
export const environment = {
apiUrl: 'API_URL_PLACEHOLDER',
apiKey: 'API_KEY_PLACEHOLDER'
};

GitHub Actions workflow:

- name: Inject environment variables
run: |
sed -i "s|API_URL_PLACEHOLDER|${{ secrets.API_URL }}|g" src/environments/environment.prod.ts
sed -i "s|API_KEY_PLACEHOLDER|${{ secrets.API_KEY }}|g" src/environments/environment.prod.ts

File after sed (if API_URL = "https://api.prod.com" and API_KEY = "secret123"):

// environment.prod.ts
export const environment = {
apiUrl: 'https://api.prod.com',
apiKey: 'secret123'
};

Relation to GitHub Actions CI/CD

GitHub Actions combines these concepts to inject variables during build:

1. Using env: block (Recommended):

- name: Build
run: npm run build
env:
REACT_APP_API_KEY: ${{ secrets.API_KEY }}
REACT_APP_API_ENDPOINT: ${{ vars.API_ENDPOINT }}
  • Variables available to the build process
  • Clear and maintainable
  • GitHub automatically handles security (masks secrets in logs)

2. Using inline variables:

- name: Build
run: REACT_APP_API_KEY=${{ secrets.API_KEY }} npm run build
  • Variable only available to that specific command
  • Less common, but works for single commands

3. Using sed for file replacement:

- name: Inject and build
run: |
sed -i "s|API_KEY_PLACEHOLDER|${{ secrets.API_KEY }}|g" config.js
npm run build
  • Physically modifies files before build
  • Useful for Angular or other frameworks that don't use process.env directly
  • File changes are temporary (only in CI environment)

All three methods achieve the same goal: Getting your GitHub secrets/variables into your application build. Choose based on your framework and preferences:

  • React/Next.js/Vite: Use env: block
  • Angular: Use sed or file overwriting
  • Node.js backend: Use env: block

Framework-Specific Implementations


React (Create React App)

Uses process.env with REACT_APP_ prefix.

- name: Build with environment variables
run: npm run build
env:
REACT_APP_API_KEY: ${{ secrets.API_KEY }}
REACT_APP_API_ENDPOINT: ${{ vars.API_ENDPOINT }}
NODE_ENV: production

Access in code:

const apiKey = process.env.REACT_APP_API_KEY;

Next.js Applications

Uses NEXT_PUBLIC_ prefix for client-side variables.

- name: Build Next.js app
run: npm run build
env:
# Server-side only
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# Client-side (needs NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}

Access in code:

// Server-side
const dbUrl = process.env.DATABASE_URL;
// Client-side
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

Vite Applications

Uses VITE_ prefix for client-side variables.

- name: Build Vite app
run: npm run build
env:
VITE_API_KEY: ${{ secrets.API_KEY }}
VITE_API_ENDPOINT: ${{ vars.API_ENDPOINT }}

Access in code:

const apiKey = import.meta.env.VITE_API_KEY;

Key Differences from Create React App:

FeatureCreate React AppVite
PrefixREACT_APP_VITE_
Accessprocess.envimport.meta.env

Dynamic Base Path Extraction

The workflow automatically extracts the repository name from package.json:

- name: Extract repository name from package.json
id: package
run: echo "name=$(node -p "require('./package.json').name")" >> $GITHUB_OUTPUT

How this works:

  1. node -p "require('./package.json').name"

    • Runs Node.js to read and print the name field from package.json
    • Example output: react-lock-console-demo
  2. echo "name=react-lock-console-demo"

    • Creates a key-value pair: name=react-lock-console-demo
  3. >> $GITHUB_OUTPUT

    • $GITHUB_OUTPUT is a special GitHub Actions environment variable
    • It's a file path where you write output data
    • GitHub Actions reads this file and makes the data available to subsequent steps
    • Think of it as a way to "export" variables between workflow steps
  4. id: package

    • This step is identified as package
    • The output can be referenced in later steps
  5. ${{ steps.package.outputs.name }}

    • steps - Refers to all workflow steps
    • package - The step ID we defined
    • outputs - All outputs from that step
    • name - The specific output key we wrote to $GITHUB_OUTPUT
    • Final value: react-lock-console-demo

Used in the build step:

- name: Build React app for GitHub Pages
env:
VITE_PASSWORD_HASH: ${{ secrets.PASSWORD_HASH }}
VITE_BASE_PATH: /${{ steps.package.outputs.name }}/
run: npm run build

This creates: VITE_BASE_PATH=/react-lock-console-demo/

Benefits:

  • ✅ Change the name in package.json → base path updates automatically
  • ✅ No manual configuration needed
  • ✅ Workflow is reusable across different projects

Angular Applications

Angular cannot access process.env directly in the browser. Use environment files instead.

Method 1: Overwriting Environment Files

- name: Generate environment.prod.ts
run: |
cat > src/environments/environment.prod.ts << EOF
export const environment = {
production: true,
apiUrl: '${{ secrets.API_URL }}',
apiKey: '${{ secrets.API_KEY }}',
appName: '${{ vars.APP_NAME }}'
};
EOF

- name: Build Angular application
run: npm run build -- --configuration=production
    name: Angular CI/CD

on:
push:
branches:
- main

jobs:
build-and-deploy:
runs-on: ubuntu-latest
env: # Workflow-level environment variable
APP_NAME: MyAngularApp
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'

- name: Install dependencies
run: npm install

- name: Build Angular application
env: # Step-level environment variable
API_URL: ${{ secrets.API_URL_PROD }} # Using a GitHub Secret for sensitive data
run: |
# Example: Overwrite environment.ts with values from GitHub Actions
echo "export const environment = { production: true, apiUrl: '${{ env.API_URL }}', appName: '${{ env.APP_NAME }}' };" > src/environments/environment.prod.ts
ng build --configuration=production

Method 2: Placeholder Replacement

Prepare environment file:

// src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'API_URL_PLACEHOLDER',
apiKey: 'API_KEY_PLACEHOLDER'
};

GitHub Actions workflow:

- name: Inject environment variables
run: |
sed -i "s|API_URL_PLACEHOLDER|${{ secrets.API_URL }}|g" src/environments/environment.prod.ts
sed -i "s|API_KEY_PLACEHOLDER|${{ secrets.API_KEY }}|g" src/environments/environment.prod.ts

- name: Build Angular application
run: npm run build -- --configuration=production
    name: Angular CI/CD

on: [push]

jobs:
build:
runs-on: ubuntu-latest
env:
API_BASE_URL: ${{ secrets.API_BASE_URL_PROD }} # Example using a secret
ANALYTICS_KEY: your-analytics-key # Example direct variable
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'

- name: Install dependencies
run: npm install

- name: Inject environment variables into Angular
run: |
sed -i "s|API_BASE_URL_PLACEHOLDER|${{ env.API_BASE_URL }}|g" src/environments/environment.prod.ts
sed -i "s|ANALYTICS_KEY_PLACEHOLDER|${{ env.ANALYTICS_KEY }}|g" src/environments/environment.prod.ts

- name: Build Angular application
run: ng build --configuration=production

Access in code:

import { environment } from '../environments/environment';

const apiUrl = environment.apiUrl;

Node.js Backend Applications

No prefix needed for server-side variables.

- name: Build Node.js app
run: npm run build
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
PORT: ${{ vars.PORT }}

Access in code:

const dbUrl = process.env.DATABASE_URL;

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Build project with injected environment variable
run: |
npm install
REACT_APP_API_KEY=${{ secrets.MY_API_KEY }} \
REACT_APP_ENV_MODE=${{ vars.ENVIRONMENT }} \
npm run build
env:
# You can also define environment variables here
# MY_VARIABLE: ${{ vars.MY_VARIABLE }}

Explanation:

  • REACT_APP_API_KEY=${{ secrets.MY_API_KEY }}: This line directly injects the value of the GitHub Secret MY_API_KEY as an environment variable named REACT_APP_API_KEY for the npm run build command.
  • Create React App automatically picks up environment variables prefixed with REACT_APP_ during the build process and makes them available in your application via process.env.REACT_APP_API_KEY.

Access the Variable in Your React App:

In your React component or JavaScript files, you can then access this environment variable:

const apiKey = process.env.REACT_APP_API_KEY;

// Use apiKey in your application logic
console.log("API Key:", apiKey);

This is common for all JavaScript applications. When deploying via GitHub Actions, if you want to inject process.env.VARIABLE_NAME during the build process, you have to include it like the example above. This is the safest method compared to using .env files.

Why is this safer than .env files?

  • No file in repository: Environment variables are injected at runtime, so there's no .env file in your repository that could accidentally be committed.
  • Centralized secrets management: GitHub Secrets are encrypted and managed centrally, reducing the risk of exposure.
  • No deployment artifacts: The .env file is not created in your build artifacts, preventing accidental leaks during deployment.

b) Create a .env file from secrets/variables:

  • For applications that primarily rely on .env files (e.g., Create React App, Next.js), you can create the .env file dynamically within your workflow using secrets or variables.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Create .env file from secrets
run: |
echo "REACT_APP_API_KEY=${{ secrets.API_KEY }}" > .env
echo "REACT_APP_ANOTHER_VAR=${{ vars.ANOTHER_VARIABLE }}" >> .env
- name: Build project
run: npm run build

**Important Considerations:**
```## Framework Comparison Table

| Framework | Prefix Required | Access Method | Build-Time Injection | Special Notes |
|-----------|----------------|---------------|---------------------|---------------|
| **Create React App** | `REACT_APP_` | `process.env` | ✅ Yes | Embedded in bundle at build time |
| **Next.js** | `NEXT_PUBLIC_` (client) | `process.env` | ✅ Yes | Server vars don't need prefix |
| **Vite** | `VITE_` | `import.meta.env` | ✅ Yes | Uses `import.meta.env` not `process.env` |
| **Angular** | None | `environment` object | ✅ Yes | Uses environment files, not `process.env` |
| **Node.js/Express** | None | `process.env` | ✅ Yes | Server-side only |

---

## Best Practices

### 1. Never Log Secrets

```yaml
# ❌ Bad
- run: echo "API Key is ${{ secrets.API_KEY }}"

# ✅ Good
- run: echo "API Key is configured"

2. Validate Variables

- name: Validate
run: |
[ -z "${{ secrets.API_KEY }}" ] && echo "Error: API_KEY not set" && exit 1
[ -z "${{ vars.API_ENDPOINT }}" ] && echo "Error: API_ENDPOINT not set" && exit 1

3. Use Environment-Specific Secrets

jobs:
deploy-production:
environment: production
steps:
- run: npm run build
env:
API_KEY: ${{ secrets.PRODUCTION_API_KEY }}

4. Document Variables

Create .env.example:

REACT_APP_API_KEY=your-key-here
REACT_APP_API_ENDPOINT=https://api.example.com

Troubleshooting

IssueSolution
Variables undefined in buildCheck framework prefix: REACT_APP_, NEXT_PUBLIC_, VITE_
Variables not updatingRe-run workflow, clear cache, verify exact variable names
Secrets unavailable in fork PRsUse Variables for non-sensitive data, or require manual approval

Complete Workflow Example

name: Build and Deploy

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- run: npm ci

- name: Validate
run: |
[ -z "${{ secrets.API_KEY }}" ] && echo "Error: API_KEY not set" && exit 1

- name: Build
run: npm run build
env:
REACT_APP_API_KEY: ${{ secrets.API_KEY }}
REACT_APP_API_ENDPOINT: ${{ vars.API_ENDPOINT }}
REACT_APP_COMMIT_SHA: ${{ github.sha }}
NODE_ENV: production

- uses: actions/upload-artifact@v4
with:
name: build-output
path: build/

- name: Deploy
if: github.ref == 'refs/heads/main'
run: npm run deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Summary

Framework Prefixes

  • React/CRA: REACT_APP_ with process.env
  • Next.js: NEXT_PUBLIC_ (client), no prefix (server)
  • Vite: VITE_ with import.meta.env
  • Angular: Use environment files, not process.env
  • Node.js: No prefix needed

Key Points

  • Use Secrets for sensitive data (API keys, tokens)
  • Use Variables for non-sensitive config (endpoints, flags)
  • Validate required variables before building
  • Never commit .env files
  • Document variables in .env.example

Angular Methods

  • Method 1: Overwrite environment file (simple configs)
  • Method 2: Placeholder replacement with sed (complex configs)

Additional Resources