总:这篇讲什么
CI 集成分两层:
- 触发层:什么时候运行测评(SKILL.md 变更时)
- 门禁层:测评结果如何影响 PR 合并(FAIL 阻止合并)
只做触发层而不做门禁层,测评是装饰品。只有两层都到位,才真正建立质量门禁。
本篇给出:
- GitHub Actions 完整 workflow 配置
- exit code 设计与 CI 状态映射
- branch protection 配置
- 历史趋势数据的积累方式
一、为什么 CI 里可以用 --dangerously-skip-permissions
这是很多人看到这个 flag 时的第一反应:「这个名字太危险了,能用吗?」
先说结论:在 CI 环境里,这个 flag 是安全的,应该用;在本地不应该用。
原因在于权限模型的不同:
| 环境 | 人工审批的意义 | skip-permissions 的影响 |
|---|---|---|
| 本地(开发者机器) | 开发者可以在测评过程中选择允许或拒绝某些操作 | 跳过后,风险操作无人审核 |
| CI 容器 | 无人值守,任何权限弹窗都会卡死流程 | 在隔离容器中跳过是正常设计 |
CI 容器是一次性的隔离环境:
- 每次 workflow 运行都是全新容器
- 容器内没有持久状态
- 容器内的操作不影响真实生产环境
- 运行完即销毁
所以 --dangerously-skip-permissions 在 CI 里的含义是:「在这个一次性容器里,跳过权限确认,让测评全程无需人工干预」。危险的是「绕过生产系统的权限控制」,而不是「跳过 AI 工具调用确认」。
二、GitHub Actions 完整配置
基础配置:SKILL.md 变更时触发
yaml
# .github/workflows/skill-eval.yml
name: SkillSentry Evaluation
on:
pull_request:
paths:
- 'skills/*/SKILL.md' # 任意 Skill 的 SKILL.md 变更时触发
push:
branches: [main]
paths:
- 'skills/*/SKILL.md'
jobs:
skill-eval:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 2 # 需要对比前一个版本
- name: Detect changed skills
id: changed-skills
run: |
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'skills/*/SKILL.md' | \
sed 's|skills/||' | sed 's|/SKILL.md||' | tr '\n' ',')
echo "skills=${CHANGED}" >> $GITHUB_OUTPUT
echo "Changed skills: ${CHANGED}"
- name: Install Claude Code
run: |
npm install -g @anthropic-ai/claude-code
# 验证安装
claude --version
- name: Run SkillSentry evaluation
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# 对每个变更的 Skill 运行 smoke 测评
IFS=',' read -ra SKILLS <<< "${{ steps.changed-skills.outputs.skills }}"
OVERALL_EXIT=0
for SKILL in "${SKILLS[@]}"; do
if [ -z "$SKILL" ]; then continue; fi
echo "=== Evaluating: $SKILL ==="
claude --dangerously-skip-permissions \
-p "smoke 测评 ${SKILL} 自动" \
--output-format stream-json | tee "eval-${SKILL}.log"
EXIT_CODE=${PIPESTATUS[0]}
if [ $EXIT_CODE -ne 0 ]; then
echo "::error::Skill ${SKILL} evaluation FAILED (exit code: ${EXIT_CODE})"
OVERALL_EXIT=1
fi
done
exit $OVERALL_EXIT
- name: Upload evaluation reports
if: always()
uses: actions/upload-artifact@v4
with:
name: skill-eval-reports-${{ github.run_id }}
path: |
**/*.eval-report.html
**/sessions/**/*.html
retention-days: 30
完整门禁配置:standard 测评 + 发布等级检查
yaml
# .github/workflows/skill-eval-full.yml
name: SkillSentry Full Evaluation (Pre-release)
on:
push:
branches: [release/*]
paths:
- 'skills/*/SKILL.md'
jobs:
detect-skills:
runs-on: ubuntu-latest
outputs:
skills: ${{ steps.changed-skills.outputs.skills }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 2
- name: Detect changed skills
id: changed-skills
run: |
SKILLS=$(git diff --name-only HEAD~1 HEAD -- 'skills/*/SKILL.md' | \
sed 's|skills/||' | sed 's|/SKILL.md||' | jq -R -s -c 'split("\n")[:-1]')
echo "skills=${SKILLS}" >> "$GITHUB_OUTPUT"
echo "Changed skills: ${SKILLS}"
skill-eval-standard:
needs: detect-skills
if: needs.detect-skills.outputs.skills != '[]'
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
matrix:
skill: ${{ fromJson(needs.detect-skills.outputs.skills) }}
fail-fast: false # 一个 Skill FAIL 不影响其他 Skill 的测评继续
steps:
- uses: actions/checkout@v6
- name: Install Claude Code
run: |
npm install -g @anthropic-ai/claude-code
claude --version
- name: Run standard evaluation
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude --dangerously-skip-permissions \
-p "standard 测评 ${{ matrix.skill }} 自动" \
--output-format stream-json
- name: Check release grade
run: |
# 从报告提取发布等级
GRADE=$(find "sessions/${{ matrix.skill }}" -name "*.json" -print0 | \
xargs -0 -r grep -h -o '"release_grade":"[A-Z]*"' | \
tail -1 | sed 's/.*:"//;s/"$//' || true)
echo "Release grade: ${GRADE}"
echo "GRADE=${GRADE}" >> "$GITHUB_ENV"
case "$GRADE" in
"S"|"A"|"B"|"C")
echo "Grade ${GRADE}: PASS"
;;
"D"|"F"|"FAIL"|"")
echo "::error::Skill ${{ matrix.skill }} did not pass (grade: ${GRADE})"
exit 1
;;
*)
echo "::error::Unknown grade for ${{ matrix.skill }}: ${GRADE}"
exit 1
;;
esac
- name: Save trend data
if: always()
run: |
# 记录历史趋势数据到 gh-pages 或 artifact
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "{\"date\":\"${DATE}\",\"skill\":\"${{ matrix.skill }}\",\"grade\":\"${GRADE}\",\"run\":${{ github.run_number }}}" \
>> trend-data.jsonl
- uses: actions/upload-artifact@v4
if: always()
with:
name: eval-${{ matrix.skill }}-${{ github.run_id }}
path: |
sessions/${{ matrix.skill }}/**/*.html
trend-data.jsonl
retention-days: 90
三、exit code 设计
CI 系统通过 exit code 判断步骤是否成功。SkillSentry 的 exit code 规范:
| exit code | 含义 | CI 步骤状态 |
|---|---|---|
| 0 | 测评通过(S/A/B 级) | ✅ success |
| 1 | 测评 FAIL | ❌ failure |
| 2 | 环境错误(MCP 不可用、Skill 找不到) | ❌ failure |
| 3 | 测评结果不稳定(quick 两次差距 > 15%) | 默认是 failure;如果团队希望只标 warning,必须显式捕获后转成 ::warning:: |
关键设计:GitHub Actions 本身只认 0 / 非 0。exit code 1(测评 FAIL)和 exit code 2(环境错误)必须阻止 PR 合并;exit code 3 如果被定义为「不稳定但不阻断」,就不能直接把 3 作为步骤退出码返回,而要在脚本里捕获后输出 warning,再由团队策略决定是否放行。
--output-format stream-json 和 --output-format json 的用途不同:
| 格式 | 适合场景 | 用法 |
|---|---|---|
stream-json |
长时间运行的 CI 步骤 | 边执行边输出日志,适合配合 tee 保存执行过程 |
json |
后续脚本需要解析最终结果 | 输出完整 JSON 到文件,适合用 jq 读取判定字段 |
前面的 workflow 用 stream-json 是为了让 CI 页面实时显示执行过程;下面的示例用 json 是为了演示如何从结构化结果里解析 pass/fail。
等待 exit code 的方式:
bash
# 方式一:直接检查 claude 命令的退出码
claude --dangerously-skip-permissions -p "smoke 测评 my-skill 自动"
if [ $? -ne 0 ]; then
echo "Evaluation failed"
exit 1
fi
# 方式二:从输出的 JSON 中解析结果
claude --dangerously-skip-permissions \
-p "smoke 测评 my-skill 自动" \
--output-format json > result.json
PASS=$(jq -r '.result.pass' result.json)
if [ "$PASS" != "true" ]; then
echo "Evaluation failed: $(jq -r '.result.reason' result.json)"
exit 1
fi
如果要把「结果不稳定」标成 warning 而不是失败,需要显式处理:
bash
set +e
claude --dangerously-skip-permissions -p "quick 测评 my-skill 自动"
EXIT_CODE=$?
set -e
case "$EXIT_CODE" in
0)
echo "Evaluation passed"
;;
3)
echo "::warning::Evaluation is unstable; mark as conditional pass and require follow-up"
exit 0
;;
*)
echo "::error::Evaluation failed with exit code ${EXIT_CODE}"
exit "$EXIT_CODE"
;;
esac
四、Branch Protection 配置
在 GitHub 仓库设置中配置 branch protection rule,让 CI 状态决定 PR 可合并性:
- 进入仓库 Settings → Branches → Add branch protection rule
- Branch name pattern:
main(或你的主分支名) - 勾选 Require status checks to pass before merging
- 在 Status checks 搜索框中添加:
skill-eval / skill-eval - 勾选 Require branches to be up to date before merging
配置完成后效果:
- PR 中有 SKILL.md 变更 → 自动触发测评 → 测评 FAIL → PR 显示红色 × → 无法合并
- 测评通过 → PR 显示绿色 ✓ → 允许合并
五、历史趋势数据
单次测评只能告诉你「现在通不过」,历史趋势能告诉你「质量在下降还是在提升」。
数据收集方案
每次 CI 运行将测评结果写入 trend JSONL 文件:
jsonl
{"date":"2026-04-10T09:00:00Z","skill":"em-reimbursement-v3","grade":"A","pass_rate":0.92,"delta":0.67,"run":142}
{"date":"2026-04-11T14:30:00Z","skill":"em-reimbursement-v3","grade":"A","pass_rate":0.91,"delta":0.65,"run":145}
{"date":"2026-04-12T11:00:00Z","skill":"em-reimbursement-v3","grade":"B","pass_rate":0.83,"delta":0.52,"run":148}
趋势预警规则
在 CI 中加入趋势分析:
bash
# 检查最近 5 次的趋势
TREND=$(tail -5 trend-data.jsonl | jq -s '[.[].pass_rate] | (last - first)')
if (( $(echo "$TREND < -0.1" | bc -l) )); then
echo "::warning::Pass rate has dropped by more than 10% in last 5 runs"
fi
报告持久化
将 HTML 报告上传到 GitHub Actions artifacts,保留 90 天:
yaml
- uses: actions/upload-artifact@v4
with:
name: skill-report-${{ github.run_number }}
path: sessions/**/*.html
retention-days: 90
配合 GitHub Pages,可以将趋势数据可视化成折线图,在团队 wiki 中直接查看质量趋势。
六、常见 CI 问题与处理
| 问题 | 原因 | 处理 |
|---|---|---|
| CI 卡死不退出 | 测评等待用户确认(忘记加 自动) |
在 prompt 末尾加 自动 参数 |
| exit code 总是 0 即使测评 FAIL | claude 命令未正确传递测评结果 | 改用 --output-format json 解析结果字段 |
| MCP 工具在 CI 容器不可用 | CI 容器没有配置 MCP Server | 确认 MCP Server 可通过网络访问,或配置为 text_generation 降级 |
| Token 消耗超出预算 | standard/full 模式在 CI 中太贵 | PR 评审用 smoke,release 分支用 standard |
| 并发 Skill 测评相互干扰 | 会话目录冲突 | 确保每个 Skill 使用独立 session 目录 |
| API key 在 PR 中无法访问 | fork 的 PR 无法读取 secrets | 使用 pull_request_target 事件,或为 fork PR 单独配置 |
七、分级门禁策略
不是所有 SKILL.md 改动都需要同等严格的测评。推荐分级策略:
| 触发条件 | 测评模式 | 阻断 PR? | 典型场景 |
|---|---|---|---|
| 任意 PR + SKILL.md 变更 | smoke | ✅ FAIL 阻断 | 日常开发迭代 |
| release 分支 + SKILL.md 变更 | standard | ✅ FAIL 阻断 | 提测前验收 |
| main 分支合并 + S/A 级 Skill | full | ✅ FAIL 阻断 | 正式发布前 |
| 定时任务(每天凌晨) | quick | ❌ 不阻断 | 环境健康检查 |
定时健康检查可以发现「Skill 没改但环境变了导致测评失败」的情况,比如 MCP Server 升级导致工具调用行为变化。
yaml
# 定时健康检查
on:
schedule:
- cron: '0 2 * * *' # 每天凌晨 2 点(UTC)
jobs:
health-check:
# ... 对所有已注册 Skill 运行 quick 测评
# FAIL 不阻断,但通过 Slack/飞书通知
八、工程落地建议:CI 分成两条线
前面的 workflow 是门禁骨架。真正落地时,建议让 CI Runner 以结构化测评结果为准,读取 grading-summary.json / session.json / authoritative_pass_rate,再返回明确的 exit code:
推荐脚本分工:
sentry_ci.py:核心判决脚本,读取grading-summary.json/session.json/authoritative_pass_rate→ PASS/FAIL 判决 → exit codeupdate_history.py:追加 history.json 历史记录,支持趋势分析和回归检测report_to_checks.py:将结果推送为 GitHub Checks 注解,PR 页面直接可见
相比前面的 YAML 骨架,结构化 Runner 的改进:
| 本文描述 | 当前实现 |
|---|---|
| grep CLI 输出判 PASS/FAIL | 读 grading-summary.json / session.json 结构化数据,不依赖 CLI 输出格式 |
| 硬编码阈值 | --threshold 参数化(默认 0.80) |
| 无历史对比 | history.json + Δ < 0 自动 FAIL |
| 单 Skill | matrix 自动检测变更的 SKILL.md 并行测评 |
| 无 GitHub Checks 集成 | report_to_checks.py 推送注解 |
CI 建议分成两条线,不要混成一条 workflow:
- 业务 Skill 质量门禁 :
skill-eval.yml,用于评估被测 Skill 的 smoke / standard / full 结果,核心仍是让 FAIL 阻断合并。 - SkillSentry 本体自检 :
skillsentry-self-test.yml,用于验证 SkillSentry 自身脚本、契约、模板和工作流没有被改坏。
本体自检的推荐入口是确定性脚本,而不是再启动一次真实 LLM 测评:
bash
python scripts/verify_deterministic.py --format json
手动触发深度自检时再打开 full:
bash
python scripts/verify_deterministic.py --full --format json
GitHub Actions 示例应使用当前 Node 运行时兼容的 action 版本:
yaml
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
这类工程落地常见坑是:workflow 模板已经升级到新 major,但仓库里的实际 workflow 还停在旧 major,容易触发运行时退役提醒或形成模板与线上不一致。后续 workflow 模板和仓库内实际 workflow 要保持一致,并用 verify_workflow_action_versions.py 扫描旧 major,避免模板修了、线上 workflow 没修。
另一个容易忽略的点是 GitHub 权限。推送 .github/workflows/*.yml 不只需要普通 repo 权限,还需要 token 带 workflow scope;否则代码改好了也会被远端拒绝。
九、实战补充:最小 CI Gate
如果团队暂时跑不起 full 测评,至少应该先做一个最小静态门禁:
text
PR 提交
-> 自动静态检查
-> 检查准入标准
-> PASS:允许进入后续流程
-> FAIL:阻断 + 通知 + 修复建议
最小准入标准可以先这样设:
| 维度 | 阈值 | 理由 |
|---|---|---|
| L1 description | >= 3/5 | 不能缺核心字段 |
| L2 HiL 风险 | >= 3/5 | 不能没有 HiL 节点 |
| L3 复杂度 | <= 25 | 复杂度不能失控 |
| 总分 | >= 15/25 | B 级以上才进入后续流程 |
最小 GitHub Actions 结构:
yaml
name: Skill Static Gate
on:
pull_request:
paths:
- "skills/*/SKILL.md"
jobs:
static-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Run static check
run: |
for skill_dir in skills/*/; do
skill_name=$(basename "$skill_dir")
python tools/sentry-static.py "$skill_dir/SKILL.md" \
--format json > "static-${skill_name}.json"
done
CI 失败时,通知里不要只写"失败",要给出:
text
Skill 名称
总分
失败维度
修复建议
报告链接
这个最小 Gate 不替代完整测评,也不替代人工发布判断。它的价值是把低质量 Skill 挡在 review 之前。
总结
把 SkillSentry 接入 CI 的三件事:
- 触发 :
paths: skills/*/SKILL.md→ SKILL.md 变更时自动运行 - 阻断:exit code 1/2 → CI 失败 → branch protection 阻止合并
- 积累:每次结果写 trend JSONL → 质量趋势可见
关键原则:测评不阻断发布 = 没有门禁。只有 FAIL 真的能阻止 PR 合并,质量保证才是真实的,而不是报表上的数字。
工具在 GitHub:github.com/xingzaidadi...