Quality Gates & Audit Checklist
When to Use
Establishing automated quality checks at different stages of development to catch issues early, enforce standards, and prevent broken code from reaching production.
Decision Framework
| Stage | Gate type | Checks | Speed target | Failure action |
|---|---|---|---|---|
| Pre-commit | Local, fast | Code style (PHPCS), basic static analysis | <30 seconds | Block commit, show errors |
| Pre-push | Local, thorough | Unit + kernel tests, coverage threshold | <2 minutes | Block push, run fixes |
| Pre-merge (CI) | Remote, complete | Full test suite, security audit, coverage report | <10 minutes | Block merge, require fixes |
| Post-merge | Monitoring | Integration tests, deployment checks | Varies | Alert team, auto-rollback |
Principle: Fast feedback loops -- cheap to fix. Slow gates (browser tests, full coverage) run in CI, not locally.
Pattern
Pre-commit gates (fastest, catches obvious errors):
Using GrumPHP (recommended for Drupal):
# grumphp.yml
grumphp:
tasks:
phplint: ~
phpcs:
standard: Drupal,DrupalPractice
triggered_by: [php, module, inc, install, theme]
phpstan:
level: 6
configuration: phpstan.neon
testsuites:
pre_commit:
tasks:
- phplint
- phpcs
- phpstan
Install: composer require --dev phpro/grumphp
Manual pre-commit hook (.git/hooks/pre-commit):
#!/bin/bash
# Get staged PHP files
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(php|module|install|theme)$')
if [ -z "$FILES" ]; then
exit 0
fi
# PHPCS check
echo "Running PHPCS..."
./vendor/bin/phpcs --standard=Drupal,DrupalPractice $FILES
if [ $? -ne 0 ]; then
echo "PHPCS failed. Run phpcbf to auto-fix or fix manually."
exit 1
fi
# PHPStan check (level 6 minimum)
echo "Running PHPStan..."
./vendor/bin/phpstan analyse --level=6 $FILES
if [ $? -ne 0 ]; then
echo "PHPStan failed. Fix errors before committing."
exit 1
fi
exit 0
Make executable: chmod +x .git/hooks/pre-commit
Pre-push gates (slower, catches logic errors):
.git/hooks/pre-push:
#!/bin/bash
echo "Running unit and kernel tests..."
./vendor/bin/phpunit --testsuite unit,kernel --stop-on-failure
if [ $? -ne 0 ]; then
echo "Tests failed. Fix before pushing."
exit 1
fi
# Optional: Check coverage threshold
echo "Checking coverage threshold..."
COVERAGE=$(./vendor/bin/phpunit --coverage-text --colors=never | grep -oP 'Lines:\s+\K\d+')
if [ "$COVERAGE" -lt 70 ]; then
echo "Coverage is $COVERAGE%, minimum is 70%"
exit 1
fi
exit 0
Composer scripts (standardized commands):
{
"scripts": {
"lint": "parallel-lint web/modules/custom web/themes/custom",
"phpcs": "phpcs --standard=Drupal,DrupalPractice web/modules/custom",
"phpcbf": "phpcbf --standard=Drupal,DrupalPractice web/modules/custom",
"phpstan": "phpstan analyse --level=6 web/modules/custom",
"test:unit": "phpunit --testsuite unit",
"test:kernel": "phpunit --testsuite kernel",
"test:all": "phpunit",
"coverage": "phpunit --coverage-html reports/",
"quality": [
"@lint",
"@phpcs",
"@phpstan",
"@test:unit",
"@test:kernel"
]
}
}
Run: composer quality before pushing.
CI/CD pipeline stages (GitLab CI example):
stages:
- lint
- static-analysis
- test
- coverage
- security
# Stage 1: Lint (30 seconds)
lint:
stage: lint
script:
- composer lint
- composer phpcs
# Stage 2: Static analysis (1-2 minutes)
phpstan:
stage: static-analysis
script:
- composer phpstan
# Stage 3: Tests (2-5 minutes)
unit-tests:
stage: test
script:
- ./vendor/bin/phpunit --testsuite unit
kernel-tests:
stage: test
script:
- ./vendor/bin/phpunit --testsuite kernel
functional-tests:
stage: test
script:
- ./vendor/bin/phpunit --testsuite functional
timeout: 30m
# Stage 4: Coverage (3-5 minutes)
coverage:
stage: coverage
script:
- ./vendor/bin/phpunit --coverage-text --coverage-cobertura=coverage.xml
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# Stage 5: Security (1 minute)
security:
stage: security
script:
- composer audit
- ./vendor/bin/phpstan analyse --level=6 --error-format=json | jq '.totals.errors'
allow_failure: false
GitHub Actions (alternative):
name: Quality Gates
on: [push, pull_request]
jobs:
lint-and-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: pcov
- run: composer install
- run: composer lint
- run: composer phpcs
- run: composer phpstan
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: pcov
- run: composer install
- run: ./vendor/bin/phpunit --testsuite unit,kernel --coverage-clover coverage.xml
- uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
Issue Triage from Quality Gate Failures
| Failure type | Severity | Response time | Fix approach |
|---|---|---|---|
| PHPCS (code style) | Low | Fix before commit | Run phpcbf to auto-fix |
| PHPStan level 6+ errors | High | Fix before push | Address type errors, null checks |
| Unit test failures | Critical | Fix immediately | Logic error, refactor code |
| Kernel test failures | Critical | Fix immediately | Integration issue, check setup |
| Browser test failures | High | Investigate within 1 hour | May be flaky, re-run; if persistent, fix |
| Coverage drop >5% | Medium | Investigate before merge | Add tests for new code |
| Security audit failures | Critical | Fix immediately | Update dependencies, patch CVEs |
Flaky test protocol: If browser/JS test fails intermittently:
1. Re-run 3 times
2. If 2/3 pass -- investigate but don't block merge
3. If 0/3 or 1/3 pass -- block merge, fix test or code
4. Tag test with @group flaky temporarily, create issue to fix
Common Mistakes
- All gates at pre-commit -- slow feedback (30+ seconds per commit), developers disable hooks
- No gates until CI -- waste CI resources on trivial errors, slow feedback loop
- Brittle coverage thresholds -- 80% coverage requirement blocks legitimate refactoring that temporarily lowers coverage
- Ignoring gate failures -- "I'll fix it later" -- technical debt accumulates
- Testing in CI without local testing first -- waste CI minutes, clog pipeline
- Not using
--stop-on-failure-- run full suite even after first failure, slow debug loop - No composer scripts -- every developer runs checks differently, inconsistent