CI/CD Integration
env-twin is designed to work seamlessly in CI/CD pipelines for automated environment configuration management. This guide covers integration patterns, best practices, and real-world examples.
Key Flags for CI/CD
Three flags make env-twin CI/CD-friendly:
—yes (Non-Interactive Mode)
Skip all confirmation prompts:
Without this flag, env-twin will prompt for confirmation, which will hang in CI pipelines.
—no-backup (Skip Backup Creation)
Disable backup creation in ephemeral environments:
env-twin sync --no-backup
Backups are unnecessary in CI since:
- Containers/runners are destroyed after each run
- Source files are in version control
- Faster execution without I/O overhead
—json (Machine-Readable Output)
Output analysis in JSON format for parsing:
env-twin sync --json > analysis.json
Enables:
- Automated validation of sync results
- Integration with other tools
- Structured logging and monitoring
- Custom reporting dashboards
GitHub Actions Integration
Basic Workflow
Simple environment sync on every push:
name: Sync Environment Files
on:
push:
branches: [main, develop]
paths:
- '.env*'
- '.github/workflows/env-sync.yml'
jobs:
sync-env:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install env-twin
run: npm install -g env-twin
- name: Sync environment files
run: env-twin sync --yes --no-backup
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .env*
git diff --quiet && git diff --staged --quiet || git commit -m "chore: sync environment files"
git push
Advanced: Validation with JSON Output
Validate environment sync and fail if issues detected:
name: Validate Environment Configuration
on:
pull_request:
paths:
- '.env*'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install env-twin
run: npm install -g env-twin
- name: Analyze environment files
id: analyze
run: |
env-twin sync --json --no-backup > analysis.json
cat analysis.json
- name: Check for inconsistencies
run: |
# Parse JSON and fail if missing keys detected
MISSING_KEYS=$(jq -r '.missingKeys | length' analysis.json)
if [ "$MISSING_KEYS" -gt 0 ]; then
echo "❌ Found $MISSING_KEYS missing environment keys!"
jq -r '.missingKeys[] | " - \(.file): \(.keys | join(", "))"' analysis.json
exit 1
fi
echo "✅ All environment files are synchronized"
- name: Upload analysis artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: env-analysis
path: analysis.json
Multi-Environment Deployment
Restore environment-specific configurations:
name: Deploy to Staging
on:
push:
branches: [staging]
jobs:
deploy:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install env-twin
run: npm install -g env-twin
- name: Restore staging environment
env:
BACKUP_TIMESTAMP: ${{ vars.STAGING_ENV_TIMESTAMP }}
run: |
# Restore specific backup for staging environment
env-twin restore $BACKUP_TIMESTAMP --yes --preserve-permissions
- name: Verify environment loaded
run: |
if [ ! -f .env ]; then
echo "❌ Environment file not restored!"
exit 1
fi
echo "✅ Environment configuration loaded"
- name: Deploy application
run: npm run deploy:staging
Backup Before Release
Create environment backup as part of release process:
name: Create Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install env-twin
run: npm install -g env-twin
- name: Create environment backup
run: |
# Sync creates backup automatically
env-twin sync --yes
# Archive backup directory
tar -czf env-backup-${{ github.ref_name }}.tar.gz .env-twin/
- name: Upload backup to release
uses: softprops/action-gh-release@v1
with:
files: env-backup-${{ github.ref_name }}.tar.gz
token: ${{ secrets.GITHUB_TOKEN }}
GitLab CI Integration
Basic Pipeline
# .gitlab-ci.yml
stages:
- validate
- deploy
variables:
NODE_VERSION: "20"
before_script:
- npm install -g env-twin
validate:env:
stage: validate
image: node:${NODE_VERSION}
script:
- env-twin sync --json --no-backup > analysis.json
- |
MISSING_KEYS=$(jq -r '.missingKeys | length' analysis.json)
if [ "$MISSING_KEYS" -gt 0 ]; then
echo "❌ Environment validation failed"
jq -r '.missingKeys[] | " - \(.file): \(.keys | join(", "))"' analysis.json
exit 1
fi
artifacts:
paths:
- analysis.json
expire_in: 1 week
only:
changes:
- .env*
deploy:production:
stage: deploy
image: node:${NODE_VERSION}
environment:
name: production
script:
# Restore production environment configuration
- env-twin restore ${PROD_ENV_TIMESTAMP} --yes --preserve-permissions
- npm run deploy
only:
- main
Multi-Environment Configuration
# .gitlab-ci.yml
stages:
- prepare
- deploy
.deploy_template: &deploy_template
image: node:20
before_script:
- npm install -g env-twin
script:
- env-twin restore ${ENV_BACKUP_TIMESTAMP} --yes --preserve-permissions
- npm ci
- npm run build
- npm run deploy:${CI_ENVIRONMENT_NAME}
deploy:staging:
<<: *deploy_template
stage: deploy
environment:
name: staging
variables:
ENV_BACKUP_TIMESTAMP: "${STAGING_ENV_TIMESTAMP}"
only:
- develop
deploy:production:
<<: *deploy_template
stage: deploy
environment:
name: production
variables:
ENV_BACKUP_TIMESTAMP: "${PROD_ENV_TIMESTAMP}"
only:
- main
when: manual
Scheduled Environment Audits
# .gitlab-ci.yml
audit:env-drift:
image: node:20
stage: validate
before_script:
- npm install -g env-twin
script:
- |
echo "🔍 Auditing environment configuration drift..."
env-twin sync --json --no-backup > audit.json
# Send to monitoring system
curl -X POST https://monitoring.example.com/api/audits \
-H "Content-Type: application/json" \
-d @audit.json
artifacts:
reports:
dotenv: audit.json
only:
- schedules
Jenkins Integration
Declarative Pipeline
// Jenkinsfile
pipeline {
agent any
environment {
NODE_VERSION = '20'
}
stages {
stage('Setup') {
steps {
script {
// Install env-twin
sh 'npm install -g env-twin'
}
}
}
stage('Validate Environment') {
when {
changeset '.env*'
}
steps {
script {
// Sync and validate
sh '''
env-twin sync --json --no-backup > analysis.json
MISSING_KEYS=$(jq -r '.missingKeys | length' analysis.json)
if [ "$MISSING_KEYS" -gt 0 ]; then
echo "❌ Environment validation failed"
jq -r '.missingKeys[] | " - \(.file): \(.keys | join(", "))"' analysis.json
exit 1
fi
echo "✅ Environment files validated"
'''
}
}
post {
always {
archiveArtifacts artifacts: 'analysis.json', fingerprint: true
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
script {
// Restore staging environment
sh "env-twin restore ${env.STAGING_BACKUP_TS} --yes --preserve-permissions"
// Deploy
sh 'npm run deploy:staging'
}
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
steps {
input message: 'Deploy to production?', ok: 'Deploy'
script {
// Create pre-deployment backup
sh '''
env-twin sync --yes
tar -czf "env-backup-${BUILD_NUMBER}.tar.gz" .env-twin/
'''
// Restore production environment
sh "env-twin restore ${env.PROD_BACKUP_TS} --yes --preserve-permissions --create-rollback"
// Deploy
sh 'npm run deploy:production'
}
}
post {
always {
archiveArtifacts artifacts: 'env-backup-*.tar.gz', fingerprint: true
}
}
}
}
post {
failure {
script {
// Alert on failure
emailext(
subject: "❌ Pipeline Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Environment configuration or deployment failed. Check console output.",
to: "${env.CHANGE_AUTHOR_EMAIL}"
)
}
}
}
}
Scripted Pipeline with Rollback
// Jenkinsfile
node {
try {
stage('Checkout') {
checkout scm
}
stage('Setup') {
sh 'npm install -g env-twin'
}
stage('Restore Environment') {
sh "env-twin restore ${PROD_BACKUP_TS} --yes --preserve-permissions --create-rollback --verbose"
}
stage('Deploy') {
sh 'npm run deploy:production'
}
stage('Health Check') {
// Verify deployment
sh '''
HEALTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health)
if [ "$HEALTH_STATUS" != "200" ]; then
echo "❌ Health check failed: $HEALTH_STATUS"
exit 1
fi
'''
}
} catch (Exception e) {
echo "❌ Deployment failed: ${e.message}"
// Rollback environment configuration
sh '''
echo "🔄 Rolling back environment configuration..."
# Find most recent rollback snapshot
ROLLBACK_ID=$(ls -t .env-twin/rollbacks/ | head -n1)
if [ -n "$ROLLBACK_ID" ]; then
env-twin restore --yes
echo "✅ Environment rolled back to: $ROLLBACK_ID"
fi
'''
throw e
}
}
Best Practices for CI/CD
1. Always Use —yes in Pipelines
Never omit --yes in automated pipelines - your pipeline will hang waiting for input!
# ❌ Bad - Will hang
env-twin restore
# ✅ Good - Non-interactive
env-twin restore --yes
2. Skip Backups in Ephemeral Environments
# CI/CD runners are destroyed after each job
env-twin sync --yes --no-backup
Benefits:
- Faster execution
- Reduced disk usage
- Simpler cleanup
3. Use JSON Output for Validation
# Generate machine-readable analysis
env-twin sync --json --no-backup > analysis.json
# Parse and validate
jq -e '.missingKeys | length == 0' analysis.json || exit 1
4. Preserve Permissions in Production
# Maintain security posture
env-twin restore --yes --preserve-permissions
5. Create Rollback Snapshots for Critical Deploys
# Enable rollback capability
env-twin restore --yes --create-rollback --preserve-permissions
6. Use Verbose Logging for Debugging
# Enable detailed logs in CI
env-twin restore --yes --verbose
7. Version Control Backup Timestamps
Store backup identifiers as environment variables:
# GitHub Actions
env:
STAGING_BACKUP: "20260304-120000"
PROD_BACKUP: "20260304-100000"
# GitLab CI/CD Variables
STAGING_ENV_TIMESTAMP="20260304-120000"
PROD_ENV_TIMESTAMP="20260304-100000"
8. Archive Backups as Build Artifacts
# GitHub Actions
- name: Archive environment backup
uses: actions/upload-artifact@v4
with:
name: env-backup
path: .env-twin/
retention-days: 30
Security Considerations
Never Commit Secrets to CI Logs
Environment files may contain secrets! Ensure CI logs don’t expose them.
# ❌ Bad - May expose secrets
- name: Show environment
run: cat .env
# ✅ Good - Safe verification
- name: Verify environment loaded
run: |
if [ ! -f .env ]; then
echo "❌ .env file missing"
exit 1
fi
echo "✅ Environment file exists ($(wc -l < .env) lines)"
Use Secure Variable Storage
- GitHub Actions: Use encrypted secrets
- GitLab CI: Use masked and protected variables
- Jenkins: Use credentials plugin
# GitHub Actions - Using secrets
- name: Restore production config
env:
PROD_TIMESTAMP: ${{ secrets.PROD_ENV_TIMESTAMP }}
run: env-twin restore $PROD_TIMESTAMP --yes
Restrict Access to Backup Artifacts
# Only allow access to backups from protected branches
artifacts:
paths:
- .env-twin/
when: on_success
expire_in: 7 days
# GitLab: Use protected artifacts
# protected: true
Troubleshooting
Pipeline Hangs
Issue: Pipeline runs indefinitely
Cause: Missing --yes flag, waiting for user input
Solution:
env-twin restore --yes --no-backup
Backup Not Found
Issue: “Backup snapshot not found”
Cause: Backup timestamp variable not set or incorrect
Solution:
# Verify variable is set
- name: Debug
run: echo "Backup timestamp: ${BACKUP_TIMESTAMP}"
# Use default (most recent) if not set
- name: Restore
run: |
if [ -n "$BACKUP_TIMESTAMP" ]; then
env-twin restore $BACKUP_TIMESTAMP --yes
else
env-twin restore --yes
fi
Permission Denied
Issue: Cannot write to .env-twin/ directory
Cause: Insufficient permissions in CI runner
Solution:
- name: Fix permissions
run: |
mkdir -p .env-twin
chmod -R 755 .env-twin
- name: Restore
run: env-twin restore --yes
JSON Parsing Fails
Issue: jq command fails to parse output
Cause: env-twin output mixed with other logs
Solution:
# Redirect only JSON to file
env-twin sync --json --no-backup 2>/dev/null > analysis.json
# Or use quiet mode if available
env-twin sync --json --no-backup --quiet > analysis.json