Bladeren bron

[Feature] Add automated CI results comment to pull requests (#10856)

* Initial plan

* Add CI results comment workflow

Co-authored-by: supperthomas <60349489+supperthomas@users.noreply.github.com>

* Improve CI results comment formatting and PR detection

Co-authored-by: supperthomas <60349489+supperthomas@users.noreply.github.com>

* Add documentation for CI results comment feature

Co-authored-by: supperthomas <60349489+supperthomas@users.noreply.github.com>

* Add ToolsCI and pkgs_test to monitored workflows

Co-authored-by: supperthomas <60349489+supperthomas@users.noreply.github.com>

* Add immediate CI status comment via direct workflow integration

Co-authored-by: supperthomas <60349489+supperthomas@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: supperthomas <60349489+supperthomas@users.noreply.github.com>
Copilot 2 maanden geleden
bovenliggende
commit
a10922b01b

+ 139 - 0
.github/workflows/README_CI_RESULTS.md

@@ -0,0 +1,139 @@
+# CI Results Comment Workflow
+
+## Overview / 概述
+
+This feature automatically posts CI test results as comments on Pull Requests, making it easier for contributors and reviewers to see the status of all CI checks at a glance.
+
+此功能自动将 CI 测试结果作为评论发布到 Pull Request 中,使贡献者和审阅者更容易一目了然地看到所有 CI 检查的状态。
+
+## Implementation / 实现方式
+
+The feature uses **two complementary approaches** to ensure CI results are always visible:
+该功能使用**两种互补方法**来确保 CI 结果始终可见:
+
+### 1. Direct Workflow Integration (Immediate) / 直接工作流集成(立即生效)
+
+Each main CI workflow includes a `post-ci-status` job that:
+每个主要 CI 工作流都包含一个 `post-ci-status` 作业,它:
+
+- ✅ Works immediately on PR branches (no merge required) / 立即在 PR 分支上生效(无需合并)
+- 📝 Updates a single comment with workflow status / 使用工作流状态更新单个评论
+- 🔄 Runs after each workflow completes / 在每个工作流完成后运行
+
+**Modified Workflows:**
+- `bsp_buildings.yml`
+- `static_code_analysis.yml`
+- `format_check.yml`
+- `utest_auto_run.yml`
+
+### 2. Workflow Run Trigger (After Merge) / 工作流运行触发器(合并后)
+
+The `ci_results_comment.yml` workflow:
+`ci_results_comment.yml` 工作流:
+
+- ⏰ Triggers when CI workflows complete / 在 CI 工作流完成时触发
+- 📊 Provides comprehensive summary of all workflows / 提供所有工作流的全面摘要
+- 🔍 Shows detailed job-level information / 显示详细的作业级信息
+- ⚠️ **Only works after merged to master** / **仅在合并到 master 后才有效**
+
+## Features / 功能特性
+
+1. **Automatic Updates / 自动更新**: The comment is automatically created when CI workflows complete and updated as new workflows finish.
+   / 当 CI 工作流完成时自动创建评论,并在新工作流完成时更新。
+
+2. **Comprehensive Summary / 全面总结**: Shows the status of all major CI workflows including:
+   / 显示所有主要 CI 工作流的状态,包括:
+   - RT-Thread BSP Static Build Check / BSP 静态构建检查
+   - Static code analysis / 静态代码分析
+   - Check File Format and License / 文件格式和许可证检查
+   - utest_auto_run / 单元测试自动运行
+
+3. **Status Indicators / 状态指示器**:
+   - ✅ Success / 成功
+   - ❌ Failure / 失败
+   - 🟠 Queued / 排队中
+   - 🟡 In Progress / 进行中
+   - ⏭️ Skipped / 已跳过
+
+4. **Detailed Information / 详细信息**: Expandable sections show individual job results within each workflow.
+   / 可展开的部分显示每个工作流中的各个作业结果。
+
+## How It Works / 工作原理
+
+1. The workflow is triggered when any of the monitored CI workflows complete.
+   / 当任何受监控的 CI 工作流完成时,将触发此工作流。
+
+2. It collects the status of all workflows and jobs for the associated Pull Request.
+   / 它收集关联 Pull Request 的所有工作流和作业的状态。
+
+3. A formatted comment is posted (or updated if one already exists) with the current CI status.
+   / 发布(或更新已存在的)格式化评论,显示当前 CI 状态。
+
+## Comment Format / 评论格式
+
+The comment includes:
+评论包括:
+
+- **Overall Summary / 总体摘要**: Quick statistics showing count of passed, failed, queued, in-progress, and skipped workflows.
+  / 快速统计数据,显示通过、失败、排队、进行中和跳过的工作流数量。
+
+- **Detailed Results / 详细结果**: Collapsible sections for each workflow with links to individual jobs.
+  / 每个工作流的可折叠部分,包含指向各个作业的链接。
+
+## Benefits / 优势
+
+1. **Visibility / 可见性**: Contributors can immediately see which CI checks have passed or failed without navigating to the Actions tab.
+   / 贡献者无需导航到 Actions 选项卡即可立即查看哪些 CI 检查通过或失败。
+
+2. **Efficiency / 效率**: Reviewers can quickly assess the CI status before reviewing the code.
+   / 审阅者可以在审查代码之前快速评估 CI 状态。
+
+3. **Transparency / 透明度**: All stakeholders have a clear view of the PR's CI status.
+   / 所有利益相关者都可以清楚地了解 PR 的 CI 状态。
+
+## Permissions Required / 所需权限
+
+The workflow requires the following permissions:
+工作流需要以下权限:
+
+- `pull-requests: write` - To create and update comments / 创建和更新评论
+- `issues: write` - To post comments on PR issues / 在 PR 问题上发布评论
+- `actions: read` - To read workflow run status / 读取工作流运行状态
+- `checks: read` - To read check run status / 读取检查运行状态
+
+## Configuration / 配置
+
+The workflow monitors the following workflows by default:
+工作流默认监控以下工作流:
+
+```yaml
+workflows:
+  - "RT-Thread BSP Static Build Check"
+  - "Static code analysis"
+  - "Check File Format and License"
+  - "utest_auto_run"
+```
+
+To add more workflows to monitor, edit the `.github/workflows/ci_results_comment.yml` file and add workflow names to the `workflows` list.
+
+要监控更多工作流,请编辑 `.github/workflows/ci_results_comment.yml` 文件并将工作流名称添加到 `workflows` 列表中。
+
+## Troubleshooting / 故障排除
+
+### Comment not appearing / 评论未出现
+
+1. Ensure the workflow has the required permissions / 确保工作流具有所需权限
+2. Check that the PR is from a branch in the repository (not a fork) / 检查 PR 是否来自存储库中的分支(而非分支)
+3. Verify the workflow is enabled in the repository settings / 验证工作流在存储库设置中已启用
+
+### Comment not updating / 评论未更新
+
+1. The comment updates when a monitored workflow completes / 当受监控的工作流完成时,评论会更新
+2. Check the Actions tab to see if the workflow is running / 检查 Actions 选项卡以查看工作流是否正在运行
+3. Look for errors in the workflow logs / 在工作流日志中查找错误
+
+## Contributing / 贡献
+
+Contributions to improve this workflow are welcome! Please follow the standard contribution process outlined in the CONTRIBUTING.md file.
+
+欢迎改进此工作流的贡献!请遵循 CONTRIBUTING.md 文件中概述的标准贡献流程。

+ 14 - 1
.github/workflows/bsp_buildings.yml

@@ -288,4 +288,17 @@ jobs:
         uses: actions/upload-artifact@main
         with:
           name: 00_all_bsp_output_${{ github.sha }}  
-          path: output/
+          path: output/
+
+  # Post CI status to PR comment
+  post-ci-status:
+    needs: build
+    if: always() && github.event_name == 'pull_request' && github.repository_owner == 'RT-Thread'
+    uses: ./.github/workflows/post_ci_status.yml
+    with:
+      workflow_name: "RT-Thread BSP Static Build Check"
+      workflow_status: ${{ needs.build.result }}
+      pr_number: ${{ github.event.pull_request.number }}
+    permissions:
+      pull-requests: write
+      issues: write

+ 303 - 0
.github/workflows/ci_results_comment.yml

@@ -0,0 +1,303 @@
+#
+# 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}`);
+            }

