Skip to content

5: From Manual to Automated

Overview

This tutorial walks you through building a progressive automation pipeline, starting from manual checks and ending with a fully automated CI/CD validation system. Each step builds on the previous, so you can adopt automation at whatever pace fits your team.

What You Will Build

Automation Pipeline Progression
================================

Level 0: Manual           - Run checks by hand, hope for the best
Level 1: EditorConfig     - Consistent formatting across all editors
Level 2: Pre-commit Hooks - Automated local checks before every commit
Level 3: CI/CD Pipeline   - Server-side validation on every push
Level 4: Auto-fixes       - Tools fix problems automatically
Level 5: Quality Gates    - Spell checking, link checking, metrics
Level 6: Monitoring       - Track trends and measure improvement

Time Estimate

Step 1: Manual Validation Baseline ............  5 min
Step 2: Add EditorConfig ......................  3 min
Step 3: Add Pre-commit Hooks ..................  8 min
Step 4: Add CI/CD Pipeline .................... 10 min
Step 5: Enable Auto-fixes .....................  5 min
Step 6: Add Quality Gates .....................  5 min
Step 7: Monitor and Measure ...................  4 min
                                               ------
Total ......................................... 40 min

Prerequisites

# Verify required tools
python3 --version   # Python 3.10+
git --version       # Git 2.30+
# Install uv (Python package manager)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install pre-commit
pip install pre-commit

# Install Node.js tooling (for spell checking and markdown linting)
npm install -g cspell markdownlint-cli
# Create a sample project to work with
mkdir -p ~/automation-tutorial/src ~/automation-tutorial/tests
cd ~/automation-tutorial
git init
# Create a sample Python file with intentional issues
cat > src/app.py << 'PYEOF'
import os
import sys
import json

def get_config(path):
    with open(path) as f:
        data=json.load(f)
    return data

def process_items(items):
    result = []
    for item in items:
        if item["status"]=="active":
            result.append(item["name"])
    return result

class  DataProcessor:
    def __init__(self,name):
        self.name=name
        self.items=[]

    def add_item(self,item):
        self.items.append(item)

    def run(self):
        return process_items(self.items)
