From b190c64fe7967445940b95b9e9ee4bea794fcaea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=93=E4=B8=B6=E6=A2=A6=E4=B8=B6=E4=BB=81?= <74444214+Daydreamer114@users.noreply.github.com> Date: Thu, 21 May 2026 20:07:53 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20cursor=20cli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/ai-issue-analysis-cursor.yml | 43 +++++ README.md | 27 ++- action.yml | 181 +++++++++++++++--- 3 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/ai-issue-analysis-cursor.yml diff --git a/.github/workflows/ai-issue-analysis-cursor.yml b/.github/workflows/ai-issue-analysis-cursor.yml new file mode 100644 index 0000000..4f42f1a --- /dev/null +++ b/.github/workflows/ai-issue-analysis-cursor.yml @@ -0,0 +1,43 @@ +name: AI Issue Analysis (Cursor) + +on: + workflow_dispatch: + inputs: + issue_number: + description: "Issue number to analyze" + required: true + type: number + +jobs: + cursor-analysis: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - name: Analyze issue with Cursor + id: analysis + uses: ./ + with: + ai-provider: cursor + copilot-github-token: ${{ secrets.COPILOT_GITHUB_TOKEN }} + cursor-api-key: ${{ secrets.CURSOR_API_KEY }} + github-token: ${{ secrets.GITHUB_TOKEN }} + bot-name: "@github-actions" + + - name: Print outputs + if: always() + env: + ANALYSIS_PROMPT: ${{ steps.analysis.outputs.analysis-prompt }} + ISSUE_NUMBER: ${{ steps.analysis.outputs.issue-number }} + COMMENT_ID: ${{ steps.analysis.outputs.comment-id }} + COMMENT_URL: ${{ steps.analysis.outputs.comment-url }} + COPILOT_OUTPUT: ${{ steps.analysis.outputs.copilot-output }} + FINAL_CONCLUSION: ${{ steps.analysis.outputs.final-conclusion }} + run: | + printf '%s\n' "$ANALYSIS_PROMPT" + echo "issue_number=$ISSUE_NUMBER" + echo "comment_id=$COMMENT_ID" + echo "comment_url=$COMMENT_URL" + printf '%s\n' "$COPILOT_OUTPUT" + printf '%s\n' "$FINAL_CONCLUSION" diff --git a/README.md b/README.md index 05d1d75..178a2d5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ai-issue-analysis -一个通用的 GitHub composite action,用来在 Issue 打开或被评论时调用 Copilot CLI 做分析,并把分析过程和最终结论持续回写到同一条评论里。 +一个通用的 GitHub composite action,用来在 Issue 打开或被评论时调用 **Copilot CLI** 或 **Cursor CLI** 做分析,并把分析过程和最终结论持续回写到同一条评论里。 实战效果展示: @@ -9,6 +9,8 @@ ## 快速接入 +### 使用 GitHub Copilot + 1. 请确保你有 Copilot Pro (当前仅支持 Copilot,以后可能适配 codex 等更多工具,欢迎 ISSUE 催更~) 2. 前往 [GitHub PAT](https://github.com/settings/personal-access-tokens) 新增一个 token @@ -28,6 +30,23 @@ 6. 新提个 issue 测试下能否正常运行了,或者在以前的 issue 里 `@github-actions` +### 使用 Cursor CLI(可选) + +1. 前往 [Cursor Dashboard](https://cursor.com/dashboard/integrations) 创建 API Key +2. 在仓库 Secrets 中添加 `CURSOR_API_KEY` +3. 调用 action 时指定 `ai-provider: cursor` 并传入 `cursor-api-key`: + +```yaml +- uses: Daydreamer114/ai-issue-analysis@feat/cursor + with: + ai-provider: cursor + github-token: ${{ secrets.GITHUB_TOKEN }} + cursor-api-key: ${{ secrets.CURSOR_API_KEY }} + copilot-github-token: ${{ secrets.COPILOT_GITHUB_TOKEN }} # action schema 仍为 required,cursor 分支不会用到 +``` + +> 不传 `ai-provider` 时默认使用 Copilot,现有 workflow 无需改动。 + > [!TIP] > > 如果你的项目有固定的日志包命名、关键日志路径、附件目录、模块映射或上游依赖,建议在这个通用版基础上微调 `SKILL.md`,分析质量会更高。最佳实践参考: @@ -45,6 +64,9 @@ - `github-token`: 用于创建和更新 Issue 评论 - `copilot-github-token`: Copilot CLI 使用的 Fine-grained token,支持传多个 token,每行一个,action 会随机选择一个使用 +- `ai-provider`: AI 后端,`copilot`(默认)或 `cursor` +- `cursor-api-key`: Cursor CLI 使用的 API Key;`ai-provider=cursor` 时在运行时必填,支持多 key 换行随机选择 +- `cursor-model`: Cursor CLI 模型名,默认 `composer-2.5` - `bot-name`: 从 `issue_comment` 正文中剥离掉的 bot mention,比如 `@YourBot` - `initial-comment-body`: 开始分析时先发出的评论正文 - `action-link-text`: 评论里展示的运行链接文字 @@ -93,7 +115,8 @@ - action 内部会自动 `checkout` 调用方仓库 - 如果调用方已经自己 checkout,或者前置步骤会生成工作区文件,可以把 `checkout-repository` 设为 `false` -- 会自动安装 `@github/copilot` +- `ai-provider` 省略时默认 `copilot`,会自动安装 `@github/copilot` +- `ai-provider=cursor` 时会安装 Cursor CLI 并调用 `cursor-agent` - 会先创建一条评论,然后持续更新这条评论 - 会导出 `comment-id`、`comment-url`、`analysis-prompt`、`copilot-output`、`final-conclusion` 等 action outputs - `copilot-output` 会包含 Copilot 启动前的参数打印和 prompt 正文,不再只是 Copilot 进程本身的 stdout/stderr diff --git a/action.yml b/action.yml index 301bf66..ccc73d6 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ name: AI Issue Analysis -description: Analyze a GitHub issue with Copilot CLI and keep a comment updated with progress and the final result. +description: Analyze a GitHub issue with Copilot CLI or Cursor CLI and keep a comment updated with progress and the final result. author: MistEO inputs: @@ -80,6 +80,18 @@ inputs: description: Extra content always appended at the bottom of every comment update. Empty by default. required: false default: "" + ai-provider: + description: AI backend to use. Supported values are copilot and cursor. Defaults to copilot when empty. + required: false + default: copilot + cursor-api-key: + description: Cursor API key for Cursor CLI. Required at runtime when ai-provider is cursor. Supports multiple keys, one per line, and randomly picks one. + required: false + default: "" + cursor-model: + description: Cursor CLI model name passed to cursor-agent --model. + required: false + default: composer-2.5 outputs: issue-number: @@ -137,6 +149,42 @@ runs: fh.write(f"issue_number={issue_number}\n") PY + - name: Resolve AI provider + id: provider + shell: bash + env: + INPUT_AI_PROVIDER: ${{ inputs.ai-provider }} + run: | + python <<'PY' + import os + + provider = os.environ["INPUT_AI_PROVIDER"].strip().lower() or "copilot" + if provider not in {"copilot", "cursor"}: + raise SystemExit(f"Unsupported ai-provider: {provider}. Supported values: copilot, cursor.") + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: + fh.write(f"provider={provider}\n") + PY + + - name: Validate provider credentials + shell: bash + env: + AI_PROVIDER: ${{ steps.provider.outputs.provider }} + INPUT_CURSOR_API_KEY: ${{ inputs.cursor-api-key }} + run: | + python <<'PY' + import os + + provider = os.environ["AI_PROVIDER"] + if provider != "cursor": + raise SystemExit(0) + + raw_value = os.environ["INPUT_CURSOR_API_KEY"] + keys = [line.strip() for line in raw_value.splitlines() if line.strip()] + if not keys: + raise SystemExit("Input cursor-api-key is required when ai-provider=cursor.") + PY + - name: Prepare runtime files id: runtime shell: bash @@ -232,11 +280,21 @@ runs: ${{ steps.runtime.outputs.action_link }} - name: Install Copilot CLI + if: ${{ steps.provider.outputs.provider == 'copilot' }} shell: bash run: npm install -g "${{ inputs.copilot-package }}" + - name: Install Cursor CLI + if: ${{ steps.provider.outputs.provider == 'cursor' }} + shell: bash + run: | + curl https://cursor.com/install -fsS | bash + echo "$HOME/.cursor/bin" >> "$GITHUB_PATH" + cursor-agent --version + - name: Resolve Copilot token id: resolve_copilot_token + if: ${{ steps.provider.outputs.provider == 'copilot' }} shell: bash env: INPUT_COPILOT_GITHUB_TOKEN: ${{ inputs.copilot-github-token }} @@ -258,10 +316,36 @@ runs: fh.write(f"copilot_token_count={len(tokens)}\n") PY - - name: Run Copilot analysis with streaming updates + - name: Resolve Cursor API key + id: resolve_cursor_api_key + if: ${{ steps.provider.outputs.provider == 'cursor' }} + shell: bash + env: + INPUT_CURSOR_API_KEY: ${{ inputs.cursor-api-key }} + run: | + python <<'PY' + import os + import secrets + + raw_value = os.environ["INPUT_CURSOR_API_KEY"] + keys = [line.strip() for line in raw_value.splitlines() if line.strip()] + + if not keys: + raise SystemExit("Input cursor-api-key is required when ai-provider=cursor.") + + selected_key = secrets.choice(keys) + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: + fh.write(f"cursor_api_key={selected_key}\n") + fh.write(f"cursor_api_key_count={len(keys)}\n") + PY + + - name: Run AI analysis with streaming updates shell: bash env: + AI_PROVIDER: ${{ steps.provider.outputs.provider }} COPILOT_GITHUB_TOKEN: ${{ steps.resolve_copilot_token.outputs.copilot_token }} + CURSOR_API_KEY: ${{ steps.resolve_cursor_api_key.outputs.cursor_api_key }} COMMENT_GITHUB_TOKEN: ${{ inputs.github-token }} GITHUB_SERVER_URL: ${{ github.server_url }} COMMENT_ID: ${{ steps.initial_comment.outputs.comment-id }} @@ -277,6 +361,7 @@ runs: DETAILS_END: ${{ steps.runtime.outputs.details_end }} COPILOT_MODEL: ${{ inputs.copilot-model }} COPILOT_REASONING_EFFORT: ${{ inputs.copilot-reasoning-effort }} + CURSOR_MODEL: ${{ inputs.cursor-model }} STREAM_UPDATE_INTERVAL: ${{ inputs.stream-update-interval-seconds }} EXTRA_COMMENT_CONTENT: ${{ inputs.extra-comment-content }} run: | @@ -284,29 +369,57 @@ runs: : > "$OUTPUT_FILE" : > "$EXECUTION_LOG_FILE" - { - echo "Copilot invocation parameters:" - echo " repo: $REPO" - echo " issue-number: $ISSUE_NUMBER" - echo " comment-id: $COMMENT_ID" - echo " comment-url: $COMMENT_URL" - echo " model: $COPILOT_MODEL" - echo " reasoning-effort: $COPILOT_REASONING_EFFORT" - echo " copilot-token-count: ${{ steps.resolve_copilot_token.outputs.copilot_token_count }}" - echo " stream-update-interval-seconds: $STREAM_UPDATE_INTERVAL" - echo " analysis-prompt-file: $ANALYSIS_PROMPT_FILE" - echo " output-file: $OUTPUT_FILE" - echo " body-file: $BODY_FILE" - echo " command: copilot --yolo --model \"$COPILOT_MODEL\" --reasoning-effort \"$COPILOT_REASONING_EFFORT\" --prompt \"\"" - echo "Prompt content begins" - cat "$ANALYSIS_PROMPT_FILE" - echo "Prompt content ends" - } | tee -a "$EXECUTION_LOG_FILE" - - copilot --yolo --model "$COPILOT_MODEL" --reasoning-effort "$COPILOT_REASONING_EFFORT" \ - --prompt "$(cat "$ANALYSIS_PROMPT_FILE")" > "$OUTPUT_FILE" 2>&1 & - COPILOT_PID=$! - echo "Copilot started with PID $COPILOT_PID" | tee -a "$EXECUTION_LOG_FILE" + + if [ "$AI_PROVIDER" = "cursor" ]; then + { + echo "Cursor invocation parameters:" + echo " repo: $REPO" + echo " issue-number: $ISSUE_NUMBER" + echo " comment-id: $COMMENT_ID" + echo " comment-url: $COMMENT_URL" + echo " model: $CURSOR_MODEL" + echo " cursor-api-key-count: ${{ steps.resolve_cursor_api_key.outputs.cursor_api_key_count }}" + echo " stream-update-interval-seconds: $STREAM_UPDATE_INTERVAL" + echo " analysis-prompt-file: $ANALYSIS_PROMPT_FILE" + echo " output-file: $OUTPUT_FILE" + echo " body-file: $BODY_FILE" + echo " command: cursor-agent -p \"\" --force --model \"$CURSOR_MODEL\" --output-format text" + echo "Prompt content begins" + cat "$ANALYSIS_PROMPT_FILE" + echo "Prompt content ends" + } | tee -a "$EXECUTION_LOG_FILE" + + cursor-agent -p "$(cat "$ANALYSIS_PROMPT_FILE")" \ + --force \ + --model "$CURSOR_MODEL" \ + --output-format text > "$OUTPUT_FILE" 2>&1 & + AGENT_PID=$! + echo "Cursor Agent started with PID $AGENT_PID" | tee -a "$EXECUTION_LOG_FILE" + else + { + echo "Copilot invocation parameters:" + echo " repo: $REPO" + echo " issue-number: $ISSUE_NUMBER" + echo " comment-id: $COMMENT_ID" + echo " comment-url: $COMMENT_URL" + echo " model: $COPILOT_MODEL" + echo " reasoning-effort: $COPILOT_REASONING_EFFORT" + echo " copilot-token-count: ${{ steps.resolve_copilot_token.outputs.copilot_token_count }}" + echo " stream-update-interval-seconds: $STREAM_UPDATE_INTERVAL" + echo " analysis-prompt-file: $ANALYSIS_PROMPT_FILE" + echo " output-file: $OUTPUT_FILE" + echo " body-file: $BODY_FILE" + echo " command: copilot --yolo --model \"$COPILOT_MODEL\" --reasoning-effort \"$COPILOT_REASONING_EFFORT\" --prompt \"\"" + echo "Prompt content begins" + cat "$ANALYSIS_PROMPT_FILE" + echo "Prompt content ends" + } | tee -a "$EXECUTION_LOG_FILE" + + copilot --yolo --model "$COPILOT_MODEL" --reasoning-effort "$COPILOT_REASONING_EFFORT" \ + --prompt "$(cat "$ANALYSIS_PROMPT_FILE")" > "$OUTPUT_FILE" 2>&1 & + AGENT_PID=$! + echo "Copilot started with PID $AGENT_PID" | tee -a "$EXECUTION_LOG_FILE" + fi last_content="" @@ -314,9 +427,9 @@ runs: sleep "$STREAM_UPDATE_INTERVAL" & sleep_pid=$! - wait -n "$COPILOT_PID" "$sleep_pid" 2>/dev/null || true + wait -n "$AGENT_PID" "$sleep_pid" 2>/dev/null || true - if ! kill -0 "$COPILOT_PID" 2>/dev/null; then + if ! kill -0 "$AGENT_PID" 2>/dev/null; then kill "$sleep_pid" 2>/dev/null || true break fi @@ -357,16 +470,24 @@ runs: fi done - wait "$COPILOT_PID" + wait "$AGENT_PID" { echo - echo "Copilot output begins" + if [ "$AI_PROVIDER" = "cursor" ]; then + echo "Cursor output begins" + else + echo "Copilot output begins" + fi if [ -f "$OUTPUT_FILE" ]; then cat "$OUTPUT_FILE" fi echo - echo "Copilot output ends" + if [ "$AI_PROVIDER" = "cursor" ]; then + echo "Cursor output ends" + else + echo "Copilot output ends" + fi } >> "$EXECUTION_LOG_FILE" - name: Full analysis From 58315a21e625675944b616b5b2075d10ae9bfaf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=93=E4=B8=B6=E6=A2=A6=E4=B8=B6=E4=BB=81?= <74444214+Daydreamer114@users.noreply.github.com> Date: Thu, 21 May 2026 20:12:11 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ai-issue-analysis-cursor.yml | 1 + .github/workflows/ai-issue-analysis.yml | 2 +- README.md | 18 +++++++++--------- action.yml | 6 +++--- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ai-issue-analysis-cursor.yml b/.github/workflows/ai-issue-analysis-cursor.yml index 4f42f1a..3ed387d 100644 --- a/.github/workflows/ai-issue-analysis-cursor.yml +++ b/.github/workflows/ai-issue-analysis-cursor.yml @@ -15,6 +15,7 @@ jobs: contents: read issues: write steps: + # 本仓库 CI 使用本地 action;接入方请改为 MistEO/ai-issue-analysis@main - name: Analyze issue with Cursor id: analysis uses: ./ diff --git a/.github/workflows/ai-issue-analysis.yml b/.github/workflows/ai-issue-analysis.yml index 41d2a55..98accc0 100644 --- a/.github/workflows/ai-issue-analysis.yml +++ b/.github/workflows/ai-issue-analysis.yml @@ -30,7 +30,7 @@ jobs: # - .claude/skills/generic-issue-log-analysis/SKILL.md - name: Analyze issue with AI id: analysis - uses: Misteo/ai-issue-analysis@main + uses: MistEO/ai-issue-analysis@main with: # 请在 https://github.com/settings/personal-access-tokens 添加一个token,填写到仓库 secret 里(请确保你有 Copilot Pro) copilot-github-token: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/README.md b/README.md index 178a2d5..3baf0fb 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ### 使用 GitHub Copilot -1. 请确保你有 Copilot Pro (当前仅支持 Copilot,以后可能适配 codex 等更多工具,欢迎 ISSUE 催更~) +1. 使用 Copilot 时请确保你有 Copilot Pro;使用 Cursor 时请准备 Cursor API Key 2. 前往 [GitHub PAT](https://github.com/settings/personal-access-tokens) 新增一个 token - Expiration (过期时间): 设为一年以内(太长反而会报错) @@ -37,7 +37,7 @@ 3. 调用 action 时指定 `ai-provider: cursor` 并传入 `cursor-api-key`: ```yaml -- uses: Daydreamer114/ai-issue-analysis@feat/cursor +- uses: MistEO/ai-issue-analysis@main with: ai-provider: cursor github-token: ${{ secrets.GITHUB_TOKEN }} @@ -84,19 +84,19 @@ - `issue-number`: 本次运行实际解析出的 Issue 编号 - `comment-id`: 创建并持续更新的评论 ID - `comment-url`: 创建并持续更新的评论 URL -- `analysis-prompt`: 本次最终传给 Copilot 的 prompt -- `copilot-output`: 完整执行日志,包含 Copilot 启动前的参数打印、prompt 正文,以及 Copilot CLI 输出 -- `final-conclusion`: Copilot 写入 `copilot-answer-file` 的最终结论 +- `analysis-prompt`: 本次最终传给 AI 后端的 prompt +- `copilot-output`: 完整执行日志,包含启动前参数、prompt 正文,以及 Copilot CLI / Cursor CLI 输出 +- `final-conclusion`: 写入 `copilot-answer-file` 的最终结论 - `analysis-prompt`、`copilot-output` 和 `final-conclusion` 在过长时会为适配 GitHub Actions output 大小限制而被截断;完整内容优先从 artifacts 读取 ## 上传产物 -- `copilot-output-issue--comment-`: 完整执行日志,包含启动前参数、prompt 正文和 Copilot CLI 输出 +- `copilot-output-issue--comment-`: 完整执行日志,包含启动前参数、prompt 正文和 AI CLI 输出 - `final-conclusion-issue--comment-`: 最终结论文本 ## Skill 配合 -- 这个 action 只负责 GitHub Actions 编排、评论更新、Copilot CLI 调用和 prompt 拼接,不内置项目领域知识 +- 这个 action 只负责 GitHub Actions 编排、评论更新、AI CLI 调用和 prompt 拼接,不内置项目领域知识 - 对需要分析 issue 附件、日志包、运行时配置、跨仓库代码路径的项目,建议配套提供项目自己的 issue 分析 skill - 一个可行的 skill 一般至少会覆盖这些步骤:读取 issue 正文和评论、定位并下载日志附件、先建立时间线再筛证据、最后回溯到代码和文档做归因 - 如果没有这层 skill,action 仍然能运行,但对日志包、截图、跨模块调用链这类问题,分析质量通常会明显下降 @@ -119,8 +119,8 @@ - `ai-provider=cursor` 时会安装 Cursor CLI 并调用 `cursor-agent` - 会先创建一条评论,然后持续更新这条评论 - 会导出 `comment-id`、`comment-url`、`analysis-prompt`、`copilot-output`、`final-conclusion` 等 action outputs -- `copilot-output` 会包含 Copilot 启动前的参数打印和 prompt 正文,不再只是 Copilot 进程本身的 stdout/stderr -- 会上传 Copilot 原始输出和最终结论两个 artifacts +- `copilot-output` 会包含启动前的参数打印和 prompt 正文,不再只是 AI CLI 进程本身的 stdout/stderr +- 会上传 AI 原始输出和最终结论两个 artifacts - 最终评论会包含最终结论、完整分析过程折叠块,以及当前 Actions 运行链接 - `copilot-github-token` 兼容单个 token,也兼容多个 token 按行填写;传多个时每次运行会随机选一个 diff --git a/action.yml b/action.yml index ccc73d6..2ff663b 100644 --- a/action.yml +++ b/action.yml @@ -104,13 +104,13 @@ outputs: description: The URL of the issue comment that was created and then updated. value: ${{ steps.export_results.outputs.comment-url }} analysis-prompt: - description: Final rendered prompt passed to Copilot for this run. + description: Final rendered prompt passed to the AI backend for this run. value: ${{ steps.export_results.outputs.analysis-prompt }} copilot-output: - description: Execution log including pre-run parameters, prompt, and Copilot stdout and stderr. + description: Execution log including pre-run parameters, prompt, and AI CLI stdout and stderr. value: ${{ steps.export_results.outputs.copilot-output }} final-conclusion: - description: Final conclusion content produced by Copilot. + description: Final conclusion content written to copilot-answer-file. value: ${{ steps.export_results.outputs.final-conclusion }} runs: From b7f77b29aaf01b2a3c0160b279a4bfee01123f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=93=E4=B8=B6=E6=A2=A6=E4=B8=B6=E4=BB=81?= <74444214+Daydreamer114@users.noreply.github.com> Date: Thu, 21 May 2026 20:16:35 +0800 Subject: [PATCH 3/5] fix: copilot review --- .../workflows/ai-issue-analysis-cursor.yml | 1 - README.md | 10 +-- action.yml | 88 ++++++++++++++++--- 3 files changed, 79 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ai-issue-analysis-cursor.yml b/.github/workflows/ai-issue-analysis-cursor.yml index 3ed387d..9ff7a9d 100644 --- a/.github/workflows/ai-issue-analysis-cursor.yml +++ b/.github/workflows/ai-issue-analysis-cursor.yml @@ -21,7 +21,6 @@ jobs: uses: ./ with: ai-provider: cursor - copilot-github-token: ${{ secrets.COPILOT_GITHUB_TOKEN }} cursor-api-key: ${{ secrets.CURSOR_API_KEY }} github-token: ${{ secrets.GITHUB_TOKEN }} bot-name: "@github-actions" diff --git a/README.md b/README.md index 3baf0fb..db54883 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ### 使用 GitHub Copilot -1. 使用 Copilot 时请确保你有 Copilot Pro;使用 Cursor 时请准备 Cursor API Key +1. 请确保你有 Copilot Pro 2. 前往 [GitHub PAT](https://github.com/settings/personal-access-tokens) 新增一个 token - Expiration (过期时间): 设为一年以内(太长反而会报错) @@ -23,12 +23,12 @@ - Name: `COPILOT_GITHUB_TOKEN` - Secret: 上一步中生成的那个 -5. 把下面两个文件拷贝到你的仓库里,文件夹不要变 +4. 把下面两个文件拷贝到你的仓库里,文件夹不要变 - [`.github/workflows/ai-issue-analysis.yml`](.github/workflows/ai-issue-analysis.yml) - [`.claude/skills/generic-issue-log-analysis/SKILL.md`](.claude/skills/generic-issue-log-analysis/SKILL.md) -6. 新提个 issue 测试下能否正常运行了,或者在以前的 issue 里 `@github-actions` +5. 新提个 issue 测试下能否正常运行了,或者在以前的 issue 里 `@github-actions` ### 使用 Cursor CLI(可选) @@ -42,7 +42,6 @@ ai-provider: cursor github-token: ${{ secrets.GITHUB_TOKEN }} cursor-api-key: ${{ secrets.CURSOR_API_KEY }} - copilot-github-token: ${{ secrets.COPILOT_GITHUB_TOKEN }} # action schema 仍为 required,cursor 分支不会用到 ``` > 不传 `ai-provider` 时默认使用 Copilot,现有 workflow 无需改动。 @@ -63,10 +62,11 @@ 如果你的 workflow_dispatch 输入名不是 `issue_number`,或者你在其他事件里调用这个 action,就显式传 `issue-number`。 - `github-token`: 用于创建和更新 Issue 评论 -- `copilot-github-token`: Copilot CLI 使用的 Fine-grained token,支持传多个 token,每行一个,action 会随机选择一个使用 +- `copilot-github-token`: Copilot CLI 使用的 Fine-grained token;`ai-provider=copilot` 时在运行时必填,支持传多个 token,每行一个,action 会随机选择一个使用 - `ai-provider`: AI 后端,`copilot`(默认)或 `cursor` - `cursor-api-key`: Cursor CLI 使用的 API Key;`ai-provider=cursor` 时在运行时必填,支持多 key 换行随机选择 - `cursor-model`: Cursor CLI 模型名,默认 `composer-2.5` +- `cursor-cli-version`: Cursor CLI lab 构建版本;留空时从官方 install 脚本元数据解析,建议生产环境固定版本 - `bot-name`: 从 `issue_comment` 正文中剥离掉的 bot mention,比如 `@YourBot` - `initial-comment-body`: 开始分析时先发出的评论正文 - `action-link-text`: 评论里展示的运行链接文字 diff --git a/action.yml b/action.yml index 2ff663b..74d4a5b 100644 --- a/action.yml +++ b/action.yml @@ -11,8 +11,9 @@ inputs: description: GitHub token used to create and update issue comments. required: true copilot-github-token: - description: Fine-grained GitHub token with Copilot access for the Copilot CLI. Supports multiple tokens, one per line, and randomly picks one. - required: true + description: Fine-grained GitHub token with Copilot access for the Copilot CLI. Required at runtime when ai-provider is copilot. Supports multiple tokens, one per line, and randomly picks one. + required: false + default: "" bot-name: description: Bot mention to strip from issue_comment messages before appending extra requirements. required: false @@ -92,6 +93,10 @@ inputs: description: Cursor CLI model name passed to cursor-agent --model. required: false default: composer-2.5 + cursor-cli-version: + description: Cursor CLI lab build id (for example 2026.05.20-2b5dd59). Empty resolves the version pinned by the official install script metadata. + required: false + default: "" outputs: issue-number: @@ -171,18 +176,23 @@ runs: env: AI_PROVIDER: ${{ steps.provider.outputs.provider }} INPUT_CURSOR_API_KEY: ${{ inputs.cursor-api-key }} + INPUT_COPILOT_GITHUB_TOKEN: ${{ inputs.copilot-github-token }} run: | python <<'PY' import os provider = os.environ["AI_PROVIDER"] - if provider != "cursor": - raise SystemExit(0) - raw_value = os.environ["INPUT_CURSOR_API_KEY"] - keys = [line.strip() for line in raw_value.splitlines() if line.strip()] - if not keys: - raise SystemExit("Input cursor-api-key is required when ai-provider=cursor.") + if provider == "cursor": + raw_value = os.environ["INPUT_CURSOR_API_KEY"] + keys = [line.strip() for line in raw_value.splitlines() if line.strip()] + if not keys: + raise SystemExit("Input cursor-api-key is required when ai-provider=cursor.") + elif provider == "copilot": + raw_value = os.environ["INPUT_COPILOT_GITHUB_TOKEN"] + tokens = [line.strip() for line in raw_value.splitlines() if line.strip()] + if not tokens: + raise SystemExit("Input copilot-github-token is required when ai-provider=copilot.") PY - name: Prepare runtime files @@ -287,9 +297,55 @@ runs: - name: Install Cursor CLI if: ${{ steps.provider.outputs.provider == 'cursor' }} shell: bash + env: + INPUT_CURSOR_CLI_VERSION: ${{ inputs.cursor-cli-version }} run: | - curl https://cursor.com/install -fsS | bash - echo "$HOME/.cursor/bin" >> "$GITHUB_PATH" + set -euo pipefail + + OS="linux" + ARCH="$(uname -m)" + case "${ARCH}" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) + echo "Unsupported architecture: ${ARCH}" + exit 1 + ;; + esac + + VERSION="${INPUT_CURSOR_CLI_VERSION}" + if [ -z "$VERSION" ]; then + INSTALL_SCRIPT="${RUNNER_TEMP}/cursor-install.sh" + curl -fsSL "https://cursor.com/install" -o "$INSTALL_SCRIPT" + VERSION="$(grep -Eo 'downloads\.cursor\.com/lab/[^/]+' "$INSTALL_SCRIPT" | head -1 | cut -d/ -f3)" + if [ -z "$VERSION" ]; then + echo "Unable to resolve Cursor CLI version from install script metadata." + exit 1 + fi + fi + + PACKAGE_URL="https://downloads.cursor.com/lab/${VERSION}/${OS}/${ARCH}/agent-cli-package.tar.gz" + PACKAGE_FILE="${RUNNER_TEMP}/cursor-agent-cli.tar.gz" + + echo "Installing Cursor CLI ${VERSION} for ${OS}/${ARCH}" + curl -fSL "$PACKAGE_URL" -o "$PACKAGE_FILE" + + if [ ! -s "$PACKAGE_FILE" ]; then + echo "Downloaded Cursor CLI package is empty." + exit 1 + fi + + tar -tzf "$PACKAGE_FILE" >/dev/null + + INSTALL_ROOT="${HOME}/.local/share/cursor-agent/versions/${VERSION}" + mkdir -p "$INSTALL_ROOT" + tar --strip-components=1 -xzf "$PACKAGE_FILE" -C "$INSTALL_ROOT" + + mkdir -p "${HOME}/.local/bin" + ln -sf "${INSTALL_ROOT}/cursor-agent" "${HOME}/.local/bin/cursor-agent" + ln -sf "${INSTALL_ROOT}/cursor-agent" "${HOME}/.local/bin/agent" + echo "${HOME}/.local/bin" >> "$GITHUB_PATH" + cursor-agent --version - name: Resolve Copilot token @@ -311,8 +367,11 @@ runs: selected_token = secrets.choice(tokens) + print(f"::add-mask::{selected_token}") + with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as fh: + fh.write(f"COPILOT_GITHUB_TOKEN={selected_token}\n") + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: - fh.write(f"copilot_token={selected_token}\n") fh.write(f"copilot_token_count={len(tokens)}\n") PY @@ -335,8 +394,11 @@ runs: selected_key = secrets.choice(keys) + print(f"::add-mask::{selected_key}") + with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as fh: + fh.write(f"CURSOR_API_KEY={selected_key}\n") + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: - fh.write(f"cursor_api_key={selected_key}\n") fh.write(f"cursor_api_key_count={len(keys)}\n") PY @@ -344,8 +406,6 @@ runs: shell: bash env: AI_PROVIDER: ${{ steps.provider.outputs.provider }} - COPILOT_GITHUB_TOKEN: ${{ steps.resolve_copilot_token.outputs.copilot_token }} - CURSOR_API_KEY: ${{ steps.resolve_cursor_api_key.outputs.cursor_api_key }} COMMENT_GITHUB_TOKEN: ${{ inputs.github-token }} GITHUB_SERVER_URL: ${{ github.server_url }} COMMENT_ID: ${{ steps.initial_comment.outputs.comment-id }} From 753b8e0e927b3e8322a8e0cdd72e7b5e51dda17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=93=E4=B8=B6=E6=A2=A6=E4=B8=B6=E4=BB=81?= <74444214+Daydreamer114@users.noreply.github.com> Date: Thu, 21 May 2026 20:30:11 +0800 Subject: [PATCH 4/5] fix: OS detect --- action.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 74d4a5b..f403434 100644 --- a/action.yml +++ b/action.yml @@ -302,7 +302,30 @@ runs: run: | set -euo pipefail - OS="linux" + if [ -n "${RUNNER_OS:-}" ]; then + case "${RUNNER_OS}" in + Linux) OS="linux" ;; + macOS) OS="darwin" ;; + Windows) + echo "Cursor CLI install is not supported on Windows runners. Use runs-on: ubuntu-latest or macos-latest." + exit 1 + ;; + *) + echo "Unsupported RUNNER_OS: ${RUNNER_OS}. Cursor CLI requires Linux or macOS runners." + exit 1 + ;; + esac + else + case "$(uname -s)" in + Linux*) OS="linux" ;; + Darwin*) OS="darwin" ;; + *) + echo "Unsupported operating system: $(uname -s). Cursor CLI requires Linux or macOS runners." + exit 1 + ;; + esac + fi + ARCH="$(uname -m)" case "${ARCH}" in x86_64|amd64) ARCH="x64" ;; From d9911ffc651bc748f237358aaf37ab1afb9d0a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=93=E4=B8=B6=E6=A2=A6=E4=B8=B6=E4=BB=81?= <74444214+Daydreamer114@users.noreply.github.com> Date: Thu, 21 May 2026 20:33:46 +0800 Subject: [PATCH 5/5] =?UTF-8?q?perf:=20=E5=9B=9E=E9=80=80cursor=20cli?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=96=B9=E6=B3=95=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=AE=98=E6=96=B9=E4=B8=8B=E8=BD=BD=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +-- action.yml | 77 ++---------------------------------------------------- 2 files changed, 3 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index db54883..49d26e3 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ - `ai-provider`: AI 后端,`copilot`(默认)或 `cursor` - `cursor-api-key`: Cursor CLI 使用的 API Key;`ai-provider=cursor` 时在运行时必填,支持多 key 换行随机选择 - `cursor-model`: Cursor CLI 模型名,默认 `composer-2.5` -- `cursor-cli-version`: Cursor CLI lab 构建版本;留空时从官方 install 脚本元数据解析,建议生产环境固定版本 - `bot-name`: 从 `issue_comment` 正文中剥离掉的 bot mention,比如 `@YourBot` - `initial-comment-body`: 开始分析时先发出的评论正文 - `action-link-text`: 评论里展示的运行链接文字 @@ -116,7 +115,7 @@ - action 内部会自动 `checkout` 调用方仓库 - 如果调用方已经自己 checkout,或者前置步骤会生成工作区文件,可以把 `checkout-repository` 设为 `false` - `ai-provider` 省略时默认 `copilot`,会自动安装 `@github/copilot` -- `ai-provider=cursor` 时会安装 Cursor CLI 并调用 `cursor-agent` +- `ai-provider=cursor` 时会通过官方 [cursor.com/install](https://cursor.com/install) 安装 Cursor CLI 并调用 `cursor-agent` - 会先创建一条评论,然后持续更新这条评论 - 会导出 `comment-id`、`comment-url`、`analysis-prompt`、`copilot-output`、`final-conclusion` 等 action outputs - `copilot-output` 会包含启动前的参数打印和 prompt 正文,不再只是 AI CLI 进程本身的 stdout/stderr diff --git a/action.yml b/action.yml index f403434..6f7c0fb 100644 --- a/action.yml +++ b/action.yml @@ -93,10 +93,6 @@ inputs: description: Cursor CLI model name passed to cursor-agent --model. required: false default: composer-2.5 - cursor-cli-version: - description: Cursor CLI lab build id (for example 2026.05.20-2b5dd59). Empty resolves the version pinned by the official install script metadata. - required: false - default: "" outputs: issue-number: @@ -297,78 +293,9 @@ runs: - name: Install Cursor CLI if: ${{ steps.provider.outputs.provider == 'cursor' }} shell: bash - env: - INPUT_CURSOR_CLI_VERSION: ${{ inputs.cursor-cli-version }} run: | - set -euo pipefail - - if [ -n "${RUNNER_OS:-}" ]; then - case "${RUNNER_OS}" in - Linux) OS="linux" ;; - macOS) OS="darwin" ;; - Windows) - echo "Cursor CLI install is not supported on Windows runners. Use runs-on: ubuntu-latest or macos-latest." - exit 1 - ;; - *) - echo "Unsupported RUNNER_OS: ${RUNNER_OS}. Cursor CLI requires Linux or macOS runners." - exit 1 - ;; - esac - else - case "$(uname -s)" in - Linux*) OS="linux" ;; - Darwin*) OS="darwin" ;; - *) - echo "Unsupported operating system: $(uname -s). Cursor CLI requires Linux or macOS runners." - exit 1 - ;; - esac - fi - - ARCH="$(uname -m)" - case "${ARCH}" in - x86_64|amd64) ARCH="x64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) - echo "Unsupported architecture: ${ARCH}" - exit 1 - ;; - esac - - VERSION="${INPUT_CURSOR_CLI_VERSION}" - if [ -z "$VERSION" ]; then - INSTALL_SCRIPT="${RUNNER_TEMP}/cursor-install.sh" - curl -fsSL "https://cursor.com/install" -o "$INSTALL_SCRIPT" - VERSION="$(grep -Eo 'downloads\.cursor\.com/lab/[^/]+' "$INSTALL_SCRIPT" | head -1 | cut -d/ -f3)" - if [ -z "$VERSION" ]; then - echo "Unable to resolve Cursor CLI version from install script metadata." - exit 1 - fi - fi - - PACKAGE_URL="https://downloads.cursor.com/lab/${VERSION}/${OS}/${ARCH}/agent-cli-package.tar.gz" - PACKAGE_FILE="${RUNNER_TEMP}/cursor-agent-cli.tar.gz" - - echo "Installing Cursor CLI ${VERSION} for ${OS}/${ARCH}" - curl -fSL "$PACKAGE_URL" -o "$PACKAGE_FILE" - - if [ ! -s "$PACKAGE_FILE" ]; then - echo "Downloaded Cursor CLI package is empty." - exit 1 - fi - - tar -tzf "$PACKAGE_FILE" >/dev/null - - INSTALL_ROOT="${HOME}/.local/share/cursor-agent/versions/${VERSION}" - mkdir -p "$INSTALL_ROOT" - tar --strip-components=1 -xzf "$PACKAGE_FILE" -C "$INSTALL_ROOT" - - mkdir -p "${HOME}/.local/bin" - ln -sf "${INSTALL_ROOT}/cursor-agent" "${HOME}/.local/bin/cursor-agent" - ln -sf "${INSTALL_ROOT}/cursor-agent" "${HOME}/.local/bin/agent" - echo "${HOME}/.local/bin" >> "$GITHUB_PATH" - + curl https://cursor.com/install -fsS | bash + echo "$HOME/.local/bin" >> "$GITHUB_PATH" cursor-agent --version - name: Resolve Copilot token