CI/CD Safety Pipeline: Automated Enforcement for AI-Generated Code
The CI/CD safety pipeline is a structured set of automated checks that run on every commit, before every merge, and before every deployment in an AI-generated codebase. It enforces the structural rules that prompt-driven development cannot enforce on its own: boundary violations are blocked at the merge gate, test coverage cannot silently degrade, and regeneration losses cannot reach production.
The pipeline is not a single tool. It is a sequence of checks ordered by speed and specificity — structural checks first (fastest, fail early), test enforcement second, preservation integrity third, build verification last. Each check has a defined failure condition. A commit that fails any check cannot merge. The pipeline is the automated enforcement layer that makes all other structural techniques durable.
This page explains the four pipeline stages, how to configure each one, and how to adapt the pipeline to different CI providers and tech stacks.
What the CI/CD Safety Pipeline Enforces
Without a CI/CD safety pipeline, every structural improvement made in a stabilization sprint is temporary — subsequent prompt sessions can reintroduce violations with no automated signal. The pipeline makes structural improvements permanent by enforcing them on every future commit:
| Without pipeline | With pipeline |
|---|---|
| Circular deps accumulate silently | Circular dep check fails the build at merge |
| Layer violations merge undetected | Boundary linter blocks the PR |
| Coverage degrades without signal | Coverage threshold fails the build |
| Regeneration losses reach production | Preservation check catches overwritten logic |
| Structural drift resumes after sprint | Enforcement runs on every commit permanently |
Pipeline Architecture
The pipeline is organized in four stages, ordered by execution speed. Faster checks run first — a commit that fails a fast structural check does not wait for the slow build step.
Stage 1: Structural integrity (~30–60 seconds)
├── Boundary linter (circular deps, cross-layer imports)
├── Naming linter (ESLint / flake8 / mypy)
└── Type checker (tsc --noEmit / mypy)
Stage 2: Test enforcement (~1–5 minutes)
├── Test suite (all tests must pass)
└── Coverage threshold (must not drop below baseline)
Stage 3: Preservation integrity (~10–30 seconds)
└── Preservation marker check (protected regions not modified)
Stage 4: Build verification (~1–3 minutes)
└── Build (application compiles and bundles)
Total: ~3–10 minutes per commit
Stage 1: Structural Integrity
# .github/workflows/ci.yml — Stage 1
- name: Boundary linter (circular deps + cross-layer imports)
run: |
# TypeScript
npx depcruise --config .dependency-cruiser.js src/ --exit-code
# or for circular deps only (faster):
# npx madge --circular --extensions ts,tsx src/ --exit-code
# Python
# python scripts/check_boundaries.py
- name: Naming linter
run: |
# TypeScript
npx eslint . --ext .ts,.tsx --max-warnings 0
# Python
# flake8 . && mypy . --strict
- name: Type checker
run: |
# TypeScript
npx tsc --noEmit
# Python (covered by mypy above)
Boundary linter configuration (TypeScript):
// .dependency-cruiser.js — complete rule set
module.exports = {
forbidden: [
{
name: "no-circular",
severity: "error",
from: {},
to: { circular: true }
},
{
name: "no-cross-domain",
severity: "error",
from: { path: "^src/domains/([^/]+)/" },
to: {
path: "^src/domains/([^/]+)/",
pathNot: "^src/domains/$1/"
}
},
{
name: "no-db-in-handlers",
severity: "error",
from: { path: "^src/(routes|handlers|controllers)/" },
to: { path: "^src/(models|db|database|prisma|repositories)/" }
},
{
name: "no-business-in-ui",
severity: "error",
from: { path: "^src/components/" },
to: { path: "^src/(services|domains|repositories)/" }
},
{
name: "no-slice-internals",
severity: "error",
comment: "Import from slice public interface only, not from internal files",
from: { pathNot: "^src/domains/([^/]+)/([^/]+)/" },
to: {
path: "^src/domains/([^/]+)/([^/]+)/(?!index\\.ts)",
pathNot: "^src/domains/([^/]+)/([^/]+)/index\\.ts"
}
}
]
};
Stage 2: Test Enforcement
# .github/workflows/ci.yml — Stage 2
- name: Run tests with coverage threshold
run: |
# TypeScript / JavaScript
npx jest \
--coverage \
--coverageReporters=text-summary \
--forceExit \
--passWithNoTests
# Coverage threshold is enforced via jest.config.js (see below)
# Python
# pytest --cov=. --cov-report=term-missing --cov-fail-under=30 --tb=short
// jest.config.js — coverage threshold enforcement
module.exports = {
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.spec.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts', // barrel files excluded
],
coverageThreshold: {
// Global minimum — prevents overall coverage degradation
global: {
lines: 30,
functions: 30,
branches: 25,
},
// Higher threshold for business logic
'./src/domains/': {
lines: 60,
functions: 60,
},
},
};
# setup.cfg — Python coverage threshold
[tool:pytest]
addopts = --cov=. --cov-fail-under=30 --cov-report=term-missing
[coverage:run]
omit =
*/test_*
*/migrations/*
*/conftest.py
setup.py
Stage 3: Preservation Integrity
# .github/workflows/ci.yml — Stage 3
- name: Check preservation markers
run: python scripts/check_preservation_markers.py
# scripts/check_preservation_markers.py
"""
Verifies that content between preservation markers has not been
modified in the current commit. Fails with exit code 1 if violations found.
Marker syntax:
# === BEGIN USER CODE ===
# === END USER CODE ===
"""
import subprocess
import sys
import re
from pathlib import Path
MARKER_BEGIN = "=== BEGIN USER CODE ==="
MARKER_END = "=== END USER CODE ==="
EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx'}
def get_staged_files():
result = subprocess.run(
['git', 'diff', '--cached', '--name-only'],
capture_output=True, text=True
)
return [f for f in result.stdout.strip().split('\n')
if Path(f).suffix in EXTENSIONS and Path(f).exists()]
def get_staged_diff(filepath):
result = subprocess.run(
['git', 'diff', '--cached', '-U0', filepath],
capture_output=True, text=True
)
return result.stdout
def check_file(filepath):
content = Path(filepath).read_text(encoding='utf-8', errors='ignore')
if MARKER_BEGIN not in content:
return [] # No markers — not protected
diff = get_staged_diff(filepath)
violations = []
in_protected = False
for line in diff.split('\n'):
if MARKER_BEGIN in line:
in_protected = True
if MARKER_END in line:
in_protected = False
if in_protected and re.match(r'^[+-]', line):
if not re.match(r'^(\+\+\+|---)', line):
violations.append(line[:120]) # truncate long lines
return violations
staged = get_staged_files()
all_violations = {}
for f in staged:
v = check_file(f)
if v:
all_violations[f] = v
if all_violations:
print(f"\n❌ Preservation marker violations in {len(all_violations)} file(s):\n")
for filepath, violations in all_violations.items():
print(f" {filepath}:")
for v in violations[:3]:
print(f" {v}")
if len(violations) > 3:
print(f" ... and {len(violations) - 3} more")
print("\nProtected regions must not be modified by regeneration.")
print("If this change is intentional, remove the markers first.")
sys.exit(1)
print(f"✓ Preservation check passed — {len(staged)} file(s) checked")
Stage 4: Build Verification
# .github/workflows/ci.yml — Stage 4
- name: Build
run: |
# TypeScript / Next.js
npm run build
# Python
# python -m build
# or: uvicorn app.main:app --host 0.0.0.0 --port 8000 &
# sleep 3 && curl -f http://localhost:8000/health || exit 1
Complete Pipeline Configuration
# .github/workflows/ci.yml — complete configuration
name: CI Safety Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
PYTHON_VERSION: '3.11'
jobs:
safety:
name: Safety Pipeline
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git diff in preservation check
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
# ── Stage 1: Structural integrity ──────────────────────────────
- name: "[S1] Boundary linter"
run: npx depcruise --config .dependency-cruiser.js src/ --exit-code
- name: "[S1] Naming linter"
run: npx eslint . --ext .ts,.tsx --max-warnings 0
- name: "[S1] Type checker"
run: npx tsc --noEmit
# ── Stage 2: Test enforcement ───────────────────────────────────
- name: "[S2] Tests + coverage threshold"
run: npx jest --coverage --forceExit --passWithNoTests
# ── Stage 3: Preservation integrity ────────────────────────────
- name: "[S3] Preservation marker check"
run: python scripts/check_preservation_markers.py
# ── Stage 4: Build verification ─────────────────────────────────
- name: "[S4] Build"
run: npm run build
Branch protection (GitHub):
Repository → Settings → Branches → Add rule → Branch name: main
✓ Require a pull request before merging
✓ Require status checks to pass before merging
Required checks: CI Safety Pipeline / safety
✓ Require branches to be up to date before merging
✓ Do not allow bypassing the above settings
✓ Restrict who can push to matching branches
Adapting to Other CI Providers
GitLab CI:
# .gitlab-ci.yml
stages: [structural, test, preservation, build]
boundary-linter:
stage: structural
script: npx depcruise --config .dependency-cruiser.js src/ --exit-code
tests:
stage: test
script: npx jest --coverage --forceExit
preservation-check:
stage: preservation
script: python scripts/check_preservation_markers.py
build:
stage: build
script: npm run build
Pre-commit hooks (local enforcement):
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: boundary-linter
name: Boundary linter
entry: npx depcruise --config .dependency-cruiser.js src/ --exit-code
language: node
pass_filenames: false
- id: preservation-check
name: Preservation marker check
entry: python scripts/check_preservation_markers.py
language: python
pass_filenames: false
Pre-commit hooks provide local enforcement — the same checks run before the commit is created, giving the developer immediate feedback without waiting for the CI pipeline. The CI pipeline remains the authoritative enforcement layer; pre-commit hooks are a developer convenience.
Pipeline Maintenance
The CI/CD safety pipeline requires periodic maintenance as the codebase evolves:
Monthly review:
✓ Check if coverage thresholds should be raised (as coverage improves)
✓ Review boundary linter rules for new domains or layers added
✓ Verify preservation markers are still in place in high-risk files
After each stabilization sprint:
✓ Raise coverage threshold to reflect the new baseline
✓ Add new boundary rules for any new architectural boundaries established
✓ Update the preservation marker list for any new protected files
After any CI/CD provider change:
✓ Verify all four stages are running in the new configuration
✓ Verify branch protection is re-configured
✓ Run a test PR that intentionally fails each stage to confirm enforcement