PYEOF
# Create a sample Bash script with issues
cat > src/deploy.sh << 'SHEOF'
#!/bin/bash
echo "Deploying application..."
if [ $1 == "production" ]; then
    echo "Deploying to production"
    rm -rf /tmp/deploy/*
fi
echo "Done"
SHEOF
chmod +x src/deploy.sh
# Create a sample Terraform file
cat > src/main.tf << 'TFEOF'
resource "aws_instance" "web" {
ami           = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "web-server"
Environment = "production"
}
}
TFEOF
# Create a YAML config with issues
cat > src/config.yaml << 'YAMLEOF'
app:
  name: my-application
  version: 1.0.0
  database:
    host: localhost
    port: 5432
    password: supersecretpassword123
  features:
    - name: feature-a
      enabled: true
    - name: feature-b
      enabled: false
YAMLEOF

Step 1: Manual Validation Baseline (5 min)

Before automating anything, understand what manual validation looks like. This is the starting point most teams live with -- running checks by hand when they remember to.

The Manual Checklist

# Manual check 1: Python formatting
echo "=== Checking Python formatting ==="
python3 -m py_compile src/app.py && echo "Syntax OK" || echo "Syntax ERROR"
# Manual check 2: Shell script issues
echo "=== Checking shell scripts ==="
bash -n src/deploy.sh && echo "Syntax OK" || echo "Syntax ERROR"
# Manual check 3: YAML validity
echo "=== Checking YAML files ==="
python3 -c "import yaml; yaml.safe_load(open('src/config.yaml'))" && echo "Valid" || echo "Invalid"
# Manual check 4: Look for secrets
echo "=== Checking for potential secrets ==="
grep -rn "password\|secret\|api_key\|token" src/ || echo "No secrets found"

Automate the Manual Checks into a Script

cat > scripts/manual_checks.sh << 'EOF'
#!/bin/bash
# manual_checks.sh - Manual validation checklist
# Run this before every commit (if you remember)

set -euo pipefail

PASS=0
FAIL=0
WARN=0

check_pass() { echo "  PASS: $1"; ((PASS++)); }
check_fail() { echo "  FAIL: $1"; ((FAIL++)); }
check_warn() { echo "  WARN: $1"; ((WARN++)); }

echo "============================================"
echo "  Manual Validation Checklist"
echo "============================================"
echo ""

# 1. Python syntax check
echo "[1/6] Python Syntax"
for f in $(find . -name "*.py" -not -path "./.venv/*"); do
    if python3 -m py_compile "$f" 2>/dev/null; then
        check_pass "$f"
    else
        check_fail "$f"
    fi
done
echo ""

# 2. Shell script syntax
echo "[2/6] Shell Script Syntax"
for f in $(find . -name "*.sh" -not -path "./.venv/*"); do
    if bash -n "$f" 2>/dev/null; then
        check_pass "$f"
    else
        check_fail "$f"
    fi
done
echo ""

# 3. YAML validity
echo "[3/6] YAML Validity"
for f in $(find . -name "*.yaml" -o -name "*.yml" | grep -v ".venv"); do
    if python3 -c "import yaml; yaml.safe_load(open('$f'))" 2>/dev/null; then
        check_pass "$f"
    else
        check_fail "$f"
    fi
done
echo ""

# 4. JSON validity
echo "[4/6] JSON Validity"
for f in $(find . -name "*.json" -not -path "./.venv/*" -not -path "./node_modules/*"); do
    if python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then
        check_pass "$f"
    else
        check_fail "$f"
    fi
done
echo ""

# 5. Secret detection
echo "[5/6] Secret Detection"
SECRETS_FOUND=$(grep -rn \
    --include="*.py" --include="*.sh" --include="*.yaml" --include="*.yml" \
    --include="*.json" --include="*.tf" \
    -E "(password|secret|api_key|token)\s*[:=]" . \
    --exclude-dir=".venv" --exclude-dir="node_modules" 2>/dev/null || true)
if [ -n "$SECRETS_FOUND" ]; then
    echo "$SECRETS_FOUND" | while read -r line; do
        check_warn "Potential secret: $line"
    done
else
    check_pass "No hardcoded secrets detected"
fi
echo ""

# 6. Trailing whitespace
echo "[6/6] Trailing Whitespace"
TRAILING=$(grep -rn ' $' \
    --include="*.py" --include="*.sh" --include="*.yaml" --include="*.yml" \
    --include="*.tf" --include="*.md" \
    . --exclude-dir=".venv" --exclude-dir="node_modules" 2>/dev/null || true)
if [ -n "$TRAILING" ]; then
    echo "$TRAILING" | head -5 | while read -r line; do
        check_warn "Trailing whitespace: $line"
    done
else
    check_pass "No trailing whitespace found"
fi
echo ""

# Summary
echo "============================================"
echo "  Results: $PASS passed, $FAIL failed, $WARN warnings"
echo "============================================"

if [ "$FAIL" -gt 0 ]; then
    echo "STATUS: FAILED"
    exit 1
else
    echo "STATUS: PASSED (with $WARN warnings)"
    exit 0
fi
EOF
chmod +x scripts/manual_checks.sh
# Run the manual checks
bash scripts/manual_checks.sh
# Expected output:
============================================
  Manual Validation Checklist
============================================

[1/6] Python Syntax
  PASS: ./src/app.py

[2/6] Shell Script Syntax
  PASS: ./src/deploy.sh

[3/6] YAML Validity
  PASS: ./src/config.yaml

[4/6] JSON Validity

[5/6] Secret Detection
  WARN: Potential secret: ./src/config.yaml:7:    password: supersecretpassword123

[6/6] Trailing Whitespace
  PASS: No trailing whitespace found

============================================
  Results: 4 passed, 0 failed, 1 warnings
============================================
STATUS: PASSED (with 1 warnings)

The Problem with Manual Checks

Why manual validation fails in practice:

1. Inconsistency  - Different team members run different checks
2. Forgetfulness   - People skip checks under deadline pressure
3. Incomplete      - Manual scripts miss edge cases
4. Slow feedback   - Issues found in review, not at commit time
5. No enforcement  - Nothing prevents bad code from merging

Checkpoint: Manual Baseline

# Verify you have the manual checks script
ls -la scripts/manual_checks.sh
# Expected: -rwxr-xr-x ... scripts/manual_checks.sh

# Verify it runs
bash scripts/manual_checks.sh && echo "Manual checks configured"

Step 2: Add EditorConfig (3 min)

EditorConfig enforces consistent formatting across all editors and IDEs without any plugins for most editors. This is the lowest-friction automation you can add.

Create the EditorConfig File

cat > .editorconfig << 'EOF'
# EditorConfig: https://EditorConfig.org
root = true

# Default settings for all files
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

# Python files
[*.py]
indent_size = 4
max_line_length = 100

# YAML files
[*.{yaml,yml}]
indent_size = 2
max_line_length = 120

# Terraform and HCL files
[*.{tf,tfvars,hcl}]
indent_size = 2
max_line_length = 120

# Shell scripts
[*.{sh,bash}]
indent_size = 2
max_line_length = 100

# PowerShell scripts
[*.{ps1,psm1,psd1}]
indent_size = 4
max_line_length = 100

# TypeScript and JavaScript
[*.{ts,tsx,js,jsx}]
indent_size = 2
max_line_length = 100

# JSON files
[*.{json,jsonc}]
indent_size = 2

# Markdown files
[*.md]
indent_size = 2
max_line_length = 120
trim_trailing_whitespace = false

# Dockerfiles
[Dockerfile*]
indent_size = 2

# Makefiles (must use tabs)
[Makefile]
indent_style = tab
indent_size = 4

[{Makefile.*,*.mk}]
indent_style = tab
indent_size = 4

# SQL files
[*.sql]
indent_size = 2
max_line_length = 100

# GitHub Actions
[.github/workflows/*.{yml,yaml}]
indent_size = 2

# Groovy / Jenkinsfiles
[*.groovy]
indent_size = 2
max_line_length = 120

[Jenkinsfile*]
indent_size = 2
max_line_length = 120
EOF

Verify EditorConfig Works

# Install the editorconfig CLI checker
pip install editorconfig-checker

# Run it against your project
editorconfig-checker src/
# Expected output (showing formatting issues EditorConfig catches):
src/app.py:8: Wrong amount of trailing whitespace characters
src/main.tf:3: Wrong indent style found (tabs instead of spaces)
# Verify the file is in place
cat .editorconfig | head -20
# Expected output:
# EditorConfig: https://EditorConfig.org
root = true

# Default settings for all files
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

# Python files
[*.py]
indent_size = 4
max_line_length = 100

What EditorConfig Catches Automatically

Before EditorConfig:
  - Tab vs spaces arguments on every PR
  - Inconsistent line endings (CRLF vs LF)
  - Missing final newlines
  - Random trailing whitespace
  - Python files with 2-space indent
  - YAML files with 4-space indent

After EditorConfig:
  - All editors use the same settings
  - Zero configuration for new team members
  - Formatting issues caught at edit time
  - Works with VS Code, IntelliJ, Vim, Emacs, and 20+ others

Checkpoint: EditorConfig

# Verify EditorConfig exists and has correct structure
grep -c '^\[' .editorconfig
# Expected: 14 (or more section headers)

# Verify root = true is set
head -3 .editorconfig | grep "root = true"
# Expected: root = true

Step 3: Add Pre-commit Hooks (8 min)

Pre-commit hooks run automatically before every git commit, catching issues at the earliest possible point. We build the configuration progressively in three stages.

Stage 1: Basic File Checks

# .pre-commit-config.yaml - Stage 1: Basic checks
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
        args: ['--unsafe']
      - id: check-json
      - id: check-added-large-files
        args: ['--maxkb=1000']
      - id: check-merge-conflict
      - id: check-case-conflict
      - id: mixed-line-ending
      - id: detect-private-key
# Install pre-commit hooks
pre-commit install

# Run against all files to see current state
pre-commit run --all-files
# Expected output:
Trim Trailing Whitespace.............................Passed
Fix End of Files.....................................Fixed
Check Yaml...........................................Passed
Check JSON...........................................(no files to check)Skipped
Check for added large files..........................Passed
Check for merge conflicts............................Passed
Check for case conflicts.............................Passed
Mixed line ending....................................Passed
Detect Private Key...................................Passed

Stage 2: Add Language Linters

# .pre-commit-config.yaml - Stage 2: Add language-specific linters
repos:
  # General file checks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
        args: ['--unsafe']
      - id: check-json
      - id: check-added-large-files
        args: ['--maxkb=1000']
      - id: check-merge-conflict
      - id: check-case-conflict
      - id: mixed-line-ending
      - id: detect-private-key

  # Python formatting
  - repo: https://github.com/psf/black
    rev: 26.1.0
    hooks:
      - id: black
        language_version: python3

  # Python linting
  - repo: https://github.com/pycqa/flake8
    rev: 7.3.0
    hooks:
      - id: flake8
        args: ['--max-line-length=100', '--extend-ignore=E203,W503']

  # YAML linting
  - repo: https://github.com/adrienverge/yamllint
    rev: v1.38.0
    hooks:
      - id: yamllint
        args: ['-d', '{extends: default, rules: {line-length: {max: 120}, document-start: disable}}']

  # Shell script linting
  - repo: https://github.com/shellcheck-py/shellcheck-py
    rev: v0.11.0.1
    hooks:
      - id: shellcheck

  # Markdown linting
  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.47.0
    hooks:
      - id: markdownlint
        args: ['--fix']
# Update hooks to install new environments
pre-commit install --install-hooks

# Run all hooks
pre-commit run --all-files
# Expected output:
Trim Trailing Whitespace.............................Passed
Fix End of Files.....................................Passed
Check Yaml...........................................Passed
Check JSON...........................................(no files to check)Skipped
Check for added large files..........................Passed
Check for merge conflicts............................Passed
Check for case conflicts.............................Passed
Mixed line ending....................................Passed
Detect Private Key...................................Passed
black................................................Failed
- hook id: black
- files were modified by this hook

reformatted src/app.py

All done!
1 file reformatted.

flake8...............................................Passed
yamllint.............................................Passed
shellcheck...........................................Failed
- hook id: shellcheck
- exit code: 1

In src/deploy.sh line 3:
if [ $1 == "production" ]; then
     ^-- SC2086: Double quote to prevent globbing and word splitting.
        ^-- SC2039: In POSIX sh, == in place of = is undefined.

Stage 3: Add Security Scanning

# .pre-commit-config.yaml - Stage 3: Full configuration with security
repos:
  # General file checks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
        args: ['--unsafe']
      - id: check-json
      - id: check-added-large-files
        args: ['--maxkb=1000']
      - id: check-merge-conflict
      - id: check-case-conflict
      - id: mixed-line-ending
      - id: detect-private-key

  # Python formatting
  - repo: https://github.com/psf/black
    rev: 26.1.0
    hooks:
      - id: black
        language_version: python3

  # Python linting
  - repo: https://github.com/pycqa/flake8
    rev: 7.3.0
    hooks:
      - id: flake8
        args: ['--max-line-length=100', '--extend-ignore=E203,W503']

  # YAML linting
  - repo: https://github.com/adrienverge/yamllint
    rev: v1.38.0
    hooks:
      - id: yamllint
        args: ['-d', '{extends: default, rules: {line-length: {max: 120}, document-start: disable}}']

  # Shell script linting
  - repo: https://github.com/shellcheck-py/shellcheck-py
    rev: v0.11.0.1
    hooks:
      - id: shellcheck

  # Markdown linting
  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.47.0
    hooks:
      - id: markdownlint
        args: ['--fix']

  # Terraform formatting
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.105.0
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_docs
        args:
          - '--args=--config=.terraform-docs.yml'

  # Security: detect secrets in code
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

  # Security: check for common vulnerabilities
  - repo: https://github.com/PyCQA/bandit
    rev: 1.8.3
    hooks:
      - id: bandit
        args: ['-c', 'pyproject.toml']
        additional_dependencies: ['bandit[toml]']
# Generate initial secrets baseline (marks existing secrets as known)
detect-secrets scan > .secrets.baseline

# Install all hooks
pre-commit install --install-hooks

# Run the full suite
pre-commit run --all-files
# Expected output:
Trim Trailing Whitespace.............................Passed
Fix End of Files.....................................Passed
Check Yaml...........................................Passed
Check JSON...........................................(no files to check)Skipped
Check for added large files..........................Passed
Check for merge conflicts............................Passed
Check for case conflicts.............................Passed
Mixed line ending....................................Passed
Detect Private Key...................................Passed
black................................................Passed
flake8...............................................Passed
yamllint.............................................Passed
shellcheck...........................................Failed
markdownlint.........................................Passed
terraform_fmt........................................Passed
Detect secrets.......................................Passed
bandit...............................................Passed

Fix the Shell Script Issues

# Fix the shellcheck issues in deploy.sh
cat > src/deploy.sh << 'SHEOF'
#!/bin/bash
set -euo pipefail

echo "Deploying application..."

if [ "${1:-}" = "production" ]; then
    echo "Deploying to production"
    rm -rf /tmp/deploy/*
fi

echo "Done"
SHEOF
# Verify all hooks pass now
pre-commit run --all-files
# Expected output:
Trim Trailing Whitespace.............................Passed
Fix End of Files.....................................Passed
Check Yaml...........................................Passed
Check JSON...........................................(no files to check)Skipped
Check for added large files..........................Passed
Check for merge conflicts............................Passed
Check for case conflicts.............................Passed
Mixed line ending....................................Passed
Detect Private Key...................................Passed
black................................................Passed
flake8...............................................Passed
yamllint.............................................Passed
shellcheck...........................................Passed
markdownlint.........................................Passed
Detect secrets.......................................Passed
bandit...............................................Passed

Checkpoint: Pre-commit Hooks

# Verify pre-commit is installed
pre-commit --version
# Expected: pre-commit 4.x.x

# Verify hooks are installed in .git
ls .git/hooks/pre-commit
# Expected: .git/hooks/pre-commit

# Count configured hooks
grep -c "id:" .pre-commit-config.yaml
# Expected: 16+ hooks

# Run all hooks and verify pass
pre-commit run --all-files && echo "All hooks pass"

Step 4: Add CI/CD Pipeline (10 min)

Pre-commit catches issues locally, but CI/CD catches everything that slips through. This step creates a GitHub Actions workflow that validates every push and pull request.

Start with a Basic Lint Job

# .github/workflows/ci.yml - Version 1: Basic linting
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install linters
        run: |
          pip install black flake8 yamllint
          pip install shellcheck-py

      - name: Run Black (check mode)
        run: black --check .

      - name: Run Flake8
        run: flake8 --max-line-length=100 --extend-ignore=E203,W503 .

      - name: Run yamllint
        run: yamllint -d '{extends: default, rules: {line-length: {max: 120}, document-start: disable}}' .

      - name: Run ShellCheck
        run: |
          find . -name "*.sh" -not -path "./.venv/*" | while read -r f; do
            echo "Checking $f"
            shellcheck "$f"
          done

Add Test and Security Jobs

# .github/workflows/ci.yml - Version 2: Multi-job pipeline
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          pip install black flake8 yamllint shellcheck-py

      - name: Run Black (check mode)
        run: black --check .

      - name: Run Flake8
        run: flake8 --max-line-length=100 --extend-ignore=E203,W503 .

      - name: Run yamllint
        run: yamllint -d '{extends: default, rules: {line-length: {max: 120}, document-start: disable}}' .

      - name: Run ShellCheck
        run: |
          find . -name "*.sh" -not -path "./.venv/*" -exec shellcheck {} +

  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          pip install pytest pytest-cov

      - name: Run tests with coverage
        run: |
          pytest tests/ \
            --cov=src \
            --cov-report=term-missing \
            --cov-fail-under=80

  security:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install security tools
        run: |
          pip install bandit detect-secrets safety

      - name: Run Bandit (Python security)
        run: bandit -r src/ -f json -o bandit-report.json || true

      - name: Run detect-secrets
        run: |
          detect-secrets scan --all-files --exclude-files '\.secrets\.baseline$' \
            | python3 -c "
          import json, sys
          results = json.load(sys.stdin)
          secrets = results.get('results', {})
          total = sum(len(v) for v in secrets.values())
          if total > 0:
              print(f'Found {total} potential secrets:')
              for file, findings in secrets.items():
                  for finding in findings:
                      print(f'  {file}:{finding[\"line_number\"]} - {finding[\"type\"]}')
              sys.exit(1)
          else:
              print('No secrets detected')
          "

      - name: Upload security reports
        if: always()
        uses: actions/upload-artifact@v6
        with:
          name: security-reports
          path: |
            bandit-report.json
          retention-days: 7

Add Documentation Build Job

# .github/workflows/ci.yml - Version 3: Full pipeline with docs
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install UV
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Add UV to PATH
        run: echo "$HOME/.local/bin" >> $GITHUB_PATH

      - name: Cache UV dependencies
        uses: actions/cache@v5
        with:
          path: |
            ~/.cache/uv
            .venv
          key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml', 'uv.lock') }}
          restore-keys: |
            ${{ runner.os }}-uv-

      - name: Sync environment
        run: uv sync

      - name: Run Black (check mode)
        run: uv run black --check .

      - name: Run Flake8
        run: uv run flake8 --max-line-length=100 --extend-ignore=E203,W503 .

      - name: Run yamllint
        run: uv run yamllint -d '{extends: default, rules: {line-length: {max: 120}, document-start: disable}}' .

      - name: Run ShellCheck
        run: |
          find . -name "*.sh" -not -path "./.venv/*" -exec shellcheck {} +

  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install UV
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Add UV to PATH
        run: echo "$HOME/.local/bin" >> $GITHUB_PATH

      - name: Sync environment
        run: uv sync

      - name: Run tests with coverage
        run: |
          uv run pytest tests/ \
            --cov=src \
            --cov-report=term-missing \
            --cov-fail-under=80

  security:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install security tools
        run: pip install bandit detect-secrets

      - name: Run Bandit
        run: bandit -r src/ -ll -ii

      - name: Run detect-secrets
        run: |
          detect-secrets scan --all-files \
            --exclude-files '\.secrets\.baseline$' \
            --exclude-files '\.git/' > /tmp/secrets-scan.json
          python3 -c "
          import json, sys
          results = json.load(open('/tmp/secrets-scan.json'))
          total = sum(len(v) for v in results.get('results', {}).values())
          if total > 0:
              print(f'ERROR: Found {total} potential secrets')
              sys.exit(1)
          print('No secrets detected')
          "

  docs:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install UV
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Add UV to PATH
        run: echo "$HOME/.local/bin" >> $GITHUB_PATH

      - name: Sync environment
        run: uv sync

      - name: Validate metadata
        run: uv run python scripts/validate_metadata.py docs/
        continue-on-error: true

      - name: Build documentation
        run: uv run mkdocs build --strict

      - name: Upload docs artifact
        uses: actions/upload-artifact@v6
        with:
          name: documentation
          path: site/
          retention-days: 7

Visualize the Pipeline

CI Pipeline Flow
=================

                    ┌──────────┐
                    │  Push /   │
                    │    PR     │
                    └─────┬────┘
                          │
                    ┌─────▼────┐
                    │   Lint   │  Black, Flake8, yamllint, ShellCheck
                    └─────┬────┘
                          │
             ┌────────────┼────────────┐
             │            │            │
       ┌─────▼────┐ ┌────▼─────┐ ┌────▼────┐
       │   Test   │ │ Security │ │  Docs   │
       │  pytest  │ │  bandit  │ │ mkdocs  │
       │ coverage │ │ secrets  │ │ strict  │
       └──────────┘ └──────────┘ └─────────┘

Checkpoint: CI/CD Pipeline

# Verify workflow file exists
ls -la .github/workflows/ci.yml
# Expected: -rw-r--r-- ... .github/workflows/ci.yml

# Validate YAML syntax
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml')); print('Valid YAML')"
# Expected: Valid YAML

# Count jobs defined
grep -c "runs-on:" .github/workflows/ci.yml
# Expected: 4 (lint, test, security, docs)

# Verify job dependencies
grep "needs:" .github/workflows/ci.yml
# Expected:
#     needs: lint
#     needs: lint
#     needs: lint

Step 5: Enable Auto-fixes (5 min)

Instead of just reporting issues, configure tools to fix them automatically. This reduces developer friction and speeds up the feedback loop.

Auto-fix vs Check-only Mode

# Check-only mode (CI): reports problems, exits non-zero
black --check src/app.py
# Output: would reformat src/app.py

# Auto-fix mode (local): fixes problems in place
black src/app.py
# Output: reformatted src/app.py
# Terraform: check-only vs auto-fix
terraform fmt -check src/main.tf    # Check mode (CI)
terraform fmt src/main.tf           # Auto-fix mode (local)
# Markdown: check-only vs auto-fix
markdownlint docs/                  # Check mode (CI)
markdownlint --fix docs/            # Auto-fix mode (local)

Configure Pre-commit Hooks for Auto-fix

# .pre-commit-config.yaml - hooks that auto-fix on commit
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      # These hooks AUTO-FIX files:
      - id: trailing-whitespace      # Removes trailing whitespace
      - id: end-of-file-fixer        # Adds missing final newline
      - id: mixed-line-ending        # Normalizes line endings
        args: ['--fix=lf']

  # Black AUTO-FORMATS Python files
  - repo: https://github.com/psf/black
    rev: 26.1.0
    hooks:
      - id: black
        language_version: python3
        # No --check flag = auto-fix mode

  # Markdown linting with AUTO-FIX
  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.47.0
    hooks:
      - id: markdownlint
        args: ['--fix']  # Auto-fix mode

  # Terraform AUTO-FORMAT
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.105.0
    hooks:
      - id: terraform_fmt  # Auto-formats .tf files

Create an Auto-fix Script for Manual Use

cat > scripts/autofix.sh << 'EOF'
#!/bin/bash
# autofix.sh - Run all auto-fix tools
# Use this to bulk-fix formatting issues before committing

set -euo pipefail

echo "============================================"
echo "  Auto-fix: Formatting & Linting"
echo "============================================"

# Python formatting
echo ""
echo "[1/5] Running Black (Python formatter)..."
if command -v black &>/dev/null; then
    black . 2>&1 | tail -1
else
    echo "  Skipped: black not installed"
fi

# Terraform formatting
echo ""
echo "[2/5] Running terraform fmt..."
if command -v terraform &>/dev/null; then
    find . -name "*.tf" -not -path "./.terraform/*" -exec terraform fmt {} \;
    echo "  Done"
else
    echo "  Skipped: terraform not installed"
fi

# Markdown formatting
echo ""
echo "[3/5] Running markdownlint --fix..."
if command -v markdownlint &>/dev/null; then
    markdownlint --fix "**/*.md" 2>&1 || true
    echo "  Done"
