fix: MCP writeNoSqlDatabaseContent 缺少嵌套对象部分更新的清晰文档与示例,易导致整块替换而非局部更新 #375
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 🤖 Issue Auto Processor | |
| on: | |
| schedule: | |
| - cron: '0 */4 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| issue_number: | |
| description: 'Issue number to process immediately (optional)' | |
| required: false | |
| type: string | |
| issue_comment: | |
| types: [created] | |
| concurrency: | |
| group: issue-auto-processor | |
| cancel-in-progress: false | |
| env: | |
| DELAY_HOURS: '4' | |
| MAX_ISSUES_PER_RUN: '5' | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| jobs: | |
| process-issues: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Install CodeBuddy CLI | |
| run: npm install -g @tencent-ai/codebuddy-code | |
| - name: Validate CodeBuddy credentials | |
| env: | |
| CODEBUDDY_AUTH_TOKEN: ${{ secrets.CODEBUDDY_AUTH_TOKEN }} | |
| CODEBUDDY_API_KEY: ${{ secrets.CODEBUDDY_API_KEY }} | |
| CODEBUDDY_INTERNET_ENVIRONMENT: ${{ vars.CODEBUDDY_INTERNET_ENVIRONMENT }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${CODEBUDDY_AUTH_TOKEN:-}" ] && [ -z "${CODEBUDDY_API_KEY:-}" ]; then | |
| echo "::error::Set CODEBUDDY_AUTH_TOKEN or CODEBUDDY_API_KEY in repository secrets before enabling this workflow." | |
| exit 1 | |
| fi | |
| if [ -n "${CODEBUDDY_API_KEY:-}" ] && [ -z "${CODEBUDDY_INTERNET_ENVIRONMENT:-}" ]; then | |
| echo "::warning::CODEBUDDY_API_KEY is set but CODEBUDDY_INTERNET_ENVIRONMENT is empty. This is fine for codebuddy.ai, but for 中国版请将 repository variable CODEBUDDY_INTERNET_ENVIRONMENT 设为 internal。" | |
| fi | |
| - name: Ensure AI labels exist | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const labels = [ | |
| { name: 'ai-processing', color: 'FBCA04', description: 'AI automation is processing this issue' }, | |
| { name: 'ai-processed', color: '0E8A16', description: 'AI automation already processed this issue' }, | |
| { name: 'ai-failed', color: 'D93F0B', description: 'AI automation failed to process this issue' }, | |
| { name: 'ai-fix', color: '0052CC', description: 'AI automation created a fix PR for this issue' }, | |
| { name: 'no-ai', color: '666666', description: 'Skip AI automation for this issue' } | |
| ]; | |
| for (const label of labels) { | |
| try { | |
| await github.rest.issues.getLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label.name | |
| }); | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ...label | |
| }); | |
| } | |
| } | |
| - name: Collect eligible issues | |
| id: collect | |
| uses: actions/github-script@v7 | |
| env: | |
| DELAY_HOURS: ${{ env.DELAY_HOURS }} | |
| MAX_ISSUES_PER_RUN: ${{ env.MAX_ISSUES_PER_RUN }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const { parseIssueCommentCommand } = require(path.join(process.cwd(), 'scripts', 'issue-auto-processor.cjs')); | |
| const delayHours = Number(process.env.DELAY_HOURS || '4'); | |
| const maxIssues = Number(process.env.MAX_ISSUES_PER_RUN || '5'); | |
| const cutoffMs = Date.now() - delayHours * 60 * 60 * 1000; | |
| async function fetchComments(issueNumber) { | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| per_page: 100, | |
| }); | |
| return comments.map((comment) => ({ | |
| id: comment.id, | |
| author: comment.user?.login || 'unknown', | |
| authorAssociation: comment.author_association || 'NONE', | |
| body: comment.body || '', | |
| createdAt: comment.created_at, | |
| url: comment.html_url, | |
| })); | |
| } | |
| async function normalizeIssue(issue, extra = {}) { | |
| return { | |
| number: issue.number, | |
| title: issue.title, | |
| body: issue.body || '', | |
| url: issue.html_url, | |
| createdAt: issue.created_at, | |
| labels: (issue.labels || []).map((label) => typeof label === 'string' ? label : label.name), | |
| comments: await fetchComments(issue.number), | |
| requestedAction: extra.requestedAction || '', | |
| command: extra.command || '', | |
| commandCommentAuthor: extra.commandCommentAuthor || '', | |
| commandCommentUrl: extra.commandCommentUrl || '', | |
| }; | |
| } | |
| let eligible = []; | |
| const manualIssueNumber = context.payload.inputs?.issue_number?.trim(); | |
| if (context.eventName === 'issue_comment') { | |
| const issuePayload = context.payload.issue; | |
| const commentPayload = context.payload.comment; | |
| const parsedCommand = parseIssueCommentCommand({ | |
| body: commentPayload?.body || '', | |
| authorAssociation: commentPayload?.author_association || '', | |
| hasPullRequest: Boolean(issuePayload?.pull_request), | |
| }); | |
| if (parsedCommand) { | |
| const { data: issue } = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issuePayload.number, | |
| }); | |
| if (!issue.pull_request) { | |
| eligible = [await normalizeIssue(issue, { | |
| requestedAction: parsedCommand.action, | |
| command: parsedCommand.command, | |
| commandCommentAuthor: commentPayload.user?.login || '', | |
| commandCommentUrl: commentPayload.html_url || '', | |
| })]; | |
| } | |
| } | |
| } else if (manualIssueNumber) { | |
| const { data: issue } = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: Number(manualIssueNumber), | |
| }); | |
| if (!issue.pull_request) { | |
| eligible = [await normalizeIssue(issue)]; | |
| } | |
| } else { | |
| const allIssues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| sort: 'created', | |
| direction: 'asc', | |
| per_page: 100, | |
| }); | |
| const scheduledIssues = allIssues | |
| .filter((issue) => !issue.pull_request) | |
| .filter((issue) => new Date(issue.created_at).getTime() <= cutoffMs) | |
| .filter((issue) => { | |
| const labels = (issue.labels || []).map((label) => typeof label === 'string' ? label : label.name); | |
| return !labels.includes('ai-processed') && !labels.includes('ai-processing') && !labels.includes('ai-failed') && !labels.includes('no-ai'); | |
| }) | |
| .slice(0, maxIssues); | |
| eligible = await Promise.all(scheduledIssues.map((issue) => normalizeIssue(issue))); | |
| } | |
| fs.writeFileSync('.issue-auto-processor-issues.json', JSON.stringify(eligible, null, 2)); | |
| core.setOutput('count', String(eligible.length)); | |
| await core.summary | |
| .addHeading('Issue Auto Processor') | |
| .addRaw(`Event: ${context.eventName}\nEligible issues: ${eligible.length}`) | |
| .write(); | |
| - name: Process issues with CodeBuddy headless mode | |
| if: steps.collect.outputs.count != '0' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CODEBUDDY_AUTH_TOKEN: ${{ secrets.CODEBUDDY_AUTH_TOKEN }} | |
| CODEBUDDY_API_KEY: ${{ secrets.CODEBUDDY_API_KEY }} | |
| CODEBUDDY_INTERNET_ENVIRONMENT: ${{ vars.CODEBUDDY_INTERNET_ENVIRONMENT }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BASE_REF="${GITHUB_SHA}" | |
| cleanup_repo() { | |
| git reset --hard "$BASE_REF" >/dev/null 2>&1 || true | |
| git clean -fd >/dev/null 2>&1 || true | |
| git switch --detach "$BASE_REF" >/dev/null 2>&1 || true | |
| } | |
| truncate_text_for_pr_body() { | |
| local max_chars="${1:-12000}" | |
| python3 -c 'import sys; limit = int(sys.argv[1]); text = sys.stdin.read(); sys.stdout.write(text if len(text) <= limit else text[:limit].rstrip() + "\n\n_[truncated by automation to keep PR creation stable]_")' "$max_chars" | |
| } | |
| lookup_open_pr_url() { | |
| local branch="$1" | |
| gh pr list --head "$branch" --state open --json url --jq '.[0].url' | |
| } | |
| build_bug_prompt() { | |
| node scripts/issue-auto-processor.cjs build-bug-prompt /tmp/issue.json > /tmp/codebuddy-prompt.txt | |
| } | |
| build_analysis_prompt() { | |
| node scripts/issue-auto-processor.cjs build-analysis-prompt /tmp/issue.json > /tmp/codebuddy-prompt.txt | |
| } | |
| detect_bug_issue() { | |
| node scripts/issue-auto-processor.cjs is-bug /tmp/issue.json | |
| } | |
| extract_result_text() { | |
| node scripts/issue-auto-processor.cjs extract-result | |
| } | |
| has_nonempty_text() { | |
| [ -n "$(printf '%s' "$1" | tr -d '[:space:]')" ] | |
| } | |
| issue_has_label() { | |
| local label="$1" | |
| jq -e --arg label "$label" '((.labels // []) | map(ascii_downcase)) | any(. == ($label | ascii_downcase))' /tmp/issue.json >/dev/null 2>&1 | |
| } | |
| sync_issue_json_label() { | |
| local action="$1" | |
| local label="$2" | |
| if [ "$action" = "add" ]; then | |
| jq --arg label "$label" ' | |
| if ((.labels // []) | map(ascii_downcase) | any(. == ($label | ascii_downcase))) | |
| then . | |
| else .labels = ((.labels // []) + [$label]) | |
| end | |
| ' /tmp/issue.json > /tmp/issue.json.next | |
| else | |
| jq --arg label "$label" ' | |
| .labels = ((.labels // []) | map(select(ascii_downcase != ($label | ascii_downcase)))) | |
| ' /tmp/issue.json > /tmp/issue.json.next | |
| fi | |
| mv /tmp/issue.json.next /tmp/issue.json | |
| } | |
| update_issue_labels() { | |
| local issue_number="$1" | |
| shift | |
| local -a args=() | |
| local spec="" | |
| local action="" | |
| local label="" | |
| for spec in "$@"; do | |
| action="${spec%%:*}" | |
| label="${spec#*:}" | |
| case "$action" in | |
| add) | |
| if ! issue_has_label "$label"; then | |
| args+=(--add-label "$label") | |
| sync_issue_json_label add "$label" | |
| fi | |
| ;; | |
| remove) | |
| if issue_has_label "$label"; then | |
| args+=(--remove-label "$label") | |
| sync_issue_json_label remove "$label" | |
| fi | |
| ;; | |
| *) | |
| echo "Unknown label action: $action" >&2 | |
| return 1 | |
| ;; | |
| esac | |
| done | |
| if [ ${#args[@]} -gt 0 ]; then | |
| gh issue edit "$issue_number" "${args[@]}" >/dev/null | |
| fi | |
| } | |
| write_issue_comment_file() { | |
| local heading="$1" | |
| local body="$2" | |
| local footer="$3" | |
| { | |
| printf '%s\n\n%s' "$heading" "$body" | |
| if [ -n "$footer" ]; then | |
| printf '\n\n---\n%s\n' "$footer" | |
| else | |
| printf '\n' | |
| fi | |
| } > /tmp/issue-comment.md | |
| } | |
| write_pr_body_file() { | |
| local issue_number="$1" | |
| local issue_url="$2" | |
| local result_text="$3" | |
| local summary="" | |
| summary=$(printf '%s' "$result_text" | truncate_text_for_pr_body 12000) | |
| { | |
| printf '%s\n\n' '## 🤖 Automated fix attempt' | |
| printf 'Fixes #%s\n\n' "$issue_number" | |
| printf 'Source issue: %s\n\n' "$issue_url" | |
| printf '### Summary\n%s\n' "$summary" | |
| } > /tmp/pr-body.md | |
| } | |
| post_file_comment() { | |
| local issue_number="$1" | |
| local file_path="$2" | |
| gh issue comment "$issue_number" --body-file "$file_path" >/dev/null | |
| } | |
| fail_with_comment() { | |
| local issue_number="$1" | |
| local heading="$2" | |
| local body="$3" | |
| write_issue_comment_file "$heading" "$body" '' | |
| post_file_comment "$issue_number" /tmp/issue-comment.md | |
| update_issue_labels "$issue_number" remove:ai-processing add:ai-failed remove:ai-processed | |
| cleanup_repo | |
| return 0 | |
| } | |
| process_issue() { | |
| local issue_json="$1" | |
| local raw_output="" | |
| local result_text="" | |
| local exit_code=0 | |
| local branch="" | |
| local number="" | |
| local title="" | |
| local issue_url="" | |
| local pr_url="" | |
| local pr_output="" | |
| local pr_lookup_exit_code=0 | |
| local is_bug="false" | |
| local requested_action="" | |
| local command="" | |
| local command_comment_author="" | |
| cleanup_repo | |
| printf '%s' "$issue_json" > /tmp/issue.json | |
| number=$(jq -r '.number' /tmp/issue.json) | |
| title=$(jq -r '.title' /tmp/issue.json) | |
| issue_url=$(jq -r '.url' /tmp/issue.json) | |
| requested_action=$(jq -r '.requestedAction // ""' /tmp/issue.json) | |
| command=$(jq -r '.command // ""' /tmp/issue.json) | |
| command_comment_author=$(jq -r '.commandCommentAuthor // ""' /tmp/issue.json) | |
| is_bug=$(detect_bug_issue) | |
| echo "Processing issue #$number: $title" | |
| if [ "$requested_action" = "skip" ]; then | |
| echo "Route: slash command -> skip" | |
| update_issue_labels "$number" add:no-ai remove:ai-processing remove:ai-failed remove:ai-processed remove:ai-fix | |
| write_issue_comment_file '## 🤖 CloudBase Automation Disabled' "Acknowledged \`$command\` from @$command_comment_author. Automatic processing is now disabled for this issue until a maintainer explicitly re-runs it." '' | |
| post_file_comment "$number" /tmp/issue-comment.md | |
| cleanup_repo | |
| return 0 | |
| fi | |
| if [ "$requested_action" = "fix" ]; then | |
| is_bug="true" | |
| fi | |
| update_issue_labels "$number" remove:no-ai remove:ai-failed remove:ai-processed remove:ai-fix add:ai-processing | |
| if [ "$is_bug" = "true" ]; then | |
| echo "Route: bug -> attempt fix" | |
| branch="ai-fix/issue-$number" | |
| git fetch origin "$DEFAULT_BRANCH" | |
| git switch -C "$branch" "origin/$DEFAULT_BRANCH" | |
| build_bug_prompt | |
| set +e | |
| raw_output=$(timeout 1200s codebuddy -p "$(cat /tmp/codebuddy-prompt.txt)" -y --output-format json --permission-mode acceptEdits --model hy3-preview-ioa </dev/null 2>&1) | |
| exit_code=$? | |
| set -e | |
| result_text=$(printf '%s' "$raw_output" | extract_result_text || true) | |
| if [ $exit_code -ne 0 ]; then | |
| local failure_detail="CodeBuddy exited with status $exit_code before producing a usable patch." | |
| if has_nonempty_text "$result_text"; then | |
| failure_detail=$(printf '%s\n\nAutomation output:\n\n```\n%s\n```' "$failure_detail" "$result_text") | |
| fi | |
| fail_with_comment "$number" '## 🤖 AI Fix Attempt Failed' "$failure_detail" | |
| return 0 | |
| fi | |
| if ! has_nonempty_text "$result_text" && git diff --quiet; then | |
| fail_with_comment "$number" '## 🤖 AI Fix Attempt Failed' 'Automation ran, but the AI response was empty or could not be parsed into a usable fix summary. Please inspect the workflow logs before retrying.' | |
| return 0 | |
| fi | |
| if git diff --quiet; then | |
| write_issue_comment_file '## 🤖 AI Bug Analysis' "AI reviewed this bug but did not produce a safe patch.\n\n$result_text" '' | |
| post_file_comment "$number" /tmp/issue-comment.md | |
| update_issue_labels "$number" remove:ai-processing add:ai-failed remove:ai-processed | |
| cleanup_repo | |
| return 0 | |
| fi | |
| git add -A | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| set +e | |
| git commit -m "fix(issue-auto): 🤖 attempt fix for issue #$number" | |
| exit_code=$? | |
| set -e | |
| if [ $exit_code -ne 0 ]; then | |
| fail_with_comment "$number" '## 🤖 AI Fix Attempt Failed' 'Automation created a branch diff, but git commit failed before a PR could be opened. Please inspect the workflow logs before retrying.' | |
| return 0 | |
| fi | |
| set +e | |
| git push origin "$branch" --force | |
| exit_code=$? | |
| set -e | |
| if [ $exit_code -ne 0 ]; then | |
| fail_with_comment "$number" '## 🤖 AI Fix Attempt Failed' 'Automation created a commit, but pushing the fix branch failed before a PR could be opened. Please inspect the workflow logs before retrying.' | |
| return 0 | |
| fi | |
| set +e | |
| pr_url=$(lookup_open_pr_url "$branch") | |
| exit_code=$? | |
| set -e | |
| if [ $exit_code -ne 0 ]; then | |
| fail_with_comment "$number" '## 🤖 AI Fix Attempt Failed' 'Automation pushed a fix branch, but failed while checking for an existing PR. Please inspect the workflow logs before retrying.' | |
| return 0 | |
| fi | |
| if [ -z "$pr_url" ]; then | |
| write_pr_body_file "$number" "$issue_url" "$result_text" | |
| set +e | |
| pr_output=$(gh pr create --base "$DEFAULT_BRANCH" --head "$branch" --title "fix: 🤖 attempt fix for issue #$number" --body-file /tmp/pr-body.md 2>&1) | |
| exit_code=$? | |
| set -e | |
| pr_url=$(printf '%s' "$pr_output" | node scripts/issue-auto-processor.cjs extract-pr-url || true) | |
| if ! has_nonempty_text "$pr_url"; then | |
| set +e | |
| pr_url=$(lookup_open_pr_url "$branch") | |
| pr_lookup_exit_code=$? | |
| set -e | |
| fi | |
| if ! has_nonempty_text "$pr_url"; then | |
| if [ $exit_code -ne 0 ]; then | |
| fail_with_comment "$number" '## 🤖 AI Fix Attempt Failed' 'Automation created and pushed a fix branch, but PR creation failed before a valid PR URL could be resolved. Please inspect the workflow logs before retrying.' | |
| elif [ $pr_lookup_exit_code -ne 0 ]; then | |
| fail_with_comment "$number" '## 🤖 AI Fix Attempt Failed' 'Automation created and pushed a fix branch, but the workflow could not verify the PR URL after creation. Please inspect the workflow logs before retrying.' | |
| else | |
| fail_with_comment "$number" '## 🤖 AI Fix Attempt Failed' 'Automation created and pushed a fix branch, but PR creation did not return a valid URL and no open PR was found for the branch. Please inspect the workflow logs before retrying.' | |
| fi | |
| return 0 | |
| fi | |
| fi | |
| local comment_body="" | |
| comment_body=$(printf 'I created a PR for this bug: %s\n\nPlease review the generated changes before merging.' "$pr_url") | |
| write_issue_comment_file '## 🤖 AI Fix Attempt' "$comment_body" '' | |
| post_file_comment "$number" /tmp/issue-comment.md | |
| update_issue_labels "$number" remove:ai-processing add:ai-processed add:ai-fix remove:ai-failed | |
| cleanup_repo | |
| return 0 | |
| fi | |
| echo "Route: non-bug -> analysis only" | |
| build_analysis_prompt | |
| set +e | |
| raw_output=$(timeout 1200s codebuddy -p "$(cat /tmp/codebuddy-prompt.txt)" -y --output-format json --permission-mode acceptEdits --model hy3-preview-ioa </dev/null 2>&1) | |
| exit_code=$? | |
| set -e | |
| result_text=$(printf '%s' "$raw_output" | extract_result_text || true) | |
| if [ $exit_code -ne 0 ]; then | |
| local failure_detail="CodeBuddy exited with status $exit_code before producing a comment." | |
| if has_nonempty_text "$result_text"; then | |
| failure_detail=$(printf '%s\n\n```\n%s\n```' "$failure_detail" "$result_text") | |
| fi | |
| fail_with_comment "$number" '## 🤖 AI Analysis Failed' "$failure_detail" | |
| return 0 | |
| fi | |
| if ! has_nonempty_text "$result_text"; then | |
| fail_with_comment "$number" '## 🤖 AI Analysis Failed' 'Automation ran, but the AI response was empty or could not be parsed into a usable comment. Please inspect the workflow logs before retrying.' | |
| return 0 | |
| fi | |
| write_issue_comment_file '## 🤖 AI Analysis' "$result_text" 'Generated automatically by CodeBuddy CLI headless mode.' | |
| post_file_comment "$number" /tmp/issue-comment.md | |
| update_issue_labels "$number" remove:ai-processing add:ai-processed remove:ai-failed | |
| cleanup_repo | |
| } | |
| mapfile -t issues < <(jq -c ".[]" .issue-auto-processor-issues.json) | |
| for issue in "${issues[@]}"; do | |
| if ! process_issue "$issue"; then | |
| number=$(printf '%s' "$issue" | jq -r '.number') | |
| title=$(printf '%s' "$issue" | jq -r '.title') | |
| echo "Unexpected failure while processing issue #$number: $title" | |
| cleanup_repo | |
| printf '%s' "$issue" > /tmp/issue.json | |
| update_issue_labels "$number" remove:ai-processing add:ai-failed remove:ai-processed || true | |
| write_issue_comment_file '## 🤖 AI Automation Error' 'The issue auto processor hit an unexpected workflow error before completion. Please inspect the workflow logs for details.' '' | |
| post_file_comment "$number" /tmp/issue-comment.md || true | |
| fi | |
| done | |
| - name: No eligible issues | |
| if: steps.collect.outputs.count == '0' | |
| run: echo 'No eligible issues found.' |