You have tests for your code. You have linting for your syntax. But what about your configuration? A single missing environment variable can take down production just as effectively as a bug in your business logic.
This guide shows you how to add environment variable validation to your CI/CD pipeline so configuration errors get caught before deployment, not after.
Why Validate in CI/CD?
Consider this scenario: your app works perfectly in staging. You deploy to production. It crashes immediately because someone forgot to set STRIPE_WEBHOOK_SECRET in the production environment.
This happens more often than anyone wants to admit. The fix is simple: validate your environment configuration as part of your deployment pipeline.
Benefits of CI/CD validation:
- Fail fast - Catch missing or invalid variables before deployment starts
- Consistent checks - Same validation runs every time, no human error
- Documentation - Your schema becomes the source of truth for required configuration
- Audit trail - Git history shows when configuration requirements changed
The Setup: Schema-Based Validation
The approach is straightforward:
- Define a schema that describes your required environment variables
- Run a validation check in your CI/CD pipeline
- Block deployment if validation fails
Here is an example schema (env.schema.json):
{
"NODE_ENV": {
"type": "enum",
"values": ["development", "staging", "production"],
"required": true
},
"DATABASE_URL": {
"type": "url",
"required": true
},
"REDIS_URL": {
"type": "url",
"required": true
},
"API_SECRET_KEY": {
"type": "string",
"required": true,
"validate": { "min_length": 32 }
},
"PORT": {
"type": "int",
"default": 3000,
"validate": { "min": 1024, "max": 65535 }
},
"LOG_LEVEL": {
"type": "enum",
"values": ["debug", "info", "warn", "error"],
"default": "info"
}
}
This schema documents every environment variable your app needs, their types, and any constraints. Commit it to your repository alongside your code.
GitHub Actions Setup
Here is how to add validation to a GitHub Actions workflow:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
validate-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install zenv
run: |
curl -sSL https://github.com/zorl-engine/zorath-env/releases/latest/download/zenv-linux-x64 -o zenv
chmod +x zenv
sudo mv zenv /usr/local/bin/
- name: Validate environment configuration
env:
NODE_ENV: production
DATABASE_URL: ${{ secrets.DATABASE_URL }}
REDIS_URL: ${{ secrets.REDIS_URL }}
API_SECRET_KEY: ${{ secrets.API_SECRET_KEY }}
run: |
zenv check --schema env.schema.json
- name: Run tests
run: npm test
- name: Deploy
if: success()
run: |
# Your deployment command here
echo "Deploying to production..."
The key is the zenv check step. If any required variable is missing or invalid, the command exits with code 1 and the workflow fails. Deployment never happens.
Using Environment Files
If your CI environment uses .env files:
- name: Validate environment configuration
run: |
# Create .env from secrets
echo "NODE_ENV=production" >> .env
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
echo "REDIS_URL=${{ secrets.REDIS_URL }}" >> .env
echo "API_SECRET_KEY=${{ secrets.API_SECRET_KEY }}" >> .env
# Validate
zenv check
# Clean up (don't leave secrets in artifacts)
rm .env
GitLab CI Setup
For GitLab CI/CD:
# .gitlab-ci.yml
stages:
- validate
- test
- deploy
variables:
NODE_ENV: production
validate-config:
stage: validate
image: rust:latest
before_script:
- cargo install zorath-env
script:
- zenv check --schema env.schema.json
rules:
- if: $CI_COMMIT_BRANCH == "main"
test:
stage: test
needs: [validate-config]
script:
- npm test
deploy:
stage: deploy
needs: [test]
script:
- echo "Deploying..."
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
GitLab CI variables are automatically available as environment variables, so zenv check can validate them directly.
Jenkins Pipeline
For Jenkins declarative pipelines:
pipeline {
agent any
environment {
NODE_ENV = 'production'
DATABASE_URL = credentials('database-url')
REDIS_URL = credentials('redis-url')
API_SECRET_KEY = credentials('api-secret-key')
}
stages {
stage('Validate Config') {
steps {
sh '''
curl -sSL https://github.com/zorl-engine/zorath-env/releases/latest/download/zenv-linux-x64 -o zenv
chmod +x zenv
./zenv check --schema env.schema.json
'''
}
}
stage('Test') {
steps {
sh 'npm test'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'echo "Deploying..."'
}
}
}
}
Docker and Container Deployments
For containerized applications, validate before the container starts:
# Dockerfile
FROM node:20-alpine
# Install zenv
RUN curl -sSL https://github.com/zorl-engine/zorath-env/releases/latest/download/zenv-linux-x64 -o /usr/local/bin/zenv \
&& chmod +x /usr/local/bin/zenv
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Validate on container start
CMD ["sh", "-c", "zenv check && node server.js"]
Or in your docker-compose setup:
# docker-compose.yml
services:
app:
build: .
env_file: .env.production
command: sh -c "zenv check && node server.js"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
Handling Validation Failures
When validation fails, you want clear, actionable error messages. A good validation tool tells you exactly what is wrong:
zenv check failed:
- DATABASE_URL: missing (required)
- API_SECRET_KEY: too short (minimum 32 characters, got 16)
- LOG_LEVEL: invalid value "verbose" (expected one of: debug, info, warn, error)
This output should go directly to your CI logs. No guessing what went wrong.
Best Practices
1. Validate Early
Put validation as the first step in your pipeline, before tests or builds. Why waste 10 minutes running tests if the deploy would fail anyway?
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: zenv check
test:
needs: validate # Only run if validation passes
runs-on: ubuntu-latest
steps:
- run: npm test
2. Keep Schema in Version Control
Your env.schema.json should be committed alongside your code. This way:
- Changes to configuration requirements go through code review
- You have a history of when requirements changed
- New team members can see exactly what configuration is needed
3. Use Different Schemas per Environment
If development and production have different requirements:
env.schema.json # Base schema
env.schema.dev.json # Development overrides
env.schema.prod.json # Production requirements
- name: Validate (Production)
if: github.ref == 'refs/heads/main'
run: zenv check --schema env.schema.prod.json
- name: Validate (Development)
if: github.ref != 'refs/heads/main'
run: zenv check --schema env.schema.dev.json
4. Document with Examples
Include an .env.example file with placeholder values:
# .env.example
NODE_ENV=development
DATABASE_URL=postgres://localhost/myapp
REDIS_URL=redis://localhost:6379
API_SECRET_KEY=your-secret-key-here-min-32-chars
PORT=3000
LOG_LEVEL=debug
This helps developers set up their local environment and shows what variables are needed.
Common Pitfalls
Pitfall 1: Hardcoding secrets in CI config
Never put actual secret values in your CI configuration files. Use your CI platform's secrets management:
# Bad - secret in plain text
env:
API_KEY: sk_live_xxxxx
# Good - reference to secret store
env:
API_KEY: ${{ secrets.API_KEY }}
Pitfall 2: Skipping validation for hotfixes
It is tempting to bypass validation for urgent fixes. Do not. A broken config in a hotfix creates a second outage.
Pitfall 3: Not validating in staging
Validate in staging too, not just production. This catches issues before they ever reach prod.
Getting Started
If you are not validating environment configuration today, start simple:
- Create an
env.schema.jsonlisting your required variables - Add a validation step to your CI pipeline
- Run it on your next deployment
It takes 15 minutes to set up and will save you from the 3 AM "why is production down" call. Worth it.
Resources
- zorath-env on crates.io - Install via Cargo
- GitHub Repository - Source code and releases
- Schema Reference - Full documentation on schema format
- Common .env Mistakes - What to watch out for