else
    echo "  Skipped: markdownlint not installed"
fi

# Trailing whitespace
echo ""
echo "[4/5] Removing trailing whitespace..."
find . -type f \( -name "*.py" -o -name "*.sh" -o -name "*.yaml" -o -name "*.yml" \
    -o -name "*.tf" -o -name "*.json" -o -name "*.ts" -o -name "*.js" \) \
    -not -path "./.venv/*" -not -path "./node_modules/*" -not -path "./.git/*" \
    -exec sed -i.bak 's/[[:space:]]*$//' {} \;
find . -name "*.bak" -delete 2>/dev/null || true
echo "  Done"

# Fix line endings
echo ""
echo "[5/5] Normalizing line endings to LF..."
find . -type f \( -name "*.py" -o -name "*.sh" -o -name "*.yaml" -o -name "*.yml" \
    -o -name "*.tf" -o -name "*.json" -o -name "*.ts" -o -name "*.js" -o -name "*.md" \) \
    -not -path "./.venv/*" -not -path "./node_modules/*" -not -path "./.git/*" \
    -exec sed -i.bak 's/\r$//' {} \;
find . -name "*.bak" -delete 2>/dev/null || true
echo "  Done"

echo ""
echo "============================================"
echo "  Auto-fix complete. Review changes with:"
echo "    git diff"
echo "============================================"
EOF
chmod +x scripts/autofix.sh
# Run auto-fix
bash scripts/autofix.sh
# Expected output:
============================================
  Auto-fix: Formatting & Linting
