How to Inject Environment Variables During Build in GitHub Actions
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
- Go to Settings → Secrets and variables → Actions
- Variables tab: Add non-sensitive config (
API_ENDPOINT, environment names) - 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 findNEW_TEXT- Text to replace withg- 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.envdirectly - 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
sedor 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:
| Feature | Create React App | Vite |
|---|---|---|
| Prefix | REACT_APP_ | VITE_ |
| Access | process.env | import.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:
-
node -p "require('./package.json').name"- Runs Node.js to read and print the
namefield frompackage.json - Example output:
react-lock-console-demo
- Runs Node.js to read and print the
-
echo "name=react-lock-console-demo"- Creates a key-value pair:
name=react-lock-console-demo
- Creates a key-value pair:
-
>> $GITHUB_OUTPUT$GITHUB_OUTPUTis 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
-
id: package- This step is identified as
package - The output can be referenced in later steps
- This step is identified as
-
${{ steps.package.outputs.name }}steps- Refers to all workflow stepspackage- The step ID we definedoutputs- All outputs from that stepname- 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
nameinpackage.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 SecretMY_API_KEYas an environment variable namedREACT_APP_API_KEYfor thenpm run buildcommand.- Create React App automatically picks up environment variables prefixed with
REACT_APP_during the build process and makes them available in your application viaprocess.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
.envfile 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
.envfile 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
.envfiles (e.g., Create React App, Next.js), you can create the.envfile 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
| Issue | Solution |
|---|---|
| Variables undefined in build | Check framework prefix: REACT_APP_, NEXT_PUBLIC_, VITE_ |
| Variables not updating | Re-run workflow, clear cache, verify exact variable names |
| Secrets unavailable in fork PRs | Use 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_withprocess.env - Next.js:
NEXT_PUBLIC_(client), no prefix (server) - Vite:
VITE_withimport.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
.envfiles - Document variables in
.env.example
Angular Methods
- Method 1: Overwrite environment file (simple configs)
- Method 2: Placeholder replacement with
sed(complex configs)
