目标:将 Claude Code 集成到 CI/CD 流水线,实现自动化代码审查和测试
预计时间:30 分钟
对应官方文档:GitHub Actions、GitLab CI/CD
CI/CD 流水线架构
CI/CD 中的 Claude Code
使用场景
| 场景 | 说明 |
|---|---|
| 自动代码审查 | PR 创建时自动审查代码 |
| 测试生成 | 为新增代码自动生成测试 |
| 文档更新 | API 变更时自动更新文档 |
| 安全扫描 | 检查潜在的安全漏洞 |
| 依赖更新 | 自动审查依赖升级影响 |
GitHub Actions 集成
基础配置
# .github/workflows/claude-review.yaml
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Claude Code
uses: anthropic/claude-code-action@v1
with:
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Run Code Review
run: |
claude review-pr \
--pr ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--output review.md
- name: Post Review
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const review = fs.readFileSync('review.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: review
});高级:多维度审查
# .github/workflows/claude-advanced-review.yaml
name: Advanced Claude Review
on:
pull_request:
paths:
- 'src/**'
- 'tests/**'
jobs:
security-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropic/claude-code-action@v1
with:
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Security Review
run: |
claude review \
--focus security \
--files ${{ steps.changed-files.outputs.all }} \
--output security-report.md
- name: Upload Security Report
uses: actions/upload-artifact@v4
with:
name: security-report
path: security-report.md
performance-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropic/claude-code-action@v1
- name: Performance Review
run: |
claude review \
--focus performance \
--output performance-report.md
test-generation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropic/claude-code-action@v1
- name: Generate Tests
run: |
claude generate-tests \
--for-files ${{ steps.changed-files.outputs.added }} \
--output new-tests/
- name: Commit Tests
run: |
git config user.name "Claude CI"
git config user.email "[email protected]"
git add new-tests/
git commit -m "test: auto-generated tests for PR #${{ github.event.pull_request.number }}"
git pushGitLab CI/CD 集成
基础配置
# .gitlab-ci.yml
stages:
- review
- test
variables:
ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY
claude-review:
stage: review
image: anthropic/claude-code:latest
script:
- claude review-mr
--mr-id $CI_MERGE_REQUEST_IID
--project $CI_PROJECT_PATH
--output claude-review.md
artifacts:
reports:
codequality: claude-review.md
expire_in: 1 week
only:
- merge_requests
claude-test-gen:
stage: test
image: anthropic/claude-code:latest
script:
- claude generate-tests
--diff-from $CI_MERGE_REQUEST_DIFF_BASE_SHA
--output generated-tests/
- pytest generated-tests/ -v
artifacts:
when: always
reports:
junit: generated-tests/report.xml自动化测试生成
配置
# .github/workflows/auto-test.yaml
name: Auto Generate Tests
on:
push:
branches: [main]
jobs:
generate-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Detect New Functions
id: detect
run: |
# 找出新增的无测试函数
python scripts/find_untested.py > untested.json
- name: Generate Tests with Claude
run: |
claude generate-tests \
--functions $(cat untested.json | jq -r '.[].name') \
--style pytest \
--output tests/auto/
- name: Run Generated Tests
run: pytest tests/auto/ -v --tb=short
- name: Create PR with Tests
uses: peter-evans/create-pull-request@v6
with:
title: "test: auto-generated tests"
body: "Generated by Claude Code CI"
branch: auto-tests完整的 GitHub Actions 生产配置
# .github/workflows/claude-prod-review.yaml
name: Claude Code Production Review
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'src/**'
- 'lib/**'
- 'app/**'
jobs:
# 阶段 1:并行分析
analyze:
runs-on: ubuntu-latest
outputs:
files: ${{ steps.changes.outputs.files }}
count: ${{ steps.changes.outputs.count }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect Changes
id: changes
run: |
files=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(py|js|ts|go|rs)$' | tr '\n' ' ')
echo "files=$files" >> $GITHUB_OUTPUT
echo "count=$(echo $files | wc -w)" >> $GITHUB_OUTPUT
# 阶段 2:安全审查(最高优先级)
security-review:
needs: analyze
if: needs.analyze.outputs.count > 0
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Security Review with Claude
uses: anthropic/claude-code-action@v1
with:
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
model: claude-opus-4-7
prompt: |
审查以下文件的安全性:
${{ needs.analyze.outputs.files }}
重点关注:
1. SQL 注入、XSS、CSRF
2. 敏感信息硬编码
3. 不安全的反序列化
4. 权限绕过
5. 依赖漏洞
输出格式:
- [CRITICAL/HIGH/MEDIUM/LOW] 问题描述
- 具体位置(文件:行号)
- 修复建议(含代码示例)
- CVSS 评分(如适用)
- name: Post Security Report
uses: actions/github-script@v7
with:
script: |
const report = require('fs').readFileSync('security-report.md', 'utf8');
const critical = (report.match(/CRITICAL/g) || []).length;
const high = (report.match(/HIGH/g) || []).length;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## 🔒 Claude Code 安全审查报告\n\n${report}\n\n**统计**: CRITICAL ${critical} | HIGH ${high}`
});
// 如果有 CRITICAL,标记检查失败
if (critical > 0) {
core.setFailed('发现 CRITICAL 安全问题!');
}
# 阶段 3:性能审查
performance-review:
needs: [analyze, security-review]
if: needs.analyze.outputs.count > 0
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Performance Review
uses: anthropic/claude-code-action@v1
with:
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
model: claude-sonnet-4-6
prompt: |
审查代码性能问题:
${{ needs.analyze.outputs.files }}
关注:
1. 时间复杂度(是否有 O(n²) 可以优化)
2. 数据库查询(N+1 问题)
3. 内存泄漏
4. 不必要的 I/O
5. 缓存策略
# 阶段 4:自动测试生成
auto-test:
needs: [analyze, security-review]
if: needs.analyze.outputs.count > 0
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate Tests
uses: anthropic/claude-code-action@v1
with:
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
model: claude-sonnet-4-6
prompt: |
为本次 PR 新增/修改的代码生成测试:
${{ needs.analyze.outputs.files }}
要求:
1. 覆盖所有新增功能
2. 包含边界条件测试
3. 包含异常处理测试
4. 遵循项目现有测试风格
- name: Commit Tests
run: |
git config user.name "Claude CI"
git config user.email "[email protected]"
git add tests/
git diff --cached --quiet || git commit -m "test: auto-generated by Claude Code"
git push
# 阶段 5:汇总报告
summary:
needs: [security-review, performance-review, auto-test]
runs-on: ubuntu-latest
if: always()
steps:
- name: Generate Summary
run: |
echo "## Claude Code CI 审查总结" >> $GITHUB_STEP_SUMMARY
echo "| 检查项 | 状态 |" >> $GITHUB_STEP_SUMMARY
echo "|--------|------|" >> $GITHUB_STEP_SUMMARY
echo "| 安全审查 | ${{ needs.security-review.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| 性能审查 | ${{ needs.performance-review.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| 测试生成 | ${{ needs.auto-test.result }} |" >> $GITHUB_STEP_SUMMARY最佳实践
| ✅ 推荐 | ❌ 避免 |
|---|---|
| 限制 AI 的权限(只读审查) | 给 CI 完全写入权限 |
| 人工最终确认重要修改 | 自动合并 AI 的更改 |
| 并行运行多个审查维度 | 串行执行浪费时间 |
| 保存审查报告备查 | 审查结果不保存 |
| 设置超时防止挂起 | 无限制运行 |
流水线触发与决策路径
上面给的工作流是「PR 一进来全跑」,但生产环境往往要按改动范围裁剪,否则成本和耗时都顶不住。下面这张图把触发条件、跳过逻辑、并行/串行关系画清楚:
关键设计:
- 用
dorny/paths-filter把每个 job 的触发条件拆开,避免改个 README 也跑全套; - 安全/性能/风格三个 review 必须并行;
auto-test只在所有 review 通过后跑,避免给坏代码生成测试;- 自动合并必须由仓库管理员手动加标签触发,AI 没有越过 branch protection 的权限。
GitLab CI 完整模板(带审计落库)
下面这份模板把审查结果同时写进 GitLab Merge Request Note 和企业内的 PostgreSQL 审计库:
# .gitlab-ci.yml
stages:
- prepare
- review
- publish
variables:
CLAUDE_VERSION: "latest"
CLAUDE_MAX_TURNS: "15"
.claude-base:
image: anthropic/claude-code:${CLAUDE_VERSION}
before_script:
- mkdir -p .claude-runtime
- cp $CLAUDE_POLICY .claude-runtime/settings.json
variables:
ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY_REVIEW
CLAUDE_CONFIG_DIR: .claude-runtime
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
artifacts:
when: always
expire_in: 30 days
paths:
- reports/
prepare:diff:
stage: prepare
script:
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME --depth=50
- git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD --name-only > reports/changed-files.txt
- git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD > reports/diff.patch
artifacts:
paths: [reports/]
review:security:
extends: .claude-base
stage: review
needs: [prepare:diff]
script:
- |
claude --print --max-turns $CLAUDE_MAX_TURNS \
--allowed-tools "Read,Grep,Glob" \
--output-format json \
"$(cat ci/prompts/security-review.md)" \
> reports/security.json
- python3 ci/scripts/grade.py reports/security.json security
review:performance:
extends: .claude-base
stage: review
needs: [prepare:diff]
script:
- |
claude --print --max-turns $CLAUDE_MAX_TURNS \
--allowed-tools "Read,Grep,Glob" \
--output-format json \
"$(cat ci/prompts/performance-review.md)" \
> reports/performance.json
- python3 ci/scripts/grade.py reports/performance.json performance
publish:mr-comment:
stage: publish
image: alpine:3.20
needs: [review:security, review:performance]
before_script:
- apk add --no-cache curl jq postgresql-client python3 py3-pip
- pip install --quiet psycopg[binary]
script:
- python3 ci/scripts/publish_review.py
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: always# ci/scripts/publish_review.py
"""汇总 Claude 审查结果,写回 GitLab MR 评论 + 落库审计。"""
import json
import os
import pathlib
import urllib.request
import psycopg
REPORTS = pathlib.Path("reports")
MR_IID = os.environ["CI_MERGE_REQUEST_IID"]
PROJECT = os.environ["CI_PROJECT_ID"]
GL_TOKEN = os.environ["GITLAB_BOT_TOKEN"]
GL_API = os.environ.get("CI_API_V4_URL", "https://gitlab.com/api/v4")
DB_DSN = os.environ["AUDIT_DB_DSN"]
def load(name: str) -> dict:
path = REPORTS / f"{name}.json"
if not path.exists():
return {"summary": f"⚠️ {name} 报告缺失", "findings": []}
return json.loads(path.read_text())
def render(parts: dict[str, dict]) -> str:
lines = ["## 🤖 Claude Code 自动审查\n"]
for name, data in parts.items():
lines.append(f"### {name}")
lines.append(data.get("summary", ""))
for f in data.get("findings", []):
sev = f.get("severity", "info").upper()
file = f.get("file", "-")
line = f.get("line", "-")
msg = f.get("message", "")
lines.append(f"- **[{sev}]** `{file}:{line}` — {msg}")
lines.append("")
return "\n".join(lines)
def post_comment(body: str) -> None:
url = f"{GL_API}/projects/{PROJECT}/merge_requests/{MR_IID}/notes"
req = urllib.request.Request(
url,
method="POST",
headers={"PRIVATE-TOKEN": GL_TOKEN, "Content-Type": "application/json"},
data=json.dumps({"body": body}).encode(),
)
with urllib.request.urlopen(req, timeout=20) as resp:
resp.read()
def audit(parts: dict[str, dict]) -> None:
with psycopg.connect(DB_DSN, autocommit=True) as conn, conn.cursor() as cur:
cur.execute(
"""INSERT INTO ai_review_audit (project_id, mr_iid, sha, payload, ts)
VALUES (%s, %s, %s, %s, now())""",
(PROJECT, MR_IID, os.environ.get("CI_COMMIT_SHA", ""),
json.dumps(parts, ensure_ascii=False)),
)
def main() -> int:
parts = {"安全审查": load("security"), "性能审查": load("performance")}
body = render(parts)
post_comment(body)
audit(parts)
high = sum(1 for p in parts.values()
for f in p.get("findings", [])
if f.get("severity") in {"high", "critical"})
return 1 if high else 0
if __name__ == "__main__":
raise SystemExit(main())配套的成绩判定脚本:
# ci/scripts/grade.py
import json, sys
THRESHOLDS = {
"security": {"critical": 0, "high": 0, "medium": 5},
"performance": {"critical": 0, "high": 2, "medium": 10},
}
report_path, kind = sys.argv[1], sys.argv[2]
data = json.loads(open(report_path).read())
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
for f in data.get("findings", []):
counts[f.get("severity", "low")] = counts.get(f.get("severity", "low"), 0) + 1
limits = THRESHOLDS[kind]
failed = [k for k, v in limits.items() if counts.get(k, 0) > v]
print(json.dumps({"counts": counts, "failed": failed}, ensure_ascii=False))
sys.exit(1 if failed else 0)企业级实战场景
场景一:Monorepo 增量审查(节省 80% Token)
背景:8 万文件、30 个子项目的 monorepo,PR 平均改 12 个文件,跨 3 个子项目;如果每次都把整个仓库喂给模型,单次审查就要 60k+ token。我们用「文件级 CODEOWNERS + AI 路由」让每个 PR 只走需要的子项目。
# .github/workflows/monorepo-claude-review.yml
name: Monorepo Claude Review
on:
pull_request:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
modules: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- id: filter
uses: dorny/paths-filter@v3
with:
list-files: json
filters: |
payments: services/payments/**
risk: services/risk/**
ledger: services/ledger/**
web: apps/web/**
shared: packages/**
review-module:
needs: detect-changes
if: needs.detect-changes.outputs.modules != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
module: ${{ fromJSON(needs.detect-changes.outputs.modules) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Build module context
run: |
python3 ci/scripts/build_module_context.py \
--module "${{ matrix.module }}" \
--diff-file <(git diff origin/main...HEAD -- "services/${{ matrix.module }}") \
--out reports/${{ matrix.module }}-context.md
- name: Claude review (module-scoped)
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_REVIEW }}
run: |
claude --print --max-turns 8 \
--allowed-tools "Read,Grep,Glob" \
--add-dir "services/${{ matrix.module }}" \
--add-dir "packages" \
--output-format json \
"$(cat reports/${{ matrix.module }}-context.md)" \
> reports/${{ matrix.module }}-review.json
- uses: actions/upload-artifact@v4
with:
name: review-${{ matrix.module }}
path: reports/${{ matrix.module }}-review.json# ci/scripts/build_module_context.py
"""为单个模块拼接评审上下文,只送相关文件给模型。"""
import argparse
import pathlib
import subprocess
TEMPLATE = """\
# 评审任务:{module}
## 模块职责
{owner_doc}
## 本次改动 diff
```diff
{diff}相关测试
{tests}
评审要求
- 只评审本模块代码,不要扩展到其他模块。
- 输出 JSON:{{ "summary": str, "findings": [{{ "severity": "critical|high|medium|low", "file": str, "line": int, "message": str }}] }}
- severity 判定标准见 ci/prompts/severity.md。 """
def read(p: pathlib.Path) -> str: return p.read_text() if p.exists() else "(空)"
def collect_tests(module: str) -> str: p = pathlib.Path(f"services/{module}/tests") if not p.exists(): return "无测试目录" files = [str(f.relative_to(".")) for f in p.rglob("test_*.py")][:20] return "\n".join(f"- {f}" for f in files)
def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--module", required=True) ap.add_argument("--diff-file", required=True) ap.add_argument("--out", required=True) args = ap.parse_args()
diff = pathlib.Path(args.diff_file).read_text() if len(diff) > 60_000: diff = diff[:60_000] + "\n... [truncated, see full diff in artifact]"
owner_doc = read(pathlib.Path(f"services/{args.module}/OWNERS.md")) tests = collect_tests(args.module)
pathlib.Path(args.out).write_text(TEMPLATE.format( module=args.module, diff=diff, owner_doc=owner_doc, tests=tests))
if name == "main": main()
落地效果:
- 矩阵 job 自动按改动模块并行拉起,没改动的子项目 0 token 消耗;
- 上下文只塞「diff + OWNERS.md + 相关测试列表」,单 PR token 从 60k 降到 8–12k;
- 各模块的 review JSON 单独入库,可以按模块统计 false-positive 率,逐月调优 prompt。
---
### 场景二:合规线「评审 + 蓝绿发布」全自动闭环
**背景**:金融科技团队要求每次发版必须有 AI 审查 + SAST + 灰度回归 + 人工放行四个 gate,且产物必须包含可追溯的 SBOM 和审计签名。
```mermaid
flowchart LR
A[Tag 推送 v*] --> B[build & sign<br/>cosign + SBOM]
B --> C[claude:release-review]
B --> D[SAST: semgrep]
B --> E[license-scan]
C & D & E --> F{全部 pass?}
F -->|否| G[gate-fail<br/>通知 release manager]
F -->|是| H[deploy-staging]
H --> I[smoke-test + 合成监控]
I --> J{SLO 守住?}
J -->|否| K[auto-rollback]
J -->|是| L[等待 release manager 批准]
L --> M[deploy-prod 蓝绿切流]
M --> N[审计归档<br/>S3 + WORM]
# .github/workflows/release-pipeline.yml
name: Release Pipeline
on:
push:
tags: ["v*.*.*"]
permissions:
contents: read
id-token: write # cosign keyless
packages: write
jobs:
build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: push
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
provenance: true
sbom: true
- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.push.outputs.digest }}
ai-release-review:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Diff since last release
run: |
PREV=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$PREV" ]; then
git log --oneline "$PREV"..HEAD > reports/changes.txt
git diff "$PREV"..HEAD > reports/release.patch
else
echo "(initial release)" > reports/changes.txt
git diff $(git rev-list --max-parents=0 HEAD)..HEAD > reports/release.patch
fi
- name: Claude release review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_REVIEW }}
run: |
claude --print --max-turns 12 \
--allowed-tools "Read,Grep,Glob" \
--output-format json \
"$(cat ci/prompts/release-review.md)" \
> reports/release-review.json
- name: Gate
run: python3 ci/scripts/grade.py reports/release-review.json security
- uses: actions/upload-artifact@v4
with:
name: release-review
path: reports/
sast:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: p/ci
generateSarif: "1"
deploy-staging:
needs: [ai-release-review, sast]
runs-on: ubuntu-latest
environment: staging
steps:
- run: ./deploy/staging.sh ${{ github.ref_name }}
- run: ./deploy/smoke-tests.sh staging
deploy-prod:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
- name: Blue/green switch
run: ./deploy/blue-green.sh ${{ github.ref_name }}
- name: Audit archive
run: |
aws s3 cp reports/ s3://compliance-audit/${{ github.ref_name }}/ \
--recursive --sse aws:kms --acl bucket-owner-full-controlci/prompts/release-review.md 关键段:
你是发布安全评审员。基于 reports/release.patch 与 reports/changes.txt:
1. 标记任何涉及 ① 鉴权/会话 ② 加密 ③ 数据导出 ④ 第三方依赖 ⑤ 数据库 schema 的变更,标 severity=high 起。
2. 对每条 finding 给出受影响场景、可观测信号、建议回滚开关。
3. 输出严格 JSON,schema 见 ci/prompts/severity.md。
4. 不要给出无法在 diff 中验证的"建议改进"——必须基于实际改动。落地要点:
- AI review、SAST、license-scan 三个 gate 任意一个失败都会阻断 staging 部署;
- staging 通过且 release manager 在 GitHub Environment 批准后才进生产,蓝绿切流配合自动回滚;
- 所有 review 报告、SBOM、cosign 签名一并归档到带 WORM 的 S3 桶,满足审计追溯。