============================================

[1/5] Running Black (Python formatter)...
All done! 1 file reformatted.

[2/5] Running terraform fmt...
  Done

[3/5] Running markdownlint --fix...
  Done

[4/5] Removing trailing whitespace...
  Done

[5/5] Normalizing line endings to LF...
  Done

============================================
  Auto-fix complete. Review changes with:
    git diff
============================================

Checkpoint: Auto-fixes

# Verify autofix script exists and is executable
ls -la scripts/autofix.sh
# Expected: -rwxr-xr-x ... scripts/autofix.sh

# Run auto-fix and verify no remaining issues
bash scripts/autofix.sh
pre-commit run --all-files && echo "All checks pass after auto-fix"

Step 6: Add Quality Gates (5 min)

Quality gates go beyond formatting to check documentation quality, spelling, links, and metrics. Some gates block merges; others provide advisory warnings.

Spell Checking (Blocking)

{
  "version": "0.2",
  "language": "en",
  "words": [
    "autofix",
    "bandit",
    "cspell",
    "devops",
    "editorconfig",
    "flake8",
    "markdownlint",
    "mkdocs",
    "mypy",
    "pycqa",
    "pytest",
    "shellcheck",
    "terraform",
    "terragrunt",
    "yamllint"
  ],
  "ignorePaths": [
    "node_modules/**",
    ".venv/**",
    "*.lock",
    ".git/**",
    "site/**"
  ]
}
# Save the config
mkdir -p .github
# (copy the JSON above to .github/cspell.json)

