From 66c5bd049a97d58d8be6ecc2da5122c44265c225 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 2 Apr 2026 10:22:44 +0800 Subject: [PATCH 01/31] feat(skills): add claude-bot and enhance github-action-diagnose --- .github/scripts/setup-claude-environment.py | 312 ++++++++++++ .github/workflows/README.md | 128 +++++ .github/workflows/_claude-code.yml | 336 +++++++++++++ .github/workflows/embeddable-caller.yml | 24 + .github/workflows/example-caller.yml | 47 ++ README.md | 6 +- skills/infrastructure/README.md | 84 ++-- skills/infrastructure/claude-bot/claude.md | 120 +++++ .../github-action-diagnose/SKILL.md | 453 +++++++++++++++--- .../github-action-diagnose/evals/evals.json | 50 +- .../references/ascend-troubleshooting.md | 1 - .../references/classification-guide.md | 76 +++ .../references/common-patterns.md | 271 +++++++++++ .../references/vllm-ascend.md | 152 ++++++ .../scripts/fetch-run.sh | 211 ++++++++ 15 files changed, 2116 insertions(+), 155 deletions(-) create mode 100644 .github/scripts/setup-claude-environment.py create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/_claude-code.yml create mode 100644 .github/workflows/embeddable-caller.yml create mode 100644 .github/workflows/example-caller.yml create mode 100644 skills/infrastructure/claude-bot/claude.md create mode 100644 skills/infrastructure/github-action-diagnose/references/classification-guide.md create mode 100644 skills/infrastructure/github-action-diagnose/references/common-patterns.md create mode 100644 skills/infrastructure/github-action-diagnose/references/vllm-ascend.md create mode 100644 skills/infrastructure/github-action-diagnose/scripts/fetch-run.sh diff --git a/.github/scripts/setup-claude-environment.py b/.github/scripts/setup-claude-environment.py new file mode 100644 index 0000000..cd09f3d --- /dev/null +++ b/.github/scripts/setup-claude-environment.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +""" +setup-claude-environment.py +============================ +为指定仓库创建/验证 GitHub Environment(默认名称 claude-bot), +并配置分支保护规则。 + +用法: + uv run https://raw.githubusercontent.com/opensourceways/agent-skills/main/.github/scripts/setup-claude-environment.py [--force] [--token TOKEN] + +示例: + uv run .github/scripts/setup-claude-environment.py vllm-project/vllm-ascend + uv run .github/scripts/setup-claude-environment.py vllm-project/vllm-ascend --force + uv run .github/scripts/setup-claude-environment.py vllm-project/vllm-ascend --token ghp_xxx + +依赖: + requests(标准场景下通过 pip/uv 安装) + +# /// script +# requires-python = ">=3.9" +# dependencies = ["requests"] +# /// +""" + +from __future__ import annotations + +import argparse +import os +import sys +from typing import Any + +import requests + +# ------------------------------------------------------------------ +# 期望的环境配置(可在此处修改默认值) +# ------------------------------------------------------------------ +DESIRED_CONFIG: dict[str, Any] = { + "environment_name": "claude-bot", + # 受保护分支列表(环境部署规则) + "protected_branches": ["main", "refs/pull/*/merge"], + # 等待计时器(分钟),0 表示不等待 + "wait_timer": 0, + # 是否要求审阅者批准 + "reviewers": [], + # 是否限制部署分支 + "deployment_branch_policy": { + "protected_branches": True, + "custom_branch_policies": False, + }, +} + +GITHUB_API = "https://api.github.com" + + +def get_token(token_arg: str | None) -> str: + """从参数或环境变量获取 GitHub Token。""" + token = token_arg or os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") + if not token: + print( + "ERROR: GitHub token not found.\n" + "Provide via --token, GH_TOKEN, or GITHUB_TOKEN environment variable.", + file=sys.stderr, + ) + sys.exit(1) + return token + + +def github_request( + method: str, + path: str, + token: str, + json: Any = None, + expected_statuses: tuple[int, ...] = (200, 201, 204), +) -> tuple[int, Any]: + """执行 GitHub API 请求,返回 (status_code, response_body)。""" + url = f"{GITHUB_API}{path}" + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + resp = requests.request(method, url, headers=headers, json=json, timeout=30) + if resp.status_code not in expected_statuses: + print( + f" [WARN] {method} {url} -> HTTP {resp.status_code}: {resp.text[:200]}", + file=sys.stderr, + ) + body = None + if resp.content: + try: + body = resp.json() + except Exception: + body = resp.text + return resp.status_code, body + + +def check_admin_permission(repo: str, token: str) -> None: + """检查当前 token 对应用户是否拥有仓库 admin 权限。""" + # 获取当前用户 + status, user = github_request("GET", "/user", token) + if status != 200 or not isinstance(user, dict): + print("ERROR: Failed to retrieve authenticated user.", file=sys.stderr) + sys.exit(1) + username = user["login"] + + # 检查权限 + status, perm = github_request( + "GET", + f"/repos/{repo}/collaborators/{username}/permission", + token, + expected_statuses=(200, 403, 404), + ) + if status != 200 or not isinstance(perm, dict): + print( + f"ERROR: Cannot check permission for {username} on {repo} (HTTP {status}).\n" + "Ensure your token has 'repo' scope and you are a collaborator.", + file=sys.stderr, + ) + sys.exit(1) + + permission = perm.get("permission", "none") + if permission != "admin": + print( + f"ERROR: You ({username}) have '{permission}' permission on {repo}.\n" + "Administrator permission is required to manage environments.", + file=sys.stderr, + ) + sys.exit(1) + + print(f" ✓ Authenticated as {username} with admin permission on {repo}.") + + +def get_environment(repo: str, env_name: str, token: str) -> dict | None: + """获取已有 Environment 配置,不存在则返回 None。""" + status, body = github_request( + "GET", + f"/repos/{repo}/environments/{env_name}", + token, + expected_statuses=(200, 404), + ) + if status == 200 and isinstance(body, dict): + return body + return None + + +def create_or_update_environment( + repo: str, + env_name: str, + token: str, + config: dict[str, Any], +) -> dict: + """ + 创建或更新 GitHub Environment。 + API 文档:https://docs.github.com/en/rest/deployments/environments + """ + payload: dict[str, Any] = { + "wait_timer": config.get("wait_timer", 0), + "reviewers": config.get("reviewers", []), + "deployment_branch_policy": config.get("deployment_branch_policy"), + } + + status, body = github_request( + "PUT", + f"/repos/{repo}/environments/{env_name}", + token, + json=payload, + expected_statuses=(200, 201), + ) + if status not in (200, 201) or not isinstance(body, dict): + print( + f"ERROR: Failed to create/update environment '{env_name}' (HTTP {status}).", + file=sys.stderr, + ) + sys.exit(1) + return body + + +def diff_config(existing: dict, desired: dict[str, Any]) -> list[str]: + """对比现有配置与期望配置,返回差异描述列表。""" + diffs: list[str] = [] + + # 等待计时器 + existing_wait = existing.get("wait_timer", 0) + desired_wait = desired.get("wait_timer", 0) + if existing_wait != desired_wait: + diffs.append(f" wait_timer: {existing_wait} -> {desired_wait}") + + # 部署分支策略 + existing_policy = existing.get("deployment_branch_policy") or {} + desired_policy = desired.get("deployment_branch_policy") or {} + for key in ("protected_branches", "custom_branch_policies"): + ev = existing_policy.get(key) + dv = desired_policy.get(key) + if ev != dv: + diffs.append(f" deployment_branch_policy.{key}: {ev} -> {dv}") + + return diffs + + +def print_environment_summary(env: dict) -> None: + """打印 Environment 摘要。""" + name = env.get("name", "?") + url = env.get("html_url", "") + policy = env.get("deployment_branch_policy") or {} + print(f" Name : {name}") + print(f" URL : {url}") + print(f" Protected branches: {policy.get('protected_branches', False)}") + print(f" Custom policies : {policy.get('custom_branch_policies', False)}") + print(f" Wait timer (min) : {env.get('wait_timer', 0)}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description=( + "Create or verify GitHub Environment 'claude-bot' for a repository.\n\n" + "This script is designed to be used with the vllm-project Claude Bot integration." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "repository", + help="Target repository in 'owner/repo' format (e.g. vllm-project/vllm-ascend)", + ) + parser.add_argument( + "--token", + default=None, + help="GitHub Personal Access Token (overrides GH_TOKEN / GITHUB_TOKEN env var)", + ) + parser.add_argument( + "--env-name", + default=DESIRED_CONFIG["environment_name"], + help=f"Environment name to create/verify (default: {DESIRED_CONFIG['environment_name']})", + ) + parser.add_argument( + "--force", + action="store_true", + help="Force overwrite existing environment configuration with desired state", + ) + args = parser.parse_args() + + repo = args.repository + env_name = args.env_name + token = get_token(args.token) + + # 覆盖期望配置中的环境名称 + desired = dict(DESIRED_CONFIG) + desired["environment_name"] = env_name + + print(f"\n{'=' * 60}") + print(f" Claude Bot Environment Setup") + print(f" Repository : {repo}") + print(f" Environment: {env_name}") + print(f"{'=' * 60}\n") + + # 1. 检查管理员权限 + print("[1/4] Checking admin permission...") + check_admin_permission(repo, token) + + # 2. 检查 Environment 是否已存在 + print(f"\n[2/4] Checking environment '{env_name}'...") + existing = get_environment(repo, env_name, token) + + if existing: + print(f" Environment '{env_name}' already exists:") + print_environment_summary(existing) + + # 3. 对比差异 + print(f"\n[3/4] Comparing with desired configuration...") + diffs = diff_config(existing, desired) + + if not diffs: + print( + " ✓ Environment configuration matches desired state. No changes needed." + ) + if not args.force: + print("\nDone.") + return + else: + print(" Differences found:") + for d in diffs: + print(d) + if not args.force: + print( + "\n To apply these changes, re-run with --force flag:\n" + f" uv run .github/scripts/setup-claude-environment.py {repo} --force" + ) + sys.exit(0) + else: + print(f" Environment '{env_name}' does not exist. It will be created.") + print("\n[3/4] Skipping diff (environment not found).") + + # 4. 创建或强制更新 + action = "Updating" if existing else "Creating" + print(f"\n[4/4] {action} environment '{env_name}'...") + result = create_or_update_environment(repo, env_name, token, desired) + print(f" ✓ Environment '{env_name}' has been {action.lower()}d:") + print_environment_summary(result) + + print(f"\n{'=' * 60}") + print(" Setup complete!") + print(f" View at: https://github.com/{repo}/settings/environments") + print(f"{'=' * 60}\n") + print( + "Next steps:\n" + f" 1. Copy .github/workflows/example-caller.yml to {repo}/.github/workflows/\n" + " 2. Set required secrets (CLAUDE_API_KEY, USAGE_PAT)\n" + " 3. Commit and push to trigger on the next @claude mention\n" + ) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..4663c78 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,128 @@ +# Claude Code 工作流集成指南 + +本节介绍如何在任意仓库中集成 Claude Code 工作流,支持通过 `@claude` 触发或在其他工作流中直接调用。 + +## 概述 + +可复用工作流(`_claude-code.yml`)提供: +- Claude Code CLI 调用(可读取 skill 和修改代码) +- 白名单检查(组织和仓库) +- 安全检查(组织成员 + 仓库写权限) +- 自动提交代码变更(可选) + +## 快速开始 + +### 方式一:在其他工作流中嵌入调用(推荐) + +将以下代码嵌入到你的工作流中: + +```yaml +jobs: + claude-code: + uses: opensourceways/agent-skills/.github/workflows/_claude-code.yml@main + secrets: inherit + with: + allowed_orgs: 'opensourceways' + model: 'claude-sonnet-4-20250514' +``` + +完整参数说明见 [embeddable-caller.yml](./embeddable-caller.yml)。 + +### 方式二:复制示例工作流 + +将 [example-caller.yml](./example-caller.yml) 复制到目标仓库: + +```bash +mkdir -p .github/workflows +curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.github/workflows/example-caller.yml \ + -o .github/workflows/claude-bot.yml +git add .github/workflows/claude-bot.yml +git commit -m "ci: add Claude Code workflow" +git push +``` + +## 触发方式 + +| 触发方式 | 说明 | +|---------|------| +| `@claude` 提及 | 在 Issue/PR 评论中包含 `@claude` 时触发 | +| 新建 Issue | 创建新 Issue 时自动触发(无需 @claude) | +| `workflow_call` | 在其他工作流中调用时触发 | + +## 参数说明 + +| 参数 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `allowed_orgs` | 否 | opensourceways | 白名单组织(逗号分隔) | +| `allowed_repos` | 否 | - | 白名单仓库(逗号分隔,如 org/repo1,org/repo2) | +| `model` | 否 | claude-sonnet-4-20250514 | Claude 模型名称 | +| `timeout_minutes` | 否 | 60 | 超时时间(分钟) | +| `org_name` | 否 | - | 组织成员检查(留空则跳过) | +| `allow_code_change` | 否 | false | 是否允许 Claude 修改代码 | +| `setup_script` | 否 | - | 初始化脚本(shell 命令) | +| `additional_claude_args` | 否 | - | 传给 Claude 的额外参数 | + +### 使用 Skill + +在调用方的仓库中创建 skill: + +``` +my-repo/ +├── .claude/ +│ └── skills/ +│ └── my-skill/ +│ └── SKILL.md +``` + +然后在 `additional_claude_args` 中指定: + +```yaml +additional_claude_args: '--skill my-skill' +``` + +## 权限要求 + +调用方需在 workflow 级别声明以下权限: + +```yaml +permissions: + contents: read # 读取代码 + pull-requests: write # 写入 PR 评论 + issues: write # 写入 Issue 评论 + # 如果 allow_code_change: true,需要: + # contents: write # 允许 Claude 修改并提交代码 +``` + +## 安全检查 + +工作流内置以下安全检查: + +1. **白名单检查** - 验证调用仓库在白名单中 +2. **组织成员检查** - 触发者必须是目标组织成员 +3. **仓库写权限检查** - 触发者需拥有 `write` 或 `admin` 权限 +4. **机器人过滤** - 自动跳过 `*[bot]` 和 `dependabot` + +## API 密钥管理 + +`CLAUDE_API_KEY` 由 `opensourceways/agent-skills` 仓库统一管理,通过 `secrets: inherit` 传递给调用方。 + +调用方无需在自身仓库配置 secrets。 + +## 故障排查 + +**问题:工作流不触发** +- 检查评论中是否包含 `@claude`(区分大小写) +- 确认调用仓库在白名单中 +- 查看 Actions 页面的工作流运行日志 + +**问题:白名单拒绝** +- 检查 `allowed_orgs` 和 `allowed_repos` 配置 +- 确认仓库名称格式正确(如 `org/repo`) + +**问题:`CLAUDE_API_KEY secret is not set`** +- 确认调用方使用了 `secrets: inherit` +- 检查 opensourceways/agent-skills 仓库的 secrets 配置 + +**问题:Claude 无法修改代码** +- 确认 `allow_code_change: true` +- 确认 workflow 声明了 `contents: write` 权限 \ No newline at end of file diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml new file mode 100644 index 0000000..d777c39 --- /dev/null +++ b/.github/workflows/_claude-code.yml @@ -0,0 +1,336 @@ +# _claude-code.yml +# 核心可复用 Claude Code 工作流,供组织内其他仓库通过 workflow_call 调用。 +# 支持通过 @claude 触发或在其他工作流中直接调用。 +# 调用方需在 workflow 级别声明以下权限: +# contents: read +# pull-requests: write +# issues: write +# contents: write (如果需要 Claude 修改代码) + +name: Claude Code (Reusable) + +on: + workflow_call: + inputs: + # API Key(由调用方显式传入,不再使用 inherit) + claude_api_key: + description: 'Anthropic API Key' + type: string + required: true + # Claude 模型名称 + model: + description: 'Claude model to use (e.g. claude-sonnet-4-20250514, claude-opus-4-20251114)' + type: string + default: 'claude-sonnet-4-20250514' + # 单次运行的最大超时时间(分钟) + timeout_minutes: + description: 'Job timeout in minutes' + type: number + default: 60 + # 可选的初始化脚本内容(shell 命令,多行用换行分隔) + setup_script: + description: 'Optional setup script to run before invoking Claude (e.g. pip install pytest)' + type: string + default: '' + # 传给 Claude CLI 的额外参数(如 skill 名称) + additional_claude_args: + description: 'Additional arguments passed to Claude CLI (e.g. --skill my-skill)' + type: string + default: '' + # 是否允许 Claude 修改代码(编辑/写入文件) + allow_code_change: + description: 'Allow Claude to edit/write files' + type: boolean + default: false + # 组织名称(用于成员检查)。留空则跳过组织检查,适用于个人仓库测试。 + org_name: + description: 'GitHub organization name for membership check. Leave empty to skip.' + type: string + default: '' + +jobs: + claude-code: + name: Invoke Claude Code + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.timeout_minutes }} + # 触发条件: + # - workflow_call: 其他工作流直接调用 + # - issue_comment: PR/Issue 评论中包含 @claude + # - issues: 新建 Issue 时自动触发 + # 排除机器人账号 + if: | + ( + github.event_name == 'workflow_call' || + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + github.event_name == 'issues' + ) && + !endsWith(github.actor, '[bot]') && + github.actor != 'dependabot' + + steps: + # --------------------------------------------------------------- + # 0. 白名单检查(硬编码,只有授权仓库可以调用) + # --------------------------------------------------------------- + - name: Check whitelist + id: check_whitelist + run: | + REPO="${{ github.repository }}" + ORG="${{ github.repository_owner }}" + + # 硬编码白名单 - 只有这些仓库可以调用 + ALLOWED_REPOS="KadenZhang3321/agent-skills,KadenZhang3321/hello-world" + + REPO_ALLOWED="false" + IFS=',' read -ra REPO_ARRAY <<< "$ALLOWED_REPOS" + for allowed_repo in "${REPO_ARRAY[@]}"; do + if [[ "$allowed_repo" == "$REPO" ]]; then + REPO_ALLOWED="true" + break + fi + done + + if [[ "$REPO_ALLOWED" != "true" ]]; then + echo "::error::Repository $REPO is not in whitelist." + echo "Allowed repos: $ALLOWED_REPOS" + exit 1 + fi + + echo "::notice::Repository $REPO is whitelisted" + + # --------------------------------------------------------------- + # 1. 安全检查(组织成员 + 写权限) + # --------------------------------------------------------------- + + # 1a. 检查触发者是否为组织成员 + - name: Check organization membership + id: check_org + if: inputs.org_name != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACTOR: ${{ github.actor }} + ORG: ${{ inputs.org_name }} + run: | + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/orgs/$ORG/members/$ACTOR") + + if [[ "$HTTP_STATUS" == "204" ]]; then + echo "is_member=true" >> "$GITHUB_OUTPUT" + echo "::notice::$ACTOR is a member of $ORG." + else + echo "is_member=false" >> "$GITHUB_OUTPUT" + echo "::warning::$ACTOR is NOT a member of $ORG (HTTP $HTTP_STATUS). Access denied." + fi + + - name: Exit if not org member + if: inputs.org_name != '' && steps.check_org.outputs.is_member == 'false' + run: | + echo "Security check failed: actor is not an organization member." + exit 1 + + # 1b. 检查触发者是否拥有仓库写权限 + - name: Check repository write permission + id: check_permission + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACTOR: ${{ github.actor }} + REPO: ${{ github.repository }} + run: | + PERMISSION=$(curl -s \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/collaborators/$ACTOR/permission" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('permission','none'))") + + echo "permission=$PERMISSION" >> "$GITHUB_OUTPUT" + + if [[ "$PERMISSION" == "write" ]] || [[ "$PERMISSION" == "admin" ]]; then + echo "has_write=true" >> "$GITHUB_OUTPUT" + echo "::notice::$ACTOR has $PERMISSION permission." + else + echo "has_write=false" >> "$GITHUB_OUTPUT" + echo "::warning::$ACTOR has insufficient permission: $PERMISSION" + fi + + - name: Exit if insufficient permission + if: steps.check_permission.outputs.has_write == 'false' + run: | + echo "Security check failed: actor does not have write permission." + exit 1 + + # --------------------------------------------------------------- + # 2. 准备环境 + # --------------------------------------------------------------- + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + # --------------------------------------------------------------- + # 3. 安装 Claude Code CLI + # --------------------------------------------------------------- + - name: Install Claude Code + run: | + # 使用 npm 安装 + npm install -g @anthropic-ai/claude-code + + # 验证安装 + claude --version + + # --------------------------------------------------------------- + # 4. 运行 setup_script + # --------------------------------------------------------------- + - name: Run setup script + if: inputs.setup_script != '' + run: | + echo "Running custom setup script..." + eval "${{ inputs.setup_script }}" + + # --------------------------------------------------------------- + # 5. 构建 prompt + # --------------------------------------------------------------- + - name: Build prompt + id: build_prompt + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + EVENT_NAME: ${{ github.event_name }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} + ISSUE_TITLE: ${{ github.event.issue.title || github.event.pull_request.title }} + ISSUE_BODY: ${{ github.event.issue.body || github.event.pull_request.body }} + COMMENT_BODY: ${{ github.event.comment.body }} + COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + ADDITIONAL_ARGS: ${{ inputs.additional_claude_args }} + run: | + python3 - <<'PYEOF' + import os, json + + event_name = os.environ.get("EVENT_NAME", "") + issue_number = os.environ.get("ISSUE_NUMBER", "") + issue_title = os.environ.get("ISSUE_TITLE", "") + issue_body = os.environ.get("ISSUE_BODY", "") or "" + comment_body = os.environ.get("COMMENT_BODY", "") or "" + comment_author = os.environ.get("COMMENT_AUTHOR", "") + additional_args = os.environ.get("ADDITIONAL_ARGS", "") or "" + repo = os.environ.get("REPO", "") + + parts = [] + if issue_title: + parts.append(f"## Issue/PR #{issue_number}: {issue_title}") + if issue_body.strip(): + parts.append(f"### Description\n{issue_body.strip()}") + if comment_body.strip(): + parts.append(f"### Comment by @{comment_author}\n{comment_body.strip()}") + if additional_args.strip(): + parts.append(f"### Additional context\n{additional_args.strip()}") + + prompt = "\n\n".join(parts) + + env_file = os.environ.get("GITHUB_ENV", "") + if env_file: + with open(env_file, "a") as f: + f.write("CLAUDE_PROMPT<> "$GITHUB_OUTPUT" + echo "::notice::Claude made some file changes" + else + echo "code_changed=false" >> "$GITHUB_OUTPUT" + fi + + # --------------------------------------------------------------- + # 7. 自动提交代码变更(如果允许) + # --------------------------------------------------------------- + - name: Commit and push changes + if: inputs.allow_code_change == true && steps.claude_run.outputs.code_changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # 配置 git + git config --global user.name "Claude Code Bot" + git config --global user.email "claude-bot@users.noreply.github.com" + + # 添加所有变更 + git add -A + + # 检查是否有变更需要提交 + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + + # 提交 + git commit -m "AI: Code changes by Claude Code" + + # 推送 + git push + + # --------------------------------------------------------------- + # 8. 获取 Claude 输出并发布评论 + # --------------------------------------------------------------- + - name: Get Claude output + id: get_output + run: | + # 尝试从日志中提取 Claude 的输出 + # 由于 CLI 输出在日志中,这里记录触发信息 + echo " Claude execution completed" >> "$GITHUB_STEP_SUMMARY" + echo "::notice::Claude Code execution completed" + + # --------------------------------------------------------------- + # 9. Token 用量报告 + # --------------------------------------------------------------- + - name: Report execution status + if: always() + run: | + echo "### Claude Code Execution" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Status | " >> "$GITHUB_STEP_SUMMARY" + echo "|--------|" >> "$GITHUB_STEP_SUMMARY" + echo "| Model | \`${{ inputs.model }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Code changes | ${{ steps.claude_run.outputs.code_changed }} |" >> "$GITHUB_STEP_SUMMARY" \ No newline at end of file diff --git a/.github/workflows/embeddable-caller.yml b/.github/workflows/embeddable-caller.yml new file mode 100644 index 0000000..a1b2cf7 --- /dev/null +++ b/.github/workflows/embeddable-caller.yml @@ -0,0 +1,24 @@ +# embeddable-caller.yml +# +# 可嵌入的 Claude Code 调用代码片段 +# 复制到任意工作流的 jobs 下即可使用 +# +# 注意:可复用工作流中已硬编码白名单,只有授权仓库可以调用。 + + claude-code: + uses: opensourceways/agent-skills/.github/workflows/_claude-code.yml@main + with: + # API Key - 使用调用方仓库的 secret + claude_api_key: ${{ secrets.CLAUDE_API_KEY }} + + # 模型配置 + model: 'claude-sonnet-4-20250514' + timeout_minutes: 60 + + # 可选配置 + # setup_script: 'pip install pytest' + # additional_claude_args: '--skill my-custom-skill' + # allow_code_change: true + + # 组织检查(留空则跳过) + # org_name: '' \ No newline at end of file diff --git a/.github/workflows/example-caller.yml b/.github/workflows/example-caller.yml new file mode 100644 index 0000000..7dc70c0 --- /dev/null +++ b/.github/workflows/example-caller.yml @@ -0,0 +1,47 @@ +# example-caller.yml +# +# 调用方示例文件 —— 可直接复制到目标仓库的 .github/workflows/ 目录使用。 +# 支持 @claude 触发和自动触发(新建 Issue 时) +# +# 注意:可复用工作流中已硬编码白名单,只有授权仓库才能调用。 +# 使用前确认你的仓库已在白名单中。 + +name: Claude Bot + +on: + # Issue/PR 评论时触发(需在评论中 @claude) + issue_comment: + types: [created] + + # 新建 Issue 时自动触发 + issues: + types: [opened] + + # 其他工作流调用时触发 + workflow_call: + +# 工作流级别权限声明(调用方负责声明) +# 如果 allow_code_change: true,需要 contents: write +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + claude-code: + uses: opensourceways/agent-skills/.github/workflows/_claude-code.yml@main + with: + # API Key - 使用调用方仓库的 secret + claude_api_key: ${{ secrets.CLAUDE_API_KEY }} + + # ---- 模型配置 ---- + model: 'claude-sonnet-4-20250514' + timeout_minutes: 60 + + # ---- 安全检查 ---- + org_name: '' + + # ---- 可选配置 ---- + # allow_code_change: false # 是否允许 CC 修改代码(默认 false) + # setup_script: '' # 初始化脚本 + # additional_claude_args: '' # 传给 CC 的额外参数(如 --skill my-skill) \ No newline at end of file diff --git a/README.md b/README.md index 2e0bf28..f1a71e8 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,10 @@ A: Claude Code 的配置文件通常在: - 全局配置:`~/.claude/config.json` - 项目配置:`/.claude/config.json` +## 🤖 Claude Bot 集成指南 + +如何将 Claude Bot 集成到组织下的任意仓库,请参阅:[`.github/workflows/README.md`](.github/workflows/README.md) + ## 📞 联系方式 如有问题或建议,请: @@ -291,4 +295,4 @@ A: Claude Code 的配置文件通常在: --- **维护者**: [添加维护者信息] -**最后更新**: 2026-02-03 +**最后更新**: 2026-03-28 diff --git a/skills/infrastructure/README.md b/skills/infrastructure/README.md index 8f6fbd3..3942c7b 100644 --- a/skills/infrastructure/README.md +++ b/skills/infrastructure/README.md @@ -5,6 +5,7 @@ ## 团队职责 Infrastructure 团队负责技术设施建设,包括: + - 基础设施搭建和维护 - DevOps 工具链建设 - CI/CD 流水线优化 @@ -13,7 +14,7 @@ Infrastructure 团队负责技术设施建设,包括: ## 目录结构建议 -``` +```text infrastructure/ ├── README.md # 本文件 ├── devops/ # DevOps 相关 @@ -30,74 +31,67 @@ infrastructure/ | 名字 | 功能 | | --- | --- | -| Github-action-diagnose | 诊断昇腾CI github action问题 | +| [github-action-diagnose](./github-action-diagnose/SKILL.md) | 诊断昇腾 CI GitHub Action 失败,定位基础设施故障与根因 | +| [claude-bot](./claude-bot/claude.md) | GitHub Issues/PR 中响应 `@claude` 的 AI 助手 | ### Github-action-diagnose #### 使用示例 -**示例 1:通过 GitHub Actions Run URL 诊断** +#### 示例 1:通过 GitHub Actions Job URL 诊断(推荐,精确到单个 Job) -``` -/github-action-diagnose https://github.com/my-org/my-repo/actions/runs/12345678901 +```text +/github-action-diagnose https://github.com/vllm-project/vllm-ascend/actions/runs/23656949484/job/68954806226 ``` -技能会自动: -1. 提取 `owner/repo` = `my-org/my-repo`,`run_id` = `12345678901` -2. 执行 `gh run view 12345678901 --repo my-org/my-repo --log-failed` -3. 进入 5 步诊断流程 +#### 示例 2:通过 Run URL 诊断(自动分析所有失败 Job) ---- +```text +/github-action-diagnose https://github.com/vllm-project/vllm-ascend/actions/runs/23282406275 +``` -**示例 2:通过 Run ID 诊断(在 repo 目录下)** +#### 示例 3:通过 Run ID 诊断 +```text +/github-action-diagnose 23282406275 ``` -/github-action-diagnose 12345678901 -``` - -技能会从当前目录推断 repo,然后执行诊断。 --- -**示例 3:直接粘贴日志内容** +#### 示例输出 A:基础设施故障(Artifact 上传超时) -``` -/github-action-diagnose -2024-01-15T10:23:45.123Z [error] npu-smi info: ERR99999 Device not found -2024-01-15T10:23:45.456Z [error] error code 507035 -2024-01-15T10:23:46.000Z Process exited with code 1 -``` +```text +# CI 故障诊断报告 ---- +**Run**: Nightly-A3 #23656949484 +**PR**: vllm-project/vllm-ascend (main) +**时间**: 2026-03-27T21:21:23Z ~ 2026-03-27T21:48:37Z -**示例输出 A:基础设施故障** +## 故障一:DeepSeek-V3.1-BF16.yaml (multi-node-deepseek-v3.1) -``` -# GitHub Action 故障诊断报告 - -## 1. 故障概览 -- **任务名称**: train-resnet50 -- **Run ID**: 12345678901 -- **故障定性**: 硬件故障 -- **判定依据**: NPU 驱动报 ERR99999,Device not found - -## 2. 详细诊断 -- **失败步骤**: Run steps (pytest) -- **报错原文**: `npu-smi info: ERR99999 error code 507035` -- **物理节点**: a3-runner-0023 -> Pod a3-0/runner-abc -> node-a3-12 -- **根因分析**: 物理机 node-a3-12 上 NPU 设备驱动异常,设备不可用 +- **定性**: 环境问题(基础设施) +- **根因**: Upload logs 步骤上传 Artifact 时网络超时,实际测试已成功完成 +- **关键标识**: `Failed to CreateArtifact: Unable to make request: ETIMEDOUT` +- **责任方**: 基础设施团队 +- **建议**: 重跑;检查 Runner 到 GitHub 的出口网络 ``` ---- +#### 示例输出 B:代码 Bug -**示例输出 B:非基础设施问题** +```text +# CI 故障诊断报告 -``` -未发现环境/基础设施故障。 +**Run**: PR CI #xxx +**PR**: vllm-project/vllm-ascend#1234 (feature/xxx) +**时间**: ... + +## 故障一:e2e-singlecard-light -分析依据:调度正常(Set up job 完成)、镜像拉取成功、NPU 挂载无报错。 -失败发生在业务代码层:Python Traceback 指向 tests/test_model.py:42, -AssertionError: expected accuracy > 0.8, got 0.75。 +- **定性**: 代码 Bug(PR 引入) +- **根因**: UnboundLocalError,PR 修改了 import 路径但未处理 ImportError 分支 +- **关键标识**: `UnboundLocalError: cannot access local variable 'huggingface_hub'` +- **责任方**: PR 作者 +- **建议**: 修改 vllm_ascend/xxx.py,在 except ImportError 分支设置默认值 ``` ## 团队规范 diff --git a/skills/infrastructure/claude-bot/claude.md b/skills/infrastructure/claude-bot/claude.md new file mode 100644 index 0000000..67ddffc --- /dev/null +++ b/skills/infrastructure/claude-bot/claude.md @@ -0,0 +1,120 @@ +# claude-bot + +## 描述 + +集成于 GitHub Issues / Pull Requests 的 AI 助手,在 vllm-project 组织内响应 `@claude` 提及,自动回复技术问题、代码审查意见和 CI 诊断建议。 + +## 使用场景 + +- 在 Issue 中 `@claude` 提问:解答 vLLM / Ascend NPU 相关技术问题 +- 在 PR 中 `@claude` 请求代码审查:检查正确性、性能、硬件兼容性 +- 新 Issue 创建时自动触发:对问题进行初步分类和响应 + +## 前置要求 + +- 调用方仓库需配置 GitHub Actions Workflow(参见 `.github/workflows/example-caller.yml`) +- 需在仓库或组织级别设置 `CLAUDE_API_KEY` Secret +- 触发者须为组织成员且拥有仓库写权限(write / admin) + +## 参数 + +本 skill 通过 GitHub 事件触发,无命令行参数。触发方式: + +| 触发事件 | 条件 | 说明 | +|---------|------|------| +| `issue_comment` | 评论包含 `@claude` | 回复指定评论 | +| `issues: opened` | 新建 Issue | 自动响应新 Issue | + +## 使用方法 + +在 GitHub Issue 或 PR 的评论中直接提及: + +``` +@claude 这个 PR 的 Ascend NPU 兼容性如何? +``` + +``` +@claude 帮我看下这段代码有没有问题 +``` + +## 示例 + +### 示例 1:代码审查请求 + +在 PR 评论中: +``` +@claude 请审查这次对 vllm_ascend/executor.py 的修改 +``` + +**预期输出**:结构化的代码审查意见,包含正确性、性能和 Ascend NPU 兼容性分析。 + +### 示例 2:技术问题咨询 + +在 Issue 评论中: +``` +@claude CANN 8.0 下 flash attention 算子覆盖情况如何? +``` + +**预期输出**:针对 Ascend NPU / CANN 的技术解答。 + +## 注意事项 + +- 仅响应组织成员(vllm-project)且拥有仓库写权限的用户 +- 机器人账号(`[bot]`、`dependabot`)触发的事件自动跳过 +- 响应语言与触发评论一致(中文评论回中文,英文评论回英文) +- 不对修复方案或操作步骤做出保证,建议以人工审核为准 + +## 相关 Skills + +- [github-action-diagnose](../github-action-diagnose/SKILL.md) — CI 故障根因诊断 + +## 更新日志 + +### v1.0.0 (2026-03-31) +- 初始版本:支持 Issue/PR 评论触发,单认证模式(api_key),Token 用量展示 + +## 作者 + +@zhangyang + +## 最后更新 + +2026-03-31 + +--- + +# Claude Bot Behavior Guide + +> 以下为 Claude Bot 在 vllm-project 组织内的行为规范,供 AI 模型参考。 + +You are an AI assistant integrated into GitHub issues and pull requests for the vllm-project organization. + +## Behavior Guidelines + +- Be helpful, concise, and professional. +- When asked to review code, focus on correctness, performance, and hardware compatibility (especially Ascend NPU). +- Do not suggest changes unrelated to the repository's scope. +- If you are unsure, ask for clarification. + +## Context + +- You are operating within the **vllm-project** GitHub organization. +- Repositories in this org include `vllm-ascend` (Ascend NPU backend) and related infrastructure. +- Common topics: vLLM inference engine, Ascend NPU support, CANN/MindSpore compatibility, CI/CD pipelines. + +## Code Review Focus + +When reviewing pull requests or code snippets: + +1. **Correctness** — Does the code do what it claims? Are there edge cases or off-by-one errors? +2. **Performance** — Are there obvious bottlenecks, unnecessary allocations, or missed optimizations? +3. **Hardware compatibility** — Does the code respect Ascend NPU constraints (operator coverage, memory layout, CANN API version)? +4. **Test coverage** — Are the changes tested? Are existing tests likely to break? +5. **Documentation** — Do public APIs have docstrings? Are complex sections commented? + +## Tone + +- Use Markdown formatting in your responses when it improves readability. +- Be direct and actionable. Prefer "Change X to Y because Z" over vague suggestions. +- Acknowledge uncertainty: say "I'm not sure" rather than guessing. +- Respond in the same language as the comment that triggered you (Chinese or English). diff --git a/skills/infrastructure/github-action-diagnose/SKILL.md b/skills/infrastructure/github-action-diagnose/SKILL.md index 5e1fc11..bb01ba3 100644 --- a/skills/infrastructure/github-action-diagnose/SKILL.md +++ b/skills/infrastructure/github-action-diagnose/SKILL.md @@ -1,99 +1,422 @@ --- name: github-action-diagnose -description: 诊断昇腾(Ascend)NPU 集群上 GitHub Actions 执行失败的原因,定位基础设施故障与根因分析 -allowed-tools: Bash(gh run view:*), Bash(gh run list:*), Bash(gh api:*), Bash(kubectl get:*), Bash(kubectl describe:*), Bash(kubectl logs:*), Bash(kubectl config:*), Read, Grep +description: GitHub Action CI 失败自动诊断,专为昇腾(Ascend)NPU 集群(ARC/K8s)基础设施设计。当用户提到 CI 失败、流水线挂掉、CI 报错、构建失败、测试失败、PR CI 不过、CI 排队超时、nightly 失败、runner 异常等情况时立即触发。自动分析日志、定位物理节点、分类根因(基础设施/代码Bug/精度回归)、识别责任方,将诊断报告写入文件。即使用户只是粘贴了一段错误日志或 CI 链接,也应触发此 skill。 +compatibility: + tools: + - Bash + - Read + - Write + - Glob + - Grep --- -## Your task +# CI 故障自动诊断 Skill -诊断昇腾(Ascend)NPU 集群上 GitHub Actions 的执行失败。仅做故障定性和根因分析,不提供修复建议。 -所有只读操作(gh CLI、kubectl 查询、日志读取)直接执行,无需用户确认。 +## 使用场景 -本技能采用 5 步诊断流程:Step 0 输入识别 + 4 步诊断逻辑(生命周期分诊 → 物理节点溯源与环境故障定位 → 非环境问题判定 → 输出报告)。 +- Ascend NPU 集群(ARC/K8s)上的 GitHub Actions 运行失败 +- PR CI 不过、nightly 任务失败、runner 异常等需要快速定位根因的场景 +- 区分故障责任方:基础设施团队(环境/硬件)vs PR 作者(代码/精度) -### Step 0: 识别输入并获取日志 +## 前置要求 -分析用户提供的输入,自动识别类型: +- `gh` CLI 已安装并已完成 GitHub 认证(`gh auth login`) +- 有目标仓库的读权限(用于拉取 run 日志) +- `kubectl` 已配置(用于物理节点溯源,无权限时跳过该步骤) -- **GitHub Actions Run URL**(包含 `/actions/runs/`):提取 owner/repo 和 run_id,执行 `gh run view --repo --log-failed` 获取失败日志。 -- **Run ID**(纯数字):需要用户确认 repo 或从当前目录推断,然后执行 `gh run view --log-failed`。 -- **粘贴的日志内容**(非 URL 非纯数字):直接作为日志内容进行分析。 +## 核心原则 -如果 `--log-failed` 信息不足,自动尝试 `gh run view --log` 获取完整日志。 -同时获取 run 的元信息:`gh run view --repo --json jobs,name,status,conclusion,headBranch,event`。 +- **静默执行**:直接运行所有只读操作(`gh` CLI、日志读取、`kubectl` 查询),不要逐步询问权限 +- **先跑后说**:收集完所有信息后,一次性生成完整报告 +- **直接输出**:诊断结果直接在对话中输出,不写入文件(Action Plan JSON 除外) +- **责任明确**:每个问题必须标注责任方(基础设施团队 / PR 作者) +- **仅在变更操作前确认**:修改代码、提交 commit、执行 `kubectl delete/patch` 才需询问 -### Step 1: 生命周期分诊 +--- + +## Step 0:解析用户输入并确定诊断范围 + +根据用户输入确定诊断范围: + +| 输入类型 | 示例 | 诊断范围 | +|---------|------|---------| +| Run ID | `23282406275` | 诊断 Run 中所有失败的 Job | +| Run URL | `github.com/.../actions/runs/23282406275` | 诊断 Run 中所有失败的 Job | +| Job URL | `github.com/.../actions/runs/.../jobs/xxx` | **只诊断这一个 Job** | + +> **重要**:如果用户提供了具体的 Job URL,**只分析这一个 Job**,不要去拉取整个 Run 的其他 Job 日志。这是为了节省时间和 token。 + +--- + +## Step 1:静默收集上下文 + +### 1a. 确定诊断范围 + +- **如果用户提供了 Job URL**:直接使用该 Job ID,跳过 Run 级别分析 +- **如果用户提供了 Run ID/URL**:分析该 Run 中所有失败的 Job + +### 1b. 收集日志 + +**必须使用脚本获取日志**,不要直接用 `gh run view --job --log`,原因: +1. 日志可能很大(几百MB),导致 API 超时或工具调用被中断 +2. tiling 编译警告会淹没真正的根因错误 +3. 脚本已内置预过滤逻辑,只输出关键错误行 + +**Job URL 场景(只诊断单个 Job)**: +```bash +bash /scripts/fetch-run.sh --job [owner/repo] +# 例如:bash scripts/fetch-run.sh --job 68954806226 vllm-project/vllm-ascend +``` + +**Run ID/URL 场景(诊断所有失败 Job)**: +```bash +bash /scripts/fetch-run.sh [owner/repo] +# 例如:bash scripts/fetch-run.sh 23326177540 +# 默认 repo 为 vllm-project/vllm-ascend +``` + +**脚本输出内容**: +- Runner 名称、起止时间、结论 +- 失败步骤列表(哪个 step 挂了) +- Annotations(通常直接揭示根因,如 ETIMEDOUT / exit code) +- Annotations 已明确根因时自动跳过日志拉取 +- 预过滤的运行时错误(RuntimeError/AssertionError/EngineDeadError/OOM 等) +- 预过滤的编译/安装错误(依赖冲突、git dubious ownership 等) +- PR 变更文件列表 + +脚本位置:`/scripts/fetch-run.sh` + +**Step 1c:依赖解析错误优先检查(Install 阶段失败时必做)** + +脚本日志不足以定位根因时,**优先使用 Step 1d 中的依赖专用命令**搜索依赖解析错误,这是最容易被遗漏的根因: + +**为什么重要**:依赖不可用(如 `modelscope==1.35.1` 不存在)会导致后续所有步骤连锁失败,日志中会出现大量 "error"、"failed",但真正的根因只在依赖解析步骤的开头。 + +### Step 1d. 按需补充的命令(脚本输出不足以定位根因时才用) + +> **脚本已内置以下逻辑,90% 场景无需手动补充命令:** +> - 失败步骤列表(哪个 step 挂了) +> - Annotations(ETIMEDOUT / exit code 等直接结论) +> - Annotations 已明确根因时自动跳过日志拉取 +> - PR 变更文件列表 + +仅在脚本输出仍不足以定位根因时,才手动补充: + +```bash +# 依赖解析错误专用 +gh run view --job --log --repo | grep -iE "unsatisfiable|No solution found|no version of|modelscope.*error" +``` + +--- + +## Step 2:生命周期快速分诊 + +根据**失败发生的步骤**做初步判断: + +| 失败步骤 | 初步定性 | +|---------|---------| +| `Set up job` | 基础设施 — Runner 调度失败 | +| `Initialize containers` | 基础设施 — 容器运行时异常 | +| `Checkout` / `Install dependencies` | 基础设施 — 网络/挂载/权限 | +| `Run test` / `Build` | **需同时执行 Step 3 和 Step 4**,不可只做其中之一 | + +> **重要**:失败在 `Run test` / `Build` 阶段时,必须同时检查环境故障(Step 3)和代码问题(Step 4),不因"看起来是代码问题"而跳过环境检查,也不因"多 Job 同时失败"而跳过代码检查。 + +**多个 Job 同步以相同步骤失败 → 强烈的基础设施信号**,但仍需完成代码侧的快速排查。 + +--- + +## Step 3:物理节点溯源(A 类必做,Run step 失败时也应尝试) + +### 3a. 获取 runner_name + +从 `Set up job` 日志或 API 获取,格式通常为: +``` +linux-aarch64-a3-2-x51bm-runner-ksnwc + ↑节点池 ↑物理机编号 ↑pod随机后缀 +``` + +### 3b. 定位 Namespace + +```bash +# 自动发现 runner pod 所在 namespace(不确定时使用) +kubectl get pods --all-namespaces | grep +``` + +### 3c. 查物理节点(区分两种场景) + +**Pod 未销毁**: +```bash +kubectl get pod -n \ + -o custom-columns=NODE:.spec.nodeName +``` + +**Pod 已销毁**:从完整日志中搜索调度记录: +```bash +# 先拉完整日志 +gh run view --job --log --repo > full.log +grep "Successfully assigned to" full.log +# 或在 Loki/ELK 中搜索相同关键词 +``` + +### 3d. 多机任务 — 优先定位 Master 节点(Rank 0) + +当任务涉及多机(multi-node)且某节点报 `Timeout` 时,**不要孤立分析报错节点**,先找 Rank 0: + +```bash +# 方法一:从日志中找 MASTER_ADDR 环境变量 +grep "MASTER_ADDR" full.log + +# 方法二:从 RANK_TABLE_FILE 中找 rank_id=0 对应的 device_ip +grep -A5 '"rank_id": "0"' +``` + +找到 Master 节点后,优先检查其日志: +- 是否有 `Unexpected Exit` +- 是否有 NPU 驱动报错(`ERR99999`、`error code 507035`) +- 是否有进程崩溃(exit `-9`、`Bus error`) + +Master 有问题 → 从节点只是连锁超时,根因在 Master。 + +### 3e. 确认 NPU 健康 + +```bash +npu-smi info # 在对应节点上执行 +kubectl get nodes --kubeconfig=<集群kubeconfig> +``` + +--- + +## Step 4:根因分类 + +### 类型 A:基础设施 / 环境故障 +信号:容器启动失败、网络超时、NPU 硬件报错(`ERR99999`)、OOM(`Bus error` / `Killed` / exit `-9`)、exit code 255(K8s 强制终止)、ModelScope 下载超时、多机 Timeout(见 Step 3d)、`shm_broadcast` 超时 + `EngineDeadError`(Nightly 资源调度不稳定,见 `references/common-patterns.md`) + +**责任方**:基础设施团队 + +### 类型 B:代码 Bug(PR 引入) +信号:exit code 1、Python 异常堆栈(`UnboundLocalError` / `AssertionError` / `AttributeError`)、失败与 PR diff 直接对应、重跑仍失败、UT 卡死 + +**责任方**:PR 作者 + +### 类型 C:精度回归 +信号:`Accuracy of ... is X, lower than Y`、精度跌幅 > 5% -根据失败发生的步骤进行初步分类: +**责任方**:PR 作者(需与算法团队确认) -| 失败步骤 | 初步定性 | 说明 | -|----------|---------|------| -| Set up job | 环境问题 | Runner 无法拉起/调度失败 | -| Initialize containers | 环境问题 | 容器运行时异常/镜像拉取失败 | -| Checkout / Setup Environment | 环境问题 | 网络/挂载/权限故障 | -| Run steps(pytest/lint 等) | 需进一步分析 | Runner 已就绪,进入 Step 2-3 细分 | +### 类型 D:YAML / 配置错误 +信号:`undefined variable "False"`、workflow 语法报错 -如果失败在前三类步骤,直接标记为环境问题并进入 Step 2。 -如果失败在 Run steps,需要同时执行 Step 2 和 Step 3 来区分环境故障与代码问题。 +**责任方**:PR 作者 / CI 维护者 -### Step 2: 物理节点溯源 + 环境故障深度定位 +### 类型 E:疑难 / 概率性问题 +信号:偶发挂死(如 triton ascend 概率挂)、无明确异常堆栈、重跑有时通过 -#### 2a. 物理节点溯源 -当判定为环境故障或怀疑硬件问题时,定位物理节点: +**常见错误模式速查**:`references/common-patterns.md` +**vllm-ascend 专项**:`references/vllm-ascend.md` -1. 从 `Set up job` 日志或 `gh api` 获取 `runner_name`(即 Pod 名称)。 -2. 从 `runner_name` 前缀或 Job Labels 识别节点池(如 `a3-0`)。 -3. 自动发现 namespace:`kubectl get pods --all-namespaces --no-headers | grep ` 提取 namespace。 -4. 反查物理机: - - Pod 未销毁:`kubectl get pod -n -o custom-columns=NODE:.spec.nodeName --no-headers` - - Pod 已销毁:在日志中搜索 `Successfully assigned to `。 +--- + +## Step 5:输出报告 + +直接在对话中输出诊断结果,格式如下(每个失败 Job 一节): + +``` +# CI 故障诊断报告 + +**Run**: [Workflow名称] #[Run ID] +**PR**: [仓库/PR号] ([分支名]) +**时间**: [开始] ~ [结束] + +--- + +## 故障一:[Job 名称] + +- **定性**: [环境问题 / 代码Bug / 精度回归 / 配置错误 / 疑难] +- **根因**: [一句话直接原因,如"K8s 容器运行时在测试阶段强制终止,exit code 255"] +- **关键标识**: `[最关键的一行错误,如 ERR99999 / AssertionError / exit code 255]` +- **责任方**: [基础设施团队 / PR 作者] +- **建议**: [重跑 / 修改 XX 文件 XX 行 / 上报运维 / 检查 Master 节点 XX] +- **节点**: [仅硬件故障时填写:Runner Pod名 → 物理节点名,其他类型省略] + +--- + +[其他 Job 以同样格式继续] +``` + +**输出精简原则**: +- 不粘贴大段原始日志,只引用最关键的一行错误标识 +- 不描述诊断过程(不写"我们检查了A,发现B"),直接给结论 +- 节点池、物理机等环境信息仅在硬件故障时填写,其他类型省略 +- 若同一 Run 中多个 Job 根因相同,可合并为一条说明 +- **不输出步骤分析表格,不输出汇总表格** + +--- + +## Step 6:等待人工确认 + +输出诊断结果后停止,等待确认: +- "确认" / "没问题" → 生成 Action Plan(仅类型 B/C/D) +- 提出修正 → 在对话中重新输出修订后的诊断结果;若修正揭示了漏判或误判的模式,同步更新 `references/` 中对应的参考文件,防止下次重犯 +- 基础设施问题 → 提供操作建议后结束,不执行代码修改 + +> **重要**:不要跳过 Step 6 等待确认,直接进入 Step 7 + +--- + +## Step 7:生成 Action Plan + +用户确认后,生成结构化 Action Plan JSON 文件供后续 AI 修复使用。 + +> **注意**:只有代码 Bug/配置错误/精度回归 需要生成 Action Plan,基础设施问题不需要。 + +### Step 7.1:检查是否需要生成 + +| 问题类型 | 是否生成 Action Plan | 说明 | +|---------|-------------------|------| +| 类型 B(代码Bug) | ✅ 是 | PR 引入的 Bug,需要修改代码 | +| 类型 C(精度回归) | ✅ 是 | 需要调整参数或回滚 | +| 类型 D(YAML/配置错误) | ✅ 是 | 需要修改配置 | +| 类型 A(环境/基础设施) | ❌ 否 | 基础设施问题,不涉及代码修改 | +| 类型 E(疑难) | ❌ 否 | 偶发问题,重跑尝试 | + +### Step 7.2:生成 JSON 文件 + +``` +输出文件:/reports/action-plan-.json +``` + +```json +{ + "run_id": "", + "pr": "#", + "jobs_to_fix": [ + { + "job_id": "", + "job_name": "", + "issue_type": "code_bug|config_error|accuracy_regression", + "root_cause": "<一句话描述>", + "fix_actions": [ + { + "type": "code_change", + "file": "<文件路径>", + "action": "modify|add|remove", + "location": "<文件:行号 或 函数名>", + "original": "<原内容(可选)>", + "replacement": "<新内容>", + "reason": "<为什么这么改>" + } + ], + "verification": { + "command": "<验证命令>", + "expected": "<预期结果>" + } + } + ] +} +``` -#### 2b. 环境故障深度定位 -检查以下故障模式: +### Step 7.3:验证 Action Plan -- **驱动/硬件失效**:`npu-smi info` 报错、`ERR99999`、`error code 507035`、`Device not found`。 -- **资源溢出 (OOM)**:系统级 `Killed` 信号、`Bus error`(SHM 不足)、`No space left on device`。 -- **多机连锁超时**:多机任务中某节点报 `Timeout`,优先识别并检查 Master 节点。Master 节点识别方法: - - 检查 `RANK_TABLE_FILE` 中 `rank_id=0` 对应的节点 - - 或在日志中搜索 `master_addr` / `MASTER_ADDR` 环境变量指向的节点 - - 确认 Master 节点是否有 `Unexpected Exit` 或驱动报错 +生成后,在对话中输出: +- Action Plan 已写入文件路径 +- 总结每个 fix_action(文件、修改内容) -### Step 3: 非环境问题判定 +用户确认后执行实际代码修改。 -如果 Step 2 未发现环境/硬件故障,检查以下非环境因素: +--- + +## 参考资料 + +- **常见错误模式速查**:`references/common-patterns.md`(网络超时/容器崩溃/UT卡死/triton挂/NPU硬件等) +- **vllm-ascend 专用**:`references/vllm-ascend.md`(Runner 类型、内部服务、workflow 触发逻辑) +- **分类判断详细逻辑**:`references/classification-guide.md` + +--- -- **YAML 语法错误**:如 `undefined variable "False"`(应为小写 `false`)。 -- **业务逻辑报错**:`AssertionError`、Python Traceback 指向业务源码、测试用例失败。 -- **依赖问题**:pip/npm 安装失败但非网络原因(版本冲突、包不存在)。 +## 常见误判与规避 -### Step 4: 输出诊断报告 +### 1. tiling 编译警告 ≠ 测试失败根因 -根据诊断结果选择输出模式: +**现象**:日志中大量 `Register tiling func failed`、`Get op tiling func failed` +**实际情况**:这是 CANN SDK 在编译 optiling 时的 DEBUG 输出,属于正常行为 +**规避**:grep 时排除 tiling 相关模式,优先捕获 `RuntimeError`、`AssertionError`、`exit code 255` -#### 情况 A:确认为基础设施/硬件故障 +### 2. 日志"淹没"问题 -严格按照以下格式输出: +**现象**:高volume日志(如 tiling DEBUG)掩盖低volume但真正的根因 +**规避**:先按错误类型/阶段分离日志输出,高优先级错误(OOM/AssertionError)先展示 + +### 3. exit code 255 = K8s 终止 + +**现象**:测试实际已失败,但日志最后是 `command terminated with exit code 255` +**实际情况**:exit code 255 通常是 K8s 强制终止,说明测试进程已崩溃 +**分析**:往前搜索 `RuntimeError`、`AssertionError` 找到真正失败原因 + +--- + +## 使用示例 + +### 示例 1:通过 GitHub Actions Run URL 诊断 ``` -# GitHub Action 故障诊断报告 +/github-action-diagnose https://github.com/my-org/my-repo/actions/runs/12345678901 +``` + +Skill 会自动提取 `owner/repo` 和 `run_id`,执行 `gh run view --log-failed`,进入完整诊断流程。 -## 1. 故障概览 -- **任务名称**: [Job Name] -- **Run ID**: [Run ID] -- **故障定性**: 环境问题 / 硬件故障 / 集群调度异常 -- **判定依据**: [驱动报错/调度异常/硬件死锁/OOM 等] +### 示例 2:通过 Run ID 诊断(在 repo 目录下) -## 2. 详细诊断 -- **失败步骤**: [Set up job / Initialize containers / Run steps 等] -- **报错原文**: [引用关键日志片段] -- **物理节点**: [Runner 名] -> [Pod] -> [物理机/节点池] -- **根因分析**: [深层原因说明] ``` +/github-action-diagnose 12345678901 +``` + +从当前目录推断 repo,然后执行诊断。 + +### 示例 3:直接粘贴日志内容 + +``` +/github-action-diagnose +2024-01-15T10:23:45.123Z [error] npu-smi info: ERR99999 Device not found +2024-01-15T10:23:45.456Z [error] error code 507035 +``` + +--- + +## 注意事项 + +- 本 skill 仅做故障**定性与根因分析**,不输出修复建议或操作手册 +- 所有只读操作(`gh` CLI、`kubectl` 查询、日志读取)直接执行,无需用户确认 +- 诊断报告中的物理节点信息依赖 `kubectl` 访问权限,无权限时跳过节点溯源步骤 +- 参考资料位于 `references/` 目录,包括常见错误模式、vllm-ascend 专项说明、Ascend NPU 故障排查手册 + +## 相关 Skills + +- [claude-bot](../claude-bot/claude.md) — GitHub Issues/PR 中的 AI 助手,可触发本诊断流程 + +--- + +## 更新日志 + +### v2.0.0 (2026-03-31) +- 重构诊断流程:扩展至 7 步(原 5 步),增加精度回归、代码 Bug、Action Plan 生成阶段 +- 新增责任方标注(基础设施团队 / PR 作者) +- 新增 `references/` 参考资料目录(common-patterns、vllm-ascend、classification-guide、ascend-troubleshooting) +- 输出格式由纯报告改为结构化 Action Plan JSON(代码问题时) +- 新增常见误判规避说明(tiling 警告、日志淹没、exit code 255) + +### v1.0.0 (2026-03-16) +- 初始版本:5 步诊断流程(输入识别 → 生命周期分诊 → 节点溯源 → 非环境判定 → 输出报告) +- 支持 URL / Run ID / 粘贴日志三种输入方式 +- 支持基础设施故障(情况A)和非基础设施问题(情况B)两种输出模式 + +--- + +## 作者 -#### 情况 B:环境正常,非基础设施问题 +- v1.0.0: @hdsong2 +- v2.0.0: @zhangyang -采用自然语言简要反馈: -1. **结论**:直接告知"未发现环境/基础设施故障"。 -2. **依据**:简述分析了哪些环节(调度、镜像拉取、NPU 挂载等均正常)。 +## 最后更新 -注意:两种情况均不输出修复建议或操作手册。 +2026-03-31 diff --git a/skills/infrastructure/github-action-diagnose/evals/evals.json b/skills/infrastructure/github-action-diagnose/evals/evals.json index 6f78850..1a4a35c 100644 --- a/skills/infrastructure/github-action-diagnose/evals/evals.json +++ b/skills/infrastructure/github-action-diagnose/evals/evals.json @@ -1,53 +1,17 @@ { - "skill_name": "github-action-diagnose", + "skill_name": "ci-diagnosis", "evals": [ { "id": 1, - "prompt": "https://github.com/tile-ai/tilelang-ascend/actions/runs/22718535502", - "expected_output": "识别为基础设施故障:镜像拉取失败(image pull failure),输出结构化诊断报告", - "files": [], - "assertions": [ - { - "name": "输出结构化报告(情况A格式)", - "description": "输出包含 '# GitHub Action 故障诊断报告' 标题,说明技能正确识别为基础设施故障并使用了规定格式" - }, - { - "name": "识别为镜像拉取失败", - "description": "输出中包含 '镜像' 或 'image pull' 或 'pull' 相关关键词,说明技能正确定性了故障类型" - }, - { - "name": "包含故障定性字段", - "description": "输出包含 '故障定性' 字段,符合规定的报告结构" - }, - { - "name": "不包含修复建议", - "description": "输出不包含 '建议' 或 '修复' 或 '解决方案' 等字样,符合技能只做诊断不提供修复建议的要求" - } - ] + "prompt": "我们的 PR CI 挂了,Run ID 是 23326177540,repo 是 vllm-project/vllm-ascend,帮我诊断一下哪里出问题了", + "expected_output": "诊断所有失败 Job,写入 reports/diagnosis-23326177540.md,对话输出简短摘要和汇总表格", + "files": [] }, { "id": 2, - "prompt": "https://github.com/sgl-project/sglang/actions/runs/22573924024/job/65388415192", - "expected_output": "识别为基础设施故障:网络问题,输出结构化诊断报告", - "files": [], - "assertions": [ - { - "name": "输出结构化报告(情况A格式)", - "description": "输出包含 '# GitHub Action 故障诊断报告' 标题,说明技能正确识别为基础设施故障并使用了规定格式" - }, - { - "name": "识别为网络问题", - "description": "输出中包含 '网络' 或 'network' 或 'timeout' 或 'connection' 相关关键词,说明技能正确定性了故障类型" - }, - { - "name": "包含故障定性字段", - "description": "输出包含 '故障定性' 字段,符合规定的报告结构" - }, - { - "name": "不包含修复建议", - "description": "输出不包含 '建议' 或 '修复' 或 '解决方案' 等字样,符合技能只做诊断不提供修复建议的要求" - } - ] + "prompt": "这个 Job 一直失败,帮我看下:https://github.com/vllm-project/vllm-ascend/actions/runs/23282406275/job/67875153370", + "expected_output": "只诊断这一个 Job(不拉取整个 Run),写入 reports/diagnosis-job-67875153370.md,对话输出简短摘要和汇总表格", + "files": [] } ] } diff --git a/skills/infrastructure/github-action-diagnose/references/ascend-troubleshooting.md b/skills/infrastructure/github-action-diagnose/references/ascend-troubleshooting.md index 60118bf..ee57dea 100644 --- a/skills/infrastructure/github-action-diagnose/references/ascend-troubleshooting.md +++ b/skills/infrastructure/github-action-diagnose/references/ascend-troubleshooting.md @@ -36,4 +36,3 @@ kubectl logs -n kube-system -l app=ascend-device-plugin Look for: - `get npu device count failed` - `npu device is unhealthy` - diff --git a/skills/infrastructure/github-action-diagnose/references/classification-guide.md b/skills/infrastructure/github-action-diagnose/references/classification-guide.md new file mode 100644 index 0000000..6991964 --- /dev/null +++ b/skills/infrastructure/github-action-diagnose/references/classification-guide.md @@ -0,0 +1,76 @@ +# 问题分类判断树 + +分类体系与 SKILL.md 保持一致:**类型 A**(基础设施)/ **类型 B**(代码Bug)/ **类型 C**(精度回归)/ **类型 D**(配置错误)/ **类型 E**(疑难) + +--- + +## 第一步:失败发生在哪个阶段? + +| 失败步骤 | 初步定性 | +|---------|---------| +| `Set up job` / `Initialize containers` | **类型 A** — 直接归基础设施,无需看日志 | +| `Checkout` / `Install dependencies` | **倾向类型 A**,但需确认是网络还是版本号写错(见场景 B) | +| `Stream logs` / `Upload logs` | **类型 A** — 测试本身可能已通过,是 Runner 与 GitHub 通信失败 | +| `Run test` / `Build` | **需同时排查 A 和 B**,不可只做其中之一 | + +--- + +## 第二步:是类型 A 还是类型 B?(Run test/Build 阶段失败时) + +**倾向类型 A(基础设施)的信号:** +- 多个 PR / 多个 Job 同时失败,且根因相同 +- 重跑后通过 +- 错误指向网络、存储、NPU 硬件(`ERR99999`)、OOM、exit code 255 +- `shm_broadcast` 超时 + `EngineDeadError`(Nightly 调度不稳定) +- 无 Python 异常堆栈,进程被外部终止 + +**倾向类型 B(代码 Bug)的信号:** +- 有明确 Python 异常堆栈,且指向 PR 新增/修改的代码 +- 重跑仍然失败(稳定复现) +- 其他 PR 没有同样的失败 +- PR diff 与失败路径直接对应 + +**两者都有信号时 → 先排查 A,再排查 B**(基础设施问题可以掩盖代码问题,反之不成立) + +--- + +## 第三步:细分类型 + +### 类型 C:精度回归 +- 有明确精度数值报错:`Accuracy of ... is X, lower than Y` +- 跌幅 < 3%:先重跑确认是否 flaky +- 跌幅 > 5%:PR 引入功能性回归,检查推理路径改动 + +### 类型 D:配置/YAML 错误 +- `undefined variable "False"`(YAML 大小写) +- workflow 语法报错(actionlint) +- 路径/权限配置错误 + +### 类型 E:疑难 / 概率性 +- 无明确异常堆栈,进程静默挂死 +- 重跑有时通过、有时挂 +- 常见:triton ascend 概率挂、CPU offload 死锁 + +--- + +## 常见混淆场景 + +**场景 A:`AssertionError` 是代码 Bug 还是精度回归?** +- `AssertionError: Test0:` / `model_utils.py` 断言 → **类型 C**,精度回归 +- `AssertionError` 指向功能逻辑(如形状不匹配、返回值错误)→ **类型 B**,代码 Bug + +**场景 B:依赖失败 — 网络问题还是版本号写错?** +- `No solution found` / `no version of xxx==Y.Y.Y` → **类型 A**,版本不存在于镜像源(或写错) +- `connection refused` / 超时 → **类型 A**,网络/基础设施 + +**场景 C:NPU function error — 硬件还是代码兼容性?** +- `error code 507xxx` / `ERR99999` → **类型 A**,硬件/驱动故障 +- `error code 107xxx`(如 107030)→ CANN runtime 参数非法,**不是硬件信号**;若发生在 `model init` 且 PR 是版本升级/main2main → **类型 B**(新版本 init 路径与 torch_npu 不兼容) + +**场景 D:`EngineDeadError` — 代码 Bug 还是环境?** +- 伴随 `shm_broadcast` 60s 超时连续出现,且为 Nightly 任务 → **类型 A**,资源调度不稳定 +- 启动阶段即崩溃,有明确 Python 堆栈 → **类型 B** + +**场景 E:exit code 255** +- 通常是 K8s 强制终止,本身不是根因 +- 往前找 `RuntimeError`、`AssertionError` 才是真正失败原因 diff --git a/skills/infrastructure/github-action-diagnose/references/common-patterns.md b/skills/infrastructure/github-action-diagnose/references/common-patterns.md new file mode 100644 index 0000000..ba1a8f8 --- /dev/null +++ b/skills/infrastructure/github-action-diagnose/references/common-patterns.md @@ -0,0 +1,271 @@ +# 常见 CI 错误模式速查(昇腾 NPU / ARC / K8s 环境) + +## 基础设施类 + +### NPU 硬件错误(⚠️ 注意 error code 范围) +``` +ERR99999 +error code 507035 +npu-smi info: device X not found +``` +- **定性**:类型 A,硬件层故障 +- **分析**:用 `npu-smi info` 确认 NPU 状态;查看是否是特定节点的问题 +- **处理**:标记该节点为 SchedulingDisabled,上报硬件团队 +- **⚠️ 易误判**:`NPU function error: aclrtMemcpy, error code is 107xxx`(如 107030)**不是硬件错误**,是 CANN runtime 参数非法;若发生在 `model init` 阶段且 PR 是版本升级/main2main → 定性为**类型 B**(代码兼容性),参见 classification-guide.md 场景 D + +### 容器启动超时(Initialize containers 阶段) +``` +[error] Executing the custom container implementation failed. +Please contact your self hosted runner administrator. +``` +日志在 `12:50:13` 开始执行 `k8s/index.js` 但 1 小时后无任何进展: +- **定性**:类型 A +- **可能原因**:镜像拉取超时(网络抖动/仓库限流)、NPU 资源争用(节点上 NPU 全被占用)、K8s 容器编排脚本卡死(API Server 响应慢) +- **溯源**:查 Runner Pod 状态,反查物理节点,确认镜像拉取是否有失败事件 + +### 网络代理绕过(Install dependencies 阶段) +``` +Connecting to github.com|20.205.243.166|:443... connected. +Unable to establish SSL connection. +Error: command terminated with exit code 4 +``` +``` +ERROR 618: jwt:expired. +Error: command terminated with exit code 8 +``` +- **定性**:类型 A,基础设施/网络 +- **根因**:安装脚本用 `wget` 直连 `github.com`,未经集群内代理(如 `gh-proxy.test.osinfra.cn`)转发,导致 SSL 失败 / JWT 签名 URL 过期 +- **触发位置**:通常在 `scripts/ci/npu/npu_ci_install_dependency.sh` +- **修复方案 A(推荐)**:将下载 URL 替换为代理地址: + ```bash + GH_PROXY="https://gh-proxy.test.osinfra.cn" + wget "${GH_PROXY}/https://github.com/..." + ``` +- **修复方案 B**:将依赖预缓存到内部 cache-service,彻底脱离 GitHub 实时依赖 + +### OOM / 内存不足 +``` +Bus error (核心已转储) +Killed +exit code -9 +``` +- **定性**:类型 A +- **Bus error**:通常是 SHM(共享内存)不足,多机/多卡训练时常见 +- **Killed / exit code -9**:系统 OOM Killer 终止进程 +- **处理**:检查 Pod 的内存 limit 配置;检查是否有其他 Pod 占用了节点内存 + +### 多机任务连锁超时 +``` +[Rank X] Timeout waiting for ... +``` +- **定性**:类型 A,但需找主节点(Master / Rank 0) +- **分析**:某 Rank 报 Timeout 不一定是该 Rank 的问题——优先检查 **Master 节点**日志,是否有 `Unexpected Exit` 或驱动报错 + +### Nightly 资源调度不稳定(Worker 进程挂死) +``` +No available shared memory broadcast block found in 60 seconds. +EngineDeadError: EngineCore encountered an issue. +``` +- **定性**:类型 A,基础设施 / 资源调度不稳定 +- **背景**:Nightly 多机任务中,Worker 进程被调度抢占或 NPU 资源争用导致卡死,`shm_broadcast` 60s 超时连续触发,最终 EngineCore 死亡。`EngineDeadError` 本身不是根因,是连锁结果 +- **判断依据**:`shm_broadcast` 超时日志连续出现(每 60s 一次);任务为 Nightly(非 PR CI);早期推理吞吐日志正常,说明模型加载成功 +- **与代码 Bug 的区分**:代码 Bug 通常在启动阶段或首次推理即失败,且有明确的 Python 异常堆栈;此模式为运行一段时间后才崩溃,且无代码层异常 +- **处理**:直接重跑;若持续复现,检查对应节点的 NPU 调度竞争情况 + +### Runner 调度失败(Set up job 阶段) +``` +Waiting for runner... (timeout) +No runners available +``` +- **定性**:类型 A +- **分析**:查看 ARC Scale Set 是否有 listener Pod 在运行;查看 Pending Pod 数量;检查节点是否有 `SchedulingDisabled` + +--- + +## 代码类错误 + +### UnboundLocalError(典型陷阱) +``` +UnboundLocalError: cannot access local variable 'xxx' where it is not associated with a value +``` +- **定性**:类型 B,代码 Bug +- **典型模式**:`try: import lib except ImportError: pass`,之后直接使用 `lib` 变量,在 except 路径中 `lib` 未被赋值 +- **修复模式**: + ```python + try: + import huggingface_hub + _HF_HUB_AVAILABLE = True + except ImportError: + _HF_HUB_AVAILABLE = False + + # 使用时: + if _HF_HUB_AVAILABLE: + try: + ... + except huggingface_hub.errors.LocalEntryNotFoundError: + ... + ``` + +### 精度回归 +``` +AssertionError: 0.515 not greater than or equal to 0.59 +Accuracy of /root/.cache/.../ModelName is 0.515, is lower than 0.59 +``` +``` +FAILED tests/e2e/singlecard/test_quantization.py::test_qwen3_w8a8_quant - AssertionError: Test0: +tests/e2e/model_utils.py:53: AssertionError +``` +- **定性**:类型 C,精度回归 +- **分析方向**:找 PR 中对推理路径的改动(量化/反量化逻辑、speculative decoding、token 采样、接受逻辑) +- **注意**:`AssertionError: TestN:` 来自 `model_utils.py` 的输出正确性断言,也是精度回归,不是功能性代码 Bug;跌幅 > 5-13% 通常是功能性回归,不是随机波动 + +### YAML 语法错误 +``` +undefined variable "False" +``` +- **定性**:类型 D +- **根因**:YAML 中 `True`/`False` 应为小写 `true`/`false` + +### 测试断言失败 +``` +AssertionError +FAIL: TestXxx +Expected: xxx, Got: yyy +``` +- **分析步骤**: + 1. 判断是测试写错(预期值不合理)还是被测逻辑错 + 2. 对比 PR diff 找到变更点 + 3. 重跑确认是否 flaky + +--- + +### ModelScope 网络不稳定(Run test 阶段) +``` +Downloading model from modelscope... +ConnectionError: ('Connection aborted.', RemoteDisconnected('...')) +urllib3.exceptions.ReadTimeoutError +HTTPSConnectionPool: Read timed out +``` +- **定性**:类型 A,网络/基础设施 +- **背景**:vllm-ascend 测试使用 ModelScope 下载模型(`VLLM_USE_MODELSCOPE=True`),模型缓存在 `/root/.cache/modelscope/`。网络抖动会导致下载中断,表现为 Run test 阶段报网络超时而非代码异常 +- **判断依据**:失败在 Run test 早期(模型加载前),错误为网络类而非 Python 异常;其他 Job / 其他时间重跑可通过 +- **处理**:直接重跑。如频繁出现,检查集群到 ModelScope CDN 的网络质量;考虑将常用模型预缓存到节点本地,避免每次实时下载 + +### UT 卡死 / 超时(主线合入脏代码) +``` +# 无明显报错,Job 运行 2-3 小时后超时取消 +Canceling since a higher priority waiting request ... exists +# 或超时被 K8s 强制终止 +command terminated with exit code 255 +``` +- **定性**:类型 B,代码 Bug(通常是主线合入了有问题的代码) +- **背景**:UT 正常运行时间约 30-60 分钟。如果 UT 运行超过 2 小时仍无进展,通常是某个测试死锁或无限等待,而非基础设施问题 +- **与基础设施超时的区分**:基础设施超时发生在 `Initialize containers` 阶段;UT 卡死发生在 `Run unit test` 阶段,且 Job 已正常启动并运行了较长时间 +- **排查步骤**: + 1. 查看 UT 日志的最后输出,定位卡在哪个测试文件/函数 + 2. 检查最近合入 main 的 commit,是否包含涉及锁、进程通信、分布式初始化的改动 + 3. 本地运行该测试用例(单独运行,加超时)复现 +- **处理**:定位并 revert 引入死锁的 commit,或修复相关代码 + +### CPU Offload 概率性挂死 +``` +# 无异常堆栈,进程在 Run e2e test 阶段静默挂起 +Error executing in Docker Container: 1 +Executing the custom container implementation failed. +``` +- **定性**:类型 E,疑难/概率性问题 +- **背景**:CPU offload 功能在特定配置下存在概率性死锁,进程不报错但不返回,被容器超时机制终止,表现为 exit code 1 +- **与真实测试失败的区分**: + - 概率挂:日志在测试中途截断,无 `FAILED` / `AssertionError` 等 pytest 汇总行 + - 真实失败:有明确的 `FAILED tests/xxx.py::test_xxx` + 异常堆栈 +- **处理**:先重跑 1-2 次确认概率性;能通过则上报 CPU offload 模块追踪死锁路径 + +### 依赖解析失败(Install 阶段) +``` +× No solution found when resolving dependencies: +╰─▶ Because there is no version of modelscope==1.35.1 and you require + modelscope==1.35.1, we can conclude that your requirements are unsatisfiable. +``` +``` +ERROR: Could not find a version that satisfies the requirement xxx +ERROR: No matching distribution found for xxx +``` +- **定性**:类型 A,依赖源配置问题 +- **背景**:依赖版本在 PyPI / 镜像源中不存在,导致依赖解析失败,后续步骤连锁失败 +- **⚠️ 常见误判**:依赖解析失败后会产生大量 "error"、"failed" 级联日志(如 `find: '/usr/lib/python3': No such file`),容易被误判为 Python 环境问题或镜像缺失 +- **关键诊断**:搜索 `unsatisfiable|No solution found|no version of|requirements are unsatisfiable` +- **处理**:检查版本号是否正确、依赖是否已发布到配置的镜像源 + +### vllm-ascend 安装失败(Install 阶段 build error) +``` +Installing build dependencies: finished with status 'error' +error: subprocess-exited-with-error +× installing build dependencies did not run successfully. exit code: 1 +ERROR: Failed to build 'file:///__w/vllm-ascend/vllm-ascend/vllm-empty' +``` +- **定性**:类型 A 或 B,需区分 +- **背景**:`Install vllm-project/vllm-ascend` 或 `Install vllm from source` 阶段,csrc 编译或 build dependency 安装失败 +- **⚠️ 常见误判**:同步出现的 apt `403 Forbidden`(针对 `jammy-backports`/`jammy-security`)是次要现象,不是根因,这两个仓库为可选安全更新源,403 不影响核心依赖安装 +- **区分根因**: + - 检查 PR 是否修改了 csrc/CMakeLists/setup.py → 可能是 PR 引入(类型 B) + - 与最近主线无关、其他 PR 同期也失败 → 可能是环境依赖版本问题(类型 A) +- **处理**:在对应硬件(310p/A3)环境本地复现;检查 build dependency 版本兼容性 + +### CANN tiling 编译警告(⚠️ 不是根因) +``` +[ERROR][xxx] Register tiling func failed, please check the class name. +[ERROR][xxx] Get op tiling func failed, please check the soc version %d +[ERROR][xxx] Do op tiling failed, cannot find soc version. +``` +- **定性**:编译阶段正常输出,**不是测试失败根因** +- **背景**:这些日志来自 CANN SDK 的 tiling 注册过程,属于 DEBUG 级别的编译输出,不代表运行时失败 +- **⚠️ 常见误判**:这些日志数量大(可达数百行),容易被 grep 脚本捕获后误认为是根因 +- **识别方法**: + - 时间戳:在 `Install vllm-ascend` 阶段(编译期)出现 + - 日志级别:DEBUG 或普通 ERROR,无 Traceback + - 真正的根因需要往后找 `RuntimeError`、`AssertionError`、`exit code 255` +- **处理**:忽略 tiling 警告,继续分析测试阶段的运行时错误 + +### Git dubious ownership(Install 阶段) +``` +git introspection failed: fatal: detected dubious ownership in repository at '/__w/xxx/xxx' +``` +- **定性**:类型 A,基础设施 — Git 安全检查阻止构建 +- **背景**:Git 仓库所有者与当前用户不匹配,通常发生在容器内运行 CI Job 时 +- **关键诊断**:搜索 `dubious ownership` +- **处理**:在 workflow 或容器启动脚本中添加 `git config --global --add safe.directory '*'` + +### Triton Ascend 概率性挂死 +``` +# 无异常堆栈,进程静默挂起 +# 或: +Traceback (most recent call last): + ... +RuntimeError: [Triton] ... +# 或 Job 超时,无有意义的错误信息 +``` +- **定性**:类型 E,疑难/概率性问题 +- **背景**:triton_ascend 在特定算子或配置下存在概率性挂死,表现为进程不报错但永远不返回,最终被超时机制终止 +- **判断依据**: + - 重跑有时通过、有时挂 + - 失败点随机(不固定在某个测试) + - 日志截断,无完整的 Python 异常堆栈 + - 涉及 triton_ascend 编译或 kernel 执行阶段 +- **处理**: + 1. 先重跑 1-2 次确认是否概率性(能通过则大概率是此类问题) + 2. 记录卡死时最后执行的算子/测试,上报 triton_ascend 团队 + 3. 短期可跳过该测试或降低并发度规避;长期需 triton_ascend 侧修复 + +--- + +## Runner Pod 命名规范 + +ARC/K8s 环境中 Runner Pod 名称的结构: +``` +linux-aarch64-a3-2-x51bm-runner-ksnwc + ↑架构 ↑节点池 ↑ ↑随机后缀 + a3-2 = 节点池编号 + x51bm = 物理机标识(有时) +``` + +常见节点池:`a3-0`、`a3-2`、`a3-4` 等,对应不同的 ARC Runner Scale Set 和物理机组。 diff --git a/skills/infrastructure/github-action-diagnose/references/vllm-ascend.md b/skills/infrastructure/github-action-diagnose/references/vllm-ascend.md new file mode 100644 index 0000000..1608b8c --- /dev/null +++ b/skills/infrastructure/github-action-diagnose/references/vllm-ascend.md @@ -0,0 +1,152 @@ +# vllm-ascend 仓库专用参考 + +仓库:`vllm-project/vllm-ascend` +这是 vLLM 在华为昇腾(Ascend)NPU 上的适配层,基于 ARC/K8s + GitHub Actions。 + +--- + +## Runner 类型与节点池 + +vllm-ascend 有两种 runner,溯源方式不同: + +### 静态 Runner(固定节点,长期运行) +| Runner 名 | 硬件 | 用途 | +|-----------|------|------| +| `linux-aarch64-a2b3-0` | Atlas 800 A2B3 | 轻量 job(trigger 解析、变更检测) | +| `linux-aarch64-a2b3-1` | Atlas 800 A2B3 | E2E singlecard 测试 | +| `linux-aarch64-a3-2` | Atlas 800 A3 | A3 单机测试 | +| `linux-aarch64-a3-4` | Atlas 800 A3 | A3 4 卡测试 | +| `linux-aarch64-310p-1` | Atlas 300I Pro (310P) | 310P 单卡测试 | +| `linux-aarch64-310p-4` | Atlas 300I Pro (310P) | 310P 4 卡测试 | +| `linux-amd64-cpu-8-hk` | x86 CPU(香港) | lint / pre-commit(无 NPU) | + +静态 runner 出问题:pod 不会销毁,可直接 `kubectl get pod` 查节点。 + +### ARC 动态 Runner(K8s 弹性扩容) +用于 nightly 等高并发场景,pod 名格式: +``` +linux-aarch64-a3-2-x51bm-runner-ksnwc + ↑架构 ↑节点池 ↑随机后缀 +``` +节点池编号(`a3-0`、`a3-2` 等)对应不同物理机组。 +动态 runner 任务完成后 pod 会销毁,需从日志系统(Loki/ELK)查调度记录。 + +--- + +## 内部服务 + +这些是集群内部地址,出现在 workflow 日志中属于正常配置,**如果访问失败则是基础设施问题**: + +| 服务 | 地址 | 用途 | +|------|------|------| +| PyPI 缓存 | `http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple` | 所有 `uv pip install` 走这里 | +| apt 镜像 | `cache-service.nginx-pypi-cache.svc.cluster.local:8081` | apt 源替换 | +| GitHub 代理 | `https://gh-proxy.test.osinfra.cn` | git clone / wget GitHub 资源 | +| Ascend PyPI | `https://mirrors.huaweicloud.com/ascend/repos/pypi` | Ascend 专用包(CANN 等) | + +**关键诊断规则:** +- `Install dependencies` 失败 + 报 `cache-service` 连接超时 → **基础设施/集群内网问题**,不是代码问题 +- `wget github.com` 直连失败 → 脚本未走 `gh-proxy` → **脚本配置问题**(代码/CI维护者) +- `git clone github.com` 失败 → 检查是否配置了 `url insteadOf gh-proxy` + +--- + +## 主要 Workflow 及触发条件 + +| Workflow | 触发 | Runner | 说明 | +|---------|------|--------|------| +| `pr_test_light.yaml` | 每个 PR 到 main/*-dev/releases/v* | a2b3-1 + amd64 | lint + e2e-singlecard-light | +| `pr_test_full.yaml` | PR 打 label 或手动触发 | a2b3-1 + a3-2/4 | 完整测试套件 | +| `schedule_nightly_test_a3.yaml` | 每天 23:45 北京时间 | a3 动态 runner | nightly 全量测试 | +| `_pre_commit.yml` | 被上述 workflow 调用 | `linux-amd64-cpu-8-hk` | lint:actionlint / markdownlint / mypy | + +**PR CI 触发变更检测逻辑(重要):** +PR CI 中有一个 `changes` job,用 `dorny/paths-filter` 判断哪些测试需要跑: +- 改了 `vllm_ascend/**`、`tests/e2e/**`、`CMakeLists.txt`、`setup.py` 等 → 触发 e2e 测试 +- 只改了 `tests/ut/**` → 只触发 unit test +- 只改了文档等 → 只跑 lint + +因此:**如果测试没有被触发,先检查变更文件是否命中了对应的 path filter**,不要误判为 CI 基础设施问题。 + +--- + +## 测试套件结构 + +测试用 `run_suite.py` 统一调度,套件定义在 `.github/workflows/scripts/config.yaml`: + +| 套件名 | 说明 | +|--------|------| +| `e2e-singlecard-light` | PR CI 默认跑的轻量单卡测试 | +| `e2e-singlecard` | 完整单卡测试 | +| `e2e-2card-light` | 2 卡轻量测试 | +| `e2e-multicard-2-cards` | 2 卡多卡测试 | +| `e2e-multicard-4-cards` | 4 卡多卡测试 | + +测试文件路径:`tests/e2e/singlecard/`、`tests/e2e/multicard/`、`tests/ut/` + +**定位具体失败用例:** +```bash +# 从 gh 日志中找到失败的具体 test 文件和函数名 +gh run view --job --log-failed --repo vllm-project/vllm-ascend | grep -E "FAILED|ERROR|assert" +``` + +--- + +## 环境与依赖特殊说明 + +### 包管理器:uv(不是 pip) +所有依赖安装用 `uv pip install`,报错格式与 pip 略有不同: +``` +# uv 典型报错 +error: Distribution ... not found (no matching distribution) +error: Failed to download ... Connection refused +``` + +### 模型来源:ModelScope(不是 HuggingFace) +所有测试环境设置了: +``` +VLLM_USE_MODELSCOPE: True +HF_HUB_OFFLINE: 1 +``` +模型缓存在 runner 节点的 `/root/.cache/modelscope/`(不是 `~/.cache/huggingface/`)。 +精度回归分析时,模型路径格式为: +``` +/root/.cache/modelscope/hub/xxx/ModelName +``` + +### CANN 环境激活 +Workflow 统一配置了: +```yaml +defaults: + run: + shell: bash -el {0} +``` +这是因为 CANN toolkit 的环境变量需要通过 shell profile 激活。如果某个 step 没有用这个 shell 配置,可能导致 NPU 驱动不可用。 + +--- + +## 常见 PR CI 失败场景速查 + +### Lint 失败(pre-commit job) +- **Runner**:`linux-amd64-cpu-8-hk`(CPU,无 NPU,不需要节点溯源) +- **常见错误**:mypy 类型检查失败、markdownlint 格式问题、actionlint workflow 语法问题 +- **责任方**:PR 作者 +- **修复**:本地运行 `pre-commit run --all-files` 复现 + +### Install dependencies 失败 +- 先判断是哪个依赖源出问题:`cache-service`(基础设施)还是 GitHub 直连(脚本配置) +- `uv pip install` 从 `cache-service` 失败 → 基础设施问题,重跑 +- `wget`/`git clone` 从 `github.com` 直连失败 → 检查 gh-proxy 配置 + +### 测试精度回归 +vllm-ascend 的精度测试会检查模型推理结果是否达到阈值,常见断言: +```python +assert accuracy >= threshold, f"Accuracy of {model_path} is {accuracy}, is lower than {threshold}" +``` +- 跌幅 < 3%:可能是随机性,先确认是否 flaky(重跑) +- 跌幅 > 5%:PR 引入了功能性回归,检查 speculative decoding、采样逻辑、kernel 改动 + +### 多机任务失败(nightly multi-node) +- nightly A3 的多机测试中,Rank N timeout 不代表 Rank N 出问题 +- 排查顺序:Master(Rank 0)→ 其他 Rank +- 检查 Master 的日志:`Unexpected Exit`、驱动报错、`ERR99999` diff --git a/skills/infrastructure/github-action-diagnose/scripts/fetch-run.sh b/skills/infrastructure/github-action-diagnose/scripts/fetch-run.sh new file mode 100644 index 0000000..cbfcac7 --- /dev/null +++ b/skills/infrastructure/github-action-diagnose/scripts/fetch-run.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# scripts/fetch-run.sh [owner/repo] +# +# 用途:一次性抓取 CI Run 所有失败 Job 的 runner 信息和关键日志 +# 将每个 Job 原始日志(可达数百行)压缩至 ~20 行关键错误行 +# 在 LLM 进行 Step 2+ 推理之前完成所有数据收集,节省 token +# +# 用法: +# bash scripts/fetch-run.sh 22935910341 # Run ID +# bash scripts/fetch-run.sh 22935910341 owner/repo # Run ID + repo +# bash scripts/fetch-run.sh --job 67875153370 owner/repo # 单个 Job ID +# +# 输出: +# - Run 概览(Job 状态列表 + Annotations) +# - 每个失败 Job:runner 名称 + 预过滤的关键日志(~20 行) +# - cancelled Job:提示去看 Annotations(queue 抢占等无需读日志) +# +# 依赖:gh CLI(已登录) + +set -uo pipefail + +# 解析参数 +JOB_ID="" +RUN_ID="" +REPO="vllm-project/vllm-ascend" + +# 解析参数:支持 "--job [repo]" 或 " [repo]" +if [[ "$1" == "--job" ]]; then + JOB_ID="$2" + REPO="${3:-vllm-project/vllm-ascend}" +else + RUN_ID="${1:-}" + REPO="${2:-vllm-project/vllm-ascend}" +fi + +if [[ -n "$JOB_ID" ]]; then + # 单 Job 模式 + echo "════════════════════════════════════════════" + echo " Single Job Mode | $REPO" + echo "════════════════════════════════════════════" + + # ── Step A:基本信息(Runner / 时间 / 结论)────────────────────────────────── + JOB_META=$(gh api "repos/$REPO/actions/jobs/$JOB_ID" 2>/dev/null) + echo "$JOB_META" | \ + python3 -c " +import json,sys +d=json.load(sys.stdin) +print(f'── [{d[\"conclusion\"].upper()}] {d[\"name\"]}') +print(f' Runner : {d.get(\"runner_name\") or \"(unknown)\"}') +print(f' 时间 : {d.get(\"started_at\",\"?\")} ~ {d.get(\"completed_at\",\"?\")}') +steps=[s for s in d.get('steps',[]) if s.get('conclusion') not in ('success','skipped',None)] +if steps: + print(' ── 失败步骤') + for s in steps: + print(f' [{s[\"conclusion\"].upper():8}] Step {s[\"number\"]}: {s[\"name\"]}') +" + + # ── Step B:PR 上下文 ───────────────────────────────────────────────────────── + RUN_ID_FOR_JOB=$(echo "$JOB_META" | python3 -c "import json,sys; print(json.load(sys.stdin)['run_id'])" 2>/dev/null || echo "") + if [[ -n "$RUN_ID_FOR_JOB" ]]; then + RUN_META=$(gh api "repos/$REPO/actions/runs/$RUN_ID_FOR_JOB" 2>/dev/null) + PR_NUM=$(echo "$RUN_META" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['pull_requests'][0]['number'] if d.get('pull_requests') else '')" 2>/dev/null || echo "") + RUN_TITLE=$(echo "$RUN_META" | python3 -c "import json,sys; print(json.load(sys.stdin).get('display_title',''))" 2>/dev/null || echo "") + RUN_BRANCH=$(echo "$RUN_META" | python3 -c "import json,sys; print(json.load(sys.stdin).get('head_branch',''))" 2>/dev/null || echo "") + echo " PR : ${PR_NUM:+#$PR_NUM }${RUN_TITLE} (${RUN_BRANCH})" + if [[ -n "$PR_NUM" ]]; then + echo " ── PR 变更文件(分类参考)" + gh pr diff "$PR_NUM" --repo "$REPO" --name-only 2>/dev/null | head -20 | sed 's/^/ /' + fi + fi + + # ── Step C:Annotations(通常直接揭示根因,如 ETIMEDOUT / exit code)───────── + echo "" + echo " ── Annotations" + ANNOTATIONS=$(gh api "repos/$REPO/check-runs/$JOB_ID/annotations" 2>/dev/null) + ANN_COUNT=$(echo "$ANNOTATIONS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0") + if [[ "$ANN_COUNT" -gt 0 ]]; then + echo "$ANNOTATIONS" | python3 -c " +import json,sys +for a in json.load(sys.stdin): + print(f' [{a.get(\"annotation_level\",\"?\").upper()}] {a.get(\"message\",\"\").splitlines()[0]}') +" + else + echo " (无 Annotations)" + fi + + # ── Step D:判断是否需要拉完整日志 ─────────────────────────────────────────── + # Annotations 中包含明确错误信号时,跳过日志拉取(节省时间) + ANN_TEXT=$(echo "$ANNOTATIONS" | python3 -c "import json,sys; print(' '.join(a.get('message','') for a in json.load(sys.stdin)))" 2>/dev/null || echo "") + NEEDS_LOG=true + if echo "$ANN_TEXT" | grep -qiE 'ETIMEDOUT|timeout|failed to create artifact|network|connection refused'; then + echo "" + echo " ── Annotations 已明确根因(网络/超时类),跳过完整日志拉取" + NEEDS_LOG=false + fi + + if $NEEDS_LOG; then + FULL_LOG=$(gh api "repos/$REPO/actions/jobs/$JOB_ID/logs" 2>&1) + + echo "" + echo " ── 运行时错误(高优先级)" + echo "$FULL_LOG" \ + | grep -iE 'RuntimeError|OutOfMemoryError|NPU out of memory|AssertionError|EngineDeadError|EngineCore encountered|shm_broadcast.*60 seconds|No available shared memory broadcast|Process completed with exit code [1-9]|exit code 255|synchronized memcpy failed|NPU function error|error code is [0-9]|httpcore\.(ReadError|ConnectError)|httpx\.(ReadError|ConnectError)|RemoteProtocolError' \ + | grep -v -E 'ops_error\.h|error_check\.h' \ + | head -15 + + RUNTIME_COUNT=$(echo "$FULL_LOG" | grep -cE 'RuntimeError|OutOfMemoryError|NPU out of memory|AssertionError|EngineDeadError|shm_broadcast.*60 seconds' || true) + if [[ "$RUNTIME_COUNT" -eq 0 ]]; then + echo " ── 编译/安装错误(次优先级)" + echo "$FULL_LOG" \ + | grep -iE 'error:.*failed|error:.*exit|fatal error|ERROR.*install|No solution found|unsatisfiable|ERROR.*Build|dubious ownership' \ + | grep -v -E '##\[group\]|##\[endgroup\]|warning.*format|ops_error\.h|error_check\.h|tiling func|tiling failed' \ + | head -5 + fi + + echo " ── 最后输出(定位失败阶段)" + echo "$FULL_LOG" \ + | grep -E '^[^[:space:]]+[[:space:]]+(UNKNOWN STEP|Run python|bash|pytest)' \ + | tail -5 + fi + + exit 0 +fi + +if [[ -z "$RUN_ID" ]]; then + echo "用法: fetch-run.sh [owner/repo]" + echo " 或: fetch-run.sh --job [owner/repo]" + exit 1 +fi + +SEP="════════════════════════════════════════════" + +# ── 1. Run 概览(Job 列表 + Annotations)──────────────────────────────────── +echo "$SEP" +echo " Run $RUN_ID | $REPO" +echo "$SEP" +gh run view "$RUN_ID" --repo "$REPO" +echo "" + +# ── PR 上下文(用于分类时区分代码 Bug 与基础设施)────────────────────────── +PR_NUM=$(gh api "repos/$REPO/actions/runs/$RUN_ID" \ + --jq '.pull_requests[0].number // empty' 2>/dev/null || echo "") +if [[ -n "$PR_NUM" ]]; then + echo "$SEP" + echo " PR #$PR_NUM 变更文件(分类参考)" + echo "$SEP" + gh pr diff "$PR_NUM" --repo "$REPO" --name-only 2>/dev/null | head -20 + echo "" +fi + +# ── 2. 获取失败 / 取消的 Job ID ────────────────────────────────────────────── +FAILED_IDS=$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \ + --jq '.jobs[] | select(.conclusion == "failure" or .conclusion == "cancelled") | .id') + +if [[ -z "$FAILED_IDS" ]]; then + echo "(未发现失败或取消的 Job)" + exit 0 +fi + +TOTAL=$(echo "$FAILED_IDS" | wc -l | tr -d ' ') +echo "$SEP" +echo " 失败 / 取消 Job 详情(共 ${TOTAL} 个)" +echo "$SEP" + +# ── 3. 逐 Job 输出 runner 信息 + 关键日志 ──────────────────────────────────── +for JOB_ID in $FAILED_IDS; do + echo "" + + # Runner 信息(一次 API 调用,获取名称 / 结论 / 时间) + gh api "repos/$REPO/actions/jobs/$JOB_ID" \ + --jq '"── [\(.conclusion | ascii_upcase)] \(.name)\n Runner : \(.runner_name // "(unknown)")\n 时间 : \(.started_at // "?") ~ \(.completed_at // "?")"' + + CONCLUSION=$(gh api "repos/$REPO/actions/jobs/$JOB_ID" --jq '.conclusion') + + if [[ "$CONCLUSION" == "failure" ]]; then + FULL_LOG=$(gh run view --job "$JOB_ID" --log --repo "$REPO" 2>&1) + + echo " ── 运行时错误(高优先级)" + # 同时捕获 CANN 底层错误(aclrtMemcpy/error code),便于区分硬件 vs 代码兼容性 + echo "$FULL_LOG" \ + | grep -iE 'RuntimeError|OutOfMemoryError|NPU out of memory|AssertionError|EngineDeadError|EngineCore encountered|shm_broadcast.*60 seconds|No available shared memory broadcast|Process completed with exit code [1-9]|exit code 255|synchronized memcpy failed|NPU function error|error code is [0-9]|httpcore\.(ReadError|ConnectError)|httpx\.(ReadError|ConnectError)|RemoteProtocolError' \ + | grep -v -E 'ops_error\.h|error_check\.h' \ + | head -15 + + RUNTIME_COUNT=$(echo "$FULL_LOG" | grep -cE 'RuntimeError|OutOfMemoryError|NPU out of memory|AssertionError|EngineDeadError|shm_broadcast.*60 seconds' || true) + if [[ "$RUNTIME_COUNT" -eq 0 ]]; then + echo " ── 编译/安装错误(次优先级)" + echo "$FULL_LOG" \ + | grep -iE 'error:.*failed|error:.*exit|fatal error|ERROR.*install|No solution found|unsatisfiable|ERROR.*Build|dubious ownership' \ + | grep -v -E '##\[group\]|##\[endgroup\]|warning.*format|ops_error\.h|error_check\.h|tiling func|tiling failed' \ + | head -5 + fi + + # 显示 Job 最后阶段的关键标识 + echo " ── 最后输出(定位失败阶段)" + echo "$FULL_LOG" \ + | grep -E '^[^[:space:]]+[[:space:]]+(UNKNOWN STEP|Run python|bash|pytest)' \ + | tail -5 + + else + echo " (cancelled — 根因见 Run 概览中的 ANNOTATIONS,通常为 queue 抢占)" + fi +done + +echo "" +echo "$SEP" +echo " 补充命令(按需使用):" +echo " 完整日志 : gh run view --job --log --repo $REPO" +echo " PR diff : gh pr diff --repo $REPO --name-only" +echo " kubectl : kubectl get pods --all-namespaces | grep " +echo "$SEP" From 35492ada9b2e00944ee56d80fbe67acb8c576521 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 2 Apr 2026 20:32:51 +0800 Subject: [PATCH 02/31] feat(claude-bot): switch to repository_dispatch with centralized API key Co-Authored-By: Claude Sonnet 4.6 --- .github/allowed-callers.json | 9 + .github/workflows/_claude-code.yml | 544 ++++++++++++------------ .github/workflows/embeddable-caller.yml | 91 +++- .github/workflows/example-caller.yml | 111 +++-- 4 files changed, 432 insertions(+), 323 deletions(-) create mode 100644 .github/allowed-callers.json diff --git a/.github/allowed-callers.json b/.github/allowed-callers.json new file mode 100644 index 0000000..6081296 --- /dev/null +++ b/.github/allowed-callers.json @@ -0,0 +1,9 @@ +{ + "allowed_orgs": [ + "opensourceways" + ], + "allowed_repos": [ + "KadenZhang3321/agent-skills", + "KadenZhang3321/hello-world" + ] +} diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index d777c39..dd2746a 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -1,336 +1,342 @@ # _claude-code.yml -# 核心可复用 Claude Code 工作流,供组织内其他仓库通过 workflow_call 调用。 -# 支持通过 @claude 触发或在其他工作流中直接调用。 -# 调用方需在 workflow 级别声明以下权限: -# contents: read -# pull-requests: write -# issues: write -# contents: write (如果需要 Claude 修改代码) +# Claude Code Handler — 由白名单仓库通过 repository_dispatch 触发。 +# +# 本仓库(opensourceways/agent-skills)需配置以下 Secrets: +# CLAUDE_API_KEY — Anthropic API Key(集中管理,调用方无需配置) +# AGENT_PAT — 具备 repo 权限的 PAT(用于读写调用方仓库、发布评论、创建 PR) -name: Claude Code (Reusable) +name: Claude Code Handler on: - workflow_call: - inputs: - # API Key(由调用方显式传入,不再使用 inherit) - claude_api_key: - description: 'Anthropic API Key' - type: string - required: true - # Claude 模型名称 - model: - description: 'Claude model to use (e.g. claude-sonnet-4-20250514, claude-opus-4-20251114)' - type: string - default: 'claude-sonnet-4-20250514' - # 单次运行的最大超时时间(分钟) - timeout_minutes: - description: 'Job timeout in minutes' - type: number - default: 60 - # 可选的初始化脚本内容(shell 命令,多行用换行分隔) - setup_script: - description: 'Optional setup script to run before invoking Claude (e.g. pip install pytest)' - type: string - default: '' - # 传给 Claude CLI 的额外参数(如 skill 名称) - additional_claude_args: - description: 'Additional arguments passed to Claude CLI (e.g. --skill my-skill)' - type: string - default: '' - # 是否允许 Claude 修改代码(编辑/写入文件) - allow_code_change: - description: 'Allow Claude to edit/write files' - type: boolean - default: false - # 组织名称(用于成员检查)。留空则跳过组织检查,适用于个人仓库测试。 - org_name: - description: 'GitHub organization name for membership check. Leave empty to skip.' - type: string - default: '' + repository_dispatch: + types: [claude-request] jobs: claude-code: - name: Invoke Claude Code + name: Handle Claude Request runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.timeout_minutes }} - # 触发条件: - # - workflow_call: 其他工作流直接调用 - # - issue_comment: PR/Issue 评论中包含 @claude - # - issues: 新建 Issue 时自动触发 - # 排除机器人账号 - if: | - ( - github.event_name == 'workflow_call' || - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - github.event_name == 'issues' - ) && - !endsWith(github.actor, '[bot]') && - github.actor != 'dependabot' + timeout-minutes: 60 steps: - # --------------------------------------------------------------- - # 0. 白名单检查(硬编码,只有授权仓库可以调用) - # --------------------------------------------------------------- - - name: Check whitelist - id: check_whitelist - run: | - REPO="${{ github.repository }}" - ORG="${{ github.repository_owner }}" - - # 硬编码白名单 - 只有这些仓库可以调用 - ALLOWED_REPOS="KadenZhang3321/agent-skills,KadenZhang3321/hello-world" - - REPO_ALLOWED="false" - IFS=',' read -ra REPO_ARRAY <<< "$ALLOWED_REPOS" - for allowed_repo in "${REPO_ARRAY[@]}"; do - if [[ "$allowed_repo" == "$REPO" ]]; then - REPO_ALLOWED="true" - break - fi - done - - if [[ "$REPO_ALLOWED" != "true" ]]; then - echo "::error::Repository $REPO is not in whitelist." - echo "Allowed repos: $ALLOWED_REPOS" - exit 1 - fi - - echo "::notice::Repository $REPO is whitelisted" - - # --------------------------------------------------------------- - # 1. 安全检查(组织成员 + 写权限) - # --------------------------------------------------------------- + # ------------------------------------------------------------------ + # 0. Checkout agent-skills(用于读取白名单和 Skill 文件) + # ------------------------------------------------------------------ + - name: Checkout agent-skills + uses: actions/checkout@v4 + with: + path: agent-skills - # 1a. 检查触发者是否为组织成员 - - name: Check organization membership - id: check_org - if: inputs.org_name != '' + # ------------------------------------------------------------------ + # 1. 白名单校验 + # ------------------------------------------------------------------ + - name: Check caller whitelist env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ACTOR: ${{ github.actor }} - ORG: ${{ inputs.org_name }} - run: | - HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: Bearer $GH_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/orgs/$ORG/members/$ACTOR") - - if [[ "$HTTP_STATUS" == "204" ]]; then - echo "is_member=true" >> "$GITHUB_OUTPUT" - echo "::notice::$ACTOR is a member of $ORG." - else - echo "is_member=false" >> "$GITHUB_OUTPUT" - echo "::warning::$ACTOR is NOT a member of $ORG (HTTP $HTTP_STATUS). Access denied." - fi - - - name: Exit if not org member - if: inputs.org_name != '' && steps.check_org.outputs.is_member == 'false' + CALLER_REPO: ${{ github.event.client_payload.caller_repo }} run: | - echo "Security check failed: actor is not an organization member." - exit 1 + python3 - <<'PYEOF' + import json, os, sys + caller_repo = os.environ['CALLER_REPO'] + caller_org = caller_repo.split('/')[0] + with open('agent-skills/.github/allowed-callers.json') as f: + config = json.load(f) + if caller_org in config.get('allowed_orgs', []) or caller_repo in config.get('allowed_repos', []): + print(f'Caller {caller_repo} is whitelisted') + else: + print(f'::error::Caller {caller_repo} is not in the whitelist. ' + f'Update .github/allowed-callers.json to grant access.') + sys.exit(1) + PYEOF - # 1b. 检查触发者是否拥有仓库写权限 - - name: Check repository write permission - id: check_permission + # ------------------------------------------------------------------ + # 2. 检查触发者在调用方仓库的权限(需要 write/admin/maintain) + # ------------------------------------------------------------------ + - name: Check user permission env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ACTOR: ${{ github.actor }} - REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.AGENT_PAT }} + CALLER_REPO: ${{ github.event.client_payload.caller_repo }} + ACTOR: ${{ github.event.client_payload.comment_author }} run: | - PERMISSION=$(curl -s \ + PERMISSION=$(curl -sf \ -H "Authorization: Bearer $GH_TOKEN" \ -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/$REPO/collaborators/$ACTOR/permission" \ - | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('permission','none'))") - - echo "permission=$PERMISSION" >> "$GITHUB_OUTPUT" + "https://api.github.com/repos/$CALLER_REPO/collaborators/$ACTOR/permission" \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','none'))") - if [[ "$PERMISSION" == "write" ]] || [[ "$PERMISSION" == "admin" ]]; then - echo "has_write=true" >> "$GITHUB_OUTPUT" - echo "::notice::$ACTOR has $PERMISSION permission." + if [[ "$PERMISSION" == "write" || "$PERMISSION" == "admin" || "$PERMISSION" == "maintain" ]]; then + echo "::notice::$ACTOR has $PERMISSION permission on $CALLER_REPO" else - echo "has_write=false" >> "$GITHUB_OUTPUT" - echo "::warning::$ACTOR has insufficient permission: $PERMISSION" + echo "::error::$ACTOR has insufficient permission ($PERMISSION) on $CALLER_REPO" + exit 1 fi - - name: Exit if insufficient permission - if: steps.check_permission.outputs.has_write == 'false' - run: | - echo "Security check failed: actor does not have write permission." - exit 1 - - # --------------------------------------------------------------- - # 2. 准备环境 - # --------------------------------------------------------------- - - name: Checkout repository + # ------------------------------------------------------------------ + # 3. Checkout 调用方仓库 + # ------------------------------------------------------------------ + - name: Checkout caller repo uses: actions/checkout@v4 with: - fetch-depth: 1 + repository: ${{ github.event.client_payload.caller_repo }} + token: ${{ secrets.AGENT_PAT }} + path: caller-repo + fetch-depth: 0 - - name: Set up Node.js + # ------------------------------------------------------------------ + # 4. 安装 Claude Code CLI + # ------------------------------------------------------------------ + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - # --------------------------------------------------------------- - # 3. 安装 Claude Code CLI - # --------------------------------------------------------------- - name: Install Claude Code run: | - # 使用 npm 安装 npm install -g @anthropic-ai/claude-code - - # 验证安装 claude --version - # --------------------------------------------------------------- - # 4. 运行 setup_script - # --------------------------------------------------------------- - - name: Run setup script - if: inputs.setup_script != '' + # ------------------------------------------------------------------ + # 5. 拉取 PR Diff(如果是 PR 评论) + # ------------------------------------------------------------------ + - name: Fetch PR diff + if: github.event.client_payload.pr_number != '' + env: + GH_TOKEN: ${{ secrets.AGENT_PAT }} + CALLER_REPO: ${{ github.event.client_payload.caller_repo }} + PR_NUMBER: ${{ github.event.client_payload.pr_number }} run: | - echo "Running custom setup script..." - eval "${{ inputs.setup_script }}" + curl -sf \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github.v3.diff" \ + "https://api.github.com/repos/$CALLER_REPO/pulls/$PR_NUMBER" \ + -o /tmp/pr.diff \ + || touch /tmp/pr.diff + echo "PR diff: $(wc -c < /tmp/pr.diff) bytes" - # --------------------------------------------------------------- - # 5. 构建 prompt - # --------------------------------------------------------------- + # ------------------------------------------------------------------ + # 6. 构建 prompt + # ------------------------------------------------------------------ - name: Build prompt - id: build_prompt env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - EVENT_NAME: ${{ github.event_name }} - ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} - ISSUE_TITLE: ${{ github.event.issue.title || github.event.pull_request.title }} - ISSUE_BODY: ${{ github.event.issue.body || github.event.pull_request.body }} - COMMENT_BODY: ${{ github.event.comment.body }} - COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - ADDITIONAL_ARGS: ${{ inputs.additional_claude_args }} + CALLER_REPO: ${{ github.event.client_payload.caller_repo }} + PR_NUMBER: ${{ github.event.client_payload.pr_number }} + COMMENT_BODY: ${{ github.event.client_payload.comment_body }} + COMMENT_AUTHOR: ${{ github.event.client_payload.comment_author }} + SKILL_NAME: ${{ github.event.client_payload.skill_name }} + ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} run: | python3 - <<'PYEOF' - import os, json - - event_name = os.environ.get("EVENT_NAME", "") - issue_number = os.environ.get("ISSUE_NUMBER", "") - issue_title = os.environ.get("ISSUE_TITLE", "") - issue_body = os.environ.get("ISSUE_BODY", "") or "" - comment_body = os.environ.get("COMMENT_BODY", "") or "" - comment_author = os.environ.get("COMMENT_AUTHOR", "") - additional_args = os.environ.get("ADDITIONAL_ARGS", "") or "" - repo = os.environ.get("REPO", "") - - parts = [] - if issue_title: - parts.append(f"## Issue/PR #{issue_number}: {issue_title}") - if issue_body.strip(): - parts.append(f"### Description\n{issue_body.strip()}") + import os + + caller_repo = os.environ.get('CALLER_REPO', '') + pr_number = os.environ.get('PR_NUMBER', '') or '' + comment_body = os.environ.get('COMMENT_BODY', '') or '' + comment_author = os.environ.get('COMMENT_AUTHOR', '') + skill_name = os.environ.get('SKILL_NAME', '') or '' + allow_code_change = os.environ.get('ALLOW_CODE_CHANGE', 'false').lower() == 'true' + + # 加载 Skill 指令 + skill_prompt = '' + if skill_name: + skill_path = f'agent-skills/skills/{skill_name}/SKILL.md' + try: + skill_prompt = open(skill_path).read() + except FileNotFoundError: + print(f'Warning: skill not found: {skill_path}') + + parts = [f'You are a code assistant for repository **{caller_repo}**.'] + + if skill_prompt: + parts.append(f'## Skill Instructions\n\n{skill_prompt}') + + if pr_number: + parts.append(f'You are reviewing **PR #{pr_number}** in {caller_repo}.') + try: + diff = open('/tmp/pr.diff').read().strip() + if diff: + if len(diff) > 100_000: + diff = diff[:100_000] + '\n\n[diff truncated at 100KB]' + parts.append(f'## PR Diff\n\n```diff\n{diff}\n```') + except FileNotFoundError: + pass + if comment_body.strip(): - parts.append(f"### Comment by @{comment_author}\n{comment_body.strip()}") - if additional_args.strip(): - parts.append(f"### Additional context\n{additional_args.strip()}") + parts.append(f'## Request from @{comment_author}\n\n{comment_body.strip()}') + + if allow_code_change: + parts.append( + 'The repository is checked out in the current working directory.\n' + 'You are allowed to **edit files** to fix issues.\n' + 'After making changes, summarize what you changed and why.' + ) + else: + parts.append( + 'The repository is checked out in the current working directory.\n' + 'Provide a detailed code review with specific, actionable suggestions.\n' + 'Do NOT modify any files.' + ) - prompt = "\n\n".join(parts) + prompt = '\n\n'.join(parts) - env_file = os.environ.get("GITHUB_ENV", "") - if env_file: - with open(env_file, "a") as f: - f.write("CLAUDE_PROMPT</tmp/claude_stderr.txt) + EXIT_CODE=$? + + echo "$CLAUDE_OUTPUT" > /tmp/claude_output.txt + echo "$CLAUDE_OUTPUT" + if git status --porcelain | grep -q .; then echo "code_changed=true" >> "$GITHUB_OUTPUT" - echo "::notice::Claude made some file changes" else echo "code_changed=false" >> "$GITHUB_OUTPUT" fi - # --------------------------------------------------------------- - # 7. 自动提交代码变更(如果允许) - # --------------------------------------------------------------- - - name: Commit and push changes - if: inputs.allow_code_change == true && steps.claude_run.outputs.code_changed == 'true' + exit $EXIT_CODE + + # ------------------------------------------------------------------ + # 8. 新建分支 + 提交 + 开 PR(仅当 Claude 修改了代码时) + # ------------------------------------------------------------------ + - name: Create PR with Claude's changes + if: steps.claude_run.outputs.code_changed == 'true' && github.event.client_payload.allow_code_change == 'true' + id: create_pr env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.AGENT_PAT }} + CALLER_REPO: ${{ github.event.client_payload.caller_repo }} + ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }} + working-directory: caller-repo run: | - # 配置 git - git config --global user.name "Claude Code Bot" - git config --global user.email "claude-bot@users.noreply.github.com" - - # 添加所有变更 + git config user.name "Claude Code Bot" + git config user.email "claude-bot@users.noreply.github.com" + + BRANCH="claude-fix-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$BRANCH" git add -A - - # 检查是否有变更需要提交 - if git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - - # 提交 - git commit -m "AI: Code changes by Claude Code" - - # 推送 - git push - - # --------------------------------------------------------------- - # 8. 获取 Claude 输出并发布评论 - # --------------------------------------------------------------- - - name: Get Claude output - id: get_output + git commit -m "fix: code improvements by Claude Code Bot" + git push "https://x-access-token:${GH_TOKEN}@github.com/${CALLER_REPO}.git" "$BRANCH" + + DEFAULT_BRANCH=$(curl -sf \ + -H "Authorization: Bearer $GH_TOKEN" \ + "https://api.github.com/repos/$CALLER_REPO" \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('default_branch','main'))") + + export PR_BRANCH="$BRANCH" + export PR_BASE="$DEFAULT_BRANCH" + export PR_ISSUE="$ISSUE_NUMBER" + + PR_URL=$(python3 - <<'PYEOF' + import json, os, subprocess + payload = json.dumps({ + 'title': 'fix: Claude Code suggested improvements', + 'body': ( + 'Automatically generated by Claude Code Bot.\n\n' + f'Related to #{os.environ["PR_ISSUE"]}' + ), + 'head': os.environ['PR_BRANCH'], + 'base': os.environ['PR_BASE'], + }) + r = subprocess.run([ + 'curl', '-sf', '-X', 'POST', + '-H', f'Authorization: Bearer {os.environ["GH_TOKEN"]}', + '-H', 'Accept: application/vnd.github+json', + f'https://api.github.com/repos/{os.environ["CALLER_REPO"]}/pulls', + '-d', payload, + ], capture_output=True, text=True) + data = json.loads(r.stdout) + print(data.get('html_url', '')) + PYEOF + ) + + echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + echo "::notice::PR created: $PR_URL" + + # ------------------------------------------------------------------ + # 9. 将 Claude 的回复发布为评论 + # ------------------------------------------------------------------ + - name: Post comment + if: always() + env: + GH_TOKEN: ${{ secrets.AGENT_PAT }} + CALLER_REPO: ${{ github.event.client_payload.caller_repo }} + ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }} + PR_URL: ${{ steps.create_pr.outputs.pr_url }} run: | - # 尝试从日志中提取 Claude 的输出 - # 由于 CLI 输出在日志中,这里记录触发信息 - echo " Claude execution completed" >> "$GITHUB_STEP_SUMMARY" - echo "::notice::Claude Code execution completed" - - # --------------------------------------------------------------- - # 9. Token 用量报告 - # --------------------------------------------------------------- - - name: Report execution status + python3 - <<'PYEOF' + import json, os, subprocess + + try: + output = open('/tmp/claude_output.txt').read().strip() + except FileNotFoundError: + output = '' + + if not output: + output = '_Claude returned an empty response._' + + pr_url = os.environ.get('PR_URL', '') + + body = f'## Claude Code Analysis\n\n{output}' + if pr_url: + body += f'\n\n---\nI made code changes and opened a PR: {pr_url}' + + comment = json.dumps({'body': body}) + repo = os.environ['CALLER_REPO'] + issue = os.environ['ISSUE_NUMBER'] + token = os.environ['GH_TOKEN'] + + r = subprocess.run([ + 'curl', '-sf', '-X', 'POST', + '-H', f'Authorization: Bearer {token}', + '-H', 'Accept: application/vnd.github+json', + f'https://api.github.com/repos/{repo}/issues/{issue}/comments', + '-d', comment, + ], capture_output=True, text=True) + + if r.returncode == 0: + print('Comment posted successfully') + else: + print(f'::warning::Failed to post comment: {r.stderr}') + PYEOF + + # ------------------------------------------------------------------ + # 10. Job 执行摘要 + # ------------------------------------------------------------------ + - name: Write job summary if: always() run: | - echo "### Claude Code Execution" >> "$GITHUB_STEP_SUMMARY" + echo "### Claude Code Run Summary" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Status | " >> "$GITHUB_STEP_SUMMARY" - echo "|--------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Model | \`${{ inputs.model }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Code changes | ${{ steps.claude_run.outputs.code_changed }} |" >> "$GITHUB_STEP_SUMMARY" \ No newline at end of file + echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" + echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" + echo "| Caller repo | \`${{ github.event.client_payload.caller_repo }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Triggered by | \`${{ github.event.client_payload.comment_author }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| PR | \`${{ github.event.client_payload.pr_number || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Model | \`${{ github.event.client_payload.model || 'claude-sonnet-4-20250514' }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Code changed | \`${{ steps.claude_run.outputs.code_changed }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| New PR | \`${{ steps.create_pr.outputs.pr_url || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/embeddable-caller.yml b/.github/workflows/embeddable-caller.yml index a1b2cf7..51713df 100644 --- a/.github/workflows/embeddable-caller.yml +++ b/.github/workflows/embeddable-caller.yml @@ -1,24 +1,73 @@ # embeddable-caller.yml # -# 可嵌入的 Claude Code 调用代码片段 -# 复制到任意工作流的 jobs 下即可使用 +# 自动触发模板 —— 将下方的 job 嵌入到你已有的 CI 工作流中, +# 实现 PR 开启时自动触发 Claude Code 分析。 # -# 注意:可复用工作流中已硬编码白名单,只有授权仓库可以调用。 - - claude-code: - uses: opensourceways/agent-skills/.github/workflows/_claude-code.yml@main - with: - # API Key - 使用调用方仓库的 secret - claude_api_key: ${{ secrets.CLAUDE_API_KEY }} - - # 模型配置 - model: 'claude-sonnet-4-20250514' - timeout_minutes: 60 - - # 可选配置 - # setup_script: 'pip install pytest' - # additional_claude_args: '--skill my-custom-skill' - # allow_code_change: true - - # 组织检查(留空则跳过) - # org_name: '' \ No newline at end of file +# 前置条件(仅需一次): +# 在目标仓库 Settings > Secrets > Actions 中添加: +# DISPATCH_TOKEN = <由 agent-skills 管理员分发的 PAT,具备 public_repo 权限> +# +# 无需配置 CLAUDE_API_KEY —— API Key 统一由 agent-skills 管理。 +# +# 用法: +# 将整个文件作为独立工作流使用,或将 jobs.trigger-claude 片段 +# 复制到你现有 CI 工作流的 jobs: 块下,与其他 job 并行运行。 + +name: Claude Bot (Auto) + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: read + +jobs: + trigger-claude: + name: Trigger Claude Code Analysis + runs-on: ubuntu-latest + # 跳过草稿 PR,避免噪音 + if: github.event.pull_request.draft == false + + steps: + - name: Dispatch to agent-skills + env: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + ISSUE_NUMBER: ${{ github.event.pull_request.number }} + COMMENT_AUTHOR: ${{ github.event.pull_request.user.login }} + CALLER_REPO: ${{ github.repository }} + run: | + python3 - <<'PYEOF' + import json, os, subprocess + + payload = { + 'caller_repo': os.environ['CALLER_REPO'], + 'pr_number': os.environ['PR_NUMBER'], + 'issue_number': os.environ['ISSUE_NUMBER'], + 'comment_body': 'Auto-triggered: Please review this PR and suggest improvements.', + 'comment_author': os.environ['COMMENT_AUTHOR'], + 'allow_code_change': 'false', # 自动模式:仅分析,不修改代码 + 'skill_name': '', + 'model': 'claude-sonnet-4-20250514', + } + + dispatch_body = json.dumps({ + 'event_type': 'claude-request', + 'client_payload': payload, + }) + + r = subprocess.run([ + 'curl', '-sf', '-X', 'POST', + '-H', f'Authorization: Bearer {os.environ["DISPATCH_TOKEN"]}', + '-H', 'Accept: application/vnd.github+json', + 'https://api.github.com/repos/KadenZhang3321/agent-skills/dispatches', + '-d', dispatch_body, + ], capture_output=True, text=True) + + if r.returncode != 0: + # 自动模式下调度失败不阻断 CI + print(f'::warning::Dispatch failed: {r.stderr}') + else: + print('Claude analysis dispatched. Results will appear as a PR comment.') + PYEOF diff --git a/.github/workflows/example-caller.yml b/.github/workflows/example-caller.yml index 7dc70c0..643a79f 100644 --- a/.github/workflows/example-caller.yml +++ b/.github/workflows/example-caller.yml @@ -1,47 +1,92 @@ # example-caller.yml # -# 调用方示例文件 —— 可直接复制到目标仓库的 .github/workflows/ 目录使用。 -# 支持 @claude 触发和自动触发(新建 Issue 时) +# 手动触发模板 —— 复制到目标仓库的 .github/workflows/ 目录使用。 +# 当 PR 或 Issue 评论中包含 @claude 时,自动触发 Claude Code 分析。 # -# 注意:可复用工作流中已硬编码白名单,只有授权仓库才能调用。 -# 使用前确认你的仓库已在白名单中。 +# 前置条件(仅需一次): +# 在目标仓库 Settings > Secrets > Actions 中添加: +# DISPATCH_TOKEN = <由 agent-skills 管理员分发的 PAT,具备 public_repo 权限> +# +# 无需配置 CLAUDE_API_KEY —— API Key 统一由 agent-skills 管理。 name: Claude Bot on: - # Issue/PR 评论时触发(需在评论中 @claude) issue_comment: types: [created] - # 新建 Issue 时自动触发 - issues: - types: [opened] - - # 其他工作流调用时触发 - workflow_call: - -# 工作流级别权限声明(调用方负责声明) -# 如果 allow_code_change: true,需要 contents: write permissions: contents: read - pull-requests: write - issues: write jobs: - claude-code: - uses: opensourceways/agent-skills/.github/workflows/_claude-code.yml@main - with: - # API Key - 使用调用方仓库的 secret - claude_api_key: ${{ secrets.CLAUDE_API_KEY }} - - # ---- 模型配置 ---- - model: 'claude-sonnet-4-20250514' - timeout_minutes: 60 - - # ---- 安全检查 ---- - org_name: '' - - # ---- 可选配置 ---- - # allow_code_change: false # 是否允许 CC 修改代码(默认 false) - # setup_script: '' # 初始化脚本 - # additional_claude_args: '' # 传给 CC 的额外参数(如 --skill my-skill) \ No newline at end of file + trigger-claude: + name: Trigger Claude Code + runs-on: ubuntu-latest + # 只响应包含 @claude 的评论,忽略机器人账号 + if: | + contains(github.event.comment.body, '@claude') && + !endsWith(github.actor, '[bot]') && + github.actor != 'dependabot' + + steps: + # 检测当前评论是否来自 PR(而非普通 Issue) + - name: Detect PR context + id: context + env: + GH_TOKEN: ${{ github.token }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + run: | + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $GH_TOKEN" \ + "https://api.github.com/repos/$REPO/pulls/$ISSUE_NUMBER") + + if [[ "$HTTP_STATUS" == "200" ]]; then + echo "pr_number=$ISSUE_NUMBER" >> "$GITHUB_OUTPUT" + else + echo "pr_number=" >> "$GITHUB_OUTPUT" + fi + + # 发送 dispatch 到 agent-skills,触发 Claude Code + - name: Dispatch to agent-skills + env: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + PR_NUMBER: ${{ steps.context.outputs.pr_number }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + COMMENT_BODY: ${{ github.event.comment.body }} + COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + CALLER_REPO: ${{ github.repository }} + run: | + python3 - <<'PYEOF' + import json, os, subprocess, sys + + payload = { + 'caller_repo': os.environ['CALLER_REPO'], + 'pr_number': os.environ.get('PR_NUMBER', ''), + 'issue_number': os.environ['ISSUE_NUMBER'], + 'comment_body': os.environ['COMMENT_BODY'], + 'comment_author': os.environ['COMMENT_AUTHOR'], + 'allow_code_change': 'true', + 'skill_name': '', + 'model': 'claude-sonnet-4-20250514', + } + + dispatch_body = json.dumps({ + 'event_type': 'claude-request', + 'client_payload': payload, + }) + + r = subprocess.run([ + 'curl', '-sf', '-X', 'POST', + '-H', f'Authorization: Bearer {os.environ["DISPATCH_TOKEN"]}', + '-H', 'Accept: application/vnd.github+json', + 'https://api.github.com/repos/KadenZhang3321/agent-skills/dispatches', + '-d', dispatch_body, + ], capture_output=True, text=True) + + if r.returncode != 0: + print(f'::error::Dispatch failed: {r.stderr}') + sys.exit(1) + + print('Dispatched to agent-skills. Claude will post a comment shortly.') + PYEOF From aa267a8e41fccf096083f72f9a952b6bce955936 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 2 Apr 2026 20:41:46 +0800 Subject: [PATCH 03/31] feat(claude-bot): add token usage and cost to job summary Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/_claude-code.yml | 36 ++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index dd2746a..5fb481b 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -204,14 +204,39 @@ jobs: TOOLS="Read,Glob,Grep,Bash" fi - CLAUDE_OUTPUT=$(claude -p "$CLAUDE_PROMPT" \ + claude -p "$CLAUDE_PROMPT" \ --model "$MODEL" \ --allowedTools "$TOOLS" \ - 2>/tmp/claude_stderr.txt) + --output-format json \ + 2>/tmp/claude_stderr.txt \ + > /tmp/claude_raw.json EXIT_CODE=$? - echo "$CLAUDE_OUTPUT" > /tmp/claude_output.txt - echo "$CLAUDE_OUTPUT" + python3 - <<'PYEOF' + import json, os + + try: + data = json.loads(open('/tmp/claude_raw.json').read()) + result = data.get('result', '') + cost = data.get('cost_usd', data.get('total_cost_usd', 0)) or 0 + usage = data.get('usage', {}) + input_tokens = usage.get('input_tokens', 'N/A') + output_tokens = usage.get('output_tokens', 'N/A') + except Exception as e: + print(f'Warning: could not parse JSON output: {e}') + result = open('/tmp/claude_raw.json').read() + cost = 'N/A' + input_tokens = 'N/A' + output_tokens = 'N/A' + + open('/tmp/claude_output.txt', 'w').write(result) + print(result[:1000]) + + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'cost_usd={cost}\n') + f.write(f'input_tokens={input_tokens}\n') + f.write(f'output_tokens={output_tokens}\n') + PYEOF if git status --porcelain | grep -q .; then echo "code_changed=true" >> "$GITHUB_OUTPUT" @@ -340,3 +365,6 @@ jobs: echo "| Model | \`${{ github.event.client_payload.model || 'claude-sonnet-4-20250514' }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Code changed | \`${{ steps.claude_run.outputs.code_changed }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| New PR | \`${{ steps.create_pr.outputs.pr_url || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Input tokens | \`${{ steps.claude_run.outputs.input_tokens }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Output tokens | \`${{ steps.claude_run.outputs.output_tokens }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Cost (USD) | \`${{ steps.claude_run.outputs.cost_usd }}\` |" >> "$GITHUB_STEP_SUMMARY" From 7fcdf56c9845ef639a358c950ed7de697570c5ae Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Fri, 3 Apr 2026 10:14:47 +0800 Subject: [PATCH 04/31] feat(claude-bot): commit to PR branch, support caller-repo skills, add param comments Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/README.md | 208 ++++++++++++++---------- .github/workflows/_claude-code.yml | 78 ++++----- .github/workflows/embeddable-caller.yml | 23 ++- .github/workflows/example-caller.yml | 20 +++ 4 files changed, 199 insertions(+), 130 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 4663c78..a630dd3 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,128 +1,172 @@ # Claude Code 工作流集成指南 -本节介绍如何在任意仓库中集成 Claude Code 工作流,支持通过 `@claude` 触发或在其他工作流中直接调用。 +在任意仓库中集成 Claude Code,通过 `@claude` 触发代码审查,或在 CI 中自动分析 PR。 +API Key 集中存储在 `KadenZhang3321/agent-skills`,调用方无需配置。 -## 概述 +## 前置条件(每个调用方仓库做一次) -可复用工作流(`_claude-code.yml`)提供: -- Claude Code CLI 调用(可读取 skill 和修改代码) -- 白名单检查(组织和仓库) -- 安全检查(组织成员 + 仓库写权限) -- 自动提交代码变更(可选) +1. 向 agent-skills 管理员申请一个 PAT(`repo` scope) +2. 在调用方仓库添加 Secret: + **Settings → Secrets and variables → Actions → New repository secret** + - Name: `DISPATCH_TOKEN` + - Value: 申请到的 PAT + +--- ## 快速开始 -### 方式一:在其他工作流中嵌入调用(推荐) +### 方式一:手动触发(`@claude` 评论) -将以下代码嵌入到你的工作流中: +复制 [example-caller.yml](./example-caller.yml) 到目标仓库: -```yaml -jobs: - claude-code: - uses: opensourceways/agent-skills/.github/workflows/_claude-code.yml@main - secrets: inherit - with: - allowed_orgs: 'opensourceways' - model: 'claude-sonnet-4-20250514' +```bash +mkdir -p .github/workflows +curl -sSL https://raw.githubusercontent.com/KadenZhang3321/agent-skills/main/.github/workflows/example-caller.yml \ + -o .github/workflows/claude-bot.yml +git add .github/workflows/claude-bot.yml +git commit -m "ci: add Claude Bot workflow" +git push ``` -完整参数说明见 [embeddable-caller.yml](./embeddable-caller.yml)。 +使用:在任意 PR 或 Issue 评论区输入 `@claude <你的问题>`,Claude 会自动回复。 + +--- -### 方式二:复制示例工作流 +### 方式二:PR 自动触发 -将 [example-caller.yml](./example-caller.yml) 复制到目标仓库: +复制 [embeddable-caller.yml](./embeddable-caller.yml) 到目标仓库: ```bash -mkdir -p .github/workflows -curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.github/workflows/example-caller.yml \ - -o .github/workflows/claude-bot.yml -git add .github/workflows/claude-bot.yml -git commit -m "ci: add Claude Code workflow" -git push +curl -sSL https://raw.githubusercontent.com/KadenZhang3321/agent-skills/main/.github/workflows/embeddable-caller.yml \ + -o .github/workflows/claude-auto.yml ``` -## 触发方式 +使用:每次 PR 开启或更新时,Claude 自动分析代码并发评论。 -| 触发方式 | 说明 | -|---------|------| -| `@claude` 提及 | 在 Issue/PR 评论中包含 `@claude` 时触发 | -| 新建 Issue | 创建新 Issue 时自动触发(无需 @claude) | -| `workflow_call` | 在其他工作流中调用时触发 | +--- ## 参数说明 -| 参数 | 必填 | 默认值 | 说明 | -|------|------|--------|------| -| `allowed_orgs` | 否 | opensourceways | 白名单组织(逗号分隔) | -| `allowed_repos` | 否 | - | 白名单仓库(逗号分隔,如 org/repo1,org/repo2) | -| `model` | 否 | claude-sonnet-4-20250514 | Claude 模型名称 | -| `timeout_minutes` | 否 | 60 | 超时时间(分钟) | -| `org_name` | 否 | - | 组织成员检查(留空则跳过) | -| `allow_code_change` | 否 | false | 是否允许 Claude 修改代码 | -| `setup_script` | 否 | - | 初始化脚本(shell 命令) | -| `additional_claude_args` | 否 | - | 传给 Claude 的额外参数 | +在 caller 文件的 `payload` 中配置以下参数: -### 使用 Skill +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `allow_code_change` | `'false'` | `'true'` 允许 Claude 修改文件并直接 commit 到当前 PR 分支;`'false'` 只分析 | +| `skill_name` | `''` | 指定 Skill 路径(见下方说明),留空不使用 | +| `model` | `'claude-sonnet-4-20250514'` | Claude 模型名称 | -在调用方的仓库中创建 skill: +### allow_code_change 示例 +```yaml +# 只审查,不改代码(推荐用于自动触发) +'allow_code_change': 'false', + +# 允许改代码,直接 commit 到当前 PR 分支 +'allow_code_change': 'true', ``` -my-repo/ -├── .claude/ -│ └── skills/ -│ └── my-skill/ -│ └── SKILL.md -``` -然后在 `additional_claude_args` 中指定: +### skill_name 示例 + +Skill 按以下顺序查找,优先使用调用方仓库自己的: + +1. **调用方仓库**:`.github/skills//SKILL.md` +2. **agent-skills**:`skills//SKILL.md` ```yaml -additional_claude_args: '--skill my-skill' +# 使用 agent-skills 内置 Skill +'skill_name': 'infrastructure/github-action-diagnose', +'skill_name': 'infrastructure/docker-image-pr-fix', +'skill_name': 'upstream/vllm-ascend-releasing-note', + +# 使用调用方仓库自定义 Skill +# 在调用方仓库创建 .github/skills/my-skill/SKILL.md,然后: +'skill_name': 'my-skill', + +# 不使用 Skill +'skill_name': '', ``` -## 权限要求 +agent-skills 内置 Skill 列表见 [skills/](../../skills/)。 -调用方需在 workflow 级别声明以下权限: +### model 示例 ```yaml -permissions: - contents: read # 读取代码 - pull-requests: write # 写入 PR 评论 - issues: write # 写入 Issue 评论 - # 如果 allow_code_change: true,需要: - # contents: write # 允许 Claude 修改并提交代码 +# 默认(速度与能力均衡,推荐) +'model': 'claude-sonnet-4-20250514', + +# 最强模型,适合复杂分析 +'model': 'claude-opus-4-20251114', + +# 最快,适合轻量任务 +'model': 'claude-haiku-4-5-20251001', ``` -## 安全检查 +--- + +## 白名单管理 -工作流内置以下安全检查: +允许访问的组织和仓库在 [../.github/allowed-callers.json](../allowed-callers.json) 中配置: + +```json +{ + "allowed_orgs": [ + "opensourceways" + ], + "allowed_repos": [ + "KadenZhang3321/agent-skills", + "KadenZhang3321/hello-world" + ] +} +``` + +- `allowed_orgs`:整个组织下所有仓库都可以使用 +- `allowed_repos`:单独授权某个仓库(不在上述组织内也可以) + +新增仓库或组织,修改此文件并合并到 main 即可,无需其他改动。 + +--- + +## 工作原理 + +``` +调用方仓库 agent-skills +────────────────── ──────────────────────────────── +@claude 评论触发 + └─ example-caller.yml + └─ 发送 repository_dispatch ──→ _claude-code.yml + 1. 白名单校验 + 2. 用户权限校验 + 3. Checkout 调用方仓库 + 4. 拉取 PR Diff + 5. 构建 Prompt(含 Skill) + 6. 调用 Claude(使用集中的 API Key) + 7. 有代码变更 → 新建分支 + 开 PR + 8. 在原 PR/Issue 发布评论 +``` -1. **白名单检查** - 验证调用仓库在白名单中 -2. **组织成员检查** - 触发者必须是目标组织成员 -3. **仓库写权限检查** - 触发者需拥有 `write` 或 `admin` 权限 -4. **机器人过滤** - 自动跳过 `*[bot]` 和 `dependabot` +--- -## API 密钥管理 +## agent-skills 需配置的 Secrets -`CLAUDE_API_KEY` 由 `opensourceways/agent-skills` 仓库统一管理,通过 `secrets: inherit` 传递给调用方。 +| Secret | 用途 | +|--------|------| +| `CLAUDE_API_KEY` | Anthropic API Key,用于调用 Claude | +| `AGENT_PAT` | PAT(`repo` scope),用于读写调用方仓库、发评论、创建 PR | -调用方无需在自身仓库配置 secrets。 +--- ## 故障排查 -**问题:工作流不触发** -- 检查评论中是否包含 `@claude`(区分大小写) -- 确认调用仓库在白名单中 -- 查看 Actions 页面的工作流运行日志 +**agent-skills Actions 没有触发** +- 检查 `DISPATCH_TOKEN` 是否配置了 `repo` scope(`public_repo` 不够) +- 确认 agent-skills 的 Actions 已开启 -**问题:白名单拒绝** -- 检查 `allowed_orgs` 和 `allowed_repos` 配置 -- 确认仓库名称格式正确(如 `org/repo`) +**白名单拒绝** +- 在 `allowed-callers.json` 中添加对应的 org 或 repo -**问题:`CLAUDE_API_KEY secret is not set`** -- 确认调用方使用了 `secrets: inherit` -- 检查 opensourceways/agent-skills 仓库的 secrets 配置 +**Claude 没有发评论** +- 检查 `AGENT_PAT` 是否有目标仓库的写权限 +- 查看 agent-skills Actions 中该次 run 的 `Post comment` 步骤日志 -**问题:Claude 无法修改代码** -- 确认 `allow_code_change: true` -- 确认 workflow 声明了 `contents: write` 权限 \ No newline at end of file +**Token/Cost 显示 N/A** +- 检查 `CLAUDE_API_KEY` 是否正确配置 diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 5fb481b..77390d9 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -134,13 +134,22 @@ jobs: allow_code_change = os.environ.get('ALLOW_CODE_CHANGE', 'false').lower() == 'true' # 加载 Skill 指令 + # 查找顺序:1) 调用方仓库的 .github/skills/ 2) agent-skills 的 skills/ skill_prompt = '' if skill_name: - skill_path = f'agent-skills/skills/{skill_name}/SKILL.md' - try: - skill_prompt = open(skill_path).read() - except FileNotFoundError: - print(f'Warning: skill not found: {skill_path}') + candidates = [ + f'caller-repo/.github/skills/{skill_name}/SKILL.md', + f'agent-skills/skills/{skill_name}/SKILL.md', + ] + for skill_path in candidates: + try: + skill_prompt = open(skill_path).read() + print(f'Loaded skill from: {skill_path}') + break + except FileNotFoundError: + continue + if not skill_prompt: + print(f'Warning: skill "{skill_name}" not found in caller-repo/.github/skills/ or agent-skills/skills/') parts = [f'You are a code assistant for repository **{caller_repo}**.'] @@ -247,60 +256,35 @@ jobs: exit $EXIT_CODE # ------------------------------------------------------------------ - # 8. 新建分支 + 提交 + 开 PR(仅当 Claude 修改了代码时) + # 8. 直接提交到当前 PR 分支(仅当 Claude 修改了代码时) # ------------------------------------------------------------------ - - name: Create PR with Claude's changes + - name: Commit Claude's changes to PR branch if: steps.claude_run.outputs.code_changed == 'true' && github.event.client_payload.allow_code_change == 'true' id: create_pr env: GH_TOKEN: ${{ secrets.AGENT_PAT }} CALLER_REPO: ${{ github.event.client_payload.caller_repo }} - ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }} + PR_NUMBER: ${{ github.event.client_payload.pr_number }} working-directory: caller-repo run: | git config user.name "Claude Code Bot" git config user.email "claude-bot@users.noreply.github.com" - BRANCH="claude-fix-$(date +%Y%m%d-%H%M%S)" - git checkout -b "$BRANCH" - git add -A - git commit -m "fix: code improvements by Claude Code Bot" - git push "https://x-access-token:${GH_TOKEN}@github.com/${CALLER_REPO}.git" "$BRANCH" - - DEFAULT_BRANCH=$(curl -sf \ - -H "Authorization: Bearer $GH_TOKEN" \ - "https://api.github.com/repos/$CALLER_REPO" \ - | python3 -c "import sys,json; print(json.load(sys.stdin).get('default_branch','main'))") - - export PR_BRANCH="$BRANCH" - export PR_BASE="$DEFAULT_BRANCH" - export PR_ISSUE="$ISSUE_NUMBER" - - PR_URL=$(python3 - <<'PYEOF' - import json, os, subprocess - payload = json.dumps({ - 'title': 'fix: Claude Code suggested improvements', - 'body': ( - 'Automatically generated by Claude Code Bot.\n\n' - f'Related to #{os.environ["PR_ISSUE"]}' - ), - 'head': os.environ['PR_BRANCH'], - 'base': os.environ['PR_BASE'], - }) - r = subprocess.run([ - 'curl', '-sf', '-X', 'POST', - '-H', f'Authorization: Bearer {os.environ["GH_TOKEN"]}', - '-H', 'Accept: application/vnd.github+json', - f'https://api.github.com/repos/{os.environ["CALLER_REPO"]}/pulls', - '-d', payload, - ], capture_output=True, text=True) - data = json.loads(r.stdout) - print(data.get('html_url', '')) - PYEOF - ) + # 如果是 PR 评论触发,获取 PR 的 head 分支并切换过去 + if [[ -n "$PR_NUMBER" ]]; then + PR_BRANCH=$(curl -sf \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$CALLER_REPO/pulls/$PR_NUMBER" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['head']['ref'])") + git checkout "$PR_BRANCH" 2>/dev/null || git checkout -b "$PR_BRANCH" + echo "::notice::Committing to PR branch: $PR_BRANCH" + fi - echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" - echo "::notice::PR created: $PR_URL" + git add -A + git commit -m "fix: code improvements suggested by Claude Code Bot" + git push "https://x-access-token:${GH_TOKEN}@github.com/${CALLER_REPO}.git" HEAD + echo "pr_url=" >> "$GITHUB_OUTPUT" # ------------------------------------------------------------------ # 9. 将 Claude 的回复发布为评论 diff --git a/.github/workflows/embeddable-caller.yml b/.github/workflows/embeddable-caller.yml index 51713df..c3b1995 100644 --- a/.github/workflows/embeddable-caller.yml +++ b/.github/workflows/embeddable-caller.yml @@ -47,8 +47,29 @@ jobs: 'issue_number': os.environ['ISSUE_NUMBER'], 'comment_body': 'Auto-triggered: Please review this PR and suggest improvements.', 'comment_author': os.environ['COMMENT_AUTHOR'], - 'allow_code_change': 'false', # 自动模式:仅分析,不修改代码 + + # 是否允许 Claude 修改代码并自动开 PR + # 自动触发模式建议保持 'false',避免每次 push 都产生新 PR + # 'true' → Claude 可编辑文件,修改后直接 commit 到当前 PR 分支 + # 'false' → 只分析,不修改任何文件 + 'allow_code_change': 'false', + + # 使用的 Skill(留空则不使用) + # 查找顺序: + # 1) 调用方仓库的 .github/skills//SKILL.md + # 2) agent-skills 仓库的 skills//SKILL.md + # 示例(agent-skills 内置): + # 'infrastructure/github-action-diagnose' + # 'infrastructure/docker-image-pr-fix' + # 'upstream/vllm-ascend-releasing-note' + # 示例(调用方仓库自定义,放在 .github/skills/ 下): + # 'my-custom-skill' 'skill_name': '', + + # Claude 模型,可选值: + # 'claude-sonnet-4-20250514' (默认,速度与能力均衡) + # 'claude-opus-4-20251114' (最强,较慢) + # 'claude-haiku-4-5-20251001' (最快,轻量任务) 'model': 'claude-sonnet-4-20250514', } diff --git a/.github/workflows/example-caller.yml b/.github/workflows/example-caller.yml index 643a79f..5f94c80 100644 --- a/.github/workflows/example-caller.yml +++ b/.github/workflows/example-caller.yml @@ -66,8 +66,28 @@ jobs: 'issue_number': os.environ['ISSUE_NUMBER'], 'comment_body': os.environ['COMMENT_BODY'], 'comment_author': os.environ['COMMENT_AUTHOR'], + + # 是否允许 Claude 修改代码并自动开 PR + # 'true' → Claude 可编辑文件,修改后直接 commit 到当前 PR 分支 + # 'false' → 只分析,不修改任何文件 'allow_code_change': 'true', + + # 使用的 Skill(留空则不使用) + # 查找顺序: + # 1) 调用方仓库的 .github/skills//SKILL.md + # 2) agent-skills 仓库的 skills//SKILL.md + # 示例(agent-skills 内置): + # 'infrastructure/github-action-diagnose' + # 'infrastructure/docker-image-pr-fix' + # 'upstream/vllm-ascend-releasing-note' + # 示例(调用方仓库自定义,放在 .github/skills/ 下): + # 'my-custom-skill' 'skill_name': '', + + # Claude 模型,可选值: + # 'claude-sonnet-4-20250514' (默认,速度与能力均衡) + # 'claude-opus-4-20251114' (最强,较慢) + # 'claude-haiku-4-5-20251001' (最快,轻量任务) 'model': 'claude-sonnet-4-20250514', } From d4abef80f18f89a954d328051488293fee6ceea9 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Fri, 3 Apr 2026 10:23:01 +0800 Subject: [PATCH 05/31] fix(claude-bot): respond in user's language Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/_claude-code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 77390d9..c4d5849 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -151,7 +151,7 @@ jobs: if not skill_prompt: print(f'Warning: skill "{skill_name}" not found in caller-repo/.github/skills/ or agent-skills/skills/') - parts = [f'You are a code assistant for repository **{caller_repo}**.'] + parts = [f'You are a code assistant for repository **{caller_repo}**. Always respond in the same language as the user\'s request. If the user writes in Chinese, respond in Chinese.'] if skill_prompt: parts.append(f'## Skill Instructions\n\n{skill_prompt}') From f791a2cc140ce9237f5fcecbc56ca20ad66c434c Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Fri, 3 Apr 2026 10:41:56 +0800 Subject: [PATCH 06/31] small modify --- .github/workflows/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index a630dd3..3781ad0 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -21,7 +21,7 @@ API Key 集中存储在 `KadenZhang3321/agent-skills`,调用方无需配置。 ```bash mkdir -p .github/workflows -curl -sSL https://raw.githubusercontent.com/KadenZhang3321/agent-skills/main/.github/workflows/example-caller.yml \ +curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.github/workflows/example-caller.yml \ -o .github/workflows/claude-bot.yml git add .github/workflows/claude-bot.yml git commit -m "ci: add Claude Bot workflow" @@ -37,7 +37,7 @@ git push 复制 [embeddable-caller.yml](./embeddable-caller.yml) 到目标仓库: ```bash -curl -sSL https://raw.githubusercontent.com/KadenZhang3321/agent-skills/main/.github/workflows/embeddable-caller.yml \ +curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.github/workflows/embeddable-caller.yml \ -o .github/workflows/claude-auto.yml ``` @@ -140,7 +140,7 @@ agent-skills 内置 Skill 列表见 [skills/](../../skills/)。 4. 拉取 PR Diff 5. 构建 Prompt(含 Skill) 6. 调用 Claude(使用集中的 API Key) - 7. 有代码变更 → 新建分支 + 开 PR + 7. 有代码变更 → 在原PR上新提交commit 8. 在原 PR/Issue 发布评论 ``` From c5d2abb0fe4652d2ec1e11690bf0a02cba159fc4 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Fri, 3 Apr 2026 17:34:47 +0800 Subject: [PATCH 07/31] feat(claude-code): add caller_image container support and verify_command - Add container configuration with caller_image parameter - Add verify_command step to run validation after Claude modifies code - Only push code when verify_command passes or is not provided - Update job summary with new fields (caller_image, verify_command, verify_result) - Update README with new parameter documentation --- .github/workflows/README.md | 48 ++++++++++++++++++++----- .github/workflows/_claude-code.yml | 24 +++++++++++-- .github/workflows/embeddable-caller.yml | 8 +++++ 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 3781ad0..63f0d18 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -54,6 +54,30 @@ curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.gi | `allow_code_change` | `'false'` | `'true'` 允许 Claude 修改文件并直接 commit 到当前 PR 分支;`'false'` 只分析 | | `skill_name` | `''` | 指定 Skill 路径(见下方说明),留空不使用 | | `model` | `'claude-sonnet-4-20250514'` | Claude 模型名称 | +| `caller_image` | `''` | 调用方提供的镜像(仅工作流自动触发使用),留空使用 `ubuntu-latest` | +| `verify_command` | `''` | 验证命令(仅工作流自动触发使用),验证失败则不 push 代码 | + +### caller_image 示例(仅工作流自动触发) + +```yaml +# 调用方提供的镜像,镜像需要预装 Node.js、Git、依赖已安装 +'caller_image': 'ghcr.io/caller/project:main', + +# 使用 ubuntu-latest(默认) +'caller_image': '', +``` + +### verify_command 示例(仅工作流自动触发) + +```yaml +# 验证命令,验证失败则 job 失败,不 push 代码 +'verify_command': 'npm run build && npm test', + +# 不验证(默认) +'verify_command': '', +``` + +> 注意:`caller_image` 和 `verify_command` 仅在 **工作流自动触发**(embeddable-caller.yml)时使用,手动 `@claude` 触发时不传这两个参数。 ### allow_code_change 示例 @@ -130,18 +154,24 @@ agent-skills 内置 Skill 列表见 [skills/](../../skills/)。 ``` 调用方仓库 agent-skills -────────────────── ──────────────────────────────── +───────────────── ──────────────────────────────── @claude 评论触发 └─ example-caller.yml └─ 发送 repository_dispatch ──→ _claude-code.yml - 1. 白名单校验 - 2. 用户权限校验 - 3. Checkout 调用方仓库 - 4. 拉取 PR Diff - 5. 构建 Prompt(含 Skill) - 6. 调用 Claude(使用集中的 API Key) - 7. 有代码变更 → 在原PR上新提交commit - 8. 在原 PR/Issue 发布评论 + 1. 白名单校验 + 2. 用户权限校验 + 3. 使用 caller_image 创建容器(如未传则用 ubuntu-latest) + 4. 安装 Claude Code + 5. Checkout 调用方仓库 + 6. 拉取 PR Diff + 7. 构建 Prompt(含 Skill) + 8. 调用 Claude(使用集中的 API Key) + 9. verify_command 不为空? + ├─ 否 → 直接 push + └─ 是 → 执行验证命令 + ├─ 成功 → push + └─ 失败 → 不 push + 10. 在原 PR/Issue 发布评论 ``` --- diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index c4d5849..973710c 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -16,6 +16,9 @@ jobs: name: Handle Claude Request runs-on: ubuntu-latest timeout-minutes: 60 + container: + image: ${{ github.event.client_payload.caller_image || 'ubuntu:latest' }} + options: --user root steps: # ------------------------------------------------------------------ @@ -81,9 +84,10 @@ jobs: fetch-depth: 0 # ------------------------------------------------------------------ - # 4. 安装 Claude Code CLI + # 4. 安装 Claude Code CLI(如果容器内没有 Node.js) # ------------------------------------------------------------------ - name: Setup Node.js + if: github.event.client_payload.caller_image == '' uses: actions/setup-node@v4 with: node-version: '20' @@ -256,10 +260,21 @@ jobs: exit $EXIT_CODE # ------------------------------------------------------------------ - # 8. 直接提交到当前 PR 分支(仅当 Claude 修改了代码时) + # 8. 执行验证命令(仅当 verify_command 不为空时) + # ------------------------------------------------------------------ + - name: Run verify command + if: github.event.client_payload.verify_command != '' + id: verify + working-directory: caller-repo + run: | + ${{ github.event.client_payload.verify_command }} + timeout-minutes: 30 + + # ------------------------------------------------------------------ + # 9. 直接提交到当前 PR 分支(仅当 Claude 修改了代码时) # ------------------------------------------------------------------ - name: Commit Claude's changes to PR branch - if: steps.claude_run.outputs.code_changed == 'true' && github.event.client_payload.allow_code_change == 'true' + if: steps.claude_run.outputs.code_changed == 'true' && github.event.client_payload.allow_code_change == 'true' && (github.event.client_payload.verify_command == '' || steps.verify.conclusion == 'success') id: create_pr env: GH_TOKEN: ${{ secrets.AGENT_PAT }} @@ -348,6 +363,9 @@ jobs: echo "| PR | \`${{ github.event.client_payload.pr_number || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Model | \`${{ github.event.client_payload.model || 'claude-sonnet-4-20250514' }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Code changed | \`${{ steps.claude_run.outputs.code_changed }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Caller image | \`${{ github.event.client_payload.caller_image || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Verify command | \`${{ github.event.client_payload.verify_command || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Verify result | \`${{ steps.verify.outcome || 'skipped' }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| New PR | \`${{ steps.create_pr.outputs.pr_url || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Input tokens | \`${{ steps.claude_run.outputs.input_tokens }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Output tokens | \`${{ steps.claude_run.outputs.output_tokens }}\` |" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/embeddable-caller.yml b/.github/workflows/embeddable-caller.yml index c3b1995..b8d5e5a 100644 --- a/.github/workflows/embeddable-caller.yml +++ b/.github/workflows/embeddable-caller.yml @@ -71,6 +71,14 @@ jobs: # 'claude-opus-4-20251114' (最强,较慢) # 'claude-haiku-4-5-20251001' (最快,轻量任务) 'model': 'claude-sonnet-4-20250514', + + # 调用方提供的镜像(可选,不传则使用 ubuntu-latest) + # 镜像需要预装 Node.js、Git、依赖已安装 + 'caller_image': '', + + # 验证命令(可选,不传则不验证) + # 验证失败则 job 失败,不 push 代码 + 'verify_command': '', } dispatch_body = json.dumps({ From 89d4f41ceb6b9ce69269f9b1069e21f60cdb49d5 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Tue, 7 Apr 2026 11:19:04 +0800 Subject: [PATCH 08/31] fix: install python3 in container for workflow execution --- .github/workflows/_claude-code.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 973710c..b6d0f9e 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -21,6 +21,10 @@ jobs: options: --user root steps: + # 安装 Python3(容器可能没有预装) + - name: Install Python3 + run: apt-get update && apt-get install -y python3 python3-pip + # ------------------------------------------------------------------ # 0. Checkout agent-skills(用于读取白名单和 Skill 文件) # ------------------------------------------------------------------ From 825e67b625adab0b130aa819078da31221b7edfe Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Tue, 7 Apr 2026 11:21:52 +0800 Subject: [PATCH 09/31] fix: also install curl in container --- .github/workflows/_claude-code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index b6d0f9e..ca07bf1 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -23,7 +23,7 @@ jobs: steps: # 安装 Python3(容器可能没有预装) - name: Install Python3 - run: apt-get update && apt-get install -y python3 python3-pip + run: apt-get update && apt-get install -y python3 python3-pip curl git # ------------------------------------------------------------------ # 0. Checkout agent-skills(用于读取白名单和 Skill 文件) From 2344b12d8295ec57904058b31ac0f3c1bdefa171 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Tue, 7 Apr 2026 11:28:04 +0800 Subject: [PATCH 10/31] fix: install bash and use bash shell for all steps --- .github/workflows/_claude-code.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index ca07bf1..e156008 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -23,7 +23,8 @@ jobs: steps: # 安装 Python3(容器可能没有预装) - name: Install Python3 - run: apt-get update && apt-get install -y python3 python3-pip curl git + run: apt-get update && apt-get install -y python3 python3-pip curl git bash + shell: bash # ------------------------------------------------------------------ # 0. Checkout agent-skills(用于读取白名单和 Skill 文件) @@ -40,6 +41,7 @@ jobs: env: CALLER_REPO: ${{ github.event.client_payload.caller_repo }} run: | + shell: bash python3 - <<'PYEOF' import json, os, sys caller_repo = os.environ['CALLER_REPO'] @@ -63,6 +65,7 @@ jobs: CALLER_REPO: ${{ github.event.client_payload.caller_repo }} ACTOR: ${{ github.event.client_payload.comment_author }} run: | + shell: bash PERMISSION=$(curl -sf \ -H "Authorization: Bearer $GH_TOKEN" \ -H "Accept: application/vnd.github+json" \ @@ -98,6 +101,7 @@ jobs: - name: Install Claude Code run: | + shell: bash npm install -g @anthropic-ai/claude-code claude --version @@ -111,6 +115,7 @@ jobs: CALLER_REPO: ${{ github.event.client_payload.caller_repo }} PR_NUMBER: ${{ github.event.client_payload.pr_number }} run: | + shell: bash curl -sf \ -H "Authorization: Bearer $GH_TOKEN" \ -H "Accept: application/vnd.github.v3.diff" \ @@ -131,6 +136,7 @@ jobs: SKILL_NAME: ${{ github.event.client_payload.skill_name }} ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} run: | + shell: bash python3 - <<'PYEOF' import os @@ -210,6 +216,7 @@ jobs: ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} working-directory: caller-repo run: | + shell: bash if [[ -z "$ANTHROPIC_API_KEY" ]]; then echo "::error::CLAUDE_API_KEY is not configured in the agent-skills repository secrets" exit 1 @@ -271,6 +278,7 @@ jobs: id: verify working-directory: caller-repo run: | + shell: bash ${{ github.event.client_payload.verify_command }} timeout-minutes: 30 @@ -286,6 +294,7 @@ jobs: PR_NUMBER: ${{ github.event.client_payload.pr_number }} working-directory: caller-repo run: | + shell: bash git config user.name "Claude Code Bot" git config user.email "claude-bot@users.noreply.github.com" @@ -316,6 +325,7 @@ jobs: ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }} PR_URL: ${{ steps.create_pr.outputs.pr_url }} run: | + shell: bash python3 - <<'PYEOF' import json, os, subprocess @@ -358,6 +368,7 @@ jobs: - name: Write job summary if: always() run: | + shell: bash echo "### Claude Code Run Summary" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" From 43665820fa729e518ae2eda2dcdc3f6353fdbedf Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Tue, 7 Apr 2026 11:36:07 +0800 Subject: [PATCH 11/31] fix: move shell: bash to correct position --- .github/workflows/_claude-code.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index e156008..5f8fcf8 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -41,7 +41,6 @@ jobs: env: CALLER_REPO: ${{ github.event.client_payload.caller_repo }} run: | - shell: bash python3 - <<'PYEOF' import json, os, sys caller_repo = os.environ['CALLER_REPO'] @@ -55,6 +54,7 @@ jobs: f'Update .github/allowed-callers.json to grant access.') sys.exit(1) PYEOF + shell: bash # ------------------------------------------------------------------ # 2. 检查触发者在调用方仓库的权限(需要 write/admin/maintain) @@ -65,7 +65,6 @@ jobs: CALLER_REPO: ${{ github.event.client_payload.caller_repo }} ACTOR: ${{ github.event.client_payload.comment_author }} run: | - shell: bash PERMISSION=$(curl -sf \ -H "Authorization: Bearer $GH_TOKEN" \ -H "Accept: application/vnd.github+json" \ @@ -78,6 +77,7 @@ jobs: echo "::error::$ACTOR has insufficient permission ($PERMISSION) on $CALLER_REPO" exit 1 fi + shell: bash # ------------------------------------------------------------------ # 3. Checkout 调用方仓库 @@ -101,9 +101,9 @@ jobs: - name: Install Claude Code run: | - shell: bash npm install -g @anthropic-ai/claude-code claude --version + shell: bash # ------------------------------------------------------------------ # 5. 拉取 PR Diff(如果是 PR 评论) @@ -115,7 +115,6 @@ jobs: CALLER_REPO: ${{ github.event.client_payload.caller_repo }} PR_NUMBER: ${{ github.event.client_payload.pr_number }} run: | - shell: bash curl -sf \ -H "Authorization: Bearer $GH_TOKEN" \ -H "Accept: application/vnd.github.v3.diff" \ @@ -123,6 +122,7 @@ jobs: -o /tmp/pr.diff \ || touch /tmp/pr.diff echo "PR diff: $(wc -c < /tmp/pr.diff) bytes" + shell: bash # ------------------------------------------------------------------ # 6. 构建 prompt @@ -136,7 +136,6 @@ jobs: SKILL_NAME: ${{ github.event.client_payload.skill_name }} ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} run: | - shell: bash python3 - <<'PYEOF' import os @@ -216,7 +215,6 @@ jobs: ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} working-directory: caller-repo run: | - shell: bash if [[ -z "$ANTHROPIC_API_KEY" ]]; then echo "::error::CLAUDE_API_KEY is not configured in the agent-skills repository secrets" exit 1 @@ -278,9 +276,9 @@ jobs: id: verify working-directory: caller-repo run: | - shell: bash ${{ github.event.client_payload.verify_command }} timeout-minutes: 30 + shell: bash # ------------------------------------------------------------------ # 9. 直接提交到当前 PR 分支(仅当 Claude 修改了代码时) @@ -294,7 +292,6 @@ jobs: PR_NUMBER: ${{ github.event.client_payload.pr_number }} working-directory: caller-repo run: | - shell: bash git config user.name "Claude Code Bot" git config user.email "claude-bot@users.noreply.github.com" @@ -313,6 +310,7 @@ jobs: git commit -m "fix: code improvements suggested by Claude Code Bot" git push "https://x-access-token:${GH_TOKEN}@github.com/${CALLER_REPO}.git" HEAD echo "pr_url=" >> "$GITHUB_OUTPUT" + shell: bash # ------------------------------------------------------------------ # 9. 将 Claude 的回复发布为评论 @@ -325,7 +323,6 @@ jobs: ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }} PR_URL: ${{ steps.create_pr.outputs.pr_url }} run: | - shell: bash python3 - <<'PYEOF' import json, os, subprocess @@ -368,7 +365,6 @@ jobs: - name: Write job summary if: always() run: | - shell: bash echo "### Claude Code Run Summary" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" From 02a2dd2b063da96be35c41fbba4573370666d684 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Tue, 7 Apr 2026 11:45:49 +0800 Subject: [PATCH 12/31] fix: install openjdk-17-jdk for Java compilation --- .github/workflows/_claude-code.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 5f8fcf8..3614b55 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -21,9 +21,9 @@ jobs: options: --user root steps: - # 安装 Python3(容器可能没有预装) - - name: Install Python3 - run: apt-get update && apt-get install -y python3 python3-pip curl git bash + # 安装 Python3 和 Java(容器可能没有预装) + - name: Install Python3 and Java + run: apt-get update && apt-get install -y python3 python3-pip curl git bash openjdk-17-jdk shell: bash # ------------------------------------------------------------------ From a5a66fb1924e8ef08d28a2c8c38cc3e0151f1964 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Tue, 7 Apr 2026 11:48:17 +0800 Subject: [PATCH 13/31] fix: clarify Claude has full permission to edit files --- .github/workflows/_claude-code.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 3614b55..dded4c6 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -186,7 +186,8 @@ jobs: if allow_code_change: parts.append( 'The repository is checked out in the current working directory.\n' - 'You are allowed to **edit files** to fix issues.\n' + 'You have FULL permission to **edit, create, and delete files** to fix issues or implement improvements.\n' + 'Please directly make the necessary code changes, then run verify_command to validate.\n' 'After making changes, summarize what you changed and why.' ) else: From 54540c7b51cd0b031a3ffdd899bef7e0aec9f2d7 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Tue, 7 Apr 2026 14:54:59 +0800 Subject: [PATCH 14/31] feat: add skill_url support and .ai/skills/ path for skill loading --- .github/workflows/README.md | 29 +++++++++++++++++++------ .github/workflows/_claude-code.yml | 19 +++++++++++++--- .github/workflows/embeddable-caller.yml | 25 +++++++++++++-------- .github/workflows/example-caller.yml | 25 +++++++++++++-------- 4 files changed, 70 insertions(+), 28 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 63f0d18..da2327a 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -52,7 +52,8 @@ curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.gi | 参数 | 默认值 | 说明 | |------|--------|------| | `allow_code_change` | `'false'` | `'true'` 允许 Claude 修改文件并直接 commit 到当前 PR 分支;`'false'` 只分析 | -| `skill_name` | `''` | 指定 Skill 路径(见下方说明),留空不使用 | +| `skill_name` | `''` | 指定 Skill 名称(见下方说明),留空不使用 | +| `skill_url` | `''` | 远程 Skill 文件的 raw URL,优先级高于 skill_name | | `model` | `'claude-sonnet-4-20250514'` | Claude 模型名称 | | `caller_image` | `''` | 调用方提供的镜像(仅工作流自动触发使用),留空使用 `ubuntu-latest` | | `verify_command` | `''` | 验证命令(仅工作流自动触发使用),验证失败则不 push 代码 | @@ -91,19 +92,19 @@ curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.gi ### skill_name 示例 -Skill 按以下顺序查找,优先使用调用方仓库自己的: +Skill 按以下顺序查找: -1. **调用方仓库**:`.github/skills//SKILL.md` -2. **agent-skills**:`skills//SKILL.md` +1. **远程 URL**:如果指定了 `skill_url`,从 URL 下载(优先级最高) +2. **调用方仓库(.github)**:`.github/skills//SKILL.md` +3. **调用方仓库(.ai)**:`.ai/skills//SKILL.md` +4. **agent-skills**:`skills//SKILL.md` ```yaml # 使用 agent-skills 内置 Skill 'skill_name': 'infrastructure/github-action-diagnose', 'skill_name': 'infrastructure/docker-image-pr-fix', -'skill_name': 'upstream/vllm-ascend-releasing-note', -# 使用调用方仓库自定义 Skill -# 在调用方仓库创建 .github/skills/my-skill/SKILL.md,然后: +# 使用调用方仓库自定义 Skill(会按上述顺序查找) 'skill_name': 'my-skill', # 不使用 Skill @@ -112,6 +113,20 @@ Skill 按以下顺序查找,优先使用调用方仓库自己的: agent-skills 内置 Skill 列表见 [skills/](../../skills/)。 +### skill_url 示例 + +使用远程仓库的 Skill 文件(优先于 skill_name): + +```yaml +# 使用远程 Skill(注意:URL 必须是 raw 内容链接) +'skill_url': 'https://raw.githubusercontent.com/owner/repo/main/.ai/skills/my-skill/SKILL.md', + +# 远程 Skill 与 skill_name 同时存在时,优先使用 skill_url +# 'skill_url': 'https://raw.githubusercontent.com/opensourceways/om-webserver/main/.ai/skills/architect-project-analysis/SKILL.md', +``` + +> 提示:GitHub raw URL 格式为 `https://raw.githubusercontent.com////` + ### model 示例 ```yaml diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index dded4c6..01c5c87 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -144,14 +144,27 @@ jobs: comment_body = os.environ.get('COMMENT_BODY', '') or '' comment_author = os.environ.get('COMMENT_AUTHOR', '') skill_name = os.environ.get('SKILL_NAME', '') or '' + skill_url = os.environ.get('SKILL_URL', '') or '' allow_code_change = os.environ.get('ALLOW_CODE_CHANGE', 'false').lower() == 'true' # 加载 Skill 指令 - # 查找顺序:1) 调用方仓库的 .github/skills/ 2) agent-skills 的 skills/ + # 优先级:1) skill_url(远程URL)2) skill_name - 调用方 .github/skills/ 3) skill_name - 调用方 .ai/skills/ 4) skill_name - agent-skills skills/ skill_prompt = '' - if skill_name: + + # 优先:远程 URL + if skill_url: + try: + import urllib.request + skill_prompt = urllib.request.urlopen(skill_url).read().decode() + print(f'Loaded skill from URL: {skill_url}') + except Exception as e: + print(f'Warning: failed to load skill from URL: {e}') + skill_prompt = '' + # 其次:本地文件 + elif skill_name: candidates = [ f'caller-repo/.github/skills/{skill_name}/SKILL.md', + f'caller-repo/.ai/skills/{skill_name}/SKILL.md', f'agent-skills/skills/{skill_name}/SKILL.md', ] for skill_path in candidates: @@ -162,7 +175,7 @@ jobs: except FileNotFoundError: continue if not skill_prompt: - print(f'Warning: skill "{skill_name}" not found in caller-repo/.github/skills/ or agent-skills/skills/') + print(f'Warning: skill "{skill_name}" not found in any of: {candidates}') parts = [f'You are a code assistant for repository **{caller_repo}**. Always respond in the same language as the user\'s request. If the user writes in Chinese, respond in Chinese.'] diff --git a/.github/workflows/embeddable-caller.yml b/.github/workflows/embeddable-caller.yml index b8d5e5a..5b579d3 100644 --- a/.github/workflows/embeddable-caller.yml +++ b/.github/workflows/embeddable-caller.yml @@ -55,16 +55,23 @@ jobs: 'allow_code_change': 'false', # 使用的 Skill(留空则不使用) - # 查找顺序: - # 1) 调用方仓库的 .github/skills//SKILL.md - # 2) agent-skills 仓库的 skills//SKILL.md - # 示例(agent-skills 内置): - # 'infrastructure/github-action-diagnose' - # 'infrastructure/docker-image-pr-fix' - # 'upstream/vllm-ascend-releasing-note' - # 示例(调用方仓库自定义,放在 .github/skills/ 下): - # 'my-custom-skill' + # Skill 加载优先级: + # 1) skill_url(远程 raw URL) + # 2) skill_name - 调用方仓库 .github/skills//SKILL.md + # 3) skill_name - 调用方仓库 .ai/skills//SKILL.md + # 4) skill_name - agent-skills 仓库 skills//SKILL.md + # + # skill_url 示例(远程 Skill): + # 'skill_url': 'https://raw.githubusercontent.com/owner/repo/main/.ai/skills/my-skill/SKILL.md', + # + # skill_name 示例(agent-skills 内置): + # 'skill_name': 'infrastructure/github-action-diagnose' + # 'skill_name': 'infrastructure/docker-image-pr-fix' + # + # skill_name 示例(调用方仓库自定义): + # 'skill_name': 'my-custom-skill' # 会在 .github/skills/my-custom-skill/SKILL.md 或 .ai/skills/my-custom-skill/SKILL.md 查找 'skill_name': '', + 'skill_url': '', # Claude 模型,可选值: # 'claude-sonnet-4-20250514' (默认,速度与能力均衡) diff --git a/.github/workflows/example-caller.yml b/.github/workflows/example-caller.yml index 5f94c80..f244537 100644 --- a/.github/workflows/example-caller.yml +++ b/.github/workflows/example-caller.yml @@ -73,16 +73,23 @@ jobs: 'allow_code_change': 'true', # 使用的 Skill(留空则不使用) - # 查找顺序: - # 1) 调用方仓库的 .github/skills//SKILL.md - # 2) agent-skills 仓库的 skills//SKILL.md - # 示例(agent-skills 内置): - # 'infrastructure/github-action-diagnose' - # 'infrastructure/docker-image-pr-fix' - # 'upstream/vllm-ascend-releasing-note' - # 示例(调用方仓库自定义,放在 .github/skills/ 下): - # 'my-custom-skill' + # Skill 加载优先级: + # 1) skill_url(远程 raw URL) + # 2) skill_name - 调用方仓库 .github/skills//SKILL.md + # 3) skill_name - 调用方仓库 .ai/skills//SKILL.md + # 4) skill_name - agent-skills 仓库 skills//SKILL.md + # + # skill_url 示例(远程 Skill): + # 'skill_url': 'https://raw.githubusercontent.com/owner/repo/main/.ai/skills/my-skill/SKILL.md', + # + # skill_name 示例(agent-skills 内置): + # 'skill_name': 'infrastructure/github-action-diagnose' + # 'skill_name': 'infrastructure/docker-image-pr-fix' + # + # skill_name 示例(调用方仓库自定义): + # 'skill_name': 'my-custom-skill' # 会在 .github/skills/my-custom-skill/SKILL.md 或 .ai/skills/my-custom-skill/SKILL.md 查找 'skill_name': '', + 'skill_url': '', # Claude 模型,可选值: # 'claude-sonnet-4-20250514' (默认,速度与能力均衡) From ff36ba11c070777cc8d384ac79b377dc79f6de7f Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Wed, 8 Apr 2026 15:31:25 +0800 Subject: [PATCH 15/31] feat: add support for caller_env_vars, custom_prompt, dangerously_skip_permissions, allowed_tools, show_full_output --- .github/workflows/_claude-code.yml | 62 +++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 01c5c87..70f3f05 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -105,6 +105,19 @@ jobs: claude --version shell: bash + # ------------------------------------------------------------------ + # 4.1 设置调用方仓库的环境变量(从 client_payload 传入) + # ------------------------------------------------------------------ + - name: Setup caller environment variables + env: + CALLER_ENV_VARS: ${{ github.event.client_payload.caller_env_vars }} + run: | + if [ -n "$CALLER_ENV_VARS" ]; then + echo "$CALLER_ENV_VARS" >> $GITHUB_ENV + echo "::notice::Loaded caller environment variables" + fi + shell: bash + # ------------------------------------------------------------------ # 5. 拉取 PR Diff(如果是 PR 评论) # ------------------------------------------------------------------ @@ -134,6 +147,8 @@ jobs: COMMENT_BODY: ${{ github.event.client_payload.comment_body }} COMMENT_AUTHOR: ${{ github.event.client_payload.comment_author }} SKILL_NAME: ${{ github.event.client_payload.skill_name }} + SKILL_URL: ${{ github.event.client_payload.skill_url }} + CUSTOM_PROMPT: ${{ github.event.client_payload.custom_prompt }} ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} run: | python3 - <<'PYEOF' @@ -145,14 +160,19 @@ jobs: comment_author = os.environ.get('COMMENT_AUTHOR', '') skill_name = os.environ.get('SKILL_NAME', '') or '' skill_url = os.environ.get('SKILL_URL', '') or '' + custom_prompt = os.environ.get('CUSTOM_PROMPT', '') or '' allow_code_change = os.environ.get('ALLOW_CODE_CHANGE', 'false').lower() == 'true' # 加载 Skill 指令 - # 优先级:1) skill_url(远程URL)2) skill_name - 调用方 .github/skills/ 3) skill_name - 调用方 .ai/skills/ 4) skill_name - agent-skills skills/ + # 优先级:1) custom_prompt(直接传入的 prompt)2) skill_url(远程URL)3) skill_name - 调用方 .github/skills/ 4) skill_name - 调用方 .ai/skills/ 5) skill_name - agent-skills skills/ skill_prompt = '' - # 优先:远程 URL - if skill_url: + # 最高优先级:直接传入的 custom_prompt + if custom_prompt: + skill_prompt = custom_prompt + print(f'Using custom_prompt directly') + # 远程 URL + elif skill_url: try: import urllib.request skill_prompt = urllib.request.urlopen(skill_url).read().decode() @@ -227,6 +247,9 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.CLAUDE_API_KEY }} MODEL: ${{ github.event.client_payload.model || 'claude-sonnet-4-20250514' }} ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} + DANGEROUSLY_SKIP_PERMISSIONS: ${{ github.event.client_payload.dangerously_skip_permissions }} + ALLOWED_TOOLS: ${{ github.event.client_payload.allowed_tools }} + SHOW_FULL_OUTPUT: ${{ github.event.client_payload.show_full_output }} working-directory: caller-repo run: | if [[ -z "$ANTHROPIC_API_KEY" ]]; then @@ -234,20 +257,39 @@ jobs: exit 1 fi - if [[ "$ALLOW_CODE_CHANGE" == "true" ]]; then - TOOLS="Read,Edit,Write,Glob,Grep,Bash" + # 如果启用 show_full_output,使用 group 展示完整输出 + if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then + echo "::group::Claude Code Full Output" + fi + + # 构建 Claude 参数 + CLAUDE_ARGS="--model $MODEL --output-format json" + + # 如果传递了 dangerously_skip_permissions 且为 true + if [[ "$DANGEROUSLY_SKIP_PERMISSIONS" == "true" ]]; then + CLAUDE_ARGS="$CLAUDE_ARGS --dangerously-skip-permissions" + fi + + # 如果传递了 allowed_tools,优先使用;否则根据 allow_code_change 使用默认 + if [[ -n "$ALLOWED_TOOLS" ]]; then + CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools $ALLOWED_TOOLS" else - TOOLS="Read,Glob,Grep,Bash" + if [[ "$ALLOW_CODE_CHANGE" == "true" ]]; then + CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools Read,Edit,Write,Glob,Grep,Bash" + else + CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools Read,Glob,Grep,Bash" + fi fi - claude -p "$CLAUDE_PROMPT" \ - --model "$MODEL" \ - --allowedTools "$TOOLS" \ - --output-format json \ + claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS \ 2>/tmp/claude_stderr.txt \ > /tmp/claude_raw.json EXIT_CODE=$? + if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then + echo "::endgroup::" + fi + python3 - <<'PYEOF' import json, os From 27a31e6b8ad4cbbb7ade52b879ac5f4e6fe7c77e Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Wed, 8 Apr 2026 19:42:36 +0800 Subject: [PATCH 16/31] feat: upload patch instead of direct commit/push, add template --- .github/workflows/_claude-code.yml | 45 ++-- .../workflows/template-invoke-claude-code.yml | 214 ++++++++++++++++++ 2 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/template-invoke-claude-code.yml diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 70f3f05..f388dc9 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -337,36 +337,27 @@ jobs: shell: bash # ------------------------------------------------------------------ - # 9. 直接提交到当前 PR 分支(仅当 Claude 修改了代码时) + # 9. 生成 patch 文件并上传 artifact(仅当 Claude 修改了代码时) + # 不直接 commit/push,把改动发回给调用方处理 # ------------------------------------------------------------------ - - name: Commit Claude's changes to PR branch + - name: Create patch file + id: create_patch if: steps.claude_run.outputs.code_changed == 'true' && github.event.client_payload.allow_code_change == 'true' && (github.event.client_payload.verify_command == '' || steps.verify.conclusion == 'success') - id: create_pr - env: - GH_TOKEN: ${{ secrets.AGENT_PAT }} - CALLER_REPO: ${{ github.event.client_payload.caller_repo }} - PR_NUMBER: ${{ github.event.client_payload.pr_number }} working-directory: caller-repo run: | - git config user.name "Claude Code Bot" - git config user.email "claude-bot@users.noreply.github.com" - - # 如果是 PR 评论触发,获取 PR 的 head 分支并切换过去 - if [[ -n "$PR_NUMBER" ]]; then - PR_BRANCH=$(curl -sf \ - -H "Authorization: Bearer $GH_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/$CALLER_REPO/pulls/$PR_NUMBER" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['head']['ref'])") - git checkout "$PR_BRANCH" 2>/dev/null || git checkout -b "$PR_BRANCH" - echo "::notice::Committing to PR branch: $PR_BRANCH" - fi - - git add -A - git commit -m "fix: code improvements suggested by Claude Code Bot" - git push "https://x-access-token:${GH_TOKEN}@github.com/${CALLER_REPO}.git" HEAD - echo "pr_url=" >> "$GITHUB_OUTPUT" - shell: bash + git diff --no-color > /tmp/claude-changes.patch + PATCH_SIZE=$(wc -c < /tmp/claude-changes.patch) + echo "patch_size=$PATCH_SIZE" >> "$GITHUB_OUTPUT" + echo "patch_path=/tmp/claude-changes.patch" >> "$GITHUB_OUTPUT" + echo "::notice::Created patch file: $PATCH_SIZE bytes" + + - name: Upload patch as artifact + if: steps.create_patch.outputs.patch_path + uses: actions/upload-artifact@v4 + with: + name: claude-changes-patch + path: ${{ steps.create_patch.outputs.patch_path }} + retention-days: 1 # ------------------------------------------------------------------ # 9. 将 Claude 的回复发布为评论 @@ -377,7 +368,7 @@ jobs: GH_TOKEN: ${{ secrets.AGENT_PAT }} CALLER_REPO: ${{ github.event.client_payload.caller_repo }} ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }} - PR_URL: ${{ steps.create_pr.outputs.pr_url }} + PR_URL: "" run: | python3 - <<'PYEOF' import json, os, subprocess diff --git a/.github/workflows/template-invoke-claude-code.yml b/.github/workflows/template-invoke-claude-code.yml new file mode 100644 index 0000000..6cbca1d --- /dev/null +++ b/.github/workflows/template-invoke-claude-code.yml @@ -0,0 +1,214 @@ +# template-invoke-claude-code.yml +# Claude Code 调用模板 — 供其他仓库的工作流复制使用 +# +# 使用方式: +# 1. 确保调用方仓库在 agent-skills/.github/allowed-callers.json 白名单中 +# 2. 复制下面的 jobs 到你的 workflow 中 +# 3. 根据需要调整环境变量和参数 +# +# ==================================================================== +# 参数说明: +# ------------------------------------------------------------------- +# caller_env_vars: 传递给 Claude 的环境变量(格式:key1=value1\nkey2=value2) +# custom_prompt: 自定义 prompt(优先级最高,会覆盖 skill_name) +# skill_name: Skill 名称(如果 custom_prompt 为空则使用) +# model: 使用的模型(默认 qwen/qwen3.6-plus:free) +# allow_code_change: 是否允许 Claude 修改代码(默认 true) +# dangerously_skip_permissions: 跳过权限检查(默认 false) +# allowed_tools: 允许的工具(如 "Bash(git *),Read,Write,Edit") +# show_full_output: 在日志中显示完整输出(默认 false) +# ==================================================================== + +jobs: + invoke-claude-code-with-wait: + name: Invoke Claude Code (with wait) + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + AGENT_SKILLS_REPO: opensourceways/agent-skills + # 以下变量根据你的仓库修改 + CALLER_REPO: your-org/your-repo + SKILL_NAME: your-skill-name + MODEL: qwen/qwen3.6-plus:free + COMMENT_AUTHOR: workflow-bot + + steps: + - name: Invoke Claude via agent-skills + id: dispatch + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} + run: | + # 示例:传递环境变量(根据需要修改) + # CALLER_ENV_VARS=$(jq -n \ + # --arg key1 "value1" \ + # --arg key2 "value2" \ + # '{key1: $key1, key2: $key2}' | jq -r '. | to_entries | map("\(.key)=\(.value)") | join("\n")) + + # 示例:传递自定义 prompt(根据需要修改) + # CUSTOM_PROMPT="Your custom prompt here" + + gh api --method POST "https://api.github.com/repos/${{ env.AGENT_SKILLS_REPO }}/dispatches" \ + -f event_type="claude-request" \ + -f client_payload="$(jq -n \ + --arg caller_repo "${{ env.CALLER_REPO }}" \ + --arg skill_name "${{ env.SKILL_NAME }}" \ + --arg model "${{ env.MODEL }}" \ + --arg allow_code_change "true" \ + --arg comment_author "${{ env.COMMENT_AUTHOR }}" \ + --arg caller_env_vars "" \ + --arg custom_prompt "" \ + --arg dangerously_skip_permissions "false" \ + --arg allowed_tools "" \ + --arg show_full_output "false" \ + '{ + caller_repo: $caller_repo, + skill_name: $skill_name, + model: $model, + allow_code_change: $allow_code_change, + comment_author: $comment_author, + caller_env_vars: $caller_env_vars, + custom_prompt: $custom_prompt, + dangerously_skip_permissions: $dangerously_skip_permissions, + allowed_tools: $allowed_tools, + show_full_output: $show_full_output + }')" \ + --silent --output /dev/null --fail + + echo "dispatched=true" >> "$GITHUB_OUTPUT" + + - name: Wait for Claude execution + if: steps.dispatch.outputs.dispatched == 'true' + id: wait + run: | + RUN_ID="" + for attempt in $(seq 1 60); do + RUN_ID=$(gh run list \ + --repo "${{ env.AGENT_SKILLS_REPO }}" \ + --workflow "_claude-code.yml" \ + -L 5 \ + --json databaseId,displayTitle \ + --jq '[.[] | select(.displayTitle | contains("Handle Claude Request"))][0].databaseId // empty') + if [ -n "$RUN_ID" ]; then + break + fi + sleep 10 + done + + if [ -z "$RUN_ID" ]; then + echo "::error::Timeout waiting for Claude execution" + exit 1 + fi + + echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" + gh run watch "$RUN_ID" --repo "${{ env.AGENT_SKILLS_REPO }}" --exit-status + + - name: Check execution result + if: steps.wait.outputs.run_id + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} + run: | + CONCLUSION=$(gh run view "${{ steps.wait.outputs.run_id }}" \ + --repo "${{ env.AGENT_SKILLS_REPO }}" \ + --json conclusion --jq '.conclusion') + + if [ "$CONCLUSION" != "success" ]; then + echo "::error::Claude execution failed: $CONCLUSION" + exit 1 + fi + + echo "execution_success=true" >> "$GITHUB_OUTPUT" + +# ==================================================================== +# 最小化版本:直接复制到现有 job 中使用 +# ==================================================================== +# 使用方式:将下面的 steps 复制到你的现有 job 中 +# +# - name: Invoke Claude via agent-skills +# id: dispatch +# env: +# GH_TOKEN: ${{ secrets.PAT_TOKEN }} +# CALLER_REPO: your-org/your-repo +# SKILL_NAME: your-skill-name +# MODEL: qwen/qwen3.6-plus:free +# run: | +# # 传递环境变量示例(可选) +# # CALLER_ENV_VARS=$(jq -n \ +# # --arg key1 "value1" \ +# # --arg key2 "value2" \ +# # '{key1: $key1, key2: $key2}' | jq -r '. | to_entries | map("\(.key)=\(.value)") | join("\n")) +# +# # 传递自定义 prompt 示例(可选) +# # CUSTOM_PROMPT="Your custom prompt here" +# +# # 传递权限控制参数示例(可选) +# # DANGEROUSLY_SKIP_PERMISSIONS="true" +# # ALLOWED_TOOLS="Bash(git *),Bash(gh *),Read,Write,Edit,Glob,Grep" +# +# gh api --method POST "https://api.github.com/repos/opensourceways/agent-skills/dispatches" \ +# -f event_type="claude-request" \ +# -f client_payload="$(jq -n \ +# --arg caller_repo "$CALLER_REPO" \ +# --arg skill_name "$SKILL_NAME" \ +# --arg model "$MODEL" \ +# --arg allow_code_change "true" \ +# --arg comment_author "workflow-bot" \ +# --arg caller_env_vars "$CALLER_ENV_VARS" \ +# --arg custom_prompt "$CUSTOM_PROMPT" \ +# --arg dangerously_skip_permissions "false" \ +# --arg allowed_tools "" \ +# '{ +# caller_repo: $caller_repo, +# skill_name: $skill_name, +# model: $model, +# allow_code_change: $allow_code_change, +# comment_author: $comment_author, +# caller_env_vars: $caller_env_vars, +# custom_prompt: $custom_prompt, +# dangerously_skip_permissions: $dangerously_skip_permissions, +# allowed_tools: $allowed_tools +# }')" \ +# --silent --output /dev/null --fail +# +# echo "dispatched=true" >> "$GITHUB_OUTPUT" +# +# - name: Wait for Claude execution +# if: steps.dispatch.outputs.dispatched == 'true' +# id: wait +# run: | +# RUN_ID="" +# for attempt in $(seq 1 60); do +# RUN_ID=$(gh run list \ +# --repo "opensourceways/agent-skills" \ +# --workflow "_claude-code.yml" \ +# -L 5 \ +# --json databaseId,displayTitle \ +# --jq '[.[] | select(.displayTitle | contains("Handle Claude Request"))][0].databaseId // empty') +# if [ -n "$RUN_ID" ]; then +# break +# fi +# sleep 10 +# done +# +# if [ -z "$RUN_ID" ]; then +# echo "::error::Timeout waiting for Claude execution" +# exit 1 +# fi +# +# echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" +# gh run watch "$RUN_ID" --repo "opensourceways/agent-skills" --exit-status +# +# - name: Check execution result +# if: steps.wait.outputs.run_id +# env: +# GH_TOKEN: ${{ secrets.PAT_TOKEN }} +# run: | +# CONCLUSION=$(gh run view "${{ steps.wait.outputs.run_id }}" \ +# --repo "opensourceways/agent-skills" \ +# --json conclusion --jq '.conclusion') +# +# if [ "$CONCLUSION" != "success" ]; then +# echo "::error::Claude execution failed: $CONCLUSION" +# exit 1 +# fi +# +# echo "execution_success=true" >> "$GITHUB_OUTPUT" \ No newline at end of file From fa5ea56b1dafbb9ad14301d063433ea1d690e770 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Wed, 8 Apr 2026 20:10:37 +0800 Subject: [PATCH 17/31] chore: remove ANTHROPIC_API_KEY validation - caller's responsibility --- .github/workflows/_claude-code.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index f388dc9..198d7cf 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -244,7 +244,6 @@ jobs: - name: Invoke Claude Code id: claude_run env: - ANTHROPIC_API_KEY: ${{ secrets.CLAUDE_API_KEY }} MODEL: ${{ github.event.client_payload.model || 'claude-sonnet-4-20250514' }} ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} DANGEROUSLY_SKIP_PERMISSIONS: ${{ github.event.client_payload.dangerously_skip_permissions }} @@ -252,11 +251,6 @@ jobs: SHOW_FULL_OUTPUT: ${{ github.event.client_payload.show_full_output }} working-directory: caller-repo run: | - if [[ -z "$ANTHROPIC_API_KEY" ]]; then - echo "::error::CLAUDE_API_KEY is not configured in the agent-skills repository secrets" - exit 1 - fi - # 如果启用 show_full_output,使用 group 展示完整输出 if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then echo "::group::Claude Code Full Output" From 0e54678648d16950d4ace270b18e63482671c813 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 10:12:53 +0800 Subject: [PATCH 18/31] chore: add Meihan-chen/vllm-benchmarks to allowed callers --- .github/allowed-callers.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/allowed-callers.json b/.github/allowed-callers.json index 6081296..fd1a9df 100644 --- a/.github/allowed-callers.json +++ b/.github/allowed-callers.json @@ -4,6 +4,7 @@ ], "allowed_repos": [ "KadenZhang3321/agent-skills", - "KadenZhang3321/hello-world" + "KadenZhang3321/hello-world", + "Meihan-chen/vllm-benchmarks" ] } From d4de0a1ac4c072ff6ca922770f247a425c2dfbd7 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 15:09:01 +0800 Subject: [PATCH 19/31] refactor: simplify workflow to repository_dispatch only --- .github/workflows/_claude-code.yml | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 198d7cf..bfd0d83 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -72,7 +72,7 @@ jobs: | python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','none'))") if [[ "$PERMISSION" == "write" || "$PERMISSION" == "admin" || "$PERMISSION" == "maintain" ]]; then - echo "::notice::$ACTOR has $PERMISSION permission on $CALLER_REPO" + echo "::$ACTOR has $PERMISSION permission on $CALLER_REPO" else echo "::error::$ACTOR has insufficient permission ($PERMISSION) on $CALLER_REPO" exit 1 @@ -106,7 +106,7 @@ jobs: shell: bash # ------------------------------------------------------------------ - # 4.1 设置调用方仓库的环境变量(从 client_payload 传入) + # 5. 设置调用方仓库的环境变量 # ------------------------------------------------------------------ - name: Setup caller environment variables env: @@ -119,7 +119,7 @@ jobs: shell: bash # ------------------------------------------------------------------ - # 5. 拉取 PR Diff(如果是 PR 评论) + # 6. 拉取 PR Diff(如果是 PR 评论) # ------------------------------------------------------------------ - name: Fetch PR diff if: github.event.client_payload.pr_number != '' @@ -138,7 +138,7 @@ jobs: shell: bash # ------------------------------------------------------------------ - # 6. 构建 prompt + # 7. 构建 prompt # ------------------------------------------------------------------ - name: Build prompt env: @@ -164,14 +164,11 @@ jobs: allow_code_change = os.environ.get('ALLOW_CODE_CHANGE', 'false').lower() == 'true' # 加载 Skill 指令 - # 优先级:1) custom_prompt(直接传入的 prompt)2) skill_url(远程URL)3) skill_name - 调用方 .github/skills/ 4) skill_name - 调用方 .ai/skills/ 5) skill_name - agent-skills skills/ skill_prompt = '' - # 最高优先级:直接传入的 custom_prompt if custom_prompt: skill_prompt = custom_prompt print(f'Using custom_prompt directly') - # 远程 URL elif skill_url: try: import urllib.request @@ -180,7 +177,6 @@ jobs: except Exception as e: print(f'Warning: failed to load skill from URL: {e}') skill_prompt = '' - # 其次:本地文件 elif skill_name: candidates = [ f'caller-repo/.github/skills/{skill_name}/SKILL.md', @@ -239,7 +235,7 @@ jobs: PYEOF # ------------------------------------------------------------------ - # 7. 调用 Claude Code + # 8. 调用 Claude Code # ------------------------------------------------------------------ - name: Invoke Claude Code id: claude_run @@ -251,20 +247,16 @@ jobs: SHOW_FULL_OUTPUT: ${{ github.event.client_payload.show_full_output }} working-directory: caller-repo run: | - # 如果启用 show_full_output,使用 group 展示完整输出 if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then echo "::group::Claude Code Full Output" fi - # 构建 Claude 参数 CLAUDE_ARGS="--model $MODEL --output-format json" - # 如果传递了 dangerously_skip_permissions 且为 true if [[ "$DANGEROUSLY_SKIP_PERMISSIONS" == "true" ]]; then CLAUDE_ARGS="$CLAUDE_ARGS --dangerously-skip-permissions" fi - # 如果传递了 allowed_tools,优先使用;否则根据 allow_code_change 使用默认 if [[ -n "$ALLOWED_TOOLS" ]]; then CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools $ALLOWED_TOOLS" else @@ -319,7 +311,7 @@ jobs: exit $EXIT_CODE # ------------------------------------------------------------------ - # 8. 执行验证命令(仅当 verify_command 不为空时) + # 9. 执行验证命令(仅当 verify_command 不为空时) # ------------------------------------------------------------------ - name: Run verify command if: github.event.client_payload.verify_command != '' @@ -331,8 +323,7 @@ jobs: shell: bash # ------------------------------------------------------------------ - # 9. 生成 patch 文件并上传 artifact(仅当 Claude 修改了代码时) - # 不直接 commit/push,把改动发回给调用方处理 + # 10. 生成 patch 文件并上传 artifact # ------------------------------------------------------------------ - name: Create patch file id: create_patch @@ -354,7 +345,7 @@ jobs: retention-days: 1 # ------------------------------------------------------------------ - # 9. 将 Claude 的回复发布为评论 + # 11. 将 Claude 的回复发布为评论 # ------------------------------------------------------------------ - name: Post comment if: always() @@ -401,7 +392,7 @@ jobs: PYEOF # ------------------------------------------------------------------ - # 10. Job 执行摘要 + # 12. Job 执行摘要 # ------------------------------------------------------------------ - name: Write job summary if: always() @@ -418,7 +409,6 @@ jobs: echo "| Caller image | \`${{ github.event.client_payload.caller_image || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Verify command | \`${{ github.event.client_payload.verify_command || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Verify result | \`${{ steps.verify.outcome || 'skipped' }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| New PR | \`${{ steps.create_pr.outputs.pr_url || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Input tokens | \`${{ steps.claude_run.outputs.input_tokens }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Output tokens | \`${{ steps.claude_run.outputs.output_tokens }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Cost (USD) | \`${{ steps.claude_run.outputs.cost_usd }}\` |" >> "$GITHUB_STEP_SUMMARY" From 3741ffefffea40dbfc8368e4727836f1f7ba0422 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 16:02:51 +0800 Subject: [PATCH 20/31] chore: add KadenZhang3321/vllm-benchmarks to whitelist --- .github/allowed-callers.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/allowed-callers.json b/.github/allowed-callers.json index fd1a9df..6a95821 100644 --- a/.github/allowed-callers.json +++ b/.github/allowed-callers.json @@ -5,6 +5,7 @@ "allowed_repos": [ "KadenZhang3321/agent-skills", "KadenZhang3321/hello-world", - "Meihan-chen/vllm-benchmarks" + "Meihan-chen/vllm-benchmarks", + "KadenZhang3321/vllm-benchmarks" ] } From 34411048b4b0ee4a76a9c6afc1fcdcf59b843494 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 16:35:44 +0800 Subject: [PATCH 21/31] fix: add shell: bash to Invoke Claude Code step --- .github/workflows/_claude-code.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index bfd0d83..21092fc 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -239,6 +239,7 @@ jobs: # ------------------------------------------------------------------ - name: Invoke Claude Code id: claude_run + shell: bash env: MODEL: ${{ github.event.client_payload.model || 'claude-sonnet-4-20250514' }} ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} From b9ae93b098920a11c131b6d2aa42ac4e3f88fdf8 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 17:02:36 +0800 Subject: [PATCH 22/31] debug: add diagnostic output for claude command --- .github/workflows/_claude-code.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 21092fc..36a0fce 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -252,6 +252,11 @@ jobs: echo "::group::Claude Code Full Output" fi + # Debug: 显示环境变量 + echo "Debug: CLAUDE_PROMPT length: ${#CLAUDE_PROMPT}" + echo "Debug: MODEL=$MODEL" + echo "Debug: Working directory: $(pwd)" + CLAUDE_ARGS="--model $MODEL --output-format json" if [[ "$DANGEROUSLY_SKIP_PERMISSIONS" == "true" ]]; then @@ -268,11 +273,20 @@ jobs: fi fi + echo "Debug: Running claude with args: $CLAUDE_ARGS" + claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS \ 2>/tmp/claude_stderr.txt \ > /tmp/claude_raw.json EXIT_CODE=$? + # 显示错误输出(如果有) + if [ -s /tmp/claude_stderr.txt ]; then + echo "=== Claude Stderr ===" + cat /tmp/claude_stderr.txt + echo "=====================" + fi + if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then echo "::endgroup::" fi From 2a89343786cd63ce19413203415f008604f344a7 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 17:09:46 +0800 Subject: [PATCH 23/31] debug: check if claude command exists --- .github/workflows/_claude-code.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 36a0fce..b87d37d 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -257,6 +257,11 @@ jobs: echo "Debug: MODEL=$MODEL" echo "Debug: Working directory: $(pwd)" + # Debug: 检查 claude 是否存在 + echo "Debug: Checking claude installation..." + which claude || echo "WARNING: claude not in PATH" + claude --version 2>&1 || echo "WARNING: claude --version failed" + CLAUDE_ARGS="--model $MODEL --output-format json" if [[ "$DANGEROUSLY_SKIP_PERMISSIONS" == "true" ]]; then From eb3721668aa703dd7fadf4c490b0497955ce7935 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 17:19:21 +0800 Subject: [PATCH 24/31] fix: add CLAUDE_API_KEY to Invoke Claude Code step --- .github/workflows/_claude-code.yml | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index b87d37d..b96f1c3 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -246,6 +246,7 @@ jobs: DANGEROUSLY_SKIP_PERMISSIONS: ${{ github.event.client_payload.dangerously_skip_permissions }} ALLOWED_TOOLS: ${{ github.event.client_payload.allowed_tools }} SHOW_FULL_OUTPUT: ${{ github.event.client_payload.show_full_output }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} working-directory: caller-repo run: | if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then @@ -279,19 +280,21 @@ jobs: fi echo "Debug: Running claude with args: $CLAUDE_ARGS" - - claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS \ - 2>/tmp/claude_stderr.txt \ - > /tmp/claude_raw.json - EXIT_CODE=$? - - # 显示错误输出(如果有) - if [ -s /tmp/claude_stderr.txt ]; then - echo "=== Claude Stderr ===" - cat /tmp/claude_stderr.txt - echo "=====================" + + # 检查 API key 是否设置(不显示值) + if [ -z "$CLAUDE_API_KEY" ]; then + echo "ERROR: CLAUDE_API_KEY is not set!" + else + echo "Debug: CLAUDE_API_KEY is set (length: ${#CLAUDE_API_KEY})" fi + # 直接运行,让错误显示在控制台 + claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS 2>&1 | tee /tmp/claude_output.log || true + EXIT_CODE=${PIPESTATUS[0]} + + # 保存输出到文件 + cp /tmp/claude_output.log /tmp/claude_raw.json 2>/dev/null || echo "" > /tmp/claude_raw.json + if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then echo "::endgroup::" fi From e3fbc0028905f9e815821f8689d2d58b18bbe8ab Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 17:25:23 +0800 Subject: [PATCH 25/31] debug: show all claude output and check exit code --- .github/workflows/_claude-code.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index b96f1c3..dff1036 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -288,12 +288,18 @@ jobs: echo "Debug: CLAUDE_API_KEY is set (length: ${#CLAUDE_API_KEY})" fi - # 直接运行,让错误显示在控制台 - claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS 2>&1 | tee /tmp/claude_output.log || true + # 直接运行,显示所有输出 + echo "=== Starting Claude Code ===" + set -o pipefail + claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS 2>&1 | tee /tmp/claude_output.log EXIT_CODE=${PIPESTATUS[0]} + echo "=== Claude Code Exit Code: $EXIT_CODE ===" - # 保存输出到文件 - cp /tmp/claude_output.log /tmp/claude_raw.json 2>/dev/null || echo "" > /tmp/claude_raw.json + # 检查输出文件 + echo "Debug: Output file size: $(wc -c < /tmp/claude_output.log 2>/dev/null || echo 0) bytes" + + # 保存输出到文件用于后续解析 + cp /tmp/claude_output.log /tmp/claude_raw.json 2>/dev/null || echo "{\"error\": \"no output\"}" > /tmp/claude_raw.json if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then echo "::endgroup::" From 125d0fdab4fe16d24ae3816396b60e02def7d75a Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 17:39:22 +0800 Subject: [PATCH 26/31] fix: add claude login before running --- .github/workflows/_claude-code.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index dff1036..bdceed5 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -284,10 +284,19 @@ jobs: # 检查 API key 是否设置(不显示值) if [ -z "$CLAUDE_API_KEY" ]; then echo "ERROR: CLAUDE_API_KEY is not set!" + exit 1 else echo "Debug: CLAUDE_API_KEY is set (length: ${#CLAUDE_API_KEY})" fi + # 登录 Claude Code + echo "Logging in to Claude Code..." + echo "$CLAUDE_API_KEY" | claude login --stdin || { + echo "ERROR: Failed to login to Claude Code" + exit 1 + } + echo "Login successful" + # 直接运行,显示所有输出 echo "=== Starting Claude Code ===" set -o pipefail From 715d84e562486743b4ace05acc4c37f86429dff4 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 17:53:28 +0800 Subject: [PATCH 27/31] fix: use ANTHROPIC_API_KEY instead of CLAUDE_API_KEY, remove login --- .github/workflows/_claude-code.yml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index bdceed5..0c0e1fb 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -246,7 +246,7 @@ jobs: DANGEROUSLY_SKIP_PERMISSIONS: ${{ github.event.client_payload.dangerously_skip_permissions }} ALLOWED_TOOLS: ${{ github.event.client_payload.allowed_tools }} SHOW_FULL_OUTPUT: ${{ github.event.client_payload.show_full_output }} - CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.CLAUDE_API_KEY }} working-directory: caller-repo run: | if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then @@ -282,21 +282,13 @@ jobs: echo "Debug: Running claude with args: $CLAUDE_ARGS" # 检查 API key 是否设置(不显示值) - if [ -z "$CLAUDE_API_KEY" ]; then - echo "ERROR: CLAUDE_API_KEY is not set!" + if [ -z "$ANTHROPIC_API_KEY" ]; then + echo "ERROR: ANTHROPIC_API_KEY is not set!" exit 1 else - echo "Debug: CLAUDE_API_KEY is set (length: ${#CLAUDE_API_KEY})" + echo "Debug: ANTHROPIC_API_KEY is set (length: ${#ANTHROPIC_API_KEY})" fi - # 登录 Claude Code - echo "Logging in to Claude Code..." - echo "$CLAUDE_API_KEY" | claude login --stdin || { - echo "ERROR: Failed to login to Claude Code" - exit 1 - } - echo "Login successful" - # 直接运行,显示所有输出 echo "=== Starting Claude Code ===" set -o pipefail From be43f035011e6c78aa352cd1183e96ac846718a7 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 19:10:14 +0800 Subject: [PATCH 28/31] test: working version with claude auth fix and debug output --- .github/workflows/_claude-code.yml | 43 +++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index 0c0e1fb..c2fc679 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -253,6 +253,39 @@ jobs: echo "::group::Claude Code Full Output" fi + # 配置 Claude CLI + if [ -n "$ANTHROPIC_API_KEY" ]; then + echo "Configuring Claude CLI..." + mkdir -p ~/.config/claude + echo "{\"apiKey\":\"$ANTHROPIC_API_KEY\"}" > ~/.config/claude/settings.json + echo "Claude CLI configured" + fi + + CLAUDE_ARGS="--model $MODEL --output-format json" + + if [[ "$DANGEROUSLY_SKIP_PERMISSIONS" == "true" ]]; then + CLAUDE_ARGS="$CLAUDE_ARGS --dangerously-skip-permissions" + fi + + if [[ -n "$ALLOWED_TOOLS" ]]; then + CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools $ALLOWED_TOOLS" + else + if [[ "$ALLOW_CODE_CHANGE" == "true" ]]; then + CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools Read,Edit,Write,Glob,Grep,Bash" + else + CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools Read,Glob,Grep,Bash" + fi + fi + + claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS \ + 2>/tmp/claude_stderr.txt \ + > /tmp/claude_raw.json + EXIT_CODE=$? + + if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then + echo "::endgroup::" + fi + # Debug: 显示环境变量 echo "Debug: CLAUDE_PROMPT length: ${#CLAUDE_PROMPT}" echo "Debug: MODEL=$MODEL" @@ -357,7 +390,8 @@ jobs: # ------------------------------------------------------------------ - name: Create patch file id: create_patch - if: steps.claude_run.outputs.code_changed == 'true' && github.event.client_payload.allow_code_change == 'true' && (github.event.client_payload.verify_command == '' || steps.verify.conclusion == 'success') + # 简化条件:只要代码变更就创建 patch + if: steps.claude_run.outputs.code_changed == 'true' working-directory: caller-repo run: | git diff --no-color > /tmp/claude-changes.patch @@ -365,6 +399,13 @@ jobs: echo "patch_size=$PATCH_SIZE" >> "$GITHUB_OUTPUT" echo "patch_path=/tmp/claude-changes.patch" >> "$GITHUB_OUTPUT" echo "::notice::Created patch file: $PATCH_SIZE bytes" + + # Debug: 显示 patch 内容预览 + if [ $PATCH_SIZE -gt 0 ]; then + echo "=== Patch Preview ===" + head -30 /tmp/claude-changes.patch + echo "=====================" + fi - name: Upload patch as artifact if: steps.create_patch.outputs.patch_path From 8634883c3b00d1ada59399105e47e110e463dddf Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Thu, 9 Apr 2026 19:18:21 +0800 Subject: [PATCH 29/31] docs: add migration guide for CC integration --- docs/migration-guide.md | 235 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/migration-guide.md diff --git a/docs/migration-guide.md b/docs/migration-guide.md new file mode 100644 index 0000000..8cc05ba --- /dev/null +++ b/docs/migration-guide.md @@ -0,0 +1,235 @@ +# 使用 Agent-Skills 替换直接 CC 调用 + +本文档说明如何将 `main2main_auto.yaml` 中直接调用 Claude Code 的方式,替换为调用 Agent-Skills。 + +## 架构对比 + +### 方案 A:直接调用(原方案) +```yaml +- uses: anthropics/claude-code-action/base-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} + prompt: "..." + claude_args: "..." +``` + +**优点**: +- 简单直接,一步完成 +- GitHub Action 自动处理认证 + +**缺点**: +- 需要在每个调用方仓库配置 API key +- 无法集中管理 CC 版本和配置 + +### 方案 B:通过 Agent-Skills(新方案) +```yaml +- name: Trigger agent-skills + run: | + curl -X POST \ + -H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \ + "https://api.github.com/repos//agent-skills/dispatches" \ + -d '{ + "event_type": "claude-request", + "client_payload": { + "caller_repo": "owner/repo", + "custom_prompt": "...", + "model": "claude-sonnet-4-20250514" + } + }' +``` + +**优点**: +- API key 集中管理在 agent-skills +- 可统一升级 CC 版本 +- 支持白名单权限控制 + +**缺点**: +- 流程更复杂(触发→等待→下载→应用) +- 需要处理异步调用 +- 容器内不能使用 `--dangerously-skip-permissions` + +## 迁移步骤 + +### 1. 准备工作 + +#### Agent-Skills 端配置 +1. 在 `allowed-callers.json` 中添加调用方仓库 +2. 确保 `CLAUDE_API_KEY` secret 已设置 +3. 确保 `AGENT_PAT` secret 已设置(用于检出调用方仓库) + +#### 调用方仓库配置 +1. 在仓库 Settings → Secrets 添加 `PAT_TOKEN`: + - 需要有 `repo` 和 `workflow` 权限 + - 需要能触发 agent-skills 仓库的 workflow + +### 2. 修改 Workflow + +将原来的 CC 调用步骤替换为以下三个步骤: + +```yaml +# 步骤 1: 触发 Agent-Skills +- name: Trigger agent-skills workflow + id: trigger + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} + run: | + # 构建 JSON payload + PAYLOAD=$(cat </agent-skills/dispatches" \ + -d "$PAYLOAD" + + sleep 15 # 等待 workflow 启动 + +# 步骤 2: 获取 Workflow Run ID 并等待完成 +- name: Wait for agent-skills completion + id: wait + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} + run: | + # 获取最新的 workflow run + RUN_ID=$(gh run list \ + --repo "/agent-skills" \ + --workflow _claude-code.yml \ + -L 1 \ + --json databaseId \ + --jq '.[0].databaseId') + + # 轮询等待完成 + for i in {1..180}; do + STATUS=$(gh run view "$RUN_ID" \ + --repo "/agent-skills" \ + --json conclusion \ + --jq '.conclusion') + + if [[ "$STATUS" == "success" || "$STATUS" == "failure" ]]; then + break + fi + sleep 10 + done + +# 步骤 3: 下载 patch 并应用 +- name: Download and apply patch + run: | + # 下载 artifact + gh run download "$RUN_ID" \ + --repo "/agent-skills" \ + --name claude-changes-patch \ + --dir /tmp/patch + + # 应用 patch + git apply /tmp/patch/claude-changes.patch +``` + +### 3. 参数映射表 + +| 原参数 | 新参数 | 说明 | +|--------|--------|------| +| `anthropic_api_key` | 无需传递 | 由 agent-skills 管理 | +| `prompt` | `custom_prompt` | 直接传递 prompt 内容 | +| `claude_args` | `model`, `allowed_tools` 等 | 拆分为独立参数 | +| 环境变量 | `caller_env_vars` | 多行字符串格式 | + +### 4. 注意事项 + +#### 关于 `--dangerously-skip-permissions` +- **原方案**:可以使用(GitHub Action 内部处理) +- **新方案**:容器内使用 root 用户,**必须设为 false** +- 影响:CC 会提示权限确认,但测试显示可以正常工作 + +#### 关于环境变量 +原方案通过 `env:` 直接设置: +```yaml +env: + KEY: value +``` + +新方案通过 `caller_env_vars` 传递(多行字符串): +```yaml +"caller_env_vars": "KEY1=value1\nKEY2=value2" +``` + +#### 关于模型 +- 原方案可以使用任意模型(包括第三方如 minimax) +- 新方案使用 Anthropic 官方模型(如 `claude-sonnet-4-20250514`) + +## 测试建议 + +在实际迁移前,建议: +1. 先创建测试 workflow(类似 test-cc-phase1.yml) +2. 验证参数传递是否正确 +3. 验证 patch 能成功下载和应用 +4. 确认无误后再修改 main2main + +## 回滚方案 + +如果新方案有问题,可以快速回滚: +1. 恢复原来的 `uses: anthropics/claude-code-action/base-action@v1` 步骤 +2. 删除新增的触发/等待/下载步骤 +3. 无需修改 agent-skills + +## 示例:Phase 1 完整替换 + +### 替换前(原方案) +```yaml +- name: Claude adapt to new vLLM + uses: anthropics/claude-code-action/base-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} + prompt: | + Use the main2main skill to adapt... + NEW_COMMIT: ${{ steps.detect.outputs.new_commit }} + claude_args: "--model claude-sonnet-4-20250514 --dangerously-skip-permissions ..." + env: + OLD_COMMIT: ${{ steps.detect.outputs.old_commit }} + NEW_COMMIT: ${{ steps.detect.outputs.new_commit }} +``` + +### 替换后(新方案) +```yaml +- name: Trigger agent-skills + run: | + curl -X POST ... + # payload 包含: + # - caller_repo: "owner/repo" + # - custom_prompt: "Use the main2main skill..." + # - model: "claude-sonnet-4-20250514" + # - allowed_tools: "..." + # - caller_env_vars: "OLD_COMMIT=...\nNEW_COMMIT=..." + +- name: Wait for completion + run: | + # 轮询等待 agent-skills 完成 + +- name: Apply patch + run: | + # 下载 artifact 并应用 +``` + +## 参考 + +- Agent-Skills 仓库:https://github.com/opensourceways/agent-skills +- 测试示例:vllm-benchmarks/.github/workflows/test-cc-phase1.yml +- 原始 main2main:nv-action/vllm-benchmarks/.github/workflows/main2main_auto.yaml From 81adad28a04651c23f11ab0f35f430694e4f92c7 Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Fri, 10 Apr 2026 10:46:56 +0800 Subject: [PATCH 30/31] docs: update migration guide with simplified steps and correct repository references --- docs/migration-guide.md | 233 +++++++++++++++++++++------------------- 1 file changed, 120 insertions(+), 113 deletions(-) diff --git a/docs/migration-guide.md b/docs/migration-guide.md index 8cc05ba..7ea1626 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -1,61 +1,15 @@ -# 使用 Agent-Skills 替换直接 CC 调用 +# 迁移指南:从直接 CC 调用到 Agent-Skills -本文档说明如何将 `main2main_auto.yaml` 中直接调用 Claude Code 的方式,替换为调用 Agent-Skills。 - -## 架构对比 - -### 方案 A:直接调用(原方案) -```yaml -- uses: anthropics/claude-code-action/base-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} - prompt: "..." - claude_args: "..." -``` - -**优点**: -- 简单直接,一步完成 -- GitHub Action 自动处理认证 - -**缺点**: -- 需要在每个调用方仓库配置 API key -- 无法集中管理 CC 版本和配置 - -### 方案 B:通过 Agent-Skills(新方案) -```yaml -- name: Trigger agent-skills - run: | - curl -X POST \ - -H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \ - "https://api.github.com/repos//agent-skills/dispatches" \ - -d '{ - "event_type": "claude-request", - "client_payload": { - "caller_repo": "owner/repo", - "custom_prompt": "...", - "model": "claude-sonnet-4-20250514" - } - }' -``` - -**优点**: -- API key 集中管理在 agent-skills -- 可统一升级 CC 版本 -- 支持白名单权限控制 - -**缺点**: -- 流程更复杂(触发→等待→下载→应用) -- 需要处理异步调用 -- 容器内不能使用 `--dangerously-skip-permissions` +本文档说明如何将 workflow 中直接调用 Claude Code 的方式,替换为通过 Agent-Skills 调用。 ## 迁移步骤 ### 1. 准备工作 #### Agent-Skills 端配置 -1. 在 `allowed-callers.json` 中添加调用方仓库 -2. 确保 `CLAUDE_API_KEY` secret 已设置 -3. 确保 `AGENT_PAT` secret 已设置(用于检出调用方仓库) +1. 在 `allowed-callers.json` 中添加调用方仓库路径 +2. 确保 `CLAUDE_API_KEY` secret 已配置 +3. 确保 `AGENT_PAT` secret 已配置(用于检出调用方仓库) #### 调用方仓库配置 1. 在仓库 Settings → Secrets 添加 `PAT_TOKEN`: @@ -64,16 +18,16 @@ ### 2. 修改 Workflow -将原来的 CC 调用步骤替换为以下三个步骤: +将原来的 `anthropics/claude-code-action/base-action@v1` 步骤替换为以下三个步骤: + +#### 步骤 1: 触发 Agent-Skills ```yaml -# 步骤 1: 触发 Agent-Skills - name: Trigger agent-skills workflow id: trigger env: GH_TOKEN: ${{ secrets.PAT_TOKEN }} run: | - # 构建 JSON payload PAYLOAD=$(cat </agent-skills/dispatches" \ + "https://api.github.com/repos/kadenzhang3321/agent-skills/dispatches" \ -d "$PAYLOAD" sleep 15 # 等待 workflow 启动 +``` -# 步骤 2: 获取 Workflow Run ID 并等待完成 +#### 步骤 2: 等待完成 + +```yaml - name: Wait for agent-skills completion id: wait env: GH_TOKEN: ${{ secrets.PAT_TOKEN }} run: | - # 获取最新的 workflow run - RUN_ID=$(gh run list \ - --repo "/agent-skills" \ - --workflow _claude-code.yml \ - -L 1 \ - --json databaseId \ - --jq '.[0].databaseId') - - # 轮询等待完成 for i in {1..180}; do - STATUS=$(gh run view "$RUN_ID" \ - --repo "/agent-skills" \ - --json conclusion \ - --jq '.conclusion') + RUN_ID=$(gh run list \ + --repo "kadenzhang3321/agent-skills" \ + --workflow _claude-code.yml \ + -L 1 \ + --json databaseId \ + --jq '.[0].databaseId') - if [[ "$STATUS" == "success" || "$STATUS" == "failure" ]]; then - break + if [ -n "$RUN_ID" ]; then + STATUS=$(gh run view "$RUN_ID" \ + --repo "kadenzhang3321/agent-skills" \ + --json conclusion \ + --jq '.conclusion') + + if [[ "$STATUS" == "success" || "$STATUS" == "failure" ]]; then + echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" + break + fi fi sleep 10 done +``` + +#### 步骤 3: 下载并应用 Patch -# 步骤 3: 下载 patch 并应用 +```yaml - name: Download and apply patch run: | - # 下载 artifact - gh run download "$RUN_ID" \ - --repo "/agent-skills" \ + gh run download "${{ steps.wait.outputs.run_id }}" \ + --repo "kadenzhang3321/agent-skills" \ --name claude-changes-patch \ --dir /tmp/patch - # 应用 patch git apply /tmp/patch/claude-changes.patch ``` @@ -150,7 +108,7 @@ | `anthropic_api_key` | 无需传递 | 由 agent-skills 管理 | | `prompt` | `custom_prompt` | 直接传递 prompt 内容 | | `claude_args` | `model`, `allowed_tools` 等 | 拆分为独立参数 | -| 环境变量 | `caller_env_vars` | 多行字符串格式 | +| 环境变量 | `caller_env_vars` | 多行字符串格式,用 `\n` 分隔 | ### 4. 注意事项 @@ -163,36 +121,34 @@ 原方案通过 `env:` 直接设置: ```yaml env: - KEY: value + OLD_COMMIT: abc123 + NEW_COMMIT: def456 ``` 新方案通过 `caller_env_vars` 传递(多行字符串): ```yaml -"caller_env_vars": "KEY1=value1\nKEY2=value2" +"caller_env_vars": "OLD_COMMIT=abc123\nNEW_COMMIT=def456" ``` #### 关于模型 - 原方案可以使用任意模型(包括第三方如 minimax) - 新方案使用 Anthropic 官方模型(如 `claude-sonnet-4-20250514`) -## 测试建议 - -在实际迁移前,建议: -1. 先创建测试 workflow(类似 test-cc-phase1.yml) -2. 验证参数传递是否正确 -3. 验证 patch 能成功下载和应用 -4. 确认无误后再修改 main2main - -## 回滚方案 +#### 关于长内容传递 +如果需要传递文件内容(如 bisect summary): +```bash +# 读取并转义(限制大小避免超出 payload 限制) +CONTENT=$(head -c 3000 /path/to/file.md | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g') +``` -如果新方案有问题,可以快速回滚: -1. 恢复原来的 `uses: anthropics/claude-code-action/base-action@v1` 步骤 -2. 删除新增的触发/等待/下载步骤 -3. 无需修改 agent-skills +然后在 payload 中使用: +```yaml +"caller_env_vars": "FILE_CONTENT=${CONTENT}" +``` -## 示例:Phase 1 完整替换 +### 5. 完整示例 -### 替换前(原方案) +#### 替换前(原方案) ```yaml - name: Claude adapt to new vLLM uses: anthropics/claude-code-action/base-action@v1 @@ -201,35 +157,86 @@ env: prompt: | Use the main2main skill to adapt... NEW_COMMIT: ${{ steps.detect.outputs.new_commit }} - claude_args: "--model claude-sonnet-4-20250514 --dangerously-skip-permissions ..." + claude_args: "--model minimax/minimax-m2.5:free --dangerously-skip-permissions --allowed-tools 'Bash(git *),Read,Write'" env: OLD_COMMIT: ${{ steps.detect.outputs.old_commit }} NEW_COMMIT: ${{ steps.detect.outputs.new_commit }} + CLAUDE_WORKING_DIR: ${{ github.workspace }}/work-dir ``` -### 替换后(新方案) +#### 替换后(新方案) ```yaml - name: Trigger agent-skills + id: trigger + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} run: | - curl -X POST ... - # payload 包含: - # - caller_repo: "owner/repo" - # - custom_prompt: "Use the main2main skill..." - # - model: "claude-sonnet-4-20250514" - # - allowed_tools: "..." - # - caller_env_vars: "OLD_COMMIT=...\nNEW_COMMIT=..." - -- name: Wait for completion + PAYLOAD=$(cat <> "$GITHUB_OUTPUT" + break + fi + fi + sleep 10 + done -- name: Apply patch +- name: Download and apply patch run: | - # 下载 artifact 并应用 + gh run download "${{ steps.wait.outputs.run_id }}" \ + --repo "kadenzhang3321/agent-skills" \ + --name claude-changes-patch \ + --dir /tmp/patch + + git apply /tmp/patch/claude-changes.patch ``` -## 参考 +### 6. 回滚方案 -- Agent-Skills 仓库:https://github.com/opensourceways/agent-skills -- 测试示例:vllm-benchmarks/.github/workflows/test-cc-phase1.yml -- 原始 main2main:nv-action/vllm-benchmarks/.github/workflows/main2main_auto.yaml +如果新方案有问题,可以快速回滚: +1. 恢复原来的 `uses: anthropics/claude-code-action/base-action@v1` 步骤 +2. 删除新增的触发/等待/下载步骤 +3. 无需修改 agent-skills From 60e09442c0b0508264098ee7bad831ad4be421cc Mon Sep 17 00:00:00 2001 From: ZhangYang Date: Wed, 15 Apr 2026 16:50:02 +0800 Subject: [PATCH 31/31] refactor: optimize and simplify agent-skills workflows - Inline whitelist, permission and prompt logic into _claude-code.yml to remove dependency on external python scripts - Fix agent-skills checkout to use correct repository reference - Rename AGENT_PAT to DISPATCH_TOKEN for cross-repo access - Move dependency installation before inline python checks - Sanitize response_summary newlines for GITHUB_OUTPUT compatibility - Install gh CLI for posting comments via gh api - Simplify retry logic and remove obsolete template files - Update caller templates to default allow_code_change=false - Add apply-patch jobs that create draft PRs instead of direct push - Update documentation to reflect current workflow_call-only design --- .github/workflows/README.md | 88 ++- .github/workflows/TEMPLATE-CALLER.yml | 281 +++++++++ .github/workflows/_claude-code.yml | 581 ++++++++++-------- .github/workflows/embeddable-caller.yml | 109 ---- .github/workflows/example-caller.yml | 119 ---- .../workflows/template-invoke-claude-code.yml | 214 ------- EXAMPLE-USAGE.md | 287 +++++++++ README.md | 83 ++- docs/migration-guide.md | 242 -------- .../scripts/collect-failed-runs.js | 369 +++++++++++ .../scripts/diagnose-ci-runs.js | 495 +++++++++++++++ 11 files changed, 1882 insertions(+), 986 deletions(-) create mode 100644 .github/workflows/TEMPLATE-CALLER.yml delete mode 100644 .github/workflows/embeddable-caller.yml delete mode 100644 .github/workflows/example-caller.yml delete mode 100644 .github/workflows/template-invoke-claude-code.yml create mode 100644 EXAMPLE-USAGE.md delete mode 100644 docs/migration-guide.md create mode 100644 skills/infrastructure/github-action-diagnose/scripts/collect-failed-runs.js create mode 100644 skills/infrastructure/github-action-diagnose/scripts/diagnose-ci-runs.js diff --git a/.github/workflows/README.md b/.github/workflows/README.md index da2327a..436ec86 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -15,79 +15,65 @@ API Key 集中存储在 `KadenZhang3321/agent-skills`,调用方无需配置。 ## 快速开始 -### 方式一:手动触发(`@claude` 评论) - -复制 [example-caller.yml](./example-caller.yml) 到目标仓库: +复制 [TEMPLATE-CALLER.yml](./TEMPLATE-CALLER.yml) 到目标仓库: ```bash mkdir -p .github/workflows -curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.github/workflows/example-caller.yml \ +curl -sSL https://raw.githubusercontent.com/KadenZhang3321/agent-skills/main/.github/workflows/TEMPLATE-CALLER.yml \ -o .github/workflows/claude-bot.yml git add .github/workflows/claude-bot.yml git commit -m "ci: add Claude Bot workflow" git push ``` -使用:在任意 PR 或 Issue 评论区输入 `@claude <你的问题>`,Claude 会自动回复。 - ---- - -### 方式二:PR 自动触发 - -复制 [embeddable-caller.yml](./embeddable-caller.yml) 到目标仓库: - -```bash -curl -sSL https://raw.githubusercontent.com/opensourceways/agent-skills/main/.github/workflows/embeddable-caller.yml \ - -o .github/workflows/claude-auto.yml -``` - -使用:每次 PR 开启或更新时,Claude 自动分析代码并发评论。 +`TEMPLATE-CALLER.yml` 已内置三种触发方式,按需保留即可: +- **手动触发**(`workflow_dispatch`) +- **PR 评论触发**(`issue_comment`,需包含 `@claude`) +- **PR 自动触发**(`pull_request`,opened/synchronize) --- ## 参数说明 -在 caller 文件的 `payload` 中配置以下参数: +在 caller 文件的 `with` 中配置以下参数: | 参数 | 默认值 | 说明 | |------|--------|------| -| `allow_code_change` | `'false'` | `'true'` 允许 Claude 修改文件并直接 commit 到当前 PR 分支;`'false'` 只分析 | +| `allow_code_change` | `false` | `true` 允许 Claude 修改文件并生成 patch artifact;`false` 只分析 | | `skill_name` | `''` | 指定 Skill 名称(见下方说明),留空不使用 | | `skill_url` | `''` | 远程 Skill 文件的 raw URL,优先级高于 skill_name | | `model` | `'claude-sonnet-4-20250514'` | Claude 模型名称 | -| `caller_image` | `''` | 调用方提供的镜像(仅工作流自动触发使用),留空使用 `ubuntu-latest` | -| `verify_command` | `''` | 验证命令(仅工作流自动触发使用),验证失败则不 push 代码 | +| `caller_image` | `''` | 调用方提供的镜像,留空使用 `ubuntu:24.04` | +| `verify_command` | `''` | 验证命令,验证失败则 job 失败 | -### caller_image 示例(仅工作流自动触发) +### caller_image 示例 ```yaml # 调用方提供的镜像,镜像需要预装 Node.js、Git、依赖已安装 'caller_image': 'ghcr.io/caller/project:main', -# 使用 ubuntu-latest(默认) +# 使用 ubuntu:24.04(默认) 'caller_image': '', ``` -### verify_command 示例(仅工作流自动触发) +### verify_command 示例 ```yaml -# 验证命令,验证失败则 job 失败,不 push 代码 +# 验证命令,验证失败则 job 失败 'verify_command': 'npm run build && npm test', # 不验证(默认) 'verify_command': '', ``` -> 注意:`caller_image` 和 `verify_command` 仅在 **工作流自动触发**(embeddable-caller.yml)时使用,手动 `@claude` 触发时不传这两个参数。 - ### allow_code_change 示例 ```yaml # 只审查,不改代码(推荐用于自动触发) -'allow_code_change': 'false', +'allow_code_change': false, -# 允许改代码,直接 commit 到当前 PR 分支 -'allow_code_change': 'true', +# 允许改代码,生成 patch artifact +'allow_code_change': true, ``` ### skill_name 示例 @@ -120,9 +106,6 @@ agent-skills 内置 Skill 列表见 [skills/](../../skills/)。 ```yaml # 使用远程 Skill(注意:URL 必须是 raw 内容链接) 'skill_url': 'https://raw.githubusercontent.com/owner/repo/main/.ai/skills/my-skill/SKILL.md', - -# 远程 Skill 与 skill_name 同时存在时,优先使用 skill_url -# 'skill_url': 'https://raw.githubusercontent.com/opensourceways/om-webserver/main/.ai/skills/architect-project-analysis/SKILL.md', ``` > 提示:GitHub raw URL 格式为 `https://raw.githubusercontent.com////` @@ -170,23 +153,22 @@ agent-skills 内置 Skill 列表见 [skills/](../../skills/)。 ``` 调用方仓库 agent-skills ───────────────── ──────────────────────────────── -@claude 评论触发 - └─ example-caller.yml - └─ 发送 repository_dispatch ──→ _claude-code.yml - 1. 白名单校验 - 2. 用户权限校验 - 3. 使用 caller_image 创建容器(如未传则用 ubuntu-latest) - 4. 安装 Claude Code - 5. Checkout 调用方仓库 - 6. 拉取 PR Diff - 7. 构建 Prompt(含 Skill) - 8. 调用 Claude(使用集中的 API Key) - 9. verify_command 不为空? - ├─ 否 → 直接 push - └─ 是 → 执行验证命令 - ├─ 成功 → push - └─ 失败 → 不 push - 10. 在原 PR/Issue 发布评论 +PR 评论 / PR 事件 / 手动触发 + └─ TEMPLATE-CALLER.yml + └─ workflow_call ───────────> _claude-code.yml + 1. 安装基础依赖(含 python3、gh) + 2. Checkout agent-skills + 3. 白名单校验 + 4. 用户权限校验 + 5. 安装 Claude Code + 6. Checkout 调用方仓库 + 7. 拉取 PR Diff + 8. 构建 Prompt(含 Skill) + 9. 调用 Claude(使用集中的 API Key) + 10. verify_command 不为空? + └─ 执行验证命令 + 11. 生成 patch artifact(如有变更) + 12. 在原 PR/Issue 发布评论 ``` --- @@ -196,7 +178,7 @@ agent-skills 内置 Skill 列表见 [skills/](../../skills/)。 | Secret | 用途 | |--------|------| | `CLAUDE_API_KEY` | Anthropic API Key,用于调用 Claude | -| `AGENT_PAT` | PAT(`repo` scope),用于读写调用方仓库、发评论、创建 PR | +| `DISPATCH_TOKEN` | PAT(`repo` scope),用于读写调用方仓库、发评论、创建 PR | --- @@ -210,7 +192,7 @@ agent-skills 内置 Skill 列表见 [skills/](../../skills/)。 - 在 `allowed-callers.json` 中添加对应的 org 或 repo **Claude 没有发评论** -- 检查 `AGENT_PAT` 是否有目标仓库的写权限 +- 检查 `DISPATCH_TOKEN` 是否有目标仓库的写权限 - 查看 agent-skills Actions 中该次 run 的 `Post comment` 步骤日志 **Token/Cost 显示 N/A** diff --git a/.github/workflows/TEMPLATE-CALLER.yml b/.github/workflows/TEMPLATE-CALLER.yml new file mode 100644 index 0000000..f37285c --- /dev/null +++ b/.github/workflows/TEMPLATE-CALLER.yml @@ -0,0 +1,281 @@ +# TEMPLATE-CALLER.yml +# 通用调用模板 - 展示如何在业务仓库中调用中心 Claude Code 服务 +# +# 使用方法: +# 1. 复制此文件到你的业务仓库的 .github/workflows/ 目录 +# 2. 根据需要修改触发条件和参数 +# 3. 确保在仓库 Settings → Secrets 中配置了 DISPATCH_TOKEN 和 CLAUDE_API_KEY + +name: Claude Code Caller Template + +on: + # 方式 1:手动触发(测试用) + workflow_dispatch: + inputs: + prompt: + description: '要执行的 prompt' + required: true + default: '请分析这个仓库的代码结构' + allow_code_change: + description: '是否允许代码修改' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' + + # 方式 2:PR 评论触发 + issue_comment: + types: [created] + + # 方式 3:PR 打开或更新时自动触发 + pull_request: + types: [opened, synchronize] + +jobs: + # -------------------------------------------------------------------------- + # 方式 A:手动触发 + # -------------------------------------------------------------------------- + claude-manual: + if: github.event_name == 'workflow_dispatch' + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + caller_repo: ${{ github.repository }} + custom_prompt: ${{ github.event.inputs.prompt }} + allow_code_change: ${{ github.event.inputs.allow_code_change == 'true' }} + model: 'claude-sonnet-4-20250514' + show_full_output: true + allowed_tools: 'Bash(git *),Bash(gh *),Read,Write,Edit,Glob,Grep' + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + + claude-manual-result: + if: github.event_name == 'workflow_dispatch' + needs: claude-manual + runs-on: ubuntu-latest + steps: + - name: Check Claude result + run: | + echo "Changes detected: ${{ needs.claude-manual.outputs.changes_detected }}" + echo "Cost: ${{ needs.claude-manual.outputs.cost_usd }} USD" + echo "Input tokens: ${{ needs.claude-manual.outputs.input_tokens }}" + echo "Output tokens: ${{ needs.claude-manual.outputs.output_tokens }}" + echo "Response summary: ${{ needs.claude-manual.outputs.response_summary }}" + + if [[ "${{ needs.claude-manual.outputs.error_message }}" != "" ]]; then + echo "Error: ${{ needs.claude-manual.outputs.error_message }}" + exit 1 + fi + + # 【可选】自动应用 Claude 的代码修改 + # 仅当 changes_detected == 'true' 时才会执行 + claude-manual-apply: + if: ${{ needs.claude-manual.outputs.changes_detected == 'true' && github.event_name == 'workflow_dispatch' }} + needs: claude-manual + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.DISPATCH_TOKEN }} + fetch-depth: 0 + + - name: Download patch artifact + uses: actions/download-artifact@v4 + with: + name: claude-changes-patch + path: /tmp/patch + + - name: Apply patch and create draft PR + env: + GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + run: | + git config user.name "Claude Bot" + git config user.email "claude-bot@users.noreply.github.com" + if [ ! -s /tmp/patch/claude-changes.patch ]; then + echo "::warning::Patch file is empty, nothing to apply" + exit 0 + fi + + BASE_BRANCH=$(git branch --show-current) + NEW_BRANCH="claude-patch-${{ github.run_id }}" + + git checkout -b "$NEW_BRANCH" + git apply /tmp/patch/claude-changes.patch + git add -A + git commit -m "AI: apply Claude suggestions" + git push origin "$NEW_BRANCH" + + gh pr create \ + --base "$BASE_BRANCH" \ + --head "$NEW_BRANCH" \ + --title "[Claude] Auto patch from run ${{ github.run_id }}" \ + --body "This PR was generated by applying the Claude patch artifact. Please review before merging." \ + --draft + + echo "::notice::Created draft PR from branch $NEW_BRANCH to $BASE_BRANCH" + + # -------------------------------------------------------------------------- + # 方式 B:PR 评论触发(仅响应包含 @claude 的 PR 评论) + # -------------------------------------------------------------------------- + claude-comment: + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '@claude') && + !endsWith(github.actor, '[bot]') && + github.actor != 'dependabot' + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + caller_repo: ${{ github.repository }} + pr_number: ${{ github.event.issue.number }} + issue_number: ${{ github.event.issue.number }} + comment_author: ${{ github.event.comment.user.login }} + comment_body: ${{ github.event.comment.body }} + allow_code_change: false + model: 'claude-sonnet-4-20250514' + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + + # 【可选】自动应用 Claude 的代码修改 + # 仅当 changes_detected == 'true' 时才会执行 + claude-comment-apply: + if: ${{ needs.claude-comment.outputs.changes_detected == 'true' && github.event_name == 'issue_comment' }} + needs: claude-comment + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Get PR info + id: pr + env: + GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + run: | + HEAD=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefName -q .headRefName) + BASE=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json baseRefName -q .baseRefName) + echo "head=$HEAD" >> "$GITHUB_OUTPUT" + echo "base=$BASE" >> "$GITHUB_OUTPUT" + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ steps.pr.outputs.head }} + token: ${{ secrets.DISPATCH_TOKEN }} + fetch-depth: 0 + + - name: Download patch artifact + uses: actions/download-artifact@v4 + with: + name: claude-changes-patch + path: /tmp/patch + + - name: Apply patch and create draft PR + env: + GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + run: | + git config user.name "Claude Bot" + git config user.email "claude-bot@users.noreply.github.com" + if [ ! -s /tmp/patch/claude-changes.patch ]; then + echo "::warning::Patch file is empty, nothing to apply" + exit 0 + fi + + BASE_BRANCH="${{ steps.pr.outputs.base }}" + NEW_BRANCH="claude-patch-${{ github.run_id }}" + + git checkout -b "$NEW_BRANCH" + git apply /tmp/patch/claude-changes.patch + git add -A + git commit -m "AI: apply Claude suggestions" + git push origin "$NEW_BRANCH" + + gh pr create \ + --base "$BASE_BRANCH" \ + --head "$NEW_BRANCH" \ + --title "[Claude] Auto patch from run ${{ github.run_id }}" \ + --body "This PR was generated by applying the Claude patch artifact. Please review before merging." \ + --draft + + echo "::notice::Created draft PR from branch $NEW_BRANCH to $BASE_BRANCH" + + # -------------------------------------------------------------------------- + # 方式 C:PR 自动触发 + # -------------------------------------------------------------------------- + claude-auto-pr: + if: | + github.event_name == 'pull_request' && + github.event.pull_request.draft == false + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + caller_repo: ${{ github.repository }} + pr_number: ${{ github.event.pull_request.number }} + issue_number: ${{ github.event.pull_request.number }} + comment_author: ${{ github.event.pull_request.user.login }} + comment_body: 'Auto-triggered: Please review this PR and suggest improvements.' + allow_code_change: false + skill_name: 'github-action-diagnose' + model: 'claude-sonnet-4-20250514' + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + + # 【可选】自动应用 Claude 的代码修改 + # 仅当 changes_detected == 'true' 时才会执行 + claude-auto-pr-apply: + if: ${{ needs.claude-auto-pr.outputs.changes_detected == 'true' && github.event_name == 'pull_request' }} + needs: claude-auto-pr + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.DISPATCH_TOKEN }} + fetch-depth: 0 + + - name: Download patch artifact + uses: actions/download-artifact@v4 + with: + name: claude-changes-patch + path: /tmp/patch + + - name: Apply patch and create draft PR + env: + GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + run: | + git config user.name "Claude Bot" + git config user.email "claude-bot@users.noreply.github.com" + if [ ! -s /tmp/patch/claude-changes.patch ]; then + echo "::warning::Patch file is empty, nothing to apply" + exit 0 + fi + + BASE_BRANCH="${{ github.event.pull_request.base.ref }}" + NEW_BRANCH="claude-patch-${{ github.run_id }}" + + git checkout -b "$NEW_BRANCH" + git apply /tmp/patch/claude-changes.patch + git add -A + git commit -m "AI: apply Claude suggestions" + git push origin "$NEW_BRANCH" + + gh pr create \ + --base "$BASE_BRANCH" \ + --head "$NEW_BRANCH" \ + --title "[Claude] Auto patch from run ${{ github.run_id }}" \ + --body "This PR was generated by applying the Claude patch artifact. Please review before merging." \ + --draft + + echo "::notice::Created draft PR from branch $NEW_BRANCH to $BASE_BRANCH" diff --git a/.github/workflows/_claude-code.yml b/.github/workflows/_claude-code.yml index c2fc679..a5d5dd5 100644 --- a/.github/workflows/_claude-code.yml +++ b/.github/workflows/_claude-code.yml @@ -1,15 +1,126 @@ # _claude-code.yml -# Claude Code Handler — 由白名单仓库通过 repository_dispatch 触发。 +# Claude Code Handler — 仅支持 workflow_call 触发器。 # -# 本仓库(opensourceways/agent-skills)需配置以下 Secrets: +# 本仓库(KadenZhang3321/agent-skills)需配置以下 Secrets: # CLAUDE_API_KEY — Anthropic API Key(集中管理,调用方无需配置) -# AGENT_PAT — 具备 repo 权限的 PAT(用于读写调用方仓库、发布评论、创建 PR) +# DISPATCH_TOKEN — 具备 repo 权限的 PAT(用于读写调用方仓库、发布评论、创建 PR) name: Claude Code Handler on: - repository_dispatch: - types: [claude-request] + workflow_call: + inputs: + caller_repo: + description: '调用方仓库路径 (owner/repo)' + required: true + type: string + custom_prompt: + description: '自定义 prompt(可选)' + required: false + type: string + default: '' + skill_name: + description: 'Skill 名称(可选)' + required: false + type: string + default: '' + skill_url: + description: 'Skill URL(可选)' + required: false + type: string + default: '' + allow_code_change: + description: '是否允许代码修改' + required: false + type: boolean + default: false + model: + description: 'Claude 模型名称' + required: false + type: string + default: 'claude-sonnet-4-20250514' + dangerously_skip_permissions: + description: '跳过权限确认' + required: false + type: boolean + default: false + allowed_tools: + description: '允许的工具列表' + required: false + type: string + default: '' + show_full_output: + description: '显示完整输出' + required: false + type: boolean + default: false + caller_env_vars: + description: '调用方环境变量(\n 分隔)' + required: false + type: string + default: '' + caller_image: + description: '自定义容器镜像' + required: false + type: string + default: '' + verify_command: + description: '验证命令' + required: false + type: string + default: '' + pr_number: + description: 'PR 编号' + required: false + type: string + default: '' + issue_number: + description: 'Issue 编号' + required: false + type: string + default: '' + comment_author: + description: '评论作者' + required: false + type: string + default: 'workflow' + comment_body: + description: '评论内容' + required: false + type: string + default: '' + secrets: + DISPATCH_TOKEN: + description: 'GitHub PAT(repo 权限)' + required: true + CLAUDE_API_KEY: + description: 'Anthropic API Key' + required: true + outputs: + changes_detected: + description: '是否检测到代码变更' + value: ${{ jobs.claude-code.outputs.changes_detected }} + commit_sha: + description: '提交 SHA(如有变更)' + value: ${{ jobs.claude-code.outputs.commit_sha }} + pr_url: + description: '创建的 PR URL(如有)' + value: ${{ jobs.claude-code.outputs.pr_url }} + cost_usd: + description: 'API 调用成本(USD)' + value: ${{ jobs.claude-code.outputs.cost_usd }} + input_tokens: + description: '输入 token 数' + value: ${{ jobs.claude-code.outputs.input_tokens }} + output_tokens: + description: '输出 token 数' + value: ${{ jobs.claude-code.outputs.output_tokens }} + response_summary: + description: 'Claude 回复摘要' + value: ${{ jobs.claude-code.outputs.response_summary }} + error_message: + description: '错误信息(如有)' + value: ${{ jobs.claude-code.outputs.error_message }} jobs: claude-code: @@ -17,29 +128,53 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 container: - image: ${{ github.event.client_payload.caller_image || 'ubuntu:latest' }} + image: ${{ inputs.caller_image || 'ubuntu:24.04' }} options: --user root + outputs: + changes_detected: ${{ steps.claude_run.outputs.changes_detected }} + commit_sha: ${{ steps.claude_run.outputs.commit_sha }} + pr_url: ${{ steps.claude_run.outputs.pr_url }} + cost_usd: ${{ steps.claude_run.outputs.cost_usd }} + input_tokens: ${{ steps.claude_run.outputs.input_tokens }} + output_tokens: ${{ steps.claude_run.outputs.output_tokens }} + response_summary: ${{ steps.claude_run.outputs.response_summary }} + error_message: ${{ steps.claude_run.outputs.error_message }} steps: - # 安装 Python3 和 Java(容器可能没有预装) - - name: Install Python3 and Java - run: apt-get update && apt-get install -y python3 python3-pip curl git bash openjdk-17-jdk - shell: bash - - # ------------------------------------------------------------------ - # 0. Checkout agent-skills(用于读取白名单和 Skill 文件) - # ------------------------------------------------------------------ + # Checkout agent-skills(用于读取白名单和 Skill 文件) - name: Checkout agent-skills uses: actions/checkout@v4 with: + repository: KadenZhang3321/agent-skills + ref: main path: agent-skills - # ------------------------------------------------------------------ - # 1. 白名单校验 - # ------------------------------------------------------------------ + # Install base dependencies + - name: Install base dependencies + if: inputs.caller_image == '' + run: | + apt-get update && apt-get install -y python3 python3-pip curl git bash gh + rm -rf /var/lib/apt/lists/* + shell: bash + + - name: Setup Node.js + if: inputs.caller_image == '' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Claude Code + run: | + if ! command -v claude &> /dev/null; then + npm install -g @anthropic-ai/claude-code + fi + claude --version + shell: bash + + # 白名单校验 - name: Check caller whitelist env: - CALLER_REPO: ${{ github.event.client_payload.caller_repo }} + CALLER_REPO: ${{ inputs.caller_repo }} run: | python3 - <<'PYEOF' import json, os, sys @@ -56,14 +191,12 @@ jobs: PYEOF shell: bash - # ------------------------------------------------------------------ - # 2. 检查触发者在调用方仓库的权限(需要 write/admin/maintain) - # ------------------------------------------------------------------ + # 检查触发者在调用方仓库的权限 - name: Check user permission env: - GH_TOKEN: ${{ secrets.AGENT_PAT }} - CALLER_REPO: ${{ github.event.client_payload.caller_repo }} - ACTOR: ${{ github.event.client_payload.comment_author }} + GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CALLER_REPO: ${{ inputs.caller_repo }} + ACTOR: ${{ inputs.comment_author }} run: | PERMISSION=$(curl -sf \ -H "Authorization: Bearer $GH_TOKEN" \ @@ -79,54 +212,30 @@ jobs: fi shell: bash - # ------------------------------------------------------------------ - # 3. Checkout 调用方仓库 - # ------------------------------------------------------------------ + # Checkout 调用方仓库 - name: Checkout caller repo uses: actions/checkout@v4 with: - repository: ${{ github.event.client_payload.caller_repo }} - token: ${{ secrets.AGENT_PAT }} + repository: ${{ inputs.caller_repo }} + token: ${{ secrets.DISPATCH_TOKEN }} path: caller-repo fetch-depth: 0 - # ------------------------------------------------------------------ - # 4. 安装 Claude Code CLI(如果容器内没有 Node.js) - # ------------------------------------------------------------------ - - name: Setup Node.js - if: github.event.client_payload.caller_image == '' - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install Claude Code - run: | - npm install -g @anthropic-ai/claude-code - claude --version - shell: bash - - # ------------------------------------------------------------------ - # 5. 设置调用方仓库的环境变量 - # ------------------------------------------------------------------ + # 设置调用方仓库的环境变量 - name: Setup caller environment variables - env: - CALLER_ENV_VARS: ${{ github.event.client_payload.caller_env_vars }} + if: inputs.caller_env_vars != '' run: | - if [ -n "$CALLER_ENV_VARS" ]; then - echo "$CALLER_ENV_VARS" >> $GITHUB_ENV - echo "::notice::Loaded caller environment variables" - fi + echo "${{ inputs.caller_env_vars }}" >> $GITHUB_ENV + echo "::notice::Loaded caller environment variables" shell: bash - # ------------------------------------------------------------------ - # 6. 拉取 PR Diff(如果是 PR 评论) - # ------------------------------------------------------------------ + # 拉取 PR Diff - name: Fetch PR diff - if: github.event.client_payload.pr_number != '' + if: inputs.pr_number != '' env: - GH_TOKEN: ${{ secrets.AGENT_PAT }} - CALLER_REPO: ${{ github.event.client_payload.caller_repo }} - PR_NUMBER: ${{ github.event.client_payload.pr_number }} + GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CALLER_REPO: ${{ inputs.caller_repo }} + PR_NUMBER: ${{ inputs.pr_number }} run: | curl -sf \ -H "Authorization: Bearer $GH_TOKEN" \ @@ -137,19 +246,17 @@ jobs: echo "PR diff: $(wc -c < /tmp/pr.diff) bytes" shell: bash - # ------------------------------------------------------------------ - # 7. 构建 prompt - # ------------------------------------------------------------------ + # 构建 prompt - name: Build prompt env: - CALLER_REPO: ${{ github.event.client_payload.caller_repo }} - PR_NUMBER: ${{ github.event.client_payload.pr_number }} - COMMENT_BODY: ${{ github.event.client_payload.comment_body }} - COMMENT_AUTHOR: ${{ github.event.client_payload.comment_author }} - SKILL_NAME: ${{ github.event.client_payload.skill_name }} - SKILL_URL: ${{ github.event.client_payload.skill_url }} - CUSTOM_PROMPT: ${{ github.event.client_payload.custom_prompt }} - ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} + CALLER_REPO: ${{ inputs.caller_repo }} + PR_NUMBER: ${{ inputs.pr_number }} + COMMENT_BODY: ${{ inputs.comment_body }} + COMMENT_AUTHOR: ${{ inputs.comment_author }} + SKILL_NAME: ${{ inputs.skill_name }} + SKILL_URL: ${{ inputs.skill_url }} + CUSTOM_PROMPT: ${{ inputs.custom_prompt }} + ALLOW_CODE_CHANGE: ${{ inputs.allow_code_change }} run: | python3 - <<'PYEOF' import os @@ -163,7 +270,6 @@ jobs: custom_prompt = os.environ.get('CUSTOM_PROMPT', '') or '' allow_code_change = os.environ.get('ALLOW_CODE_CHANGE', 'false').lower() == 'true' - # 加载 Skill 指令 skill_prompt = '' if custom_prompt: @@ -233,75 +339,28 @@ jobs: print(f'Prompt ready: {len(prompt)} chars') PYEOF + shell: bash - # ------------------------------------------------------------------ - # 8. 调用 Claude Code - # ------------------------------------------------------------------ + # 调用 Claude Code - name: Invoke Claude Code id: claude_run shell: bash env: - MODEL: ${{ github.event.client_payload.model || 'claude-sonnet-4-20250514' }} - ALLOW_CODE_CHANGE: ${{ github.event.client_payload.allow_code_change }} - DANGEROUSLY_SKIP_PERMISSIONS: ${{ github.event.client_payload.dangerously_skip_permissions }} - ALLOWED_TOOLS: ${{ github.event.client_payload.allowed_tools }} - SHOW_FULL_OUTPUT: ${{ github.event.client_payload.show_full_output }} + MODEL: ${{ inputs.model }} + ALLOW_CODE_CHANGE: ${{ inputs.allow_code_change }} + DANGEROUSLY_SKIP_PERMISSIONS: ${{ inputs.dangerously_skip_permissions }} + ALLOWED_TOOLS: ${{ inputs.allowed_tools }} + SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }} ANTHROPIC_API_KEY: ${{ secrets.CLAUDE_API_KEY }} working-directory: caller-repo run: | - if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then - echo "::group::Claude Code Full Output" - fi - - # 配置 Claude CLI - if [ -n "$ANTHROPIC_API_KEY" ]; then - echo "Configuring Claude CLI..." - mkdir -p ~/.config/claude - echo "{\"apiKey\":\"$ANTHROPIC_API_KEY\"}" > ~/.config/claude/settings.json - echo "Claude CLI configured" - fi - + # 构建 Claude 参数(只构建一次) CLAUDE_ARGS="--model $MODEL --output-format json" - - if [[ "$DANGEROUSLY_SKIP_PERMISSIONS" == "true" ]]; then - CLAUDE_ARGS="$CLAUDE_ARGS --dangerously-skip-permissions" - fi - - if [[ -n "$ALLOWED_TOOLS" ]]; then - CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools $ALLOWED_TOOLS" - else - if [[ "$ALLOW_CODE_CHANGE" == "true" ]]; then - CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools Read,Edit,Write,Glob,Grep,Bash" - else - CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools Read,Glob,Grep,Bash" - fi - fi - - claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS \ - 2>/tmp/claude_stderr.txt \ - > /tmp/claude_raw.json - EXIT_CODE=$? - - if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then - echo "::endgroup::" - fi - - # Debug: 显示环境变量 - echo "Debug: CLAUDE_PROMPT length: ${#CLAUDE_PROMPT}" - echo "Debug: MODEL=$MODEL" - echo "Debug: Working directory: $(pwd)" - - # Debug: 检查 claude 是否存在 - echo "Debug: Checking claude installation..." - which claude || echo "WARNING: claude not in PATH" - claude --version 2>&1 || echo "WARNING: claude --version failed" - - CLAUDE_ARGS="--model $MODEL --output-format json" - + if [[ "$DANGEROUSLY_SKIP_PERMISSIONS" == "true" ]]; then CLAUDE_ARGS="$CLAUDE_ARGS --dangerously-skip-permissions" fi - + if [[ -n "$ALLOWED_TOOLS" ]]; then CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools $ALLOWED_TOOLS" else @@ -311,37 +370,55 @@ jobs: CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools Read,Glob,Grep,Bash" fi fi - - echo "Debug: Running claude with args: $CLAUDE_ARGS" - # 检查 API key 是否设置(不显示值) - if [ -z "$ANTHROPIC_API_KEY" ]; then - echo "ERROR: ANTHROPIC_API_KEY is not set!" - exit 1 - else - echo "Debug: ANTHROPIC_API_KEY is set (length: ${#ANTHROPIC_API_KEY})" - fi - - # 直接运行,显示所有输出 - echo "=== Starting Claude Code ===" - set -o pipefail - claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS 2>&1 | tee /tmp/claude_output.log - EXIT_CODE=${PIPESTATUS[0]} - echo "=== Claude Code Exit Code: $EXIT_CODE ===" + # 重试机制(最多 3 次,指数退避) + MAX_RETRIES=3 + EXIT_CODE=1 - # 检查输出文件 - echo "Debug: Output file size: $(wc -c < /tmp/claude_output.log 2>/dev/null || echo 0) bytes" + for attempt in 1 2 3; do + if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then + echo "::group::Claude Code Full Output (Attempt $attempt/$MAX_RETRIES)" + fi + + echo "=== Claude Code Attempt $attempt/$MAX_RETRIES ===" + + set +e + claude -p "$CLAUDE_PROMPT" $CLAUDE_ARGS \ + 2>/tmp/claude_stderr.txt \ + > /tmp/claude_raw.json + EXIT_CODE=$? + set -e + + if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then + echo "::endgroup::" + fi + + if [[ $EXIT_CODE -eq 0 ]]; then + echo "=== Claude Code succeeded on attempt $attempt ===" + break + fi + + if [[ $attempt -lt $MAX_RETRIES ]]; then + WAIT_TIME=$((10 * attempt)) + echo "=== Claude Code failed (exit $EXIT_CODE). Retrying in ${WAIT_TIME}s... ===" + sleep $WAIT_TIME + fi + done - # 保存输出到文件用于后续解析 - cp /tmp/claude_output.log /tmp/claude_raw.json 2>/dev/null || echo "{\"error\": \"no output\"}" > /tmp/claude_raw.json - - if [[ "$SHOW_FULL_OUTPUT" == "true" ]]; then - echo "::endgroup::" + # 导出 EXIT_CODE 供 Python 使用 + echo "EXIT_CODE=$EXIT_CODE" >> $GITHUB_ENV + + if [[ $EXIT_CODE -ne 0 ]]; then + echo "::error::Claude Code failed after $MAX_RETRIES attempts" fi - + + # 解析 JSON 输出 python3 - <<'PYEOF' - import json, os - + import json, os, sys + + exit_code = int(os.environ.get('EXIT_CODE', '1')) + error_message = '' + try: data = json.loads(open('/tmp/claude_raw.json').read()) result = data.get('result', '') @@ -349,63 +426,73 @@ jobs: usage = data.get('usage', {}) input_tokens = usage.get('input_tokens', 'N/A') output_tokens = usage.get('output_tokens', 'N/A') + + # 提取回复摘要(前 500 字符) + response_summary = result[:500].replace('\n', ' ').replace('\r', ' ') if result else 'No response' except Exception as e: print(f'Warning: could not parse JSON output: {e}') - result = open('/tmp/claude_raw.json').read() + result = open('/tmp/claude_raw.json').read() if os.path.exists('/tmp/claude_raw.json') else '' cost = 'N/A' input_tokens = 'N/A' output_tokens = 'N/A' - + response_summary = 'Failed to parse Claude output' + error_message = f'JSON parse error: {str(e)}' + + # 保存完整回复 open('/tmp/claude_output.txt', 'w').write(result) - print(result[:1000]) - + print(f'Response summary: {response_summary[:200]}') + + # 写入 outputs with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f'cost_usd={cost}\n') f.write(f'input_tokens={input_tokens}\n') f.write(f'output_tokens={output_tokens}\n') + f.write(f'response_summary={response_summary}\n') + if error_message: + f.write(f'error_message={error_message}\n') PYEOF - + + # 检测代码变更 if git status --porcelain | grep -q .; then - echo "code_changed=true" >> "$GITHUB_OUTPUT" + echo "changes_detected=true" >> "$GITHUB_OUTPUT" + + # 获取当前 commit SHA + COMMIT_SHA=$(git rev-parse HEAD) + echo "commit_sha=$COMMIT_SHA" >> "$GITHUB_OUTPUT" + + # 简化处理,实际可能需要更复杂的逻辑来检测新 PR + echo "pr_url=" >> "$GITHUB_OUTPUT" else - echo "code_changed=false" >> "$GITHUB_OUTPUT" + echo "changes_detected=false" >> "$GITHUB_OUTPUT" + echo "commit_sha=" >> "$GITHUB_OUTPUT" + echo "pr_url=" >> "$GITHUB_OUTPUT" fi - + exit $EXIT_CODE - # ------------------------------------------------------------------ - # 9. 执行验证命令(仅当 verify_command 不为空时) - # ------------------------------------------------------------------ + # 执行验证命令 - name: Run verify command - if: github.event.client_payload.verify_command != '' + if: inputs.verify_command != '' id: verify working-directory: caller-repo run: | - ${{ github.event.client_payload.verify_command }} + ${{ inputs.verify_command }} timeout-minutes: 30 shell: bash - # ------------------------------------------------------------------ - # 10. 生成 patch 文件并上传 artifact - # ------------------------------------------------------------------ + # 生成 patch 文件并上传 artifact - name: Create patch file id: create_patch - # 简化条件:只要代码变更就创建 patch - if: steps.claude_run.outputs.code_changed == 'true' + if: steps.claude_run.outputs.changes_detected == 'true' working-directory: caller-repo run: | - git diff --no-color > /tmp/claude-changes.patch + git add -A + git diff --cached --no-color > /tmp/claude-changes.patch + git reset PATCH_SIZE=$(wc -c < /tmp/claude-changes.patch) echo "patch_size=$PATCH_SIZE" >> "$GITHUB_OUTPUT" echo "patch_path=/tmp/claude-changes.patch" >> "$GITHUB_OUTPUT" echo "::notice::Created patch file: $PATCH_SIZE bytes" - - # Debug: 显示 patch 内容预览 - if [ $PATCH_SIZE -gt 0 ]; then - echo "=== Patch Preview ===" - head -30 /tmp/claude-changes.patch - echo "=====================" - fi - name: Upload patch as artifact if: steps.create_patch.outputs.patch_path @@ -415,71 +502,83 @@ jobs: path: ${{ steps.create_patch.outputs.patch_path }} retention-days: 1 - # ------------------------------------------------------------------ - # 11. 将 Claude 的回复发布为评论 - # ------------------------------------------------------------------ + # Post Claude response as comment - name: Post comment - if: always() + if: always() && inputs.issue_number != '' + shell: bash env: - GH_TOKEN: ${{ secrets.AGENT_PAT }} - CALLER_REPO: ${{ github.event.client_payload.caller_repo }} - ISSUE_NUMBER: ${{ github.event.client_payload.issue_number }} - PR_URL: "" + GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CALLER_REPO: ${{ inputs.caller_repo }} + ISSUE_NUMBER: ${{ inputs.issue_number }} + ERROR_MESSAGE: ${{ steps.claude_run.outputs.error_message }} run: | - python3 - <<'PYEOF' - import json, os, subprocess + if [[ -n "$ERROR_MESSAGE" ]]; then + printf -v body '## Claude Code Failed\n\n%s' "$ERROR_MESSAGE" + else + output=$(cat /tmp/claude_output.txt 2>/dev/null || echo "") + if [[ -z "$output" ]]; then + output="_Claude returned an empty response._" + fi + printf -v body '## Claude Code Analysis\n\n%s' "$output" + fi - try: - output = open('/tmp/claude_output.txt').read().strip() - except FileNotFoundError: - output = '' - - if not output: - output = '_Claude returned an empty response._' - - pr_url = os.environ.get('PR_URL', '') - - body = f'## Claude Code Analysis\n\n{output}' - if pr_url: - body += f'\n\n---\nI made code changes and opened a PR: {pr_url}' - - comment = json.dumps({'body': body}) - repo = os.environ['CALLER_REPO'] - issue = os.environ['ISSUE_NUMBER'] - token = os.environ['GH_TOKEN'] - - r = subprocess.run([ - 'curl', '-sf', '-X', 'POST', - '-H', f'Authorization: Bearer {token}', - '-H', 'Accept: application/vnd.github+json', - f'https://api.github.com/repos/{repo}/issues/{issue}/comments', - '-d', comment, - ], capture_output=True, text=True) - - if r.returncode == 0: - print('Comment posted successfully') - else: - print(f'::warning::Failed to post comment: {r.stderr}') - PYEOF + if gh api repos/$CALLER_REPO/issues/$ISSUE_NUMBER/comments \ + --method POST \ + --field body="$body" >/dev/null 2>&1; then + echo "Comment posted successfully" + else + echo "::warning::Failed to post comment" + fi - # ------------------------------------------------------------------ - # 12. Job 执行摘要 - # ------------------------------------------------------------------ + # Job 执行摘要 - name: Write job summary if: always() + env: + CALLER_REPO: ${{ inputs.caller_repo }} + COMMENT_AUTHOR: ${{ inputs.comment_author }} + PR_NUMBER: ${{ inputs.pr_number || 'N/A' }} + MODEL: ${{ inputs.model }} + CHANGES_DETECTED: ${{ steps.claude_run.outputs.changes_detected }} + COMMIT_SHA: ${{ steps.claude_run.outputs.commit_sha || 'N/A' }} + PR_URL: ${{ steps.claude_run.outputs.pr_url || 'N/A' }} + CALLER_IMAGE: ${{ inputs.caller_image || 'N/A' }} + VERIFY_COMMAND: ${{ inputs.verify_command || 'N/A' }} + VERIFY_RESULT: ${{ steps.verify.outcome || 'N/A' }} + INPUT_TOKENS: ${{ steps.claude_run.outputs.input_tokens }} + OUTPUT_TOKENS: ${{ steps.claude_run.outputs.output_tokens }} + COST_USD: ${{ steps.claude_run.outputs.cost_usd }} + RESPONSE_SUMMARY: ${{ steps.claude_run.outputs.response_summary }} + ERROR_MESSAGE: ${{ steps.claude_run.outputs.error_message }} run: | - echo "### Claude Code Run Summary" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" - echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Caller repo | \`${{ github.event.client_payload.caller_repo }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Triggered by | \`${{ github.event.client_payload.comment_author }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| PR | \`${{ github.event.client_payload.pr_number || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Model | \`${{ github.event.client_payload.model || 'claude-sonnet-4-20250514' }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Code changed | \`${{ steps.claude_run.outputs.code_changed }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Caller image | \`${{ github.event.client_payload.caller_image || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Verify command | \`${{ github.event.client_payload.verify_command || 'N/A' }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Verify result | \`${{ steps.verify.outcome || 'skipped' }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Input tokens | \`${{ steps.claude_run.outputs.input_tokens }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Output tokens | \`${{ steps.claude_run.outputs.output_tokens }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Cost (USD) | \`${{ steps.claude_run.outputs.cost_usd }}\` |" >> "$GITHUB_STEP_SUMMARY" + python3 - <<'PYEOF' + import os + + summary_path = os.environ['GITHUB_STEP_SUMMARY'] + lines = [ + "### Claude Code Run Summary", + "", + "| Field | Value |", + "|-------|-------|", + f"| Caller repo | `{os.environ['CALLER_REPO']}` |", + f"| Triggered by | `{os.environ['COMMENT_AUTHOR']}` |", + f"| PR | `{os.environ['PR_NUMBER']}` |", + f"| Model | `{os.environ['MODEL']}` |", + f"| Changes detected | `{os.environ['CHANGES_DETECTED']}` |", + f"| Commit SHA | `{os.environ['COMMIT_SHA']}` |", + f"| PR URL | `{os.environ['PR_URL']}` |", + f"| Caller image | `{os.environ['CALLER_IMAGE']}` |", + f"| Verify command | `{os.environ['VERIFY_COMMAND']}` |", + f"| Verify result | `{os.environ['VERIFY_RESULT']}` |", + f"| Input tokens | `{os.environ['INPUT_TOKENS']}` |", + f"| Output tokens | `{os.environ['OUTPUT_TOKENS']}` |", + f"| Cost (USD) | `{os.environ['COST_USD']}` |", + f"| Response summary | `{os.environ['RESPONSE_SUMMARY']}` |", + ] + + error_msg = os.environ.get('ERROR_MESSAGE', '') + if error_msg: + lines.append(f"| **Error** | `{error_msg}` |") + + with open(summary_path, 'a') as f: + f.write('\n'.join(lines) + '\n') + PYEOF diff --git a/.github/workflows/embeddable-caller.yml b/.github/workflows/embeddable-caller.yml deleted file mode 100644 index 5b579d3..0000000 --- a/.github/workflows/embeddable-caller.yml +++ /dev/null @@ -1,109 +0,0 @@ -# embeddable-caller.yml -# -# 自动触发模板 —— 将下方的 job 嵌入到你已有的 CI 工作流中, -# 实现 PR 开启时自动触发 Claude Code 分析。 -# -# 前置条件(仅需一次): -# 在目标仓库 Settings > Secrets > Actions 中添加: -# DISPATCH_TOKEN = <由 agent-skills 管理员分发的 PAT,具备 public_repo 权限> -# -# 无需配置 CLAUDE_API_KEY —— API Key 统一由 agent-skills 管理。 -# -# 用法: -# 将整个文件作为独立工作流使用,或将 jobs.trigger-claude 片段 -# 复制到你现有 CI 工作流的 jobs: 块下,与其他 job 并行运行。 - -name: Claude Bot (Auto) - -on: - pull_request: - types: [opened, synchronize] - -permissions: - contents: read - -jobs: - trigger-claude: - name: Trigger Claude Code Analysis - runs-on: ubuntu-latest - # 跳过草稿 PR,避免噪音 - if: github.event.pull_request.draft == false - - steps: - - name: Dispatch to agent-skills - env: - DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - ISSUE_NUMBER: ${{ github.event.pull_request.number }} - COMMENT_AUTHOR: ${{ github.event.pull_request.user.login }} - CALLER_REPO: ${{ github.repository }} - run: | - python3 - <<'PYEOF' - import json, os, subprocess - - payload = { - 'caller_repo': os.environ['CALLER_REPO'], - 'pr_number': os.environ['PR_NUMBER'], - 'issue_number': os.environ['ISSUE_NUMBER'], - 'comment_body': 'Auto-triggered: Please review this PR and suggest improvements.', - 'comment_author': os.environ['COMMENT_AUTHOR'], - - # 是否允许 Claude 修改代码并自动开 PR - # 自动触发模式建议保持 'false',避免每次 push 都产生新 PR - # 'true' → Claude 可编辑文件,修改后直接 commit 到当前 PR 分支 - # 'false' → 只分析,不修改任何文件 - 'allow_code_change': 'false', - - # 使用的 Skill(留空则不使用) - # Skill 加载优先级: - # 1) skill_url(远程 raw URL) - # 2) skill_name - 调用方仓库 .github/skills//SKILL.md - # 3) skill_name - 调用方仓库 .ai/skills//SKILL.md - # 4) skill_name - agent-skills 仓库 skills//SKILL.md - # - # skill_url 示例(远程 Skill): - # 'skill_url': 'https://raw.githubusercontent.com/owner/repo/main/.ai/skills/my-skill/SKILL.md', - # - # skill_name 示例(agent-skills 内置): - # 'skill_name': 'infrastructure/github-action-diagnose' - # 'skill_name': 'infrastructure/docker-image-pr-fix' - # - # skill_name 示例(调用方仓库自定义): - # 'skill_name': 'my-custom-skill' # 会在 .github/skills/my-custom-skill/SKILL.md 或 .ai/skills/my-custom-skill/SKILL.md 查找 - 'skill_name': '', - 'skill_url': '', - - # Claude 模型,可选值: - # 'claude-sonnet-4-20250514' (默认,速度与能力均衡) - # 'claude-opus-4-20251114' (最强,较慢) - # 'claude-haiku-4-5-20251001' (最快,轻量任务) - 'model': 'claude-sonnet-4-20250514', - - # 调用方提供的镜像(可选,不传则使用 ubuntu-latest) - # 镜像需要预装 Node.js、Git、依赖已安装 - 'caller_image': '', - - # 验证命令(可选,不传则不验证) - # 验证失败则 job 失败,不 push 代码 - 'verify_command': '', - } - - dispatch_body = json.dumps({ - 'event_type': 'claude-request', - 'client_payload': payload, - }) - - r = subprocess.run([ - 'curl', '-sf', '-X', 'POST', - '-H', f'Authorization: Bearer {os.environ["DISPATCH_TOKEN"]}', - '-H', 'Accept: application/vnd.github+json', - 'https://api.github.com/repos/KadenZhang3321/agent-skills/dispatches', - '-d', dispatch_body, - ], capture_output=True, text=True) - - if r.returncode != 0: - # 自动模式下调度失败不阻断 CI - print(f'::warning::Dispatch failed: {r.stderr}') - else: - print('Claude analysis dispatched. Results will appear as a PR comment.') - PYEOF diff --git a/.github/workflows/example-caller.yml b/.github/workflows/example-caller.yml deleted file mode 100644 index f244537..0000000 --- a/.github/workflows/example-caller.yml +++ /dev/null @@ -1,119 +0,0 @@ -# example-caller.yml -# -# 手动触发模板 —— 复制到目标仓库的 .github/workflows/ 目录使用。 -# 当 PR 或 Issue 评论中包含 @claude 时,自动触发 Claude Code 分析。 -# -# 前置条件(仅需一次): -# 在目标仓库 Settings > Secrets > Actions 中添加: -# DISPATCH_TOKEN = <由 agent-skills 管理员分发的 PAT,具备 public_repo 权限> -# -# 无需配置 CLAUDE_API_KEY —— API Key 统一由 agent-skills 管理。 - -name: Claude Bot - -on: - issue_comment: - types: [created] - -permissions: - contents: read - -jobs: - trigger-claude: - name: Trigger Claude Code - runs-on: ubuntu-latest - # 只响应包含 @claude 的评论,忽略机器人账号 - if: | - contains(github.event.comment.body, '@claude') && - !endsWith(github.actor, '[bot]') && - github.actor != 'dependabot' - - steps: - # 检测当前评论是否来自 PR(而非普通 Issue) - - name: Detect PR context - id: context - env: - GH_TOKEN: ${{ github.token }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - REPO: ${{ github.repository }} - run: | - HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: Bearer $GH_TOKEN" \ - "https://api.github.com/repos/$REPO/pulls/$ISSUE_NUMBER") - - if [[ "$HTTP_STATUS" == "200" ]]; then - echo "pr_number=$ISSUE_NUMBER" >> "$GITHUB_OUTPUT" - else - echo "pr_number=" >> "$GITHUB_OUTPUT" - fi - - # 发送 dispatch 到 agent-skills,触发 Claude Code - - name: Dispatch to agent-skills - env: - DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} - PR_NUMBER: ${{ steps.context.outputs.pr_number }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - COMMENT_BODY: ${{ github.event.comment.body }} - COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - CALLER_REPO: ${{ github.repository }} - run: | - python3 - <<'PYEOF' - import json, os, subprocess, sys - - payload = { - 'caller_repo': os.environ['CALLER_REPO'], - 'pr_number': os.environ.get('PR_NUMBER', ''), - 'issue_number': os.environ['ISSUE_NUMBER'], - 'comment_body': os.environ['COMMENT_BODY'], - 'comment_author': os.environ['COMMENT_AUTHOR'], - - # 是否允许 Claude 修改代码并自动开 PR - # 'true' → Claude 可编辑文件,修改后直接 commit 到当前 PR 分支 - # 'false' → 只分析,不修改任何文件 - 'allow_code_change': 'true', - - # 使用的 Skill(留空则不使用) - # Skill 加载优先级: - # 1) skill_url(远程 raw URL) - # 2) skill_name - 调用方仓库 .github/skills//SKILL.md - # 3) skill_name - 调用方仓库 .ai/skills//SKILL.md - # 4) skill_name - agent-skills 仓库 skills//SKILL.md - # - # skill_url 示例(远程 Skill): - # 'skill_url': 'https://raw.githubusercontent.com/owner/repo/main/.ai/skills/my-skill/SKILL.md', - # - # skill_name 示例(agent-skills 内置): - # 'skill_name': 'infrastructure/github-action-diagnose' - # 'skill_name': 'infrastructure/docker-image-pr-fix' - # - # skill_name 示例(调用方仓库自定义): - # 'skill_name': 'my-custom-skill' # 会在 .github/skills/my-custom-skill/SKILL.md 或 .ai/skills/my-custom-skill/SKILL.md 查找 - 'skill_name': '', - 'skill_url': '', - - # Claude 模型,可选值: - # 'claude-sonnet-4-20250514' (默认,速度与能力均衡) - # 'claude-opus-4-20251114' (最强,较慢) - # 'claude-haiku-4-5-20251001' (最快,轻量任务) - 'model': 'claude-sonnet-4-20250514', - } - - dispatch_body = json.dumps({ - 'event_type': 'claude-request', - 'client_payload': payload, - }) - - r = subprocess.run([ - 'curl', '-sf', '-X', 'POST', - '-H', f'Authorization: Bearer {os.environ["DISPATCH_TOKEN"]}', - '-H', 'Accept: application/vnd.github+json', - 'https://api.github.com/repos/KadenZhang3321/agent-skills/dispatches', - '-d', dispatch_body, - ], capture_output=True, text=True) - - if r.returncode != 0: - print(f'::error::Dispatch failed: {r.stderr}') - sys.exit(1) - - print('Dispatched to agent-skills. Claude will post a comment shortly.') - PYEOF diff --git a/.github/workflows/template-invoke-claude-code.yml b/.github/workflows/template-invoke-claude-code.yml deleted file mode 100644 index 6cbca1d..0000000 --- a/.github/workflows/template-invoke-claude-code.yml +++ /dev/null @@ -1,214 +0,0 @@ -# template-invoke-claude-code.yml -# Claude Code 调用模板 — 供其他仓库的工作流复制使用 -# -# 使用方式: -# 1. 确保调用方仓库在 agent-skills/.github/allowed-callers.json 白名单中 -# 2. 复制下面的 jobs 到你的 workflow 中 -# 3. 根据需要调整环境变量和参数 -# -# ==================================================================== -# 参数说明: -# ------------------------------------------------------------------- -# caller_env_vars: 传递给 Claude 的环境变量(格式:key1=value1\nkey2=value2) -# custom_prompt: 自定义 prompt(优先级最高,会覆盖 skill_name) -# skill_name: Skill 名称(如果 custom_prompt 为空则使用) -# model: 使用的模型(默认 qwen/qwen3.6-plus:free) -# allow_code_change: 是否允许 Claude 修改代码(默认 true) -# dangerously_skip_permissions: 跳过权限检查(默认 false) -# allowed_tools: 允许的工具(如 "Bash(git *),Read,Write,Edit") -# show_full_output: 在日志中显示完整输出(默认 false) -# ==================================================================== - -jobs: - invoke-claude-code-with-wait: - name: Invoke Claude Code (with wait) - runs-on: ubuntu-latest - timeout-minutes: 60 - env: - AGENT_SKILLS_REPO: opensourceways/agent-skills - # 以下变量根据你的仓库修改 - CALLER_REPO: your-org/your-repo - SKILL_NAME: your-skill-name - MODEL: qwen/qwen3.6-plus:free - COMMENT_AUTHOR: workflow-bot - - steps: - - name: Invoke Claude via agent-skills - id: dispatch - env: - GH_TOKEN: ${{ secrets.PAT_TOKEN }} - run: | - # 示例:传递环境变量(根据需要修改) - # CALLER_ENV_VARS=$(jq -n \ - # --arg key1 "value1" \ - # --arg key2 "value2" \ - # '{key1: $key1, key2: $key2}' | jq -r '. | to_entries | map("\(.key)=\(.value)") | join("\n")) - - # 示例:传递自定义 prompt(根据需要修改) - # CUSTOM_PROMPT="Your custom prompt here" - - gh api --method POST "https://api.github.com/repos/${{ env.AGENT_SKILLS_REPO }}/dispatches" \ - -f event_type="claude-request" \ - -f client_payload="$(jq -n \ - --arg caller_repo "${{ env.CALLER_REPO }}" \ - --arg skill_name "${{ env.SKILL_NAME }}" \ - --arg model "${{ env.MODEL }}" \ - --arg allow_code_change "true" \ - --arg comment_author "${{ env.COMMENT_AUTHOR }}" \ - --arg caller_env_vars "" \ - --arg custom_prompt "" \ - --arg dangerously_skip_permissions "false" \ - --arg allowed_tools "" \ - --arg show_full_output "false" \ - '{ - caller_repo: $caller_repo, - skill_name: $skill_name, - model: $model, - allow_code_change: $allow_code_change, - comment_author: $comment_author, - caller_env_vars: $caller_env_vars, - custom_prompt: $custom_prompt, - dangerously_skip_permissions: $dangerously_skip_permissions, - allowed_tools: $allowed_tools, - show_full_output: $show_full_output - }')" \ - --silent --output /dev/null --fail - - echo "dispatched=true" >> "$GITHUB_OUTPUT" - - - name: Wait for Claude execution - if: steps.dispatch.outputs.dispatched == 'true' - id: wait - run: | - RUN_ID="" - for attempt in $(seq 1 60); do - RUN_ID=$(gh run list \ - --repo "${{ env.AGENT_SKILLS_REPO }}" \ - --workflow "_claude-code.yml" \ - -L 5 \ - --json databaseId,displayTitle \ - --jq '[.[] | select(.displayTitle | contains("Handle Claude Request"))][0].databaseId // empty') - if [ -n "$RUN_ID" ]; then - break - fi - sleep 10 - done - - if [ -z "$RUN_ID" ]; then - echo "::error::Timeout waiting for Claude execution" - exit 1 - fi - - echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" - gh run watch "$RUN_ID" --repo "${{ env.AGENT_SKILLS_REPO }}" --exit-status - - - name: Check execution result - if: steps.wait.outputs.run_id - env: - GH_TOKEN: ${{ secrets.PAT_TOKEN }} - run: | - CONCLUSION=$(gh run view "${{ steps.wait.outputs.run_id }}" \ - --repo "${{ env.AGENT_SKILLS_REPO }}" \ - --json conclusion --jq '.conclusion') - - if [ "$CONCLUSION" != "success" ]; then - echo "::error::Claude execution failed: $CONCLUSION" - exit 1 - fi - - echo "execution_success=true" >> "$GITHUB_OUTPUT" - -# ==================================================================== -# 最小化版本:直接复制到现有 job 中使用 -# ==================================================================== -# 使用方式:将下面的 steps 复制到你的现有 job 中 -# -# - name: Invoke Claude via agent-skills -# id: dispatch -# env: -# GH_TOKEN: ${{ secrets.PAT_TOKEN }} -# CALLER_REPO: your-org/your-repo -# SKILL_NAME: your-skill-name -# MODEL: qwen/qwen3.6-plus:free -# run: | -# # 传递环境变量示例(可选) -# # CALLER_ENV_VARS=$(jq -n \ -# # --arg key1 "value1" \ -# # --arg key2 "value2" \ -# # '{key1: $key1, key2: $key2}' | jq -r '. | to_entries | map("\(.key)=\(.value)") | join("\n")) -# -# # 传递自定义 prompt 示例(可选) -# # CUSTOM_PROMPT="Your custom prompt here" -# -# # 传递权限控制参数示例(可选) -# # DANGEROUSLY_SKIP_PERMISSIONS="true" -# # ALLOWED_TOOLS="Bash(git *),Bash(gh *),Read,Write,Edit,Glob,Grep" -# -# gh api --method POST "https://api.github.com/repos/opensourceways/agent-skills/dispatches" \ -# -f event_type="claude-request" \ -# -f client_payload="$(jq -n \ -# --arg caller_repo "$CALLER_REPO" \ -# --arg skill_name "$SKILL_NAME" \ -# --arg model "$MODEL" \ -# --arg allow_code_change "true" \ -# --arg comment_author "workflow-bot" \ -# --arg caller_env_vars "$CALLER_ENV_VARS" \ -# --arg custom_prompt "$CUSTOM_PROMPT" \ -# --arg dangerously_skip_permissions "false" \ -# --arg allowed_tools "" \ -# '{ -# caller_repo: $caller_repo, -# skill_name: $skill_name, -# model: $model, -# allow_code_change: $allow_code_change, -# comment_author: $comment_author, -# caller_env_vars: $caller_env_vars, -# custom_prompt: $custom_prompt, -# dangerously_skip_permissions: $dangerously_skip_permissions, -# allowed_tools: $allowed_tools -# }')" \ -# --silent --output /dev/null --fail -# -# echo "dispatched=true" >> "$GITHUB_OUTPUT" -# -# - name: Wait for Claude execution -# if: steps.dispatch.outputs.dispatched == 'true' -# id: wait -# run: | -# RUN_ID="" -# for attempt in $(seq 1 60); do -# RUN_ID=$(gh run list \ -# --repo "opensourceways/agent-skills" \ -# --workflow "_claude-code.yml" \ -# -L 5 \ -# --json databaseId,displayTitle \ -# --jq '[.[] | select(.displayTitle | contains("Handle Claude Request"))][0].databaseId // empty') -# if [ -n "$RUN_ID" ]; then -# break -# fi -# sleep 10 -# done -# -# if [ -z "$RUN_ID" ]; then -# echo "::error::Timeout waiting for Claude execution" -# exit 1 -# fi -# -# echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" -# gh run watch "$RUN_ID" --repo "opensourceways/agent-skills" --exit-status -# -# - name: Check execution result -# if: steps.wait.outputs.run_id -# env: -# GH_TOKEN: ${{ secrets.PAT_TOKEN }} -# run: | -# CONCLUSION=$(gh run view "${{ steps.wait.outputs.run_id }}" \ -# --repo "opensourceways/agent-skills" \ -# --json conclusion --jq '.conclusion') -# -# if [ "$CONCLUSION" != "success" ]; then -# echo "::error::Claude execution failed: $CONCLUSION" -# exit 1 -# fi -# -# echo "execution_success=true" >> "$GITHUB_OUTPUT" \ No newline at end of file diff --git a/EXAMPLE-USAGE.md b/EXAMPLE-USAGE.md new file mode 100644 index 0000000..d8dfccb --- /dev/null +++ b/EXAMPLE-USAGE.md @@ -0,0 +1,287 @@ +# Agent-Skills 调用指南 + +本文档说明如何在业务仓库中使用中心 Claude Code 服务。 + +## 目录 + +- [架构概述](#架构概述) +- [前置条件](#前置条件) +- [同步调用(workflow_call)](#同步调用workflow_call) +- [参数说明](#参数说明) +- [输出说明](#输出说明) +- [错误处理](#错误处理) +- [常见问题](#常见问题) + +## 架构概述 + +``` +┌─────────────────────┐ ┌──────────────────────────┐ +│ 业务仓库 │ │ agent-skills 中心仓库 │ +│ │ │ │ +│ - 触发工作流 │ ──────> │ _claude-code.yml │ +│ - 传递参数 │ │ - 白名单校验 │ +│ - 接收结果 │ <────── │ - 权限检查 │ +│ │ │ - 调用 Claude Code │ +└─────────────────────┘ │ - 返回结果 │ + └──────────────────────────┘ +``` + +**调用方式**:通过 `workflow_call` 同步调用,父工作流会等待 Claude 执行完毕。 + +## 前置条件 + +### 1. 配置 Secrets + +在业务仓库的 Settings → Secrets and variables → Actions 中添加: + +| Secret 名称 | 说明 | 权限要求 | +|-------------|------|----------| +| `DISPATCH_TOKEN` | GitHub Personal Access Token | `repo` 完整权限 | +| `CLAUDE_API_KEY` | Anthropic API Key | 无需额外权限 | + +### 2. 添加白名单 + +联系 agent-skills 仓库管理员,将你的仓库添加到 `.github/allowed-callers.json`: + +```json +{ + "allowed_orgs": ["your-org"], + "allowed_repos": ["your-org/your-repo"] +} +``` + +## 同步调用(workflow_call) + +### 基本用法 + +```yaml +jobs: + claude: + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + caller_repo: ${{ github.repository }} + custom_prompt: '请分析这个 PR 的代码变更' + allow_code_change: false + model: 'claude-sonnet-4-20250514' + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} +``` + +### 完整示例 + +```yaml +jobs: + claude: + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + # 必填:调用方仓库路径 + caller_repo: ${{ github.repository }} + + # Prompt 相关(三选一) + custom_prompt: '请修复这个 bug:...' + skill_name: 'github-action-diagnose' + skill_url: 'https://example.com/skill.md' + + # 代码变更权限 + allow_code_change: false + + # 模型配置 + model: 'claude-sonnet-4-20250514' + dangerously_skip_permissions: false + allowed_tools: 'Bash(git *),Read,Write,Edit,Glob,Grep' + show_full_output: true + + # PR/Issue 相关 + pr_number: '123' + issue_number: '123' + comment_author: ${{ github.actor }} + comment_body: '请分析这个 PR' + + # 环境变量 + caller_env_vars: 'KEY1=value1\nKEY2=value2' + + # 容器镜像 + caller_image: 'ubuntu:24.04' + + # 验证命令 + verify_command: 'pytest tests/' + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + + # 使用 Claude 的输出 + post-process: + needs: claude + runs-on: ubuntu-latest + steps: + - name: Check result + run: | + echo "Changes: ${{ needs.claude.outputs.changes_detected }}" + echo "Cost: ${{ needs.claude.outputs.cost_usd }}" +``` + +### PR 评论触发示例 + +```yaml +on: + issue_comment: + types: [created] + +jobs: + claude-on-comment: + if: | + github.event.issue.pull_request && + contains(github.event.comment.body, '@claude') && + !endsWith(github.actor, '[bot]') && + github.actor != 'dependabot' + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + caller_repo: ${{ github.repository }} + pr_number: ${{ github.event.issue.number }} + issue_number: ${{ github.event.issue.number }} + comment_author: ${{ github.event.comment.user.login }} + comment_body: ${{ github.event.comment.body }} + allow_code_change: false + model: 'claude-sonnet-4-20250514' + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} +``` + +更多调用模板请参考 [.github/workflows/TEMPLATE-CALLER.yml](../.github/workflows/TEMPLATE-CALLER.yml)。 + +## 参数说明 + +### workflow_call inputs + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `caller_repo` | string | ✅ | - | 调用方仓库路径 (owner/repo) | +| `custom_prompt` | string | ❌ | `''` | 自定义 prompt 内容 | +| `skill_name` | string | ❌ | `''` | Skill 名称(从 agent-skills 加载) | +| `skill_url` | string | ❌ | `''` | Skill URL(从远程加载) | +| `allow_code_change` | boolean | ❌ | `false` | 是否允许 Claude 修改代码 | +| `model` | string | ❌ | `claude-sonnet-4-20250514` | Claude 模型名称 | +| `dangerously_skip_permissions` | boolean | ❌ | `false` | 跳过权限确认 | +| `allowed_tools` | string | ❌ | `''` | 允许的工具列表(逗号分隔) | +| `show_full_output` | boolean | ❌ | `false` | 显示完整输出 | +| `caller_env_vars` | string | ❌ | `''` | 调用方环境变量(`\n` 分隔) | +| `caller_image` | string | ❌ | `''` | 自定义容器镜像 | +| `verify_command` | string | ❌ | `''` | 验证命令 | +| `pr_number` | string | ❌ | `''` | PR 编号 | +| `issue_number` | string | ❌ | `''` | Issue 编号 | +| `comment_author` | string | ❌ | `'workflow'` | 评论作者 | +| `comment_body` | string | ❌ | `''` | 评论内容 | + +### secrets + +| Secret | 必填 | 说明 | +|--------|------|------| +| `DISPATCH_TOKEN` | ✅ | GitHub PAT(repo 权限) | +| `CLAUDE_API_KEY` | ✅ | Anthropic API Key | + +## 输出说明 + +### workflow_call outputs + +| 输出 | 类型 | 说明 | +|------|------|------| +| `changes_detected` | boolean | 是否检测到代码变更 | +| `commit_sha` | string | 提交 SHA(如有变更) | +| `pr_url` | string | 创建的 PR URL(如有) | +| `cost_usd` | number | API 调用成本(USD) | +| `input_tokens` | number | 输入 token 数 | +| `output_tokens` | number | 输出 token 数 | +| `response_summary` | string | Claude 回复摘要(前 500 字符) | +| `error_message` | string | 错误信息(如有) | + +### 使用示例 + +```yaml +jobs: + claude: + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + caller_repo: ${{ github.repository }} + custom_prompt: '修复这个 bug' + allow_code_change: true + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + + process-result: + needs: claude + runs-on: ubuntu-latest + steps: + - name: Check if code changed + if: needs.claude.outputs.changes_detected == 'true' + run: | + echo "Claude made changes!" + echo "Commit: ${{ needs.claude.outputs.commit_sha }}" + echo "Cost: ${{ needs.claude.outputs.cost_usd }} USD" + + - name: Handle error + if: needs.claude.outputs.error_message != '' + run: | + echo "Error: ${{ needs.claude.outputs.error_message }}" + exit 1 +``` + +## 错误处理 + +### 重试机制 + +Claude 调用失败时会自动重试(最多 3 次,指数退避:10s → 20s → 30s)。 + +### 错误输出 + +如果最终失败,`error_message` 输出会包含错误信息: + +```yaml +- name: Handle error + if: needs.claude.outputs.error_message != '' + run: | + echo "Claude failed: ${{ needs.claude.outputs.error_message }}" +``` + +### 常见错误 + +| 错误 | 原因 | 解决方案 | +|------|------|----------| +| `Caller not in whitelist` | 仓库未在白名单中 | 联系管理员添加 | +| `Insufficient permission` | 用户权限不足 | 确保用户有 write/admin 权限 | +| `Claude Code failed after 3 attempts` | API 调用失败 | 检查 API Key 和网络 | +| `JSON parse error` | 输出解析失败 | 检查模型配置 | + +## 常见问题 + +### Q: 同步调用和异步调用应该选哪个? + +**A**: 当前只支持 `workflow_call` 同步调用。父工作流会等待 Claude 执行完成,适合需要立即获取结果、后续步骤依赖 Claude 输出的场景。 + +### Q: 如何允许 Claude 修改代码? + +**A**: 设置 `allow_code_change: true`,Claude 会修改文件并生成 patch artifact,可从 Actions 下载应用。 + +### Q: 如何自定义容器镜像? + +**A**: 设置 `caller_image: 'your-image:tag'`,可以跳过依赖安装步骤,加速执行。 + +### Q: 如何传递环境变量给 Claude? + +**A**: 使用 `caller_env_vars`,格式为 `KEY1=value1\nKEY2=value2`。 + +### Q: Claude 修改的代码如何获取? + +**A**: 同步调用时,通过 artifact 下载 `claude-changes-patch`。 + +### Q: 如何指定 Claude 使用的模型? + +**A**: 设置 `model` 参数,如 `model: 'claude-sonnet-4-20250514'`。 + +### Q: 白名单如何配置? + +**A**: 在 agent-skills 仓库的 `.github/allowed-callers.json` 中添加: +- `allowed_orgs`: 整个组织的仓库都可以调用 +- `allowed_repos`: 单独指定仓库 diff --git a/README.md b/README.md index f1a71e8..94a065c 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ git push origin feature/add-new-skill 在 Claude Code 中运行以下命令: ``` - /plugin marketplace add opensourceways/agent-skills + /plugin marketplace add KadenZhang3321/agent-skills ``` 2. **浏览并安装skill** @@ -79,14 +79,14 @@ git push origin feature/add-new-skill 在 Claude Code 中输入 `/plugin` 或点击插件图标,然后: - 选择 `Browse and install plugins`(浏览并安装插件) - - 选择 `opensourceways-agent-skills` marketplace + - 选择 `KadenZhang3321-agent-skills` marketplace - 选择你想要安装的技能(例如 `triton-upgrade`) - 点击 `Install now`(立即安装) **方式 B:命令行直接安装** ``` - /plugin install triton-upgrade@opensourceways-agent-skills + /plugin install triton-upgrade@KadenZhang3321-agent-skills ``` @@ -100,12 +100,12 @@ git push origin feature/add-new-skill ```json { - "opensourceways-agent-skills": { + "KadenZhang3321-agent-skills": { "source": { "source": "github", - "repo": "opensourceways/agent-skills" + "repo": "KadenZhang3321/agent-skills" }, - "installLocation": "~/.claude/plugins/marketplaces/opensourceways-agent-skills", + "installLocation": "~/.claude/plugins/marketplaces/KadenZhang3321-agent-skills", "lastUpdated": "2026-02-13T00:00:00.000Z" } } @@ -118,7 +118,7 @@ git push origin feature/add-new-skill 在终端中运行安装命令: ```bash - claude plugin install triton-upgrade@opensourceways-agent-skills + claude plugin install triton-upgrade@KadenZhang3321-agent-skills ``` @@ -281,6 +281,73 @@ A: Claude Code 的配置文件通常在: 如何将 Claude Bot 集成到组织下的任意仓库,请参阅:[`.github/workflows/README.md`](.github/workflows/README.md) +## 🚀 Claude Code 中心服务调用指南 + +本仓库提供中心化的 Claude Code 调用服务,其他业务仓库可以通过 `workflow_call` 方式调用。 + +### 架构优势 + +- **集中管理 API Key**:调用方无需配置 Anthropic API Key +- **统一权限控制**:通过白名单和权限检查确保安全 +- **自动重试机制**:失败时自动重试(最多 3 次) +- **预构建镜像支持**:可选使用预装 Claude CLI 的容器镜像,加速执行 + +### 快速开始 + +```yaml +jobs: + claude: + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + caller_repo: ${{ github.repository }} + custom_prompt: '请分析这个 PR 的代码变更' + allow_code_change: false + model: 'claude-sonnet-4-20250514' + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} +``` + +### 前置条件 + +1. **配置 Secrets**(在业务仓库的 Settings → Secrets 中): + - `DISPATCH_TOKEN`:GitHub PAT(需要 `repo` 权限) + - `CLAUDE_API_KEY`:Anthropic API Key + +2. **添加白名单**:联系本仓库管理员,将你的仓库添加到 `.github/allowed-callers.json` + +### 详细文档 + +- **完整调用示例和参数说明**:[EXAMPLE-USAGE.md](EXAMPLE-USAGE.md) +- **调用模板文件**:[.github/workflows/TEMPLATE-CALLER.yml](.github/workflows/TEMPLATE-CALLER.yml) +- **从直接 CC 调用迁移**:[docs/migration-guide.md](docs/migration-guide.md) + +### 预构建容器镜像 + +为了加速执行,可以使用预装 Claude CLI 的容器镜像: + +```dockerfile +# Dockerfile 位于仓库根目录 +# 构建命令: +# docker build -t kadenzhang3321/claude-runner:latest . +# docker push kadenzhang3321/claude-runner:latest +``` + +在调用时指定 `caller_image` 参数: + +```yaml +jobs: + claude: + uses: KadenZhang3321/agent-skills/.github/workflows/_claude-code.yml@main + with: + caller_repo: ${{ github.repository }} + caller_image: 'kadenzhang3321/claude-runner:latest' + custom_prompt: '请分析代码' + secrets: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} +``` + ## 📞 联系方式 如有问题或建议,请: @@ -295,4 +362,4 @@ A: Claude Code 的配置文件通常在: --- **维护者**: [添加维护者信息] -**最后更新**: 2026-03-28 +**最后更新**: 2026-04-15 diff --git a/docs/migration-guide.md b/docs/migration-guide.md deleted file mode 100644 index 7ea1626..0000000 --- a/docs/migration-guide.md +++ /dev/null @@ -1,242 +0,0 @@ -# 迁移指南:从直接 CC 调用到 Agent-Skills - -本文档说明如何将 workflow 中直接调用 Claude Code 的方式,替换为通过 Agent-Skills 调用。 - -## 迁移步骤 - -### 1. 准备工作 - -#### Agent-Skills 端配置 -1. 在 `allowed-callers.json` 中添加调用方仓库路径 -2. 确保 `CLAUDE_API_KEY` secret 已配置 -3. 确保 `AGENT_PAT` secret 已配置(用于检出调用方仓库) - -#### 调用方仓库配置 -1. 在仓库 Settings → Secrets 添加 `PAT_TOKEN`: - - 需要有 `repo` 和 `workflow` 权限 - - 需要能触发 agent-skills 仓库的 workflow - -### 2. 修改 Workflow - -将原来的 `anthropics/claude-code-action/base-action@v1` 步骤替换为以下三个步骤: - -#### 步骤 1: 触发 Agent-Skills - -```yaml -- name: Trigger agent-skills workflow - id: trigger - env: - GH_TOKEN: ${{ secrets.PAT_TOKEN }} - run: | - PAYLOAD=$(cat <> "$GITHUB_OUTPUT" - break - fi - fi - sleep 10 - done -``` - -#### 步骤 3: 下载并应用 Patch - -```yaml -- name: Download and apply patch - run: | - gh run download "${{ steps.wait.outputs.run_id }}" \ - --repo "kadenzhang3321/agent-skills" \ - --name claude-changes-patch \ - --dir /tmp/patch - - git apply /tmp/patch/claude-changes.patch -``` - -### 3. 参数映射表 - -| 原参数 | 新参数 | 说明 | -|--------|--------|------| -| `anthropic_api_key` | 无需传递 | 由 agent-skills 管理 | -| `prompt` | `custom_prompt` | 直接传递 prompt 内容 | -| `claude_args` | `model`, `allowed_tools` 等 | 拆分为独立参数 | -| 环境变量 | `caller_env_vars` | 多行字符串格式,用 `\n` 分隔 | - -### 4. 注意事项 - -#### 关于 `--dangerously-skip-permissions` -- **原方案**:可以使用(GitHub Action 内部处理) -- **新方案**:容器内使用 root 用户,**必须设为 false** -- 影响:CC 会提示权限确认,但测试显示可以正常工作 - -#### 关于环境变量 -原方案通过 `env:` 直接设置: -```yaml -env: - OLD_COMMIT: abc123 - NEW_COMMIT: def456 -``` - -新方案通过 `caller_env_vars` 传递(多行字符串): -```yaml -"caller_env_vars": "OLD_COMMIT=abc123\nNEW_COMMIT=def456" -``` - -#### 关于模型 -- 原方案可以使用任意模型(包括第三方如 minimax) -- 新方案使用 Anthropic 官方模型(如 `claude-sonnet-4-20250514`) - -#### 关于长内容传递 -如果需要传递文件内容(如 bisect summary): -```bash -# 读取并转义(限制大小避免超出 payload 限制) -CONTENT=$(head -c 3000 /path/to/file.md | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g') -``` - -然后在 payload 中使用: -```yaml -"caller_env_vars": "FILE_CONTENT=${CONTENT}" -``` - -### 5. 完整示例 - -#### 替换前(原方案) -```yaml -- name: Claude adapt to new vLLM - uses: anthropics/claude-code-action/base-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} - prompt: | - Use the main2main skill to adapt... - NEW_COMMIT: ${{ steps.detect.outputs.new_commit }} - claude_args: "--model minimax/minimax-m2.5:free --dangerously-skip-permissions --allowed-tools 'Bash(git *),Read,Write'" - env: - OLD_COMMIT: ${{ steps.detect.outputs.old_commit }} - NEW_COMMIT: ${{ steps.detect.outputs.new_commit }} - CLAUDE_WORKING_DIR: ${{ github.workspace }}/work-dir -``` - -#### 替换后(新方案) -```yaml -- name: Trigger agent-skills - id: trigger - env: - GH_TOKEN: ${{ secrets.PAT_TOKEN }} - run: | - PAYLOAD=$(cat <> "$GITHUB_OUTPUT" - break - fi - fi - sleep 10 - done - -- name: Download and apply patch - run: | - gh run download "${{ steps.wait.outputs.run_id }}" \ - --repo "kadenzhang3321/agent-skills" \ - --name claude-changes-patch \ - --dir /tmp/patch - - git apply /tmp/patch/claude-changes.patch -``` - -### 6. 回滚方案 - -如果新方案有问题,可以快速回滚: -1. 恢复原来的 `uses: anthropics/claude-code-action/base-action@v1` 步骤 -2. 删除新增的触发/等待/下载步骤 -3. 无需修改 agent-skills diff --git a/skills/infrastructure/github-action-diagnose/scripts/collect-failed-runs.js b/skills/infrastructure/github-action-diagnose/scripts/collect-failed-runs.js new file mode 100644 index 0000000..f1755b6 --- /dev/null +++ b/skills/infrastructure/github-action-diagnose/scripts/collect-failed-runs.js @@ -0,0 +1,369 @@ +#!/usr/bin/env node +/** + * collect-failed-runs.js - 收集 GitHub 仓库一段时间内失败的 CI,支持按 Job 名称过滤 + * + * 用法: + * node scripts/collect-failed-runs.js --hours 24 --exclude-jobs lint + * node scripts/collect-failed-runs.js --from 2024-01-01 --to 2024-01-31 + * node scripts/collect-failed-runs.js --hours 24 --exclude-jobs lint "check-style" --output custom.xlsx + * + * 依赖: + * npm install xlsx + */ + +const XLSX = require('xlsx'); +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +function parseArgs() { + const args = process.argv.slice(2); + const parsed = { + repo: 'vllm-project/vllm-ascend', + from_date: null, + to_date: null, + hours: null, + exclude_jobs: [], + output: null, + token: process.env.GITHUB_TOKEN || '', + per_page: 100, + }; + + let i = 0; + while (i < args.length) { + switch (args[i]) { + case '--repo': + parsed.repo = args[++i]; + break; + case '--from': + parsed.from_date = args[++i]; + break; + case '--to': + parsed.to_date = args[++i]; + break; + case '--hours': + parsed.hours = parseInt(args[++i], 10); + break; + case '--exclude-jobs': + while (i + 1 < args.length && !args[i + 1].startsWith('--')) { + parsed.exclude_jobs.push(args[++i]); + } + break; + case '--output': + parsed.output = args[++i]; + break; + case '--token': + parsed.token = args[++i]; + break; + case '--per-page': + parsed.per_page = parseInt(args[++i], 10); + break; + default: + console.error(`未知参数: ${args[i]}`); + process.exit(1); + } + i++; + } + + return parsed; +} + +function getTimeRange(args) { + const now = new Date(); + + if (args.from_date && args.to_date) { + const since = new Date(args.from_date + 'T00:00:00Z'); + const until = new Date(args.to_date + 'T23:59:59Z'); + return { since: since.toISOString(), until: until.toISOString() }; + } else if (args.from_date) { + const since = new Date(args.from_date + 'T00:00:00Z'); + return { since: since.toISOString(), until: now.toISOString() }; + } else if (args.hours) { + const since = new Date(now.getTime() - args.hours * 60 * 60 * 1000); + return { since: since.toISOString(), until: now.toISOString() }; + } else { + const since = new Date(now.getTime() - 2 * 60 * 60 * 1000); + return { since: since.toISOString(), until: now.toISOString() }; + } +} + +function githubGet(urlPath, token) { + return new Promise((resolve, reject) => { + const url = new URL(urlPath, 'https://api.github.com'); + const options = { + hostname: url.hostname, + path: url.pathname + url.search, + method: 'GET', + headers: { + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'collect-failed-runs-script', + }, + }; + + if (token) { + options.headers['Authorization'] = `Bearer ${token}`; + } + + https.get(options, (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + resolve(JSON.parse(data)); + } catch (e) { + reject(new Error(`Failed to parse JSON: ${e.message}`)); + } + } else { + reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + }); + }).on('error', reject); + }); +} + +async function fetchFailedRuns(repo, since, until, token, perPage) { + const allRuns = []; + let page = 1; + + while (true) { + const params = new URLSearchParams({ + status: 'failure', + per_page: perPage.toString(), + page: page.toString(), + created: `${since}..${until}`, + }); + + const urlPath = `/repos/${repo}/actions/runs?${params.toString()}`; + const data = await githubGet(urlPath, token); + const runs = data.workflow_runs || []; + + if (runs.length === 0) break; + + allRuns.push(...runs); + + if (runs.length < perPage) break; + page++; + } + + return allRuns; +} + +async function fetchRunJobs(repo, runId, token) { + const allJobs = []; + let page = 1; + + while (true) { + const params = new URLSearchParams({ + per_page: '100', + page: page.toString(), + }); + + const urlPath = `/repos/${repo}/actions/runs/${runId}/jobs?${params.toString()}`; + const data = await githubGet(urlPath, token); + const jobs = data.jobs || []; + + if (jobs.length === 0) break; + + allJobs.push(...jobs); + + if (jobs.length < 100) break; + page++; + } + + return allJobs; +} + +function shouldExcludeRun(jobs, excludePatterns) { + if (excludePatterns.length === 0) return false; + + const failedJobs = jobs.filter((j) => j.conclusion === 'failure'); + if (failedJobs.length === 0) return true; + + for (const job of failedJobs) { + const jobName = job.name || ''; + const isExcluded = excludePatterns.some((pattern) => + new RegExp(pattern, 'i').test(jobName) + ); + if (!isExcluded) return false; + } + + return true; +} + +function getExcludedJobNames(jobs, excludePatterns) { + const excluded = []; + for (const job of jobs) { + if (job.conclusion !== 'failure') continue; + const jobName = job.name || ''; + const isExcluded = excludePatterns.some((pattern) => + new RegExp(pattern, 'i').test(jobName) + ); + if (isExcluded) excluded.push(jobName); + } + return excluded; +} + +function calcDuration(isoStr1, isoStr2) { + try { + const d1 = new Date(isoStr1); + const d2 = new Date(isoStr2); + const diffMs = d2 - d1; + const totalSeconds = Math.floor(diffMs / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const parts = []; + if (hours > 0) parts.push(`${hours}h`); + if (minutes > 0) parts.push(`${minutes}m`); + parts.push(`${seconds}s`); + return parts.join(' '); + } catch { + return 'N/A'; + } +} + +function formatTime(isoStr) { + try { + const d = new Date(isoStr); + const pad = (n) => n.toString().padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; + } catch { + return isoStr; + } +} + +function writeXlsx(records, outputPath) { + const data = [ + ['URL', 'Run ID', '创建时间', '失败时间', '运行时间', '工作流名称', '分支', '用户'], + ]; + + for (const r of records) { + data.push([ + r.url, + r.run_id, + r.created_at, + r.updated_at, + r.duration, + r.workflow_name, + r.branch, + r.user, + ]); + } + + const ws = XLSX.utils.aoa_to_sheet(data); + + const colWidths = [ + { wch: 60 }, + { wch: 18 }, + { wch: 22 }, + { wch: 22 }, + { wch: 15 }, + { wch: 30 }, + { wch: 25 }, + { wch: 20 }, + ]; + ws['!cols'] = colWidths; + + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, '失败记录'); + XLSX.writeFile(wb, outputPath); +} + +async function main() { + const args = parseArgs(); + + if (!args.token) { + console.log('警告: 未设置 GitHub Token,可能触发 API 限流。'); + console.log('请设置环境变量 GITHUB_TOKEN 或使用 --token 参数。'); + } + + const { since, until } = getTimeRange(args); + console.log(`仓库: ${args.repo}`); + console.log(`时间范围: ${since} ~ ${until}`); + if (args.exclude_jobs.length > 0) { + console.log(`排除 Job: ${args.exclude_jobs.join(', ')}`); + } + console.log(); + + console.log('获取失败的 Runs...'); + const runs = await fetchFailedRuns( + args.repo, + since, + until, + args.token, + args.per_page + ); + console.log(`共获取到 ${runs.length} 个失败的 Run`); + + const records = []; + let skippedCount = 0; + const skippedReasons = []; + + for (let i = 0; i < runs.length; i++) { + const run = runs[i]; + const runId = run.id; + process.stdout.write(` 处理 [${i + 1}/${runs.length}] Run #${runId}... `); + + const jobs = await fetchRunJobs(args.repo, runId, args.token); + + if (shouldExcludeRun(jobs, args.exclude_jobs)) { + const excludedNames = getExcludedJobNames(jobs, args.exclude_jobs); + skippedCount++; + skippedReasons.push( + `Run #${runId} 被排除 (失败 Job: ${excludedNames.join(', ')})` + ); + console.log('跳过 (被过滤)'); + continue; + } + + const user = (run.triggering_actor && run.triggering_actor.login) || 'N/A'; + + records.push({ + url: `https://github.com/${args.repo}/actions/runs/${runId}`, + run_id: runId, + created_at: formatTime(run.created_at), + updated_at: formatTime(run.updated_at), + duration: calcDuration(run.created_at, run.updated_at), + workflow_name: run.name || 'N/A', + branch: run.head_branch || 'N/A', + user: user, + }); + console.log('保留'); + } + + console.log(); + console.log(`保留 ${records.length} 条记录,跳过 ${skippedCount} 条`); + + if (skippedReasons.length > 0) { + console.log('\n跳过的 Run:'); + for (const reason of skippedReasons) { + console.log(` - ${reason}`); + } + } + + if (records.length === 0) { + console.log('\n没有符合条件的记录,不生成文件。'); + return; + } + + let outputPath = args.output; + if (!outputPath) { + const outputDir = 'Fail_CI_Problem'; + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + const today = new Date().toISOString().split('T')[0]; + outputPath = path.join(outputDir, `failed-runs-${today}.xlsx`); + } + + writeXlsx(records, outputPath); + console.log(`\n已写入: ${outputPath}`); +} + +main().catch((err) => { + console.error('错误:', err.message); + process.exit(1); +}); diff --git a/skills/infrastructure/github-action-diagnose/scripts/diagnose-ci-runs.js b/skills/infrastructure/github-action-diagnose/scripts/diagnose-ci-runs.js new file mode 100644 index 0000000..a8b4c74 --- /dev/null +++ b/skills/infrastructure/github-action-diagnose/scripts/diagnose-ci-runs.js @@ -0,0 +1,495 @@ +#!/usr/bin/env node +/** + * diagnose-ci-runs.js - 从 xlsx 提取 CI URL,调用 fetch-run.sh 获取日志, + * 使用 Alibaba 大模型分析并写回诊断结果 + * + * 用法: + * node scripts/diagnose-ci-runs.js + * node scripts/diagnose-ci-runs.js --input Fail_CI_Problem/failed-runs-2026-04-10.xlsx + * node scripts/diagnose-ci-runs.js --input file.xlsx --model qwen3-coder-plus --batch-size 5 + * + * 依赖: + * npm install xlsx + * gh CLI 已安装并登录 + */ + +const XLSX = require('xlsx'); +const https = require('https'); +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// ── 配置 ────────────────────────────────────────────────────────────────────── + +const ALIBABA_API_KEY = process.env.ALIBABA_API_KEY || ''; +const DEFAULT_MODEL = 'qwen3.6-plus'; +const DEFAULT_BATCH_SIZE = 5; +const DEFAULT_REPO = 'vllm-project/vllm-ascend'; + +// ── 参数解析 ────────────────────────────────────────────────────────────────── + +function parseArgs() { + const args = process.argv.slice(2); + const parsed = { + input: null, + model: DEFAULT_MODEL, + batchSize: DEFAULT_BATCH_SIZE, + limit: null, + repo: DEFAULT_REPO, + apiKey: ALIBABA_API_KEY, + }; + + let i = 0; + while (i < args.length) { + switch (args[i]) { + case '--input': + parsed.input = args[++i]; + break; + case '--model': + parsed.model = args[++i]; + break; + case '--batch-size': + parsed.batchSize = parseInt(args[++i], 10); + break; + case '--limit': + parsed.limit = parseInt(args[++i], 10); + break; + case '--repo': + parsed.repo = args[++i]; + break; + case '--api-key': + parsed.apiKey = args[++i]; + break; + default: + console.error(`未知参数: ${args[i]}`); + process.exit(1); + } + i++; + } + + return parsed; +} + +// ── 查找最新的 xlsx 文件 ───────────────────────────────────────────────────── + +function findLatestXlsx() { + const outputDir = path.join(__dirname, '..', 'Fail_CI_Problem'); + if (!fs.existsSync(outputDir)) { + console.error('错误: 未找到 Fail_CI_Problem 目录,请先运行 collect-failed-runs.js'); + process.exit(1); + } + + const files = fs.readdirSync(outputDir) + .filter(f => f.endsWith('.xlsx') && f.startsWith('failed-runs-')) + .sort() + .reverse(); + + if (files.length === 0) { + console.error('错误: Fail_CI_Problem 目录中没有找到 failed-runs-*.xlsx 文件'); + process.exit(1); + } + + return path.join(outputDir, files[0]); +} + +// ── 读取 xlsx ───────────────────────────────────────────────────────────────── + +function readXlsx(filePath) { + const wb = XLSX.readFile(filePath); + const ws = wb.Sheets[wb.SheetNames[0]]; + const data = XLSX.utils.sheet_to_json(ws); + return { wb, ws, data, filePath }; +} + +// ── 提取 run_id 从 URL ──────────────────────────────────────────────────────── + +function extractRunId(url) { + const match = url.match(/\/runs\/(\d+)/); + return match ? match[1] : null; +} + +// ── 使用 fetch-run.sh 获取日志(SKILL.md Step 1b 要求) ────────────────────── + +const SKILL_DIR = path.join(__dirname, '..', 'skills', 'infrastructure', 'github-action-diagnose'); +const FETCH_RUN_SCRIPT = path.join(SKILL_DIR, 'scripts', 'fetch-run.sh'); +const GIT_BASH = 'C:\\Program Files\\Git\\bin\\bash.exe'; + +function fetchRunLogs(runId, repo) { + try { + // 使用 fetch-run.sh 脚本获取日志(SKILL.md Step 1b 要求) + // 原因:1) 日志可能很大导致 API 超时 2) 脚本内置预过滤逻辑 3) 排除 tiling 警告 + const scriptPath = FETCH_RUN_SCRIPT.replace(/\\/g, '/'); + const output = execSync( + `"${GIT_BASH}" "${scriptPath}" ${runId} ${repo}`, + { encoding: 'utf8', timeout: 120000, maxBuffer: 50 * 1024 * 1024 } + ); + + // 如果脚本输出为空或太短,尝试直接获取 + if (!output || output.trim().length < 100) { + return fetchRunLogsFallback(runId, repo); + } + + return output; + } catch (err) { + console.error(` fetch-run.sh 失败: ${err.message}`); + console.log(' 使用备用方式获取日志...'); + return fetchRunLogsFallback(runId, repo); + } +} + +// 备用方案:直接使用 gh CLI 获取日志 +function fetchRunLogsFallback(runId, repo) { + try { + const runView = execSync( + `gh run view ${runId} --repo ${repo} 2>&1`, + { encoding: 'utf8', timeout: 60000, maxBuffer: 10 * 1024 * 1024 } + ); + + let failedLogs = ''; + try { + const logFailed = execSync( + `gh run view ${runId} --log-failed --repo ${repo} 2>&1`, + { encoding: 'utf8', timeout: 120000, maxBuffer: 50 * 1024 * 1024 } + ); + + const lines = logFailed.split('\n'); + const keyPatterns = [ + /RuntimeError|OutOfMemoryError|NPU out of memory|AssertionError|EngineDeadError/i, + /exit code [1-9]|exit code 255|Process completed with exit code/i, + /error code [0-9]+|ERR[0-9]+/i, + /ETIMEDOUT|Connection refused|Network is unreachable/i, + /OOM|Killed|Bus error|Segmentation fault/i, + /unsatisfiable|No solution found|dependency.*failed/i, + /Accuracy.*lower than|precision.*drop/i, + /undefined variable|syntax error|yaml.*error/i, + ]; + const excludePatterns = [ + /tiling func|Register tiling|ops_error\.h|error_check\.h/i, + /##\[group\]|##\[endgroup\]/i, + /^\s*$/, + ]; + + const keyLines = lines.filter(line => { + if (excludePatterns.some(p => p.test(line))) return false; + return keyPatterns.some(p => p.test(line)); + }).slice(0, 50); + + failedLogs = keyLines.join('\n'); + + if (keyLines.length < 10) { + const lastLines = lines.slice(-20).join('\n'); + failedLogs += '\n\n=== Last 20 lines of log ===\n' + lastLines; + } + } catch (e) { + // 忽略 + } + + return `=== Run Overview ===\n${runView}\n\n=== Failed Job Logs (filtered) ===\n${failedLogs}`; + } catch (err) { + console.error(` 获取 Run #${runId} 日志失败: ${err.message}`); + return null; + } +} + +// ── 调用 Alibaba API ────────────────────────────────────────────────────────── + +function callAlibabaAPI(messages, model, apiKey) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify({ + model: model, + messages: messages, + temperature: 0.1, + max_tokens: 4096, + }); + + const options = { + hostname: 'dashscope.aliyuncs.com', + path: '/compatible-mode/v1/chat/completions', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Content-Length': Buffer.byteLength(payload), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + const json = JSON.parse(data); + const content = json.choices?.[0]?.message?.content || ''; + const usage = json.usage || {}; + resolve({ content, usage }); + } catch (e) { + reject(new Error(`Failed to parse response: ${e.message}`)); + } + } else { + reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + }); + }); + + req.on('error', reject); + req.write(payload); + req.end(); + }); +} + +// ── 构建诊断 prompt ─────────────────────────────────────────────────────────── + +const REFERENCE_RULES = `## 诊断参考规则(精简版) + +### 分类判断树 +1. **失败阶段判断**: + - Set up job / Initialize containers → 类型 A(直接定性) + - Checkout / Install dependencies → 倾向 A,确认是网络还是版本号 + - Run test / Build → 同时排查 A 和 B + +2. **类型 A vs B 区分**: + - A 信号: 多 PR 同时失败、重跑通过、网络/硬件/OOM/exit 255、无 Python 堆栈 + - B 信号: 明确 Python 异常堆栈、稳定复现、与 PR diff 对应 + +### 常见错误模式速查 +- **NPU 硬件**: ERR99999 / error code 507xxx → 类型 A +- **NPU 兼容性**: error code 107xxx(如 107030)→ 不是硬件!CANN runtime 参数非法,PR 版本升级时 → 类型 B +- **容器启动**: "custom container implementation failed" → 类型 A(镜像拉取/资源争用/K8s 编排卡死) +- **OOM**: Bus error → SHM 不足;Killed / exit -9 → OOM Killer +- **多机超时**: 不要孤立分析报错节点,先找 Master/Rank 0 +- **Nightly 调度**: shm_broadcast 60s 超时 + EngineDeadError → 类型 A(资源调度不稳定,非代码问题) +- **依赖解析**: "No solution found" / "unsatisfiable" → 类型 A(版本不存在),注意不要误判为 Python 环境问题 +- **vllm-ascend 安装**: csrc 编译失败 → 检查 PR 是否改了 setup.py/CMakeLists(类型 B),否则环境依赖(类型 A) +- **依赖下载**: wget github.com 直连失败 → 脚本未走 gh-proxy → 脚本配置问题 +- **cache-service 超时**: 集群内网问题 → 类型 A +- **ModelScope 下载超时**: 网络抖动 → 类型 A,直接重跑 +- **UT 卡死**: 无明显报错但运行 2-3h 超时 → 类型 B(主线脏代码死锁) +- **CPU offload 挂死**: 日志截断无 FAILED 行 → 类型 E(概率性) +- **Triton 挂死**: 重跑随机通过/失败 → 类型 E + +### 精度回归判断 +- AssertionError: Test0: / model_utils.py → 类型 C(精度回归,非功能 Bug) +- 跌幅 < 3%: 先重跑确认是否 flaky +- 跌幅 > 5%: PR 引入功能性回归 + +### 易混淆场景 +- AssertionError 可能是精度回归(类型 C)或代码 Bug(类型 B),看来源 +- NPU function error 107xxx 不是硬件故障 +- EngineDeadError 伴随 shm_broadcast 超时 → 类型 A,启动阶段即崩溃 → 类型 B +- exit code 255 = K8s 强制终止,往前找真正根因 +- tiling 编译警告(Register tiling func failed)= 正常 DEBUG,忽略 + +### vllm-ascend 环境 +- 包管理器: uv(不是 pip) +- 模型来源: ModelScope(VLLM_USE_MODELSCOPE=True),路径 /root/.cache/modelscope/ +- CANN 环境需 shell -el 激活 +- Lint runner: linux-amd64-cpu-8-hk(CPU,无 NPU)`; + +function buildDiagnosisPrompt(runId, runInfo, logs) { + return `你是一个 CI 故障诊断专家,专门分析 GitHub Actions 在昇腾(Ascend)NPU 集群上的失败问题。 + +请根据以下 CI Run 的日志信息,进行根因分析并输出诊断报告。 + +## CI Run 信息 +- Run ID: ${runId} +- 工作流: ${runInfo.workflow_name || 'N/A'} +- 分支: ${runInfo.branch || 'N/A'} +- 创建时间: ${runInfo.created_at || 'N/A'} +- 失败时间: ${runInfo.updated_at || 'N/A'} +- 用户: ${runInfo.user || 'N/A'} + +## 日志信息 +\`\`\` +${logs} +\`\`\` + +${REFERENCE_RULES} + +## 输出要求 +请按照以下格式输出诊断结果(每个失败 Job 一节): + +\`\`\` +## 故障一:[Job 名称] +- **定性**: [环境问题 / 代码Bug / 精度回归 / 配置错误 / 疑难] +- **根因**: [一句话直接原因] +- **关键标识**: [最关键的一行错误] +- **责任方**: [基础设施团队 / PR 作者] +- **建议**: [重跑 / 修改 XX / 上报运维] +\`\`\` + +## 注意事项 +- 不粘贴大段原始日志,只引用最关键的一行错误 +- 不描述诊断过程,直接给结论 +- 使用上述参考规则进行精准分类 + +请输出诊断结果:`; +} + +// ── 主函数 ──────────────────────────────────────────────────────────────────── + +async function main() { + const args = parseArgs(); + + // 查找输入文件 + const inputPath = args.input || findLatestXlsx(); + if (!fs.existsSync(inputPath)) { + console.error(`错误: 文件不存在: ${inputPath}`); + process.exit(1); + } + + console.log(`输入文件: ${inputPath}`); + console.log(`模型: ${args.model}`); + console.log(`批量大小: ${args.batchSize}`); + console.log(); + + // 检查 API Key + if (!args.apiKey) { + console.error('错误: 未设置 Alibaba API Key'); + console.error('请设置环境变量 ALIBABA_API_KEY 或使用 --api-key 参数'); + process.exit(1); + } + + + + // 读取 xlsx + const { wb, ws, data, filePath } = readXlsx(inputPath); + + // 过滤出没有诊断结果的记录 + const recordsToDiagnose = data.filter(r => !r['诊断结果'] || r['诊断结果'].trim() === ''); + const alreadyDiagnosed = data.length - recordsToDiagnose.length; + + if (alreadyDiagnosed > 0) { + console.log(`跳过 ${alreadyDiagnosed} 条已诊断记录`); + } + + if (recordsToDiagnose.length === 0) { + console.log('所有记录已诊断,无需处理。'); + return; + } + + console.log(`待诊断记录: ${recordsToDiagnose.length} 条`); + if (args.limit) { + console.log(`限制诊断数量: ${args.limit} 条`); + } + console.log(); + + let diagnosedCount = 0; + let errorCount = 0; + let totalInputTokens = 0; + let totalOutputTokens = 0; + const maxRecords = args.limit || recordsToDiagnose.length; + + for (let i = 0; i < Math.min(recordsToDiagnose.length, maxRecords); i++) { + const record = recordsToDiagnose[i]; + const runId = extractRunId(record['URL'] || ''); + + if (!runId) { + console.log(`[${i + 1}/${recordsToDiagnose.length}] 跳过: 无法提取 Run ID`); + continue; + } + + console.log(`[${i + 1}/${recordsToDiagnose.length}] 诊断 Run #${runId}...`); + + // 获取日志 + console.log(' 获取日志...'); + const logs = fetchRunLogs(runId, args.repo); + + if (!logs || logs.trim().length === 0) { + console.log(' 日志为空,跳过'); + errorCount++; + continue; + } + + // 调用大模型 + console.log(' 调用大模型分析...'); + const prompt = buildDiagnosisPrompt(runId, record, logs); + + try { + const result = await callAlibabaAPI( + [{ role: 'user', content: prompt }], + args.model, + args.apiKey + ); + + // 更新记录 + record['诊断结果'] = result.content; + diagnosedCount++; + + // 统计 token + const inputTokens = result.usage?.prompt_tokens || 0; + const outputTokens = result.usage?.completion_tokens || 0; + totalInputTokens += inputTokens; + totalOutputTokens += outputTokens; + + console.log(` 诊断完成 (输入: ${inputTokens} tokens, 输出: ${outputTokens} tokens)`); + + // 每处理 batchSize 条保存一次 + if (diagnosedCount % args.batchSize === 0) { + console.log(` 保存进度 (${diagnosedCount} 条)...`); + saveProgress(wb, ws, data, filePath); + } + } catch (err) { + console.error(` 诊断失败: ${err.message}`); + record['诊断结果'] = `错误: ${err.message}`; + errorCount++; + } + + // 避免 API 限流,等待 1 秒 + if (i < recordsToDiagnose.length - 1) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + // 最终保存 + console.log('\n保存最终结果...'); + saveProgress(wb, ws, data, filePath); + + console.log(); + console.log(`诊断完成: ${diagnosedCount} 条成功, ${errorCount} 条失败`); + console.log(`结果已写入: ${filePath}`); + + // 输出 Token 统计 + const totalTokens = totalInputTokens + totalOutputTokens; + const avgInput = diagnosedCount > 0 ? Math.round(totalInputTokens / diagnosedCount) : 0; + const avgOutput = diagnosedCount > 0 ? Math.round(totalOutputTokens / diagnosedCount) : 0; + console.log(); + console.log('═══════════════════════════════════════════'); + console.log(' Token 消耗统计'); + console.log('═══════════════════════════════════════════'); + console.log(` 总输入 Token: ${totalInputTokens.toLocaleString()}`); + console.log(` 总输出 Token: ${totalOutputTokens.toLocaleString()}`); + console.log(` 总 Token: ${totalTokens.toLocaleString()}`); + console.log(` 平均输入/条: ${avgInput.toLocaleString()}`); + console.log(` 平均输出/条: ${avgOutput.toLocaleString()}`); + console.log(` 预估费用: ¥${(totalInputTokens / 1e6 * 10 + totalOutputTokens / 1e6 * 30).toFixed(2)}`); + console.log('═══════════════════════════════════════════'); +} + +function saveProgress(wb, ws, data, filePath) { + // 创建新的 workbook 来避免文件锁定问题 + const newWb = XLSX.utils.book_new(); + const newWs = XLSX.utils.json_to_sheet(data); + XLSX.utils.book_append_sheet(newWb, newWs, '失败记录'); + + // 设置列宽 + newWs['!cols'] = [ + { wch: 60 }, { wch: 18 }, { wch: 22 }, { wch: 22 }, + { wch: 15 }, { wch: 30 }, { wch: 25 }, { wch: 20 }, { wch: 80 } + ]; + + // 写入临时文件(使用 .xlsx 扩展名),然后替换 + const tempPath = filePath + '.temp.xlsx'; + XLSX.writeFile(newWb, tempPath); + + // 删除原文件并重命名临时文件 + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + fs.renameSync(tempPath, filePath); +} + +main().catch(err => { + console.error('错误:', err.message); + process.exit(1); +});