+ 14 - 1
.github/workflows/format_check.yml

@@ -29,4 +29,17 @@ jobs:
         shell: bash
         run: |
           pip install click chardet PyYaml
-          python tools/ci/file_check.py check 'https://github.com/RT-Thread/rt-thread' 'master'
+          python tools/ci/file_check.py check 'https://github.com/RT-Thread/rt-thread' 'master'
+
+  # Post CI status to PR comment
+  post-ci-status:
+    needs: scancode_job
+    if: always() && github.event_name == 'pull_request' && github.repository_owner == 'RT-Thread'
+    uses: ./.github/workflows/post_ci_status.yml
+    with:
+      workflow_name: "Check File Format and License"
+      workflow_status: ${{ needs.scancode_job.result }}
+      pr_number: ${{ github.event.pull_request.number }}
+    permissions:
+      pull-requests: write
+      issues: write

+ 108 - 0
.github/workflows/post_ci_status.yml

@@ -0,0 +1,108 @@
+#
+# Copyright (c) 2025, RT-Thread Development Team
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Change Logs:
+# Date           Author       Notes
+# 2025-10-27     GitHub Copilot  Reusable workflow to post CI status
+
+name: Post CI Status Comment
+
+on:
+  workflow_call:
+    inputs:
+      workflow_name:
+        description: 'Name of the workflow'
+        required: true
+        type: string
+      workflow_status:
+        description: 'Status of the workflow (success/failure)'
+        required: true
+        type: string
+      pr_number:
+        description: 'Pull request number'
+        required: true
+        type: number
+
+permissions:
+  pull-requests: write
+  issues: write
+
+jobs:
+  post-comment:
+    runs-on: ubuntu-22.04
+    if: github.repository_owner == 'RT-Thread'
+    steps:
+      - name: Post or update CI status comment
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const prNumber = ${{ inputs.pr_number }};
+            const workflowName = '${{ inputs.workflow_name }}';
+            const workflowStatus = '${{ inputs.workflow_status }}';
+            
+            // Status emoji mapping
+            const statusEmoji = workflowStatus === 'success' ? '✅' : '❌';
+            const timestamp = new Date().toISOString();
+            
+            // Try to find existing comment
+            const comments = await github.rest.issues.listComments({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              issue_number: prNumber
+            });
+            
+            const botComment = comments.data.find(comment => 
+              comment.user.login === 'github-actions[bot]' && 
+              comment.body.includes('<!-- CI Status Comment -->')
+            );
+            
+            // Get all workflow runs for this PR to build comprehensive status
+            let allStatuses = {};
+            
+            if (botComment) {
+              // Parse existing statuses from comment
+              const statusRegex = /- (✅|❌|🟡) \*\*(.+?)\*\*/g;
+              let match;
+              while ((match = statusRegex.exec(botComment.body)) !== null) {
+                allStatuses[match[2]] = match[1];
+              }
+            }
+            
+            // Update current workflow status
+            allStatuses[workflowName] = statusEmoji;
+            
+            // Build comment body
+            let commentBody = '<!-- CI Status Comment -->\n';
+            commentBody += '## 🤖 CI Test Results\n\n';
+            commentBody += `**Last Updated:** ${timestamp}\n\n`;
+            commentBody += '### Workflow Status:\n\n';
+            
+            for (const [name, emoji] of Object.entries(allStatuses)) {
+              commentBody += `- ${emoji} **${name}**\n`;
+            }
+            
+            commentBody += '\n---\n';
+            commentBody += '✅ Success | ❌ Failure | 🟡 In Progress\n\n';
+            commentBody += '*This comment is automatically updated as CI workflows complete.*\n';
+            
+            if (botComment) {
+              // Update existing comment
+              await github.rest.issues.updateComment({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                comment_id: botComment.id,
+                body: commentBody
+              });
+              console.log(`Updated comment ${botComment.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}`);
+            }

