Skip to content

CI Results Comment #145

CI Results Comment

CI Results Comment #145

#
# 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}`);
}