# Run spell check
cspell --config .github/cspell.json "docs/**/*.md" "*.md"
# Expected output (clean):
CSpell: Files checked: 15, Issues found: 0

Spell Check Workflow (Blocking Gate)

# .github/workflows/spell-checker.yml
name: Spell Checker

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  spell-check:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '20'

      - name: Install cSpell
        run: npm install -g cspell

      - name: Run spell check
        run: |
          cspell --config .github/cspell.json "docs/**/*.md" "*.md"
# .github/workflows/link-checker.yml
name: Link Checker

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # Weekly on Mondays

jobs:
  check-links:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Check links
        uses: lycheeverse/lychee-action@v2
        with:
          args: --verbose --no-progress 'docs/**/*.md'
          fail: false  # Advisory only, does not block merge

Metadata Validation (Advisory)

# Validate @module metadata tags in documentation
python scripts/validate_metadata.py docs/
# Expected output:
Validating metadata in docs/...
  docs/02_language_guides/python.md: OK (6 tags found)
  docs/02_language_guides/terraform.md: OK (6 tags found)
  ...
Summary: 45 files checked, 42 passed, 3 warnings

Code-to-Text Ratio (Advisory)

# Check that language guides maintain 3:1 code-to-text ratio
python scripts/analyze_code_ratio.py
# Expected output:
Code-to-Text Ratio Analysis
================================================================================
Language Guide                   Code Lines   Text Lines      Ratio     Status
--------------------------------------------------------------------------------
python                                 1012          318       3.18     PASS
terraform                              2124         6285       0.34     FAIL
ansible                                2258          330       6.84     PASS
...
================================================================================
Achievement: 18/19 guides pass (target: 3:1 ratio)

