ASAASA Standard

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