第6章:Hooks自动化功能详解
Hooks(钩子)是软件开发中自动触发的脚本程序,用于在关键节点执行特定任务。核心作用包括:1)保证代码一致性,通过pre-commit等阶段自动格式化代码;2)提升效率,如自动运行测试和安全检查;3)预防错误,在提交/推送前进行语法检查和依赖扫描。常用Hooks类型涵盖代码格式化(Black/Prettier)、测试运行(单元/集成测试)、构建部署(环境验证)和安全检查(漏洞扫描)。最佳实践建议
·
一、理解Hooks概念
1.1 Hooks的核心定义
Hooks(钩子) 是在软件开发流程的关键节点自动触发的脚本或程序,用于自动化执行特定任务。类似于工厂生产线的自动化机器人,在特定环节自动完成质量检查、包装等工作。
1.2 Hooks在开发工作流中的作用
传统开发:手动执行任务 → 容易遗漏 → 效率低下
Hook驱动:关键事件触发 → 自动执行 → 保证一致性
1.3 Hooks的触发时机
代码提交前 → 格式化、语法检查 → pre-commit
代码推送前 → 运行测试、构建检查 → pre-push
合并请求时 → 集成测试、安全检查 → pre-merge
发布部署时 → 版本标记、部署验证 → pre-release
1.4 Hooks的主要价值
-
一致性保证:统一团队开发规范
-
错误预防:提前发现潜在问题
-
效率提升:自动化重复性任务
-
质量保障:强制执行质量标准
二、常用Hooks类型详解
2.1 代码格式化Hook
2.1.1 格式化工具选择
# 语言特定格式化工具
Python:
- black: 代码格式化
- isort: 导入排序
- flake8: 风格检查
JavaScript/TypeScript:
- prettier: 代码格式化
- eslint: 代码检查
Go:
- gofmt: 官方格式化
- goimports: 导入整理
Rust:
- rustfmt: 官方格式化
- clippy: 代码检查
2.1.2 pre-commit配置示例
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 23.9.1
hooks:
- id: black
args: [--line-length=88]
files: ^src/.*\.py$
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
args: [--profile=black]
files: ^src/.*\.py$
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
hooks:
- id: prettier
files: ^frontend/.*\.(js|ts|jsx|tsx|css|json)$
args: [--write]
2.1.3 格式化Hook工作流程
# 自动化格式化流程
1. 开发者修改代码
2. 执行git add .
3. 触发pre-commit hook
4. 自动运行black格式化Python代码
5. 自动运行prettier格式化前端代码
6. 检查是否通过,不通过则阻止提交
7. 通过后允许提交
2.2 测试运行Hook
2.2.1 测试Hook策略
# 分层测试Hook配置
快速测试 (pre-commit):
- 单元测试: 快速执行,验证核心逻辑
- 静态类型检查: mypy/pyright
- 代码覆盖率检查: 关键文件覆盖率
集成测试 (pre-push):
- 数据库集成测试
- API端点测试
- 外部服务模拟测试
完整测试 (CI/CD):
- 端到端测试
- 性能测试
- 安全扫描测试
2.2.2 测试Hook配置示例
# .pre-commit-config.yaml - 测试部分
repos:
- repo: local
hooks:
- id: run-unit-tests
name: Run Unit Tests
entry: python -m pytest tests/unit -xvs
language: system
pass_filenames: false
always_run: true
stages: [commit]
- id: check-types
name: Type Check
entry: python -m mypy src/
language: system
pass_filenames: false
always_run: true
stages: [commit]
- id: run-integration-tests
name: Run Integration Tests
entry: python -m pytest tests/integration -xvs
language: system
pass_filenames: false
always_run: true
stages: [push]
2.2.3 智能测试选择
#!/usr/bin/env bash
# scripts/smart-test-hook.sh
# 智能选择运行相关测试
# 检测修改的文件类型
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
# 根据文件类型运行对应测试
if echo "$CHANGED_FILES" | grep -q "\.py$"; then
echo "Running Python tests..."
python -m pytest tests/unit/python -xvs
fi
if echo "$CHANGED_FILES" | grep -q "\.tsx\?$"; then
echo "Running TypeScript tests..."
npm test -- --testPathPattern="$(echo "$CHANGED_FILES" | grep "\.tsx\?$" | tr '\n' '|')"
fi
2.3 构建部署Hook
2.3.1 构建Hook流程
# 构建阶段Hook配置
pre-build:
- 环境变量验证
- 依赖完整性检查
- 配置文件验证
during-build:
- 编译检查
- 静态资源优化
- 依赖树优化
post-build:
- 构建产物验证
- 性能基准测试
- 安全扫描
2.3.2 GitHub Actions构建Hook示例
# .github/workflows/build.yml
name: Build and Validate
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Pre-Build Check
run: |
# 检查必需的环境变量
if [ -z "${{ secrets.DATABASE_URL }}" ]; then
echo "❌ DATABASE_URL is not set"
exit 1
fi
# 检查依赖完整性
pip install pip-audit
pip-audit
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run Build
run: |
# 执行构建
python -m build
- name: Post-Build Validation
run: |
# 验证构建产物
python -m twine check dist/*
# 运行性能基准
python scripts/benchmark.py
2.3.3 Docker构建Hook
# Dockerfile with build hooks
FROM python:3.11-slim as builder
# Pre-build hook: 清理和准备
RUN apt-get update && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# During-build hook: 依赖安装和检查
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt && \
pip check # 验证依赖完整性
COPY . .
# Post-build hook: 构建验证
RUN python -m py_compile src/*.py && \
find . -name "*.pyc" -delete
# 最终镜像
FROM python:3.11-slim
COPY --from=builder /app /app
WORKDIR /app
CMD ["python", "src/main.py"]
2.4 安全检查Hook
2.4.1 安全扫描工具矩阵
# 安全Hook工具选择
代码安全:
- bandit: Python安全扫描
- semgrep: 多语言安全扫描
- gitleaks: 密钥泄露检测
依赖安全:
- safety: Python依赖漏洞检查
- npm audit: NPM包漏洞检查
- snyk: 多语言依赖扫描
容器安全:
- trivy: 容器镜像漏洞扫描
- hadolint: Dockerfile安全检测
基础设施安全:
- terrascan: Terraform安全扫描
- checkov: 云基础设施安全检查
2.4.2 综合安全Hook配置
# .pre-commit-config.yaml - 安全部分
repos:
- repo: https://github.com/PyCQA/bandit
rev: 1.7.5
hooks:
- id: bandit
args: ["-r", "src/", "-ll"]
files: ^src/.*\.py$
- repo: https://github.com/zricethezav/gitleaks
rev: v8.17.0
hooks:
- id: gitleaks
args: ["--verbose"]
- repo: local
hooks:
- id: dependency-scan
name: Dependency Vulnerability Scan
entry: |
# Python依赖扫描
safety check -r requirements.txt
# NPM依赖扫描
if [ -f "package.json" ]; then
npm audit --audit-level=high
fi
language: system
pass_filenames: false
always_run: true
stages: [commit, push]
2.4.3 自定义安全规则
# scripts/security-hooks/custom-security.py
"""
自定义安全检查Hook
"""
import ast
import os
from pathlib import Path
class SecurityVisitor(ast.NodeVisitor):
"""AST访问器,检查安全问题"""
def visit_Call(self, node):
# 检查危险的函数调用
dangerous_functions = {
'eval', 'exec', 'os.system', 'subprocess.call'
}
if isinstance(node.func, ast.Name):
if node.func.id in dangerous_functions:
print(f"⚠️ 发现潜在危险调用: {node.func.id}")
print(f" 文件: {self.current_file}")
print(f" 行号: {node.lineno}")
self.generic_visit(node)
def check_file(self, filepath):
"""检查单个文件"""
self.current_file = filepath
try:
with open(filepath, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
self.visit(tree)
except SyntaxError as e:
print(f"语法错误: {filepath}:{e.lineno}")
def check_hardcoded_secrets(filepath):
"""检查硬编码的密钥"""
secret_patterns = [
r'password\s*=\s*[\'"][^\'"]+[\'"]',
r'api_key\s*=\s*[\'"][^\'"]+[\'"]',
r'secret\s*=\s*[\'"][^\'"]+[\'"]',
]
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
for i, line in enumerate(content.splitlines(), 1):
for pattern in secret_patterns:
if re.search(pattern, line, re.IGNORECASE):
print(f"🔐 发现硬编码密钥: {filepath}:{i}")
print(f" 内容: {line.strip()}")
if __name__ == "__main__":
import re
import sys
files_to_check = sys.argv[1:] if len(sys.argv) > 1 else []
for filepath in files_to_check:
if filepath.endswith('.py'):
visitor = SecurityVisitor()
visitor.check_file(filepath)
check_hardcoded_secrets(filepath)
三、自定义Hooks创建
3.1 Hook开发框架
3.1.1 本地Hook开发结构
hooks/
├── scripts/
│ ├── pre-commit/
│ │ ├── format-code.sh
│ │ ├── run-tests.sh
│ │ └── check-types.sh
│ ├── pre-push/
│ │ ├── integration-tests.sh
│ │ └── security-scan.sh
│ └── post-merge/
│ └── update-deps.sh
├── templates/
│ └── custom-hook-template.py
└── config/
└── hook-config.yaml
3.1.2 通用Hook模板
#!/usr/bin/env python3
"""
自定义Hook模板
用法: python hook-template.py [file1] [file2] ...
"""
import sys
import os
from pathlib import Path
from typing import List, Tuple
class CustomHook:
"""自定义Hook基类"""
def __init__(self, config_path: str = None):
self.config_path = config_path
self.load_config()
def load_config(self):
"""加载配置"""
self.config = {}
if self.config_path and os.path.exists(self.config_path):
import yaml
with open(self.config_path, 'r') as f:
self.config = yaml.safe_load(f)
def should_process_file(self, filepath: str) -> bool:
"""判断是否应该处理该文件"""
extensions = self.config.get('extensions', [])
if extensions:
return any(filepath.endswith(ext) for ext in extensions)
return True
def process_files(self, files: List[str]) -> Tuple[bool, str]:
"""
处理文件
返回: (是否成功, 消息)
"""
try:
for filepath in files:
if self.should_process_file(filepath):
result = self.process_single_file(filepath)
if not result[0]:
return result
return True, "✓ 所有文件检查通过"
except Exception as e:
return False, f"❌ 处理失败: {str(e)}"
def process_single_file(self, filepath: str) -> Tuple[bool, str]:
"""处理单个文件 - 子类需实现"""
raise NotImplementedError
# 使用示例
class MyCustomHook(CustomHook):
def process_single_file(self, filepath: str) -> Tuple[bool, str]:
# 实现具体的检查逻辑
with open(filepath, 'r') as f:
content = f.read()
# 示例:检查TODO注释
if 'TODO:' in content:
return False, f"发现未完成的TODO注释: {filepath}"
return True, "✓ 检查通过"
if __name__ == "__main__":
hook = MyCustomHook('config/hook-config.yaml')
success, message = hook.process_files(sys.argv[1:])
print(message)
sys.exit(0 if success else 1)
3.2 业务特定Hook示例
3.2.1 API文档检查Hook
#!/usr/bin/env python3
"""
API文档完整性检查Hook
检查所有API端点是否有完整的文档
"""
import re
from pathlib import Path
import sys
class APIDocChecker:
def __init__(self, source_dir="src"):
self.source_dir = Path(source_dir)
self.issues = []
def check_python_endpoints(self):
"""检查Python FastAPI/Flask端点"""
pattern = re.compile(r'@(app|router)\.(get|post|put|delete|patch)\("([^"]+)"\)')
for py_file in self.source_dir.rglob("*.py"):
with open(py_file, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.splitlines()
for i, line in enumerate(lines):
match = pattern.search(line)
if match:
# 检查下一行是否是文档字符串
has_docstring = False
for j in range(i+1, min(i+5, len(lines))):
if '"""' in lines[j] or "'''" in lines[j]:
has_docstring = True
break
if not has_docstring:
self.issues.append({
'file': str(py_file),
'line': i+1,
'endpoint': match.group(3),
'method': match.group(2)
})
def generate_report(self):
"""生成报告"""
if not self.issues:
print("✓ 所有API端点都有文档")
return True
print("❌ 发现未文档化的API端点:")
for issue in self.issues:
print(f" 文件: {issue['file']}:{issue['line']}")
print(f" 端点: {issue['method'].upper()} {issue['endpoint']}")
print()
return False
if __name__ == "__main__":
checker = APIDocChecker()
checker.check_python_endpoints()
success = checker.generate_report()
sys.exit(0 if success else 1)
3.2.2 数据库迁移检查Hook
#!/usr/bin/env bash
# scripts/pre-deploy/db-migration-check.sh
# 检查数据库迁移是否完整
set -e
echo "🔍 检查数据库迁移..."
# 检查是否有未应用的迁移
if command -v alembic &> /dev/null; then
echo "检查Alembic迁移..."
if alembic current | grep -q "head"; then
echo "✓ 所有迁移已应用"
else
echo "❌ 有未应用的迁移:"
alembic current
exit 1
fi
fi
# 检查迁移文件命名规范
echo "检查迁移文件命名..."
MIGRATION_DIR="migrations/versions"
if [ -d "$MIGRATION_DIR" ]; then
INVALID_FILES=$(find "$MIGRATION_DIR" -name "*.py" ! -name "__init__.py" \
| xargs -I {} basename {} \
| grep -vE '^[a-f0-9]{12}_.+\.py$')
if [ -n "$INVALID_FILES" ]; then
echo "❌ 迁移文件名不符合规范:"
echo "$INVALID_FILES"
echo "规范: [12位hash]_[描述].py"
exit 1
else
echo "✓ 迁移文件名符合规范"
fi
fi
# 检查迁移是否有回滚操作
echo "检查迁移回滚支持..."
for migration in "$MIGRATION_DIR"/*.py; do
if [ -f "$migration" ]; then
if ! grep -q "def downgrade" "$migration"; then
echo "⚠️ 警告: $migration 缺少downgrade函数"
fi
fi
done
echo "✅ 数据库迁移检查完成"
3.3 团队协作Hook
3.3.1 提交信息规范检查
#!/usr/bin/env python3
"""
提交信息规范检查Hook
确保提交信息符合团队规范
"""
import re
import sys
from typing import Tuple
class CommitMessageChecker:
"""提交信息检查器"""
PATTERNS = {
'feat': r'^feat(\([a-z-]+\))?: .+',
'fix': r'^fix(\([a-z-]+\))?: .+',
'docs': r'^docs(\([a-z-]+\))?: .+',
'style': r'^style(\([a-z-]+\))?: .+',
'refactor': r'^refactor(\([a-z-]+\))?: .+',
'test': r'^test(\([a-z-]+\))?: .+',
'chore': r'^chore(\([a-z-]+\))?: .+',
}
def __init__(self, commit_msg_file: str):
self.commit_msg_file = commit_msg_file
def check(self) -> Tuple[bool, str]:
"""检查提交信息"""
with open(self.commit_msg_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
if not lines:
return False, "提交信息为空"
# 获取标题行(第一行)
title = lines[0].strip()
# 检查标题长度
if len(title) > 72:
return False, f"标题过长 ({len(title)}/72)"
# 检查类型前缀
type_valid = False
for pattern in self.PATTERNS.values():
if re.match(pattern, title):
type_valid = True
break
if not type_valid:
valid_types = ", ".join(self.PATTERNS.keys())
return False, f"无效的类型前缀。有效的类型: {valid_types}\n示例: feat: 添加新功能"
# 检查是否有空行分隔标题和正文
if len(lines) > 1 and lines[1].strip():
return False, "标题和正文之间需要空行"
# 检查正文行长度
for i, line in enumerate(lines[2:], start=2):
if len(line.rstrip()) > 72:
return False, f"第{i}行过长 ({len(line.rstrip())}/72)"
return True, "✓ 提交信息符合规范"
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: python check-commit-msg.py <提交信息文件>")
sys.exit(1)
checker = CommitMessageChecker(sys.argv[1])
success, message = checker.check()
print(message)
if not success:
print("\n📝 提交信息规范:")
print("格式: <类型>(<范围>): <描述>")
print("类型: feat, fix, docs, style, refactor, test, chore")
print("示例: feat(auth): 添加用户注册功能")
print(" fix(api): 修复分页参数验证")
sys.exit(0 if success else 1)
3.3.2 代码审查自动化Hook
#!/usr/bin/env bash
# scripts/pre-push/auto-review.sh
# 自动代码审查Hook
set -e
echo "🤖 开始自动代码审查..."
# 获取将要推送的提交
COMMITS=$(git log --oneline origin/$BRANCH_NAME..HEAD 2>/dev/null || git log --oneline HEAD~1..HEAD)
if [ -z "$COMMITS" ]; then
echo "没有新的提交需要审查"
exit 0
fi
echo "审查以下提交:"
echo "$COMMITS"
echo
# 检查提交是否包含大型文件
echo "🔍 检查大型文件..."
LARGE_FILES=$(git diff --cached --name-only | xargs -I {} sh -c '
if [ -f "{}" ]; then
size=$(stat -f%z "{}" 2>/dev/null || stat -c%s "{}")
if [ $size -gt 1048576 ]; then
echo " {}: $(numfmt --to=iec $size)"
fi
fi
')
if [ -n "$LARGE_FILES" ]; then
echo "⚠️ 发现大型文件:"
echo "$LARGE_FILES"
echo "考虑使用.gitignore或git LFS"
fi
# 检查调试代码
echo "🔍 检查调试代码..."
DEBUG_PATTERNS=("console.log" "print(" "debugger" "TODO:" "FIXME:")
for pattern in "${DEBUG_PATTERNS[@]}"; do
FILES_WITH_PATTERN=$(git diff --cached --name-only | xargs grep -l "$pattern" 2>/dev/null || true)
if [ -n "$FILES_WITH_PATTERN" ]; then
echo "⚠️ 发现可能包含调试代码的文件 ($pattern):"
echo "$FILES_WITH_PATTERN"
fi
done
# 运行复杂性检查
echo "🔍 检查代码复杂度..."
if command -v radon &> /dev/null; then
CHANGED_PY_FILES=$(git diff --cached --name-only | grep '\.py$')
if [ -n "$CHANGED_PY_FILES" ]; then
for file in $CHANGED_PY_FILES; do
if [ -f "$file" ]; then
COMPLEXITY=$(radon cc "$file" -s)
if echo "$COMPLEXITY" | grep -q "[BC]"; then
echo "⚠️ $file 包含复杂函数:"
echo "$COMPLEXITY" | grep "[BC]"
fi
fi
done
fi
fi
echo "✅ 自动代码审查完成"
echo "提示: 确保所有测试通过,代码符合规范后再推送"
四、Hooks触发条件配置
4.1 Git Hooks配置
4.1.1 标准Git Hooks
# .git/hooks/ 目录结构
.git/hooks/
├── pre-commit # 提交前触发
├── pre-merge-commit # 合并提交前触发
├── pre-push # 推送前触发
├── pre-rebase # 变基前触发
├── commit-msg # 提交信息验证
├── post-checkout # 检出后触发
├── post-commit # 提交后触发
├── post-merge # 合并后触发
└── post-rewrite # 重写提交历史后触发
4.1.2 多阶段Hook配置
#!/usr/bin/env bash
# .git/hooks/pre-commit
# 多阶段pre-commit hook
echo "🚀 开始pre-commit检查..."
# 阶段1: 快速检查(总是运行)
echo "阶段1: 快速语法检查..."
./hooks/scripts/quick-check.sh
if [ $? -ne 0 ]; then
echo "❌ 快速检查失败"
exit 1
fi
# 阶段2: 仅对暂存文件运行
echo "阶段2: 格式化检查..."
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
if echo "$CHANGED_FILES" | grep -q '\.py$'; then
echo "运行Python格式化检查..."
./hooks/scripts/format-python.sh --check-only
fi
if echo "$CHANGED_FILES" | grep -q '\.tsx\?$'; then
echo "运行TypeScript检查..."
./hooks/scripts/check-typescript.sh
fi
# 阶段3: 自定义检查
echo "阶段3: 自定义检查..."
./hooks/scripts/custom-checks.sh "$CHANGED_FILES"
echo "✅ pre-commit检查全部通过"
4.2 CI/CD Pipeline Hooks
4.2.1 GitHub Actions工作流Hook
# .github/workflows/full-pipeline.yml
name: Full CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
# 手动触发
workflow_dispatch:
jobs:
# Hook 1: 代码质量检查
code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Pre-Quality Hook
run: ./scripts/hooks/pre-quality.sh
- name: Run Linters
run: |
python -m flake8 src/
python -m mypy src/
npx eslint frontend/ --max-warnings=0
- name: Post-Quality Hook
run: ./scripts/hooks/post-quality.sh
if: always()
# Hook 2: 测试套件
test:
runs-on: ubuntu-latest
needs: code-quality
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Pre-Test Hook
run: ./scripts/hooks/pre-test.sh
- name: Run Tests
run: |
python -m pytest tests/ --cov=src --cov-report=xml
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
- name: Post-Test Hook
run: ./scripts/hooks/post-test.sh
# Hook 3: 构建和部署
deploy:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Pre-Deploy Hook
run: ./scripts/hooks/pre-deploy.sh
- name: Build and Push
run: |
docker build -t myapp:${{ github.sha }} .
docker push myapp:${{ github.sha }}
- name: Deploy to Staging
run: ./scripts/deploy.sh staging
- name: Post-Deploy Hook
run: ./scripts/hooks/post-deploy.sh
4.3 条件触发配置
4.3.1 基于文件类型触发
#!/usr/bin/env bash
# 基于文件类型的条件触发
# 获取修改的文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
# 初始化标志
RUN_PYTHON_CHECKS=false
RUN_JS_CHECKS=false
RUN_DOCS_CHECKS=false
# 检查文件类型
for file in $STAGED_FILES; do
case $file in
*.py)
RUN_PYTHON_CHECKS=true
;;
*.js|*.ts|*.jsx|*.tsx)
RUN_JS_CHECKS=true
;;
*.md|*.rst)
RUN_DOCS_CHECKS=true
;;
Dockerfile|docker-compose.yml)
echo "⚠️ 检测到Docker配置文件变更,建议运行容器测试"
;;
esac
done
# 条件执行
if $RUN_PYTHON_CHECKS; then
echo "运行Python检查..."
./scripts/checks/python-checks.sh
fi
if $RUN_JS_CHECKS; then
echo "运行JavaScript检查..."
./scripts/checks/js-checks.sh
fi
if $RUN_DOCS_CHECKS; then
echo "运行文档检查..."
./scripts/checks/docs-checks.sh
fi
4.3.2 基于分支策略触发
# .github/workflows/branch-specific.yml
name: Branch-Specific Hooks
on: [push, pull_request]
jobs:
main-branch-hooks:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Production Safety Checks
run: |
./scripts/hooks/production-safety.sh
./scripts/hooks/backup-verification.sh
release-branch-hooks:
if: startsWith(github.ref, 'refs/heads/release/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Release Preparation
run: |
./scripts/hooks/version-check.sh
./scripts/hooks/changelog-verification.sh
feature-branch-hooks:
if: startsWith(github.ref, 'refs/heads/feature/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Feature Branch Checks
run: |
./scripts/hooks/feature-tests.sh
./scripts/hooks/docs-required.sh
五、调试和维护Hooks
5.1 Hook调试技巧
5.1.1 调试模式配置
#!/usr/bin/env bash
# hooks/debug-mode.sh
# 启用调试模式
export HOOK_DEBUG=true
# 调试级别
# 1: 基本信息
# 2: 详细输出
# 3: 调试信息(包括命令执行)
export DEBUG_LEVEL=${DEBUG_LEVEL:-2}
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
debug() {
if [ "$HOOK_DEBUG" = "true" ] && [ "$DEBUG_LEVEL" -ge 1 ]; then
echo -e "${YELLOW}[DEBUG]${NC} $1"
fi
}
info() {
if [ "$HOOK_DEBUG" = "true" ] && [ "$DEBUG_LEVEL" -ge 2 ]; then
echo -e "${GREEN}[INFO]${NC} $1"
fi
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 在Hook脚本开头引入
# source ./hooks/debug-mode.sh
5.2 Hook性能监控
5.2.1 性能跟踪工具
#!/usr/bin/env python3
"""
Hook性能监控
"""
import time
from functools import wraps
from collections import defaultdict
import statistics
class PerformanceMonitor:
def __init__(self):
self.records = defaultdict(list)
self.current_hook = None
def track(self, name=None):
"""性能跟踪装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
hook_name = name or func.__name__
self.current_hook = hook_name
start_time = time.perf_counter()
try:
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start_time
# 记录执行时间
self.records[hook_name].append(elapsed)
# 如果执行时间过长,发出警告
threshold = self.get_threshold(hook_name)
if elapsed > threshold:
print(f"⚠️ {hook_name} 执行时间较长: {elapsed:.2f}s (阈值: {threshold:.2f}s)")
return result
finally:
self.current_hook = None
return wrapper
return decorator
def get_threshold(self, hook_name):
"""获取阈值"""
thresholds = {
'pre-commit': 5.0, # 5秒
'pre-push': 30.0, # 30秒
'test-suite': 60.0, # 60秒
'formatting': 10.0, # 10秒
}
return thresholds.get(hook_name, 30.0)
def generate_report(self):
"""生成性能报告"""
print("📊 Hook性能报告")
print("=" * 50)
for hook_name, times in self.records.items():
if times:
avg = statistics.mean(times)
median = statistics.median(times)
stddev = statistics.stdev(times) if len(times) > 1 else 0
print(f"{hook_name}:")
print(f" 执行次数: {len(times)}")
print(f" 平均时间: {avg:.2f}s")
print(f" 中位数: {median:.2f}s")
print(f" 标准差: {stddev:.2f}s")
print(f" 最长: {max(times):.2f}s")
print(f" 最短: {min(times):.2f}s")
print()
# 使用示例
monitor = PerformanceMonitor()
@monitor.track("code-formatting")
def format_code():
time.sleep(0.5) # 模拟耗时操作
return "formatted"
@monitor.track("test-execution")
def run_tests():
time.sleep(2.0) # 模拟耗时操作
return "tests passed"
if __name__ == "__main__":
# 模拟多次执行
for _ in range(5):
format_code()
run_tests()
monitor.generate_report()
5.3 Hook维护策略
5.3.1 Hook版本管理
# hooks/version-manifest.yaml
version: "1.2.0"
last_updated: "2024-01-15"
maintainer: "devops-team@company.com"
hooks:
pre-commit:
version: "1.1.0"
dependencies:
- python >= 3.8
- pre-commit >= 3.0.0
checks:
- name: code-formatting
enabled: true
command: black --check
- name: linting
enabled: true
command: flake8
- name: type-checking
enabled: false # 临时禁用
command: mypy
pre-push:
version: "1.0.3"
dependencies:
- docker
- pytest
checks:
- name: integration-tests
enabled: true
custom:
- name: api-docs-check
version: "0.9.1"
enabled: true
- name: security-scan
version: "1.2.1"
enabled: true
update_policy:
auto_update_minor: true
require_review_major: true
backup_before_update: true
5.3.2 Hook健康检查
#!/usr/bin/env bash
# scripts/maintain/hook-health-check.sh
echo "🩺 检查Hook健康状况..."
# 检查Hook文件存在性
echo "1. 检查Hook文件..."
for hook in pre-commit pre-push commit-msg; do
if [ -f ".git/hooks/$hook" ]; then
echo " ✓ $hook 存在"
# 检查可执行权限
if [ -x ".git/hooks/$hook" ]; then
echo " ✓ $hook 可执行"
else
echo " ⚠️ $hook 不可执行,正在修复..."
chmod +x ".git/hooks/$hook"
fi
else
echo " ❌ $hook 不存在"
fi
done
# 检查依赖
echo "2. 检查依赖..."
REQUIRED_TOOLS=("python" "node" "docker" "git")
for tool in "${REQUIRED_TOOLS[@]}"; do
if command -v "$tool" &> /dev/null; then
echo " ✓ $tool 已安装"
else
echo " ❌ $tool 未安装"
fi
done
# 检查配置文件
echo "3. 检查配置文件..."
CONFIG_FILES=(".pre-commit-config.yaml" "package.json" "requirements.txt")
for config in "${CONFIG_FILES[@]}"; do
if [ -f "$config" ]; then
echo " ✓ $config 存在"
# 检查配置文件有效性
case "$config" in
".pre-commit-config.yaml")
if python -c "import yaml; yaml.safe_load(open('$config'))" &> /dev/null; then
echo " ✓ $config 格式正确"
else
echo " ❌ $config 格式错误"
fi
;;
"package.json")
if jq empty "$config" &> /dev/null; then
echo " ✓ $config 格式正确"
else
echo " ❌ $config 格式错误"
fi
;;
esac
else
echo " ⚠️ $config 不存在"
fi
done
# 运行测试Hook
echo "4. 测试Hook执行..."
if [ -f ".git/hooks/pre-commit" ]; then
echo " 测试pre-commit hook..."
# 创建测试文件
TEST_FILE="hook_test_$(date +%s).py"
echo "# Test file" > "$TEST_FILE"
git add "$TEST_FILE"
# 运行pre-commit(不实际提交)
if .git/hooks/pre-commit; then
echo " ✓ pre-commit hook 运行正常"
else
echo " ❌ pre-commit hook 运行失败"
fi
# 清理
git rm --cached "$TEST_FILE" &> /dev/null
rm "$TEST_FILE"
fi
echo "✅ Hook健康检查完成"
5.3.3 Hook迁移和升级
#!/usr/bin/env python3
"""
Hook迁移工具
用于升级和迁移Hook配置
"""
import shutil
import yaml
from pathlib import Path
from datetime import datetime
import json
class HookMigrator:
def __init__(self, backup_dir=".hook-backups"):
self.backup_dir = Path(backup_dir)
self.backup_dir.mkdir(exist_ok=True)
def backup_current_hooks(self):
"""备份当前Hook配置"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = self.backup_dir / f"backup_{timestamp}"
print(f"📦 备份当前Hook配置到: {backup_path}")
# 备份.git/hooks
hooks_dir = Path(".git/hooks")
if hooks_dir.exists():
shutil.copytree(hooks_dir, backup_path / "hooks")
# 备份配置文件
config_files = [".pre-commit-config.yaml", "package.json", "requirements.txt"]
for config in config_files:
config_path = Path(config)
if config_path.exists():
shutil.copy2(config_path, backup_path / config_path.name)
# 创建备份元数据
metadata = {
"timestamp": datetime.now().isoformat(),
"backup_path": str(backup_path),
"files_backed_up": [str(p) for p in backup_path.rglob("*") if p.is_file()]
}
with open(backup_path / "backup_metadata.json", 'w') as f:
json.dump(metadata, f, indent=2)
return backup_path
def migrate_pre_commit_config(self, old_config_path, new_config_path):
"""迁移pre-commit配置"""
print(f"🔄 迁移pre-commit配置")
with open(old_config_path, 'r') as f:
old_config = yaml.safe_load(f)
# 更新到新版本格式
new_config = {
'repos': [],
'default_language_version': {
'python': 'python3.11',
'node': '18.17'
}
}
# 转换旧格式到新格式
for repo in old_config.get('repos', []):
# 更新版本到最新稳定版
if repo.get('repo') == 'https://github.com/psf/black':
repo['rev'] = '23.9.1'
elif repo.get('repo') == 'https://github.com/pycqa/flake8':
repo['rev'] = '6.1.0'
new_config['repos'].append(repo)
# 添加新的检查
new_config['repos'].append({
'repo': 'https://github.com/pre-commit/mirrors-mypy',
'rev': 'v1.5.1',
'hooks': [{
'id': 'mypy',
'args': ['--ignore-missing-imports']
}]
})
# 保存新配置
with open(new_config_path, 'w') as f:
yaml.dump(new_config, f, default_flow_style=False)
print(f"✅ 配置已更新到新格式")
def validate_migration(self):
"""验证迁移结果"""
print("🔍 验证迁移结果...")
# 检查所有Hook文件存在
required_hooks = ['pre-commit', 'pre-push']
for hook in required_hooks:
hook_path = Path(f".git/hooks/{hook}")
if not hook_path.exists():
print(f"❌ 缺少Hook文件: {hook}")
return False
# 检查配置文件
if not Path(".pre-commit-config.yaml").exists():
print("❌ 缺少pre-commit配置文件")
return False
# 运行快速测试
print(" 运行快速测试...")
import subprocess
try:
result = subprocess.run(
["pre-commit", "run", "--all-files"],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
print("✅ 所有检查通过")
return True
else:
print(f"❌ 检查失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print("❌ 测试超时")
return False
def rollback(self, backup_path):
"""回滚到备份版本"""
print(f"↩️ 回滚到备份: {backup_path}")
backup_path = Path(backup_path)
if not backup_path.exists():
print("❌ 备份不存在")
return False
# 恢复Hook文件
hooks_backup = backup_path / "hooks"
if hooks_backup.exists():
shutil.rmtree(".git/hooks", ignore_errors=True)
shutil.copytree(hooks_backup, ".git/hooks")
# 恢复配置文件
for config_file in backup_path.glob("*"):
if config_file.name != "hooks":
shutil.copy2(config_file, ".")
print("✅ 回滚完成")
return True
if __name__ == "__main__":
migrator = HookMigrator()
# 备份
backup = migrator.backup_current_hooks()
# 迁移
try:
migrator.migrate_pre_commit_config(
".pre-commit-config.yaml",
".pre-commit-config.yaml.new"
)
# 替换配置文件
Path(".pre-commit-config.yaml.new").rename(".pre-commit-config.yaml")
# 验证
if migrator.validate_migration():
print("🎉 迁移成功")
else:
print("❌ 迁移失败,正在回滚...")
migrator.rollback(backup)
except Exception as e:
print(f"❌ 迁移过程中出错: {e}")
migrator.rollback(backup)
六、最佳实践总结
6.1 Hook设计原则
设计原则:
快速失败: 尽早发现问题,减少等待时间
渐进增强: 从基本检查开始,逐步增加复杂检查
关注本地: 优先在本地运行,减少CI负担
明确反馈: 提供清晰的错误信息和修复建议
可配置性: 允许团队根据需求调整Hook行为
向后兼容: 避免破坏现有工作流
6.2 团队协作指南
# Hook团队协作指南
## 1. 统一配置
- 使用统一的.pre-commit-config.yaml
- 共享Hook脚本仓库
- 统一的工具版本
## 2. 文档要求
每个Hook需要:
- README.md说明
- 配置示例
- 常见问题解答
- 维护者信息
## 3. 更新流程
1. 在特性分支开发新Hook
2. 提交Pull Request
3. 团队成员审查
4. 在开发环境测试
5. 合并到主分支
6. 通知团队更新
## 4. 故障处理
- 提供回滚机制
- 记录详细的日志
- 设置维护者值班
- 建立紧急禁用流程
6.3 性能优化建议
优化策略:
缓存结果: 对未变更文件跳过检查
并行执行: 同时运行独立的检查
增量检查: 只检查变更的部分
延迟加载: 按需加载工具和依赖
智能排序: 快速检查在前,耗时检查在后
监控指标:
- Hook执行时间
- 成功率/失败率
- 资源使用情况
- 开发者满意度
6.4 工具生态系统
推荐工具栈:
- **管理框架**: pre-commit (Python)
- **代码格式化**: black, prettier
- **代码检查**: flake8, eslint, mypy
- **安全扫描**: bandit, npm audit, trivy
- **测试运行**: pytest, jest
- **CI/CD**: GitHub Actions, GitLab CI
- **监控**: Datadog, Prometheus
- **文档**: MkDocs, Docusaurus
通过系统化地实施Hooks自动化,团队可以:
-
提升代码质量和一致性
-
减少人为错误
-
加速开发反馈循环
-
建立可维护的自动化工作流
-
提升团队协作效率
记住:好的Hook应该是透明的帮助者,而不是烦人的障碍。目标是让开发者专注于创造价值,而不是被工具阻碍。
更多推荐
所有评论(0)