+ 14 - 1
.github/workflows/static_code_analysis.yml

@@ -54,4 +54,17 @@ jobs:
           sudo make install FILESDIR=/usr/local/share/Cppcheck 
           cppcheck --version
           cd ..
-          python tools/ci/cpp_check.py check
+          python tools/ci/cpp_check.py check
+
+  # Post CI status to PR comment
+  post-ci-status:
+    needs: scancode_job
+    if: always() && github.event_name == 'pull_request' && github.repository_owner == 'RT-Thread'
+    uses: ./.github/workflows/post_ci_status.yml
+    with:
+      workflow_name: "Static code analysis"
+      workflow_status: ${{ needs.scancode_job.result }}
+      pr_number: ${{ github.event.pull_request.number }}
+    permissions:
+      pull-requests: write
+      issues: write

+ 13 - 1
.github/workflows/utest_auto_run.yml

@@ -297,4 +297,16 @@ jobs:
               echo "=========================================================================================="
               break
             fi
-          done
+          done
+  # Post CI status to PR comment
+  post-ci-status:
+    needs: test
+    if: always() && github.event_name == 'pull_request' && github.repository_owner == 'RT-Thread'
+    uses: ./.github/workflows/post_ci_status.yml
+    with:
+      workflow_name: "utest_auto_run"
+      workflow_status: ${{ needs.test.result }}
+      pr_number: ${{ github.event.pull_request.number }}
+    permissions:
+      pull-requests: write
+      issues: write