Remove debug failed CI PR comment workflow #2
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: AI agents in CI | ||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| event_name: | ||
| description: "Event name from the caller workflow" | ||
| required: true | ||
| type: string | ||
| event_payload: | ||
| description: "JSON payload captured from the caller event" | ||
| required: true | ||
| type: string | ||
| repository: | ||
| description: "Full repository name of the caller (owner/repo)" | ||
| required: true | ||
| type: string | ||
| ref: | ||
| description: "Git ref from the caller" | ||
| required: true | ||
| type: string | ||
| sha: | ||
| description: "Git SHA from the caller" | ||
| required: true | ||
| type: string | ||
| actor: | ||
| description: "Actor that triggered the caller workflow" | ||
| required: true | ||
| type: string | ||
| failed_run_id: | ||
| description: "Workflow run ID to debug when event_name is workflow_run" | ||
| required: false | ||
| type: number | ||
| default: 0 | ||
| team: | ||
| description: "Team name" | ||
| required: false | ||
| type: string | ||
| debug_prompt: | ||
| description: "Detailed prompt for the debug ci failures jobs" | ||
| required: false | ||
| type: string | ||
| debugging_model: | ||
| description: "LLM model to use for debugging failed CI runs" | ||
| required: false | ||
| type: string | ||
| generic_model: | ||
| description: "LLM model to use for general purpose tasks" | ||
| required: false | ||
| type: string | ||
| jobs: | ||
| work-on-issues-prs: | ||
| if: | | ||
| inputs.event_name == 'issues' || | ||
| inputs.event_name == 'issue_comment' || | ||
| inputs.event_name == 'pull_request_review_comment' || | ||
| inputs.event_name == 'pull_request_review' | ||
| runs-on: gha-production-medium | ||
| container: ci-images-release.arti.tw.ee/actions_java_17_and_21 | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| issues: write | ||
| id-token: write | ||
| actions: read | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| fetch-depth: 1 | ||
| - name: "Add repo as safe directory" | ||
| run: | | ||
| git config --global --add safe.directory "$GITHUB_WORKSPACE" | ||
| - name: Sync caller event context | ||
| shell: bash | ||
| env: | ||
| CALLER_EVENT_PAYLOAD: ${{ inputs.event_payload }} | ||
| run: | | ||
| event_file="$RUNNER_TEMP/original_event.json" | ||
| printf '%s' "$CALLER_EVENT_PAYLOAD" > "$event_file" | ||
| { | ||
| echo "GITHUB_EVENT_PATH=$event_file" | ||
| echo "GITHUB_EVENT_NAME=${{ inputs.event_name }}" | ||
| echo "GITHUB_REPOSITORY=${{ inputs.repository }}" | ||
| echo "GITHUB_REF=${{ inputs.ref }}" | ||
| echo "GITHUB_SHA=${{ inputs.sha }}" | ||
| echo "GITHUB_ACTOR=${{ inputs.actor }}" | ||
| } >> "$GITHUB_ENV" | ||
| - name: Run AI Agents for Issues and PRs | ||
| uses: transferwise/claude-code-action@main | ||
| with: | ||
| trigger_phrase: "/run-ci-ai-agents" | ||
| anthropic_bedrock_base_url: ${{ secrets.ANTHROPIC_BEDROCK_BASE_URL }} | ||
| anthropic_default_sonnet_model: ${{ vars.ANTHROPIC_DEFAULT_SONNET_MODEL }} | ||
| anthropic_default_haiku_model: ${{ vars.ANTHROPIC_DEFAULT_HAIKU_MODEL }} | ||
| anthropic_default_opus_model: ${{ vars.ANTHROPIC_DEFAULT_OPUS_MODEL }} | ||
| x-wise-llm-gateway-team: ${{ inputs.team }} | ||
| x-wise-llm-gateway-lite-auth-caller-id: ${{ inputs.actor }} | ||
| experimental_allowed_domains: | | ||
| github.com | ||
| api.github.com | ||
| ${{ secrets.ANTHROPIC_BEDROCK_BASE_URL }} | ||
| claude_args: | | ||
| --allowedTools "mcp__github_inline_comment__create_inline_comment" | ||
| --model ${{ inputs.generic_model != '' && inputs.generic_model || vars.ANTHROPIC_DEFAULT_HAIKU_MODEL }} | ||
| debug-failed-ci-runs: | ||
| if: inputs.event_name == 'workflow_run' | ||
| runs-on: gha-production-medium | ||
| container: ci-images-release.arti.tw.ee/actions_java_17_and_21 | ||
| permissions: | ||
| contents: read | ||
| actions: read | ||
| pull-requests: write | ||
| id-token: write # Required for OIDC token exchange | ||
| steps: | ||
| - name: Get CI failure details | ||
| id: failure_details | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const zlib = require('zlib'); | ||
| const runId = ${{ inputs.failed_run_id }}; | ||
| const run = await github.rest.actions.getWorkflowRun({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| run_id: runId, | ||
| }); | ||
| const jobs = await github.rest.actions.listJobsForWorkflowRun({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| run_id: runId, | ||
| }); | ||
| const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure'); | ||
| let errorLogs = []; | ||
| for (const job of failedJobs) { | ||
| const logs = await github.rest.actions.downloadJobLogsForWorkflowRun({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| job_id: job.id | ||
| }); | ||
| let logText; | ||
| try { | ||
| const toBuffer = (value) => { | ||
| if (Buffer.isBuffer(value)) { | ||
| return value; | ||
| } | ||
| if (value instanceof Uint8Array) { | ||
| return Buffer.from(value); | ||
| } | ||
| if (ArrayBuffer.isView?.(value)) { | ||
| return Buffer.from(value.buffer); | ||
| } | ||
| if (value instanceof ArrayBuffer) { | ||
| return Buffer.from(value); | ||
| } | ||
| return null; | ||
| }; | ||
| const bufferData = toBuffer(logs.data); | ||
| if (bufferData) { | ||
| try { | ||
| logText = zlib.gunzipSync(bufferData).toString('utf8'); | ||
| } catch (gzipError) { | ||
| logText = bufferData.toString('utf8'); | ||
| } | ||
| } else if (typeof logs.data === 'string') { | ||
| logText = logs.data; | ||
| } else { | ||
| logText = JSON.stringify(logs.data); | ||
| } | ||
| } catch (error) { | ||
| logText = `Failed to unzip logs: ${error.message}`; | ||
| } | ||
| const errorLines = logText | ||
| .split(/\r?\n/) | ||
| .filter(line => /^(\d{4}-\d{2}-\d{2}T).*##\[error\]/.test(line)); | ||
| const filteredLog = errorLines.length > 0 | ||
| ? errorLines.join("\n") | ||
| : "No lines containing ##[error] were found in the job logs."; | ||
| errorLogs.push({ | ||
| jobName: job.name, | ||
| logs: filteredLog, | ||
| }); | ||
| } | ||
| let prNumber = run.data.pull_requests?.[0]?.number; | ||
| if (!prNumber && run.data.head_branch) { | ||
| const prs = await github.rest.pulls.list({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'open', | ||
| head: `${context.repo.owner}:${run.data.head_branch}`, | ||
| }); | ||
| if (prs.data.length > 0) { | ||
| prNumber = prs.data[0].number; | ||
| } | ||
| } | ||
| return { | ||
| runUrl: run.data.html_url, | ||
| workflowName: run.data.name, | ||
| failedJobs: failedJobs.map(j => j.name), | ||
| errorLogs: errorLogs, | ||
| headBranch: run.data.head_branch, | ||
| headSha: run.data.head_sha, | ||
| prNumber: prNumber ?? null, | ||
| }; | ||
| - name: Checkout code | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| ref: ${{ fromJSON(steps.failure_details.outputs.result).headBranch }} | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: "Add repo as safe directory" | ||
| run: | | ||
| git config --global --add safe.directory "$GITHUB_WORKSPACE" | ||
| - name: Create placeholder PR comment | ||
| if: ${{ fromJSON(steps.failure_details.outputs.result).prNumber != null }} | ||
| id: create_ci_debug_comment | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const failureDetails = ${{ steps.failure_details.outputs.result }}; | ||
| const body = `## 🤖 CI Failure Analysis | ||
| > Analysing failed workflow run... | ||
| 🔗 [View Failed Run](${failureDetails.runUrl}) | ||
| _Analysis in progress..._`; | ||
| const { data: comment } = await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: failureDetails.prNumber, | ||
| body, | ||
| }); | ||
| core.setOutput('comment_id', comment.id); | ||
| return comment.id; | ||
| - name: Resolve debug prompt | ||
| id: resolve_prompt | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| DEBUG_PROMPT: ${{ inputs.debug_prompt }} | ||
| FAILURE_DETAILS: ${{ steps.failure_details.outputs.result }} | ||
| COMMENT_ID: ${{ steps.create_ci_debug_comment.outputs.comment_id }} | ||
| with: | ||
| script: | | ||
| const debugPrompt = process.env.DEBUG_PROMPT; | ||
| const failureDetails = JSON.parse(process.env.FAILURE_DETAILS || '{}'); | ||
| const commentId = process.env.COMMENT_ID; | ||
| const failedJobs = (failureDetails.failedJobs || []).join(', '); | ||
| const errorLogs = JSON.stringify(failureDetails.errorLogs || [], null, 2); | ||
| const basePrompt = debugPrompt && debugPrompt.trim() | ||
| ? debugPrompt.trim() | ||
| : `Analyse the logs of the failed job and explain why it failed. | ||
| Summarise the root cause and provide specific recommendations to unblock engineers. | ||
| Do not modify repository files or run git commands. | ||
| Failed CI Run: ${failureDetails.runUrl} | ||
| Failed Jobs: ${failedJobs || 'None captured'} | ||
| PR Number: ${failureDetails.prNumber ?? 'N/A'} | ||
| Base Branch: ${failureDetails.headBranch} | ||
| Repository: ${{ github.repository }} | ||
| Error logs: | ||
| ${errorLogs}`; | ||
| const commentInstructions = commentId | ||
| ? `IMPORTANT: Update the existing PR comment (comment ID: ${commentId}) with your analysis. | ||
| Use the GitHub CLI to overwrite the comment body by running: | ||
| gh api repos/${{ github.repository }}/issues/comments/${commentId} -X PATCH -f body='YOUR_MARKDOWN_ANALYSIS' | ||
| Format the analysis in Markdown with clear sections for: | ||
| - 🔴 Failed Jobs: list the failed jobs. | ||
| - 📋 Root Cause Analysis: explain why the failure happened. | ||
| - 💡 Recommendations: actionable steps to unblock engineers. | ||
| - 🔗 Links: include a link to ${failureDetails.runUrl}. | ||
| ` | ||
| : `Append your findings to the GitHub Actions step summary by writing to the file referenced in the $GITHUB_STEP_SUMMARY environment variable (for example by running: printf '...' >> "$GITHUB_STEP_SUMMARY").`; | ||
| return `${basePrompt} | ||
| ${commentInstructions}`; | ||
| - name: Run AI Agent to debug failed CI run | ||
| uses: transferwise/claude-code-action@main | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| with: | ||
| anthropic_bedrock_base_url: ${{ secrets.ANTHROPIC_BEDROCK_BASE_URL }} | ||
| anthropic_default_sonnet_model: ${{ vars.ANTHROPIC_DEFAULT_SONNET_MODEL }} | ||
| anthropic_default_haiku_model: ${{ vars.ANTHROPIC_DEFAULT_HAIKU_MODEL }} | ||
| anthropic_default_opus_model: ${{ vars.ANTHROPIC_DEFAULT_OPUS_MODEL }} | ||
| x-wise-llm-gateway-team: ${{ inputs.team }} | ||
| x-wise-llm-gateway-lite-auth-caller-id: ${{ github.actor }} | ||
| github_token: ${{ secrets.GITHUB_TOKEN }} | ||
| prompt: ${{ steps.resolve_prompt.outputs.result }} | ||
| experimental_allowed_domains: | | ||
| github.com | ||
| api.github.com | ||
| ${{ secrets.ANTHROPIC_BEDROCK_BASE_URL }} | ||
| claude_args: | | ||
| --model ${{ vars.ANTHROPIC_DEFAULT_HAIKU_MODEL }} | ||
| --allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash,Bash(gh api*)' | ||