Quality Gate Summary

Quality Gate Configuration
===========================

Gate                    | Type     | Blocks Merge? | Frequency
------------------------+----------+---------------+------------------
Spell Check (cSpell)    | Blocking | Yes           | Every push/PR
Link Check (lychee)     | Advisory | No            | Weekly + push
Metadata Validation     | Advisory | No            | Every push/PR
Code-to-Text Ratio      | Advisory | No            | Every push/PR
Black Formatting        | Blocking | Yes           | Every push/PR
Flake8 Linting          | Blocking | Yes           | Every push/PR
ShellCheck              | Blocking | Yes           | Every push/PR
Secret Detection        | Blocking | Yes           | Every push/PR

Checkpoint: Quality Gates

# Verify spell check config exists
ls -la .github/cspell.json
# Expected: -rw-r--r-- ... .github/cspell.json

# Run spell check locally
cspell --config .github/cspell.json "docs/**/*.md" && echo "Spelling OK"

# Verify workflow files exist
ls .github/workflows/spell-checker.yml
ls .github/workflows/link-checker.yml
# Both should exist

Step 7: Monitor and Measure (4 min)

You cannot improve what you do not measure. Track key automation metrics to demonstrate value and identify bottlenecks.

Key Metrics to Track

Automation Health Metrics
==========================

1. Lint Pass Rate       - % of commits that pass all linters on first try
2. CI Duration          - Time from push to green/red status
3. Time-to-Merge        - PR open to merge duration
4. Pre-commit Skip Rate - How often developers run --no-verify
5. Security Findings    - Secrets or vulnerabilities caught per month
6. Auto-fix Rate        - % of issues fixed automatically vs manually

Dashboard Script

#!/usr/bin/env python3
"""
automation_dashboard.py - Track automation metrics from CI data.

Usage:
    python scripts/automation_dashboard.py
"""
import json
import subprocess
import sys
from datetime import datetime, timedelta


def run_cmd(cmd: str) -> str:
    """Run a shell command and return stdout."""
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout.strip()


def get_commit_count(days: int = 30) -> int:
    """Count commits in the last N days."""
    since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
    output = run_cmd(f'git log --since="{since}" --oneline')
    return len(output.splitlines()) if output else 0


def get_hook_skip_count(days: int = 30) -> int:
    """Count commits that skipped pre-commit hooks."""
    since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
    output = run_cmd(
        f'git log --since="{since}" --oneline --grep="no-verify" --grep="skip-hooks"'
    )
    return len(output.splitlines()) if output else 0


def get_precommit_hook_count() -> int:
    """Count configured pre-commit hooks."""
    output = run_cmd("grep -c 'id:' .pre-commit-config.yaml 2>/dev/null")
    return int(output) if output.isdigit() else 0


def get_ci_workflow_count() -> int:
    """Count CI workflow files."""
    output = run_cmd("ls .github/workflows/*.yml 2>/dev/null | wc -l")
    return int(output.strip()) if output.strip().isdigit() else 0


def get_autofix_hooks() -> list[str]:
    """List hooks that auto-fix files."""
    autofix_hooks = [
        "trailing-whitespace",
        "end-of-file-fixer",
        "mixed-line-ending",
        "black",
        "markdownlint --fix",
        "terraform_fmt",
    ]
    configured = []
    try:
        with open(".pre-commit-config.yaml") as f:
            content = f.read()
        for hook in autofix_hooks:
            hook_id = hook.split()[0]
            if hook_id in content:
                configured.append(hook)
    except FileNotFoundError:
        pass
    return configured


