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 变更时自动触发
# .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 测评与发布等级检查
# .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: Sa ve 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 的两种方式:
# 方式一:直接检查 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 而非失败,需显式处理:
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 文件:
{"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 中加入趋势分析逻辑:
# 检查最近 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 天:
- 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 升级引起工具调用行为变化。
# 定时健康检查
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 测评:
python scripts/verify_deterministic.py --format json
手动触发深度自检时再开启 full 模式:
python scripts/verify_deterministic.py --full --format json
GitHub Actions 示例应使用当前 Node 运行时兼容的 action 版本:
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 测评,至少应先部署一个最小静态门禁:
PR 提交 -> 自动静态检查 -> 检查准入标准 -> PASS:允许进入后续流程 -> FAIL:阻断 + 通知 + 修复建议
最小准入标准可先设定如下:
| 评估维度 | 阈值 | 设置理由 |
|---|---|---|
| L1 description | >= 3/5 | 核心字段不可缺失 |
| L2 HiL 风险 | >= 3/5 | 必须包含 HiL 节点 |
| L3 复杂度 | <= 25 | 复杂度需在可控范围内 |
| 总分 | >= 15/25 | B 级以上才可进入后续流程 |
最小 GitHub Actions 结构:
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 失败时,通知内容不应仅写“失败”,而应提供:
Skill 名称 | 总分 | 失败维度 | 修复建议 | 报告链接
这个最小 Gate 不替代完整测评,也不替代人工发布判断。其核心价值在于将低质量 Skill 阻挡在 review 之前。
总结
将 SkillSentry 接入 CI 的核心工作归结为三件事:
- 触发:
paths: skills/*/SKILL.md→ SKILL.md 变更时自动执行 - 阻断:exit code 1/2 → CI 失败 → branch protection 阻止合并
- 积累:每次结果写入 trend JSONL → 质量趋势清晰可见
关键原则:测评不阻断发布 = 没有门禁。只有 FAIL 真正能阻止 PR 合并,质量保证才是实质性的,而非仅仅报表上的数字。
```