Thank you for contributing to the EPA WebCMS project! This guide will help you set up your development environment and understand our development workflows.
- First-Time Setup
- Development Environment
- Daily Development Workflow
- Deployment Guide
- Git Workflow
- Testing
- Code Standards
- Helpful Commands
- Troubleshooting
Prerequisites:
- DDEV 1.24 or higher
- Docker Desktop
- Git
- Composer
- Node.js and npm
-
Clone the repository:
git clone -b main git@github.com:USEPA/webcms.git cd webcms -
Start DDEV:
cd services/drupal ddev start -
Create S3 bucket for s3fs:
ddev aws-setup
-
Obtain the latest database:
- Contact Michael Hessling for the latest database dump
- Place the
.tarfile inservices/drupal/.ddev/db/
-
Import the database:
ddev import-db
Note: For very large dumps, this may timeout. DDEV may continue the import in the background—verify with
docker stats. If DDEV kills the process, connect a MySQL client directly using the forwarded port fromddev status. -
Copy environment file:
cp .env.example .env
-
Install PHP dependencies:
ddev composer install
If you encounter errors, delete
services/drupal/.ddev/vendorand run:ddev composer clearcache -
Install theme requirements:
ddev gesso install
-
Build theme assets:
# One-time build ddev gesso build # Or watch for changes during development ddev gesso watch
-
Apply latest configuration:
⚠️ Warning: Skip this if starting from a fresh database import—it will wipe your database!# Only run if you previously ran step 5 (import-db) ddev drush si --existing-config -
Run deployment updates:
ddev drush deploy -y
-
Enable runtime caching:
Edit
services/drupal/.envand change:ENV_STATE=buildto:
ENV_STATE=run -
Unblock admin user:
ddev drush user:unblock drupalwebcms-admin
-
Install SSL certificates (first time only):
ddev stop --all mkcert -install
For Firefox users, install
nss:brew install nss mkcert -install
-
Access the site:
Open https://epa.ddev.site in your browser.
webcms/
├── .gitlab-ci.yml # CI/CD pipeline configuration
├── .gitlab/ # Pipeline includes
│ ├── docker.yml # Docker build jobs
│ └── teams-notifications.yml # MS Teams alerts
├── services/ # Application services
│ ├── drupal/ # Main Drupal codebase
│ │ ├── config/ # Configuration management
│ │ ├── drush/ # Drush commands
│ │ ├── patches/ # Composer patches
│ │ ├── scripts/ # Custom scripts
│ │ ├── web/ # Drupal web root
│ │ │ ├── modules/custom/ # Custom modules
│ │ │ └── themes/custom/ # Custom themes
│ │ ├── composer.json # PHP dependencies
│ │ └── Dockerfile # Multi-stage Docker build
│ ├── drush/ # Drush container
│ ├── minio/ # Local S3 emulation
│ ├── mysql/ # Database container
│ └── simplesaml/ # SAML authentication
├── terraform/ # Infrastructure as code
│ ├── infrastructure/ # AWS infrastructure
│ └── webcms/ # Application deployment
├── ci/ # CI automation scripts
├── push-dev.sh # Deploy to development
└── trigger-pipeline.sh # Manual pipeline trigger
- Drupal 10 - Content management system
- PHP 8.1+ - Backend language
- Docker - Containerization
- DDEV - Local development environment
- Terraform - Infrastructure provisioning
- GitLab CI/CD - Continuous integration and deployment
- AWS ECS - Container orchestration
- AWS RDS - Managed database
- AWS S3 - File storage
# Start DDEV (if not already running)
cd services/drupal
ddev start
# Pull latest changes from development branch
git checkout development
git pull origin development
# Create a feature branch
git checkout -b feature/your-feature-name
# Start watching theme changes
ddev gesso watch- PHP/Module Development: Edit files in
services/drupal/web/modules/custom/ - Theme Development: Edit files in
services/drupal/web/themes/custom/epa_theme/ - Configuration Changes: Export config with
ddev drush cex
# Clear cache
ddev drush cr
# Run updates
ddev drush updb -y
# Import configuration
ddev drush cim -y
# View site
open https://epa.ddev.site# Stage changes
git add .
# Commit with descriptive message
git commit -m "feat: Add new feature description"
# Push to GitHub
git push origin feature/your-feature-nameSee Deployment Guide below.
The WebCMS uses a GitHub → GitLab CI/CD → AWS deployment pipeline. Code is hosted on GitHub but deployed via GitLab CI/CD.
Developer → GitHub (development branch) → GitLab Mirror → CI/CD Pipeline → AWS ECS
| Branch | Environment | Purpose | Deployment Method |
|---|---|---|---|
development |
Dev site | Active development | Automatic via push-dev.sh |
live |
Stage site | Pre-production testing | Manual trigger |
live |
Production | Live public site | Manual trigger (future) |
Starting with the enhanced push-dev.sh script, build detection is now automatic. The script analyzes your changed files and intelligently determines whether a full Docker rebuild is needed.
# Simply run push-dev.sh - it will auto-detect!
./push-dev.shThe script will:
- Compare your local
HEADwithorigin/development - List all changed files
- Determine if any files require a full build
- Show you the decision and reasoning
- Trigger the appropriate pipeline (full build or deploy-only)
The script automatically detects these file patterns and triggers a full build:
PHP/Composer Dependencies:
composer.json,composer.lock,composer.patches.jsonservices/drupal/composer.json,composer.lock,composer.patches.json- Files in
services/drupal/patches/
Theme/NPM Dependencies:
services/drupal/web/themes/epa_theme/package.jsonservices/drupal/web/themes/epa_theme/package-lock.json*.scssor*.jsfiles inepa_theme/- Files in
services/drupal/web/themes/epa_theme/source/ - Gulp configuration files
- Files in
services/drupal/web/themes/epa_claro/
Docker & CI/CD:
- Any
Dockerfileinservices/ - Files in
services/drupal/scripts/ docker-compose*files.gitlab-ci.yml
Changes to these files can be deployed quickly without rebuilding Docker images:
✅ Deploy-only files:
- Custom Drupal modules:
services/drupal/web/modules/custom/** - Drupal configuration:
services/drupal/config/** - Drush commands:
services/drupal/drush/** - Custom theme PHP/Twig templates (non-compiled)
- Documentation:
*.mdfiles - Git/GitHub configs:
.github/**,.gitignore
Deployment time:
- Full build: ~12-17 minutes
- Deploy-only: ~3-5 minutes (60-70% faster!)
You can override the automatic detection when needed:
Force skip build:
./push-dev.sh --skip-buildUse when you know existing Docker images are compatible but the script detects a build is needed.
Force full build:
./push-dev.sh --force-buildUse when you want to rebuild everything (e.g., to pick up base image updates) even if no build-requiring files changed.
./push-dev.shPipeline stages:
- ✅ Build Docker images (~8-12 minutes)
webcms-preproduction-dev-drupal:development-abc1234webcms-preproduction-dev-nginx:development-abc1234webcms-preproduction-dev-drush:development-abc1234- Also tagged as
:development-latestfor reuse
- ✅ Deploy via Terraform (~2-3 minutes)
- ✅ Update via Drush (~1-2 minutes)
Total time: ~12-17 minutes
./push-dev.sh --skip-buildPipeline stages:
- ⏭️ SKIPPED - Build Docker images (saves 8-12 minutes!)
- ✅ Deploy via Terraform (~2-3 minutes) - Reuses
:development-latestimages - ✅ Update via Drush (~1-2 minutes)
Total time: ~3-5 minutes (60-70% faster!)
# 9:00 AM - First deployment of the day
git checkout development
git pull origin development
git push origin development
./push-dev.sh # Auto-detects: no code changes = full build
# 10:30 AM - Bug fix in custom module
# (edit services/drupal/web/modules/custom/epa_workflow/src/Plugin/WorkflowType/MyPlugin.php)
git add .
git commit -m "fix: Correct workflow validation logic"
git push origin development
./push-dev.sh # Auto-detects: custom module only = deploy-only! 🚀
# Output: "✅ Build NOT required - changes are deployment-only"
# 2:00 PM - Another iteration on theme template
# (edit services/drupal/web/themes/custom/epa_theme/templates/node.html.twig)
git add .
git commit -m "style: Update node template layout"
git push origin development
./push-dev.sh # Auto-detects: template only = deploy-only! 🚀
# 4:00 PM - Added a new Composer dependency
# (edit composer.json, run ddev composer update)
git add composer.json composer.lock
git commit -m "chore: Add new library dependency"
git push origin development
./push-dev.sh # Auto-detects: composer.json = FULL BUILD required! 🔨
# Output: "🔨 Build REQUIRED - detected change in: composer.json"# Critical bug found in production, need fast fix
git checkout development
# (fix the bug in custom module)
git add .
git commit -m "fix: Critical security patch for XSS vulnerability"
git push origin development
./push-dev.sh # Auto-detects: code-only change = deploy-only! Fast!
# Deploys in ~3-5 minutes instead of ~15 minutes# Need to force push and deploy quickly
./push-dev.sh --skip-build -fYou can also trigger skip-build mode directly from GitLab UI:
- Navigate to: https://gitlab.epa.gov/drupalcloud/drupalclouddeployment/-/pipelines/new
- Select branch:
development - Click "Add variable"
- Key:
SKIP_BUILD - Value:
true
- Key:
- Click "Run pipeline"
Full Build creates two tags:
webcms-preproduction-dev-drupal:development-abc1234 # Commit-specific
webcms-preproduction-dev-drupal:development-latest # Reusable tag
Skip-Build reuses existing:
webcms-preproduction-dev-drupal:development-latest # No new build
-
Enhanced Kaniko Caching
--cache-ttl=168h- Caches Docker layers for 7 days--cache-copy-layers=true- Aggressive layer reuse- Reduces build time by 30-50% even in full build mode
-
Conditional Build Stage
build:drupal:devjob skips entirely whenSKIP_BUILD=true- Saves ~8-12 minutes per deployment
-
Dynamic Image Tag Override
- Deploy stage automatically uses
:development-latestwhenSKIP_BUILD=true - Uses commit-specific tag in normal mode
- Deploy stage automatically uses
# Ensure you're on development branch
git checkout development
# Merge latest changes from main
git pull origin main
# Push to GitHub and trigger full CI/CD pipeline
./push-dev.sh-
GitHub Push
- Code pushed to
developmentbranch on GitHub push-dev.shscript triggers GitLab pipeline via API
- Code pushed to
-
GitLab Mirror Sync
- GitLab pulls latest code from GitHub mirror
- Takes ~20 seconds to sync
-
Build Stage (~8-12 minutes)
- Kaniko builds 3 Docker images in parallel:
drupal- PHP-FPM with Drupal applicationnginx- Web server with Drupal configurationdrush- CLI tools for database operations
- Images pushed to AWS ECR
- Images also pushed to GitLab Container Registry
- Kaniko builds 3 Docker images in parallel:
-
Deploy Stage (~2-3 minutes)
- Terraform initializes and validates configuration
- Terraform plans ECS service changes
- Terraform applies changes:
- Updates ECS task definitions with new image tags
- Triggers ECS service update
- ECS performs rolling deployment (zero downtime)
-
Update Stage (~1-2 minutes)
- Drush runs database updates (
drush updb) - Drush imports configuration (
drush cim) - Drush clears caches (
drush cr)
- Drush runs database updates (
-
Monitoring
- View pipeline: https://gitlab.epa.gov/drupalcloud/drupalclouddeployment/-/pipelines
- View logs in GitLab UI
- Check ECS service: AWS Console → ECS → webcms-preproduction-dev cluster
Stage deployments are only triggered from the live branch:
# Merge development into live
git checkout live
git pull origin live
git merge development
git push origin live
# GitLab CI automatically triggers stage deployment
# (No script needed - push triggers workflow)Use WEBCMS_SITE_FILTER to deploy to only one site when triggering pipelines on the live branch. This is particularly useful for:
- Spanish site fixes: Deploy changes only to the Spanish stage site without affecting English
- Emergency hotfixes: Apply fixes to one environment without triggering unnecessary builds/deploys
- Testing isolation: Validate changes on a single site before full rollout
Usage:
- Navigate to: https://gitlab.epa.gov/drupalcloud/drupalclouddeployment/-/pipelines/new
- Select branch:
live - Click "Add variable"
- Key:
WEBCMS_SITE_FILTER - Value:
stage(for stage site only) ordev(for dev site only when combined withDEPLOY_TO_DEV=true)
- Key:
- Click "Run pipeline"
Behavior:
WEBCMS_SITE_FILTER=stage- Only stage site jobs execute (both English and Spanish)WEBCMS_SITE_FILTER=dev- Only dev site jobs execute (whenDEPLOY_TO_DEV=trueis also set)- Unset (default) - All sites deploy normally
Example: Spanish Stage Site Hotfix
# 1. Make fix and push to live branch
git checkout live
# ... make changes ...
git push origin live
# 2. Trigger pipeline with filter via GitLab UI
# Set WEBCMS_SITE_FILTER=stage
# Only stage-en and stage-es jobs will runNote: This variable is ignored on the development branch - dev site always deploys there.
See README.md Advanced Pipeline Variables for full documentation on DEPLOY_TO_DEV.
Our process adapts GitHub Flow to EPA WebCMS’ multi-environment deployment model.
- Keep
mainalways releasable and aligned with production. - Ensure every change is validated in the dev environment (
developmentbranch) before moving to stage/production. - Provide a lightweight, repeatable promotion path: feature → development → live → main.
- Support urgent hotfixes without blocking regular feature work.
| Branch | Purpose | Deployment Target | Notes |
|---|---|---|---|
main |
Production source of truth | Production (future) | Locked except for release merges and hotfixes. |
live |
Stage/staging code | Stage environment | Mirrors upcoming production; runs full security scans. |
development |
Active integration branch | Dev environment | All feature work branches from here. Triggers the standard dev pipeline (push-dev.sh). |
feature/*, bugfix/* |
Short-lived work branches | None directly | Always branch from development and open PRs back into development. |
hotfix/* |
Urgent fixes for prod | Main & live | Created from main, merged back to all branches after release. |
- Sync development
git checkout development git pull origin development
- Branch from
developmentInclude ticket number in branch name if applicable.git checkout -b feature/<short-description>
- Develop & validate locally
- Use DDEV & Gesso commands in this guide.
- Follow Conventional Commits and keep commits focused.
- Periodically sync with latest changes:
git fetch origin && git rebase origin/development
- PR into
development- Target branch:
development. - Run local checks; after merge trigger
./push-dev.sh --skip-buildfor fast validation. - Resolve review feedback and merge (squash or rebase preferred).
- Target branch:
- Promote to Stage (
live)- On release cadence, merge
development→live. - Push to
liveto run full stage pipeline (security scans included). - EPA staff only
- On release cadence, merge
- Promote to Production (
main)- After stage sign-off, merge
live→main. - Tag the release (e.g.,
vYYYY.MM.DD); production deploy pipeline will consumemain. - EPA staff only
- After stage sign-off, merge
- Back-merge to development
- After production release, merge
main→developmentto sync any hotfixes or production adjustments. - This keeps
developmentup-to-date with production state.
- After production release, merge
git checkout -b hotfix/<issue> main- Implement fix and open PR targeting
main(bypassesdevelopmentto minimize risk). - After merging into
main, cherry-pick/merge intoliveanddevelopmentso branches stay in sync. - Deploy via stage → production promotion path as usual.
- Industry standard (Git Flow): Features integrate continuously in the active development branch.
- Early conflict detection: Features see each other immediately, catching integration issues during development rather than at PR time.
- Simplified workflow: Eliminates need for developers to manually rebase against
developmentas it diverges frommain. - Natural integration: Multiple features can be tested together in the dev environment before promotion.
- Aligned with deployment model: The branch you develop in is the branch that deploys to dev.
- Daily Dev Deploys: Merge PRs into
developmentas they're approved. Use skip-build for quick iterations; run one full build per day or after dependency changes. - Stage Deploys: At least once per sprint (or as needed). Merge
development→live, let the stage pipeline run, and perform QA. - Production Deploys: After stage validation, merge
live→main, tag the release, and trigger production deployment when available. - Back-merge: After each production release, immediately merge
main→developmentto sync production state back to active development.
- Target branches: Feature work →
development, hotfixes →main. - Write well-documented PRs, following the template generated when creating a new PR.
- Keep PRs reviewable (< ~1 day of work). Use draft PRs for early feedback.
- Run
ddev drush updb,ddev drush cex,ddev composer phpcs,phpstan, theme builds, and relevant tests before creating PR and requesting review. - Require at least one maintainer approval.
- Do NOT bundle multiple module updates into one PR, unless they're related (like metatag and metatag_schema).
- Feature PRs: Squash or rebase onto
developmentto maintain a clean history. - Promotions (
development→live,live→main): Use merge commits so a release corresponds to a single identifiable merge. - Back-merges: After promoting, fast-forward lower environments (e.g., rebase
developmentonmain) to avoid drift.
- Why not branch from
maindirectly? Branching frommaincauses feature branches to diverge from active development work. By the time a feature is ready, it's missing weeks of changes merged todevelopment, leading to late-stage conflicts. - Can I deploy from a feature branch? No. Only
development(dev) andlive(stage) trigger deployments. - What if a feature spans multiple sprints? Periodically rebase on
developmentto stay current with integrated work. Use feature flags for incomplete functionality. - How do freeze periods work? Pause merges into
development, finish testing onlive, promote tomain, then mergemain→developmentwhen the freeze lifts to sync any last-minute hotfixes.
Follow Conventional Commits:
<type>(<scope>): <subject>
<body>
<footer>
Common types: feat, fix, docs, style, refactor, perf, test, chore.
Examples:
git commit -m "feat(workflow): add approval step for content editors"
git commit -m "fix(theme): correct responsive menu breakpoint"
git commit -m "docs: update deployment guide with skip-build instructions"# Clear cache
ddev drush cr
# Run database updates
ddev drush updb -y
# Import configuration
ddev drush cim -y
# Check status
ddev drush status
# Run cron
ddev drush cron
# Rebuild cache
ddev drush rebuild# PHP CodeSniffer
ddev composer phpcs
# PHP CodeSniffer auto-fix
ddev composer phpcbf
# PHPStan static analysis
ddev composer phpstan
# Run all checks
ddev composer checkAfter deploying to dev environment:
-
Verify Deployment:
- Check GitLab pipeline completed successfully
- Verify ECS service updated in AWS Console
-
Smoke Test:
- Access dev site URL
- Login as admin
- Create/edit/delete content
- Test key workflows
-
Configuration Verification:
# SSH into ECS task (via AWS Console or ECS Exec) drush status drush config:status
- Follow Drupal Coding Standards
- Use PHP 8.1+ features where appropriate
- Type hint all parameters and return values
- Document all public methods with PHPDoc
- Follow BEM naming convention
- Use design tokens from theme configuration
- Mobile-first responsive design
- Accessibility: WCAG 2.1 AA compliance
- ES6+ syntax
- Use
constandlet(novar) - Drupal behaviors for initialization
- Document complex functions
-
Always export configuration after changes:
ddev drush cex
-
Never commit configuration in code and database simultaneously
-
Test configuration imports in clean environment
- Never commit secrets or credentials
- Use environment variables for sensitive data
- Sanitize all user input
- Follow Drupal Security Best Practices
| Command | Description |
|---|---|
ddev start |
Start the development environment |
ddev stop |
Stop the development environment |
ddev restart |
Restart all containers |
ddev ssh |
SSH into the web container |
ddev describe |
Show project details and URLs |
ddev logs |
View container logs |
ddev import-db |
Import a database dump |
ddev export-db |
Export database with timestamp |
ddev phpmyadmin |
Open PhpMyAdmin in browser |
ddev aws-setup |
Configure local S3 emulation |
| Command | Description |
|---|---|
ddev drush cr |
Clear all caches |
ddev drush updb -y |
Run database updates |
ddev drush cim -y |
Import configuration |
ddev drush cex |
Export configuration |
ddev drush deploy -y |
Run deployment workflow (updb + cim + cr) |
ddev drush status |
Show Drupal status |
ddev drush uli |
Generate one-time login link |
ddev drush user:unblock <username> |
Unblock a user account |
ddev drush sqlq "SELECT * FROM users" |
Run SQL query |
| Command | Description |
|---|---|
ddev gesso install |
Install node modules for theme |
ddev gesso build |
Build CSS and Pattern Lab |
ddev gesso watch |
Watch for changes and rebuild |
ddev gesso lint |
Lint CSS and JavaScript |
| Command | Description |
|---|---|
ddev composer install |
Install PHP dependencies |
ddev composer update |
Update PHP dependencies |
ddev composer require <package> |
Add new dependency |
ddev composer remove <package> |
Remove dependency |
ddev composer clearcache |
Clear Composer cache |
|| Command | Description |
||---------|-------------|
|| ./push-dev.sh | Auto-detect build need & deploy to dev |
|| ./push-dev.sh --skip-build | Force skip build (fast, reuse images) |
|| ./push-dev.sh --force-build | Force full build even if not detected |
|| ./push-dev.sh -f | Force push with auto-detection |
|| ./push-dev.sh --skip-build -f | Force push with skip-build |
|| ./trigger-pipeline.sh development | Manually trigger GitLab pipeline |
If you encounter Elasticsearch errors:
ddev poweroff
docker volume rm ddev-epa-ddev_elasticsearch
ddev startThen re-index content:
ddev drush search-api:reindex
ddev drush search-api:indexIf ddev composer install fails:
# Delete vendor directory and clear cache
rm -rf services/drupal/.ddev/vendor
ddev composer clearcache
ddev composer installFor large database imports that timeout:
- Check if import is running in background:
docker stats - If process was killed, connect MySQL client directly:
# Get MySQL port ddev status # Connect directly (use port from status) mysql -h 127.0.0.1 -P <port> -u db -pdb db < backup.sql
Install mkcert certificates:
ddev stop --all
mkcert -install
ddev startFor Firefox users:
brew install nss
mkcert -install
ddev startProblem: Skip-build deployment fails with "image not found" error.
Cause: No :development-latest image exists yet.
Solution: Run a full build first:
./push-dev.sh # Without --skip-buildProblem: Deployed successfully but changes aren't visible on dev site.
Possible causes:
- Browser cache - Hard refresh (Ctrl+Shift+R)
- Drupal cache - Run Drush update job manually in GitLab
- Wrong image deployed - Check ECS task definition in AWS Console
Solution:
# SSH into ECS container and verify
drush cr
drush statusProblem: push-dev.sh completes but pipeline doesn't start.
Causes:
- GitLab token expired or invalid
- GitHub → GitLab mirror not syncing
- GitLab project path changed
Solution:
# 1. Verify token exists
echo $GITLAB_TOKEN
# 2. Create new token if needed
# https://gitlab.epa.gov/-/user_settings/personal_access_tokens
# Required scope: "api"
# 3. Set token
export GITLAB_TOKEN="your-token-here"
# 4. Manually trigger mirror sync in GitLab UI
# https://gitlab.epa.gov/drupalcloud/drupalclouddeployment/-/settings/repository
# Click "Update now" next to GitHub mirrorIf you encounter PHP memory limit errors:
# Increase PHP memory limit in .ddev/config.yaml
# Add or modify:
php_version: "8.1"
webserver_type: nginx-fpm
php_memory_limit: "512M"
# Restart DDEV
ddev restart- Slack: #webcms-dev channel
- Email: webcms-team@epa.gov
- GitLab Issues: https://gitlab.epa.gov/drupalcloud/drupalclouddeployment/-/issues
- Documentation: See
docs/directory
- CI/CD Pipeline Documentation
- Terraform Infrastructure
- Terraform WebCMS Deployment
- Docker Build Configuration
- Drupal Documentation
- DDEV Documentation
See LICENSE file for details.
The United States Environmental Protection Agency (EPA) GitHub project code is provided on an "as is" basis and the user assumes responsibility for its use. EPA has relinquished control of the information and no longer has responsibility to protect the integrity, confidentiality, or availability of the information. Any reference to specific commercial products, processes, or services by service mark, trademark, manufacturer, or otherwise, does not constitute or imply their endorsement, recommendation or favoring by EPA. The EPA seal and logo shall not be used in any manner to imply endorsement of any commercial product or activity by EPA or the United States Government.