CI Results Comment #145
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
| # | |
| # Copyright (c) 2025, RT-Thread Development Team | |
| # | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # | |
| # Change Logs: | |
| # Date Author Notes | |
| # 2025-10-27 GitHub Copilot Post CI results to PR comments | |
| name: CI Results Comment | |
| on: | |
| workflow_run: | |
| workflows: | |
| - "RT-Thread BSP Static Build Check" | |
| - "Static code analysis" | |
| - "Check File Format and License" | |
| - "utest_auto_run" | |
| - "ToolsCI" | |
| - "pkgs_test" | |
| types: | |
| - completed | |
| permissions: | |
| pull-requests: write | |
| issues: write | |
| actions: read | |
| checks: read | |
| jobs: | |
| comment-ci-results: | |
| runs-on: ubuntu-22.04 | |
| if: github.event.workflow_run.event == 'pull_request' && github.repository_owner == 'RT-Thread' | |
| steps: | |
| - name: Get PR number | |
| id: get-pr | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Get PR number from workflow_run | |
| const prNumber = context.payload.workflow_run.pull_requests[0]?.number; | |
| if (!prNumber) { | |
| console.log('No PR found in workflow_run'); | |
| // Fallback: search for PR by branch | |
| const pulls = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}` | |
| }); | |
| if (pulls.data.length === 0) { | |
| console.log('No open PR found for this branch'); | |
| return null; | |
| } | |
| const pr = pulls.data[0]; | |
| console.log(`Found PR #${pr.number}`); | |
| return pr.number; | |
| } | |
| console.log(`Found PR #${prNumber}`); | |
| return prNumber; | |
| - name: Get workflow run details | |
| if: steps.get-pr.outputs.result != 'null' | |
| id: workflow-details | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prNumber = ${{ steps.get-pr.outputs.result }}; | |
| if (!prNumber) { | |
| return { success: false, message: 'No PR found' }; | |
| } | |
| // Get all workflow runs for this PR | |
| const workflowRuns = await github.rest.actions.listWorkflowRunsForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| event: 'pull_request', | |
| per_page: 100 | |
| }); | |
| // Filter runs for this specific PR | |
| const prRuns = workflowRuns.data.workflow_runs.filter(run => { | |
| return run.pull_requests.some(pr => pr.number === prNumber); | |
| }); | |
| // Get the latest run for each workflow | |
| const workflowMap = new Map(); | |
| for (const run of prRuns) { | |
| const existing = workflowMap.get(run.name); | |
| if (!existing || new Date(run.created_at) > new Date(existing.created_at)) { | |
| workflowMap.set(run.name, run); | |
| } | |
| } | |
| // Prepare results summary | |
| const results = []; | |
| for (const [name, run] of workflowMap) { | |
| let status = '🟡'; | |
| let statusText = 'In Progress'; | |
| if (run.status === 'completed') { | |
| if (run.conclusion === 'success') { | |
| status = '✅'; | |
| statusText = 'Success'; | |
| } else if (run.conclusion === 'failure') { | |
| status = '❌'; | |
| statusText = 'Failure'; | |
| } else if (run.conclusion === 'cancelled') { | |
| status = '⏭️'; | |
| statusText = 'Cancelled'; | |
| } else if (run.conclusion === 'skipped') { | |
| status = '⏭️'; | |
| statusText = 'Skipped'; | |
| } | |
| } else if (run.status === 'queued') { | |
| status = '🟠'; | |
| statusText = 'Queued'; | |
| } | |
| results.push({ | |
| name: name, | |
| status: status, | |
| statusText: statusText, | |
| url: run.html_url, | |
| conclusion: run.conclusion, | |
| runId: run.id | |
| }); | |
| } | |
| return { | |
| success: true, | |
| results: results, | |
| prNumber: prNumber | |
| }; | |
| - name: Get job details | |
| if: steps.get-pr.outputs.result != 'null' | |
| id: job-details | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const workflowDetails = ${{ steps.workflow-details.outputs.result }}; | |
| if (!workflowDetails || !workflowDetails.success) { | |
| return { jobs: [] }; | |
| } | |
| const allJobs = []; | |
| for (const result of workflowDetails.results) { | |
| try { | |
| const jobs = await github.rest.actions.listJobsForWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: result.runId, | |
| per_page: 100 | |
| }); | |
| for (const job of jobs.data.jobs) { | |
| let jobStatus = '⌛'; | |
| if (job.status === 'completed') { | |
| if (job.conclusion === 'success') { | |
| jobStatus = '✅'; | |
| } else if (job.conclusion === 'failure') { | |
| jobStatus = '❌'; | |
| } else if (job.conclusion === 'skipped') { | |
| jobStatus = '⏭️'; | |
| } | |
| } else if (job.status === 'in_progress') { | |
| jobStatus = '🔄'; | |
| } else if (job.status === 'queued') { | |
| jobStatus = '🟠'; | |
| } | |
| allJobs.push({ | |
| workflow: result.name, | |
| name: job.name, | |
| status: jobStatus, | |
| conclusion: job.conclusion || job.status, | |
| url: job.html_url | |
| }); | |
| } | |
| } catch (error) { | |
| console.log(`Error getting jobs for workflow ${result.name}: ${error.message}`); | |
| } | |
| } | |
| return { jobs: allJobs }; | |
| - name: Post or update comment | |
| if: steps.get-pr.outputs.result != 'null' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prNumber = ${{ steps.get-pr.outputs.result }}; | |
| const workflowDetails = ${{ steps.workflow-details.outputs.result }}; | |
| const jobDetails = ${{ steps.job-details.outputs.result }}; | |
| if (!workflowDetails || !workflowDetails.success) { | |
| console.log('No workflow details available'); | |
| return; | |
| } | |
| // Prepare comment body | |
| const now = new Date(); | |
| const timestamp = now.toISOString(); | |
| const results = workflowDetails.results; | |
| const jobs = jobDetails.jobs || []; | |
| let commentBody = '<!-- CI Results Comment -->\n'; | |
| commentBody += '## 🤖 CI Test Results\n\n'; | |
| commentBody += `**Last Updated:** ${timestamp}\n\n`; | |
| commentBody += '### Test Spec & Results:\n\n'; | |
| commentBody += '✅ Success | ❌ Failure | 🟠 Queued | 🟡 Progress | ⏭️ Skipped | ⚠️ Quarantine\n\n'; | |
| // Group jobs by workflow | |
| const jobsByWorkflow = new Map(); | |
| for (const job of jobs) { | |
| if (!jobsByWorkflow.has(job.workflow)) { | |
| jobsByWorkflow.set(job.workflow, []); | |
| } | |
| jobsByWorkflow.get(job.workflow).push(job); | |
| } | |
| // Calculate overall statistics | |
| let totalSuccess = 0; | |
| let totalFailure = 0; | |
| let totalQueued = 0; | |
| let totalProgress = 0; | |
| let totalSkipped = 0; | |
| for (const result of results) { | |
| if (result.conclusion === 'success') totalSuccess++; | |
| else if (result.conclusion === 'failure') totalFailure++; | |
| else if (result.statusText === 'Queued') totalQueued++; | |
| else if (result.statusText === 'In Progress') totalProgress++; | |
| else if (result.conclusion === 'skipped' || result.conclusion === 'cancelled') totalSkipped++; | |
| } | |
| // Summary line | |
| commentBody += '#### Overall Summary\n\n'; | |
| commentBody += `- ✅ **Success:** ${totalSuccess}\n`; | |
| commentBody += `- ❌ **Failure:** ${totalFailure}\n`; | |
| commentBody += `- 🟠 **Queued:** ${totalQueued}\n`; | |
| commentBody += `- 🟡 **In Progress:** ${totalProgress}\n`; | |
| commentBody += `- ⏭️ **Skipped:** ${totalSkipped}\n\n`; | |
| commentBody += '---\n\n'; | |
| commentBody += '### Detailed Results\n\n'; | |
| // Build detailed results | |
| for (const result of results) { | |
| commentBody += `<details>\n`; | |
| commentBody += `<summary>${result.status} <strong>${result.name}</strong> - ${result.statusText}</summary>\n\n`; | |
| commentBody += `**Workflow:** [${result.name}](${result.url})\n\n`; | |
| // Show jobs for this workflow | |
| const workflowJobs = jobsByWorkflow.get(result.name) || []; | |
| if (workflowJobs.length > 0) { | |
| commentBody += '**Jobs:**\n\n'; | |
| for (const job of workflowJobs) { | |
| commentBody += `- ${job.status} [${job.name}](${job.url})\n`; | |
| } | |
| } | |
| commentBody += '\n</details>\n\n'; | |
| } | |
| commentBody += '\n---\n'; | |
| commentBody += '*🤖 This comment is automatically generated and updated by the CI system.*\n'; | |
| // Check if comment already exists | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber | |
| }); | |
| const existingComment = comments.data.find(comment => | |
| comment.user.login === 'github-actions[bot]' && | |
| comment.body.includes('<!-- CI Results Comment -->') | |
| ); | |
| if (existingComment) { | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existingComment.id, | |
| body: commentBody | |
| }); | |
| console.log(`Updated comment ${existingComment.id} on PR #${prNumber}`); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: commentBody | |
| }); | |
| console.log(`Created new comment on PR #${prNumber}`); | |
| } |