def print_dashboard() -> None:
    """Print the automation dashboard."""
    print("=" * 60)
    print("  AUTOMATION DASHBOARD")
    print(f"  Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print("=" * 60)
    print()

    # Commit activity
    commits_30d = get_commit_count(30)
    commits_7d = get_commit_count(7)
    print(f"  Commits (last 30 days):  {commits_30d}")
    print(f"  Commits (last 7 days):   {commits_7d}")
    print()

    # Hook metrics
    hook_count = get_precommit_hook_count()
    skip_count = get_hook_skip_count(30)
    skip_rate = (skip_count / commits_30d * 100) if commits_30d > 0 else 0
    print(f"  Pre-commit hooks:        {hook_count}")
    print(f"  Hook skips (30 days):    {skip_count}")
    print(f"  Skip rate:               {skip_rate:.1f}%")
    print()

    # Auto-fix hooks
    autofix = get_autofix_hooks()
    print(f"  Auto-fix hooks:          {len(autofix)}")
    for hook in autofix:
        print(f"    - {hook}")
    print()

    # CI workflows
    workflow_count = get_ci_workflow_count()
    print(f"  CI workflows:            {workflow_count}")
    print()

    # Automation maturity score
    score = 0
    if hook_count > 0:
        score += 20
    if hook_count >= 10:
        score += 10
    if workflow_count > 0:
        score += 20
    if workflow_count >= 3:
        score += 10
    if len(autofix) >= 3:
        score += 15
    if skip_rate < 5:
        score += 15
    if skip_rate == 0:
        score += 10

    print(f"  Automation Score:        {score}/100")
    print()
    print("=" * 60)


if __name__ == "__main__":
    print_dashboard()
# Save the script and run it
python scripts/automation_dashboard.py
# Expected output:
============================================================
  AUTOMATION DASHBOARD
  Generated: 2026-02-14 10:30:00
============================================================

  Commits (last 30 days):  47
  Commits (last 7 days):   12

  Pre-commit hooks:        16
  Hook skips (30 days):    0
  Skip rate:               0.0%

  Auto-fix hooks:          6
    - trailing-whitespace
    - end-of-file-fixer
    - mixed-line-ending
    - black
    - markdownlint --fix
    - terraform_fmt

  CI workflows:            4

  Automation Score:        100/100

============================================================

CI Duration Tracking with GitHub CLI

# Get recent workflow run durations
gh run list --workflow=ci.yml --limit=10 \
    --json conclusion,updatedAt,createdAt \
    --jq '.[] | "\(.conclusion)\t\(.createdAt)\t\(.updatedAt)"'
# Expected output:
success   2026-02-14T09:00:00Z   2026-02-14T09:03:42Z
success   2026-02-13T15:22:00Z   2026-02-13T15:25:18Z
failure   2026-02-13T14:10:00Z   2026-02-13T14:12:55Z
# Calculate average CI duration (in seconds) for last 10 runs
gh run list --workflow=ci.yml --limit=10 \
    --json createdAt,updatedAt \
    --jq '[.[] | ((.updatedAt | fromdateiso8601) - (.createdAt | fromdateiso8601))] | add / length | round'

Checkpoint: Monitoring

# Verify dashboard script exists
ls -la scripts/automation_dashboard.py
# Expected: -rw-r--r-- ... scripts/automation_dashboard.py

# Run the dashboard
python scripts/automation_dashboard.py && echo "Dashboard works"

Automation Maturity Model

Use this model to assess where your project stands and what to adopt next.

Automation Maturity Model
==========================

Level | Name           | Tools & Practices                    | Time to Adopt
------+----------------+--------------------------------------+---------------
  0   | Manual         | Manual checklist, code review only    | 0 min
  1   | Editor-Aware   | EditorConfig, IDE settings            | 5 min
  2   | Pre-commit     | Pre-commit hooks, local linting       | 15 min
  3   | CI/CD          | GitHub Actions, multi-job pipeline    | 30 min
  4   | Auto-fix       | Auto-formatting, auto-remediation     | 10 min
  5   | Quality Gates  | Spell check, link check, metrics      | 15 min
Level Characteristic What Gets Caught What Slips Through
0 - Manual Nothing automated Whatever you remember Everything else
1 - Editor-Aware Formatting on save Indentation, line endings, whitespace Logic errors, security, naming
2 - Pre-commit Checks before commit Formatting, syntax, basic security Complex logic, integration issues
3 - CI/CD Checks on every push Everything local + cross-platform Quality metrics, documentation
4 - Auto-fix Tools fix issues Formatting auto-fixed, less friction Non-fixable issues still require review
5 - Quality Gates Full quality pipeline Spelling, links, code ratios, metrics Novel issues, architectural problems

Mapping Tools to Maturity Levels

Level 0 (Manual):
  - scripts/manual_checks.sh
  - Code review checklists

Level 1 (Editor-Aware):
  - .editorconfig
  - .vscode/settings.json
  - .idea/codeStyles/

Level 2 (Pre-commit):
  - .pre-commit-config.yaml
  - trailing-whitespace, end-of-file-fixer
  - black, flake8, shellcheck
  - detect-private-key

Level 3 (CI/CD):
  - .github/workflows/ci.yml
  - Lint, test, security, docs jobs
  - Artifact uploads, caching

Level 4 (Auto-fix):
  - black (no --check flag)
  - markdownlint --fix
  - terraform fmt
  - scripts/autofix.sh

Level 5 (Quality Gates):
  - .github/workflows/spell-checker.yml
  - .github/workflows/link-checker.yml
  - scripts/analyze_code_ratio.py
  - scripts/validate_metadata.py
  - scripts/automation_dashboard.py

Checkpoint: Final Verification

Run through this complete checklist to verify your automation pipeline.

# 1. EditorConfig exists and has sections for all languages
test -f .editorconfig && echo "PASS: .editorconfig exists" || echo "FAIL"

# 2. Pre-commit hooks are installed
test -f .git/hooks/pre-commit && echo "PASS: hooks installed" || echo "FAIL"

# 3. Pre-commit config has 10+ hooks
HOOK_COUNT=$(grep -c "id:" .pre-commit-config.yaml)
[ "$HOOK_COUNT" -ge 10 ] && echo "PASS: $HOOK_COUNT hooks configured" || echo "FAIL: only $HOOK_COUNT hooks"

# 4. CI workflow exists and is valid YAML
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" 2>/dev/null \
    && echo "PASS: CI workflow valid" || echo "FAIL: CI workflow invalid"

# 5. CI workflow has multiple jobs
JOB_COUNT=$(grep -c "runs-on:" .github/workflows/ci.yml)
[ "$JOB_COUNT" -ge 3 ] && echo "PASS: $JOB_COUNT CI jobs" || echo "FAIL: only $JOB_COUNT CI jobs"

# 6. Spell check config exists
test -f .github/cspell.json && echo "PASS: spell check configured" || echo "FAIL"

# 7. Auto-fix script exists
test -x scripts/autofix.sh && echo "PASS: autofix script executable" || echo "FAIL"

# 8. Dashboard script exists
test -f scripts/automation_dashboard.py && echo "PASS: dashboard exists" || echo "FAIL"

# 9. All pre-commit hooks pass
pre-commit run --all-files && echo "PASS: all hooks pass" || echo "FAIL: hooks failing"

# 10. Git is clean (all fixes committed)
[ -z "$(git status --porcelain)" ] && echo "PASS: working tree clean" || echo "WARN: uncommitted changes"
# Expected output:
PASS: .editorconfig exists
PASS: hooks installed
PASS: 16 hooks configured
PASS: CI workflow valid
PASS: 4 CI jobs
PASS: spell check configured
PASS: autofix script executable
PASS: dashboard exists
PASS: all hooks pass
PASS: working tree clean

Common Troubleshooting

Problem: Pre-commit hooks fail on first run

Symptom:
  An error occurred during hook execution.
  The hook 'black' failed with exit code 1.

Cause:
  Hook environments are not installed yet.

Solution:
  pre-commit install --install-hooks
  pre-commit run --all-files

Problem: Black and Flake8 disagree on formatting

Symptom:
  Black formats code one way, Flake8 complains about it.

Cause:
  Flake8's E203 (whitespace before ':') and W503 (line break before binary operator)
  conflict with Black's formatting.

Solution:
  Configure Flake8 to ignore these rules:
    flake8 --extend-ignore=E203,W503

  In .pre-commit-config.yaml:
    - id: flake8
      args: ['--max-line-length=100', '--extend-ignore=E203,W503']

Problem: ShellCheck reports SC2086 (unquoted variables)

# Symptom:
# In src/deploy.sh line 3:
# if [ $1 == "production" ]; then
#      ^-- SC2086: Double quote to prevent globbing and word splitting.

# Solution: Quote variables and use = instead of ==
# Before:
if [ $1 == "production" ]; then
    echo "deploying"
fi

# After:
if [ "${1:-}" = "production" ]; then
    echo "deploying"
fi

Problem: detect-secrets flags known safe values

# Symptom:
# detect-secrets reports false positives on test fixtures or example configs

# Solution: Update the baseline file to mark known values as safe
detect-secrets scan --update .secrets.baseline

# Then audit and mark false positives
detect-secrets audit .secrets.baseline
# Answer 'n' (not a real secret) for each false positive

Problem: CI is too slow (over 10 minutes)

Symptom:
  CI pipeline takes 15+ minutes to complete.

Causes and Solutions:

1. No dependency caching
   Fix: Add actions/cache for pip, uv, npm

2. Sequential jobs that could run in parallel
   Fix: Remove unnecessary 'needs:' dependencies

3. Installing tools from scratch every run
   Fix: Use setup-* actions with caching

4. Running all checks even when only docs changed
   Fix: Add path filters to workflow triggers
# Example: Path-filtered triggers to speed up CI
on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'pyproject.toml'
      - '.github/workflows/ci.yml'
  pull_request:
    branches: [main]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'pyproject.toml'
      - '.github/workflows/ci.yml'

Problem: Pre-commit hooks modified files but commit still failed

Symptom:
  Pre-commit says "files were modified by this hook" and the commit fails.

Cause:
  Auto-fix hooks (black, trailing-whitespace, end-of-file-fixer) modified files.
  Git needs the modified files to be re-staged.

Solution:
  1. Review the changes: git diff
  2. Stage the auto-fixed files: git add -u
  3. Commit again: git commit -m "your message"

  The second commit will pass because files are already formatted.

Problem: yamllint complains about line length

Symptom:
  yamllint reports "line too long (150 > 120 characters)"

Solution:
  Option 1: Break the line using YAML multiline syntax

  Before:
    description: "This is a very long description that exceeds the line length limit and causes yamllint to fail"

  After:
    description: >-
      This is a very long description that exceeds the line length
      limit and causes yamllint to fail

  Option 2: Adjust the yamllint max line length in .pre-commit-config.yaml:
    args: ['-d', '{extends: default, rules: {line-length: {max: 150}}}']

Next Steps

After completing this tutorial, explore these resources to deepen your automation:

Recommended Next Steps
=======================

1. Tutorial 1: Zero to Validated Python Project
   - Apply automation to a real Python project
   - See: docs/12_tutorials/python_project.md

2. Tutorial 3: Full-Stack App with Multiple Languages
   - Scale automation across Python, TypeScript, and Terraform
   - See: docs/12_tutorials/fullstack_app.md

3. CI/CD Performance Optimization Guide
   - Advanced caching, parallelization, and pipeline tuning
   - See: docs/05_ci_cd/ci_cd_performance.md

4. Progressive Enhancement Roadmap
   - Long-term automation adoption strategy
   - See: docs/07_integration/progressive_enhancement.md

5. Anti-Patterns Documentation
   - Common automation mistakes and how to avoid them
   - See: docs/08_anti_patterns/
Summary of What You Built
==========================

File                              Purpose
---------------------------------+----------------------------------------
.editorconfig                     Editor-level formatting consistency
.pre-commit-config.yaml           Local pre-commit validation hooks
.github/workflows/ci.yml          Server-side CI/CD pipeline
.github/workflows/spell-checker.yml  Spelling quality gate (blocking)
.github/workflows/link-checker.yml   Link validation (advisory)
.github/cspell.json               Spell check configuration
scripts/manual_checks.sh          Manual validation baseline
scripts/autofix.sh                Bulk auto-fix formatting
scripts/automation_dashboard.py   Automation metrics dashboard