From 07d42bca4804546344f35867262577267b1624aa Mon Sep 17 00:00:00 2001 From: kurisaw <2053731441@qq.com> Date: Mon, 7 Apr 2025 10:02:10 +0800 Subject: [PATCH] feat: CI script assigns PR reviews based on the list of maintainers --- .github/workflows/auto-assign-reviewers.yml | 292 ++++++++++++++++++++ MAINTAINERS | 57 ++++ 2 files changed, 349 insertions(+) create mode 100644 .github/workflows/auto-assign-reviewers.yml create mode 100644 MAINTAINERS diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml new file mode 100644 index 00000000000..19fe224b491 --- /dev/null +++ b/.github/workflows/auto-assign-reviewers.yml @@ -0,0 +1,292 @@ +# +# Copyright (c) 2006-2025, RT-Thread Development Team +# +# SPDX-License-Identifier: Apache-2.0 +# +# Change Logs: +# Date Author Notes +# 2025-01-21 kurisaW Initial version +# 2025-03-14 hydevcode + +# Script Function Description: Assign PR reviews based on the MAINTAINERS list. + +name: Auto Review Assistant + +on: + pull_request_target: + branches: [ master ] + types: [opened, synchronize, reopened] + +jobs: + assign-reviewers: + runs-on: ubuntu-22.04 + if: github.repository_owner == 'RT-Thread' + permissions: + issues: read + pull-requests: write + contents: read + steps: + - name: Extract PR number + id: extract-pr + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_OUTPUT + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: master + sparse-checkout: MAINTAINERS + persist-credentials: false + - name: Get changed files + id: changed_files + run: | + # 通过 GitHub API 获取 PR 的变更文件列表 + changed_files=$(curl -s \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.extract-pr.outputs.PR_NUMBER }}/files" | \ + jq -r '.[].filename') # 使用 jq 提取文件名 + echo "$changed_files" | grep -v '^MAINTAINERS$' > changed_files.txt + + existing_comment=$(curl -s \ + "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \ + jq -r '.[] | select(.user.login == "github-actions[bot]") | {body: .body} | @base64') + + comment_body="" + if [[ ! -z "$existing_comment" ]]; then + comment_body=$(echo "$existing_comment" | head -1 | base64 -d | jq -r .body|sed -nE 's/.*Last Updated: ([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2} UTC).*/\1/p') + + comment_time=$(date -d "$comment_body" +%s) + + echo "${comment_body}" + echo "COMMENT_TIME=${comment_time}" >> $GITHUB_OUTPUT + else + comment_time="" + echo "COMMENT_TIME=${comment_time}" >> $GITHUB_OUTPUT + fi + echo "COMMENT_TIME=${comment_time}" + - name: Parse MAINTAINERS file + id: parse_maintainer + run: | + # 使用 AWK 解析 MAINTAINERS 文件格式: + # 提取 tag(标签)、path(路径)和 owners(维护者 GitHub ID) + awk ' + /^tag:/ { + tag = substr($0, index($0, $2)) # 提取标签内容 + } + /^path:/ { + path = substr($0, index($0, $2)) # 提取路径内容 + } + /^owners:/ { + owners = substr($0, index($0, $2)) # 提取维护者信息 + split(owners, parts, /[()]/) # 拆分出 GitHub ID(括号内内容) + github_ids = "" + for (i=2; i<=length(parts); i+=2) { + github_ids = github_ids "@" parts[i] " " # 拼接为 @user 格式 + } + print tag "|" path "|" github_ids + } + ' MAINTAINERS > tag_data.csv + - name: Generate reviewers list + id: generate_reviewers + run: | + # 根据变更文件路径匹配维护者规则 + rm -f triggered_reviewers.txt + rm -f triggered_tags.txt + while IFS='|' read -r tag path reviewers; do + # 使用正则匹配路径(支持子目录) + if grep -qE "^$path(/|$)" changed_files.txt; then + echo "$reviewers" | tr ' ' '\n' >> triggered_reviewers.txt + echo "$tag" | tr ' ' '\n' >> triggered_tags.txt + fi + done < tag_data.csv + awk 'NF && !seen[$0]++' triggered_reviewers.txt > unique_reviewers.txt + awk 'NF && !seen[$0]++' triggered_tags.txt > unique_tags.txt + - name: Restore Reviewers Cache + id: reviewers-cache-restore + if: ${{ steps.changed_files.outputs.COMMENT_TIME != '' }} + uses: actions/cache/restore@v4 + with: + path: | + unique_tags_bak.txt + unique_reviewers_bak.txt + key: ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.changed_files.outputs.COMMENT_TIME }} + - name: Get approval status + id: get_approval + run: | + current_time=$(date -u +"%Y-%m-%d %H:%M UTC") + reviewers=$(cat unique_reviewers.txt | tr '\n' '|') + + # 获取 PR 的所有评论 + comments=$(curl -s \ + "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments") + + echo '#!/bin/bash' > approval_data.sh + echo 'declare -A approvals=()' >> approval_data.sh + + # 使用 jq 解析包含 LGTM 的有效评论 + jq -r --arg reviewers "$reviewers" ' + .[] | + select(.user.login != "github-actions[bot]") | # 排除 bot 的评论 + select(.body | test("^\\s*LGTM\\s*$"; "i")) | # 匹配 LGTM 评论(不区分大小写) + .user.login as $user | + "@\($user)" as $mention | + select($mention | inside($reviewers)) | # 过滤有效审查者 + "approvals[\"\($mention)\"]=\"\(.created_at)\"" # 记录审批时间 + ' <<< "$comments" >> approval_data.sh + + # 加载审查数据并生成状态报告 + chmod +x approval_data.sh + source ./approval_data.sh + + jq -r --arg reviewers "$reviewers" ' + .[] | + select(.user.login != "github-actions[bot]") | # 排除 bot 的评论 + select(.body | test("^\\s*LGTM\\s*$"; "i")) | # 匹配 LGTM 评论(不区分大小写) + .user.login as $user | + "@\($user)" as $mention | + select($mention | inside($reviewers)) | # 过滤有效审查者 + "\($mention) \(.created_at)" # 输出审查者和时间 + ' <<< "$comments" >> approval_data.txt + + notified_users="" + if [[ -f unique_reviewers_bak.txt ]]; then + notified_users=$(cat unique_reviewers_bak.txt | xargs) + else + notified_users="" + fi + + { + echo "---" + echo "### 📊 Current Review Status (Last Updated: $current_time)" + while read -r reviewer; do + formatted_reviewers="" + for r in $reviewers; do + if [[ " ${notified_users[@]} " =~ " $reviewer " ]]; then + formatted_reviewers+="${reviewer#@}" + else + formatted_reviewers+="$reviewer" + fi + done + + if [[ -n "${approvals[$reviewer]}" ]]; then + timestamp=$(date -d "${approvals[$reviewer]}" -u +"%Y-%m-%d %H:%M UTC") + + echo "- ✅ **$formatted_reviewers** Reviewed On $timestamp" + else + echo "- ⌛ **$formatted_reviewers** Pending Review" + fi + done < unique_reviewers.txt + } > review_status.md + + echo "CURRENT_TIME=${current_time}" >> $GITHUB_OUTPUT + - name: Generate review data + id: generate_review + run: | + unique_tags="" + unique_tags=$(cat unique_tags.txt | xargs) + unique_tags_bak="" + if [[ -f unique_tags_bak.txt ]]; then + unique_tags_bak=$(cat unique_tags_bak.txt | xargs) + fi + + existing_tags="" + for r in $unique_tags; do + if [[ " ${unique_tags_bak[@]} " =~ " $r " ]]; then + echo "$r 不存在于数组中" + else + existing_tags+="$r " + fi + done + + current_time=$(date -u +"%Y-%m-%d %H:%M UTC") + { + + # 生成审查分配信息 + echo "## 📌 Code Review Assignment" + echo "" + + while IFS='|' read -r tag path reviewers; do + if grep -qE "^$path(/|$)" changed_files.txt; then + echo "### 🏷️ Tag: $tag" + echo "**Path:** \`$path\` " + + if [[ " ${existing_tags[@]} " =~ " $tag " ]]; then + echo "**Reviewers:** $reviewers " + else + formatted_reviewers="" + for r in $reviewers; do + formatted_reviewers+="${r#@} " + done + echo "**Reviewers:** $formatted_reviewers " + fi + + echo "
" + echo "Changed Files (Click to expand)" + echo "" + grep -E "^$path(/|$)" changed_files.txt | sed 's/^/- /' # 列出匹配的变更文件 + echo "" + echo "
" + echo "" + fi + done < tag_data.csv + # 插入审查状态 + cat review_status.md + + echo "---" + echo "### 📝 Review Instructions" + echo "" + echo "1. **维护者可以通过单击此处来刷新审查状态:** [🔄 刷新状态](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" + echo " **Maintainers can refresh the review status by clicking here:** [🔄 Refresh Status](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" + echo "" + echo "2. **确认审核通过后评论 \`LGTM/lgtm\`**" + echo " **Comment \`LGTM/lgtm\` after confirming approval**" + echo "" + echo "3. **PR合并前需至少一位维护者确认**" + echo " **PR must be confirmed by at least one maintainer before merging**" + echo "" + echo "> ℹ️ **刷新CI状态操作需要具备仓库写入权限。**" + echo "> ℹ️ **Refresh CI status operation requires repository Write permission.**" + } > review_data.md + - name: Post/Update comment + id: post_comment + run: | + # 查找现有的 bot 评论 + existing_comment=$(curl -s \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \ + jq -r '.[] | select(.user.login == "github-actions[bot]") | {id: .id, body: .body} | @base64') + + if [[ -n "$existing_comment" ]]; then + # 更新现有评论 + comment_id=$(echo "$existing_comment" | head -1 | base64 -d | jq -r .id) + echo "Updating existing comment $comment_id" + response=$(curl -s -X PATCH \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -d "$(jq -n --arg body "$(cat review_data.md)" '{body: $body}')" \ + "https://api.github.com/repos/${{ github.repository }}/issues/comments/$comment_id") + else + # 创建新评论 + echo "Creating new comment" + response=$(curl -s -X POST \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -d "$(jq -n --arg body "$(cat review_data.md)" '{body: $body}')" \ + "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments") + fi + - name: Get Comment Time + id: get_comment_time + run: | + existing_comment=$(curl -s \ + "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \ + jq -r '.[] | select(.user.login == "github-actions[bot]") | {body: .body} | @base64') + comment_body="${{ steps.get_approval.outputs.CURRENT_TIME }}" + comment_time=$(date -d "$comment_body" +%s) + echo "CURRENT_TIME=${comment_time}" >> $GITHUB_OUTPUT + cp unique_reviewers.txt unique_reviewers_bak.txt + cp unique_tags.txt unique_tags_bak.txt + - name: Restore Reviewers Save + id: reviewers-cache-save + uses: actions/cache/save@v4 + with: + path: | + unique_tags_bak.txt + unique_reviewers_bak.txt + key: ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.get_comment_time.outputs.CURRENT_TIME }} \ No newline at end of file diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 00000000000..80509dcf18a --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,57 @@ +# List of maintainers of the RT-Thread open-source community +# +# This file manages the maintainers and their associated sections in the repository. +# Developers should update this file according to project needs. +# The following are explanations of each field and guidelines for adding new maintainer entries. +# +# When adding new entries, please follow the format: +# +# 1. **tag** - Assign a unique tag to each entry for identifying the code module or functionality. +# - The tag should be concise and descriptive, such as `workflow`, `libc`... +# - **Rule for Adding**: Use a new tag when adding a new functionality or module to ensure it clearly describes the area of responsibility. +# +# 2. **path** - Specify the directory or file path that the maintainer is responsible for. +# - The path must be relative to the repository's root directory and can refer to either a single file or a folder. +# - If the maintainer is responsible for all files in a directory, use the directory path; if it's for a specific file, provide the full file path. +# - **Rule for Adding**: Ensure that the path correctly points to the relevant code location. Please note that a tag should correspond to only one path. Currently, multiple paths are not supported. +# +# 3. **owners** - List the maintainers responsible for the section, including their GitHub usernames and contact information. +# - The owners should be listed as a comma-separated list if there are multiple maintainers. +# - Format: `Name(GitHub username)`. +# - **Rule for Adding**: Ensure that the listed GitHub usernames are correct, and the maintainers are aware of their responsibilities and duties. +# +# Example: How to Add a Maintainer Entry +# +# The following is a template for adding new entries in the MAINTAINER file: +# +# tag: +# path: +# owners: , , ... +# +# When adding entries, please follow these guidelines: +# - Ensure the `tag` is unique and descriptive. +# - Ensure the `path` points to the correct location in the repository. +# - Ensure the `owners` are accurate and that all new maintainers are aware of their responsibilities. +# +# Example Entry: +# tag: example-module +# path: example/module/path +# owners: John Doe(johndoe), Jane Smith(janesmith) + +# Note: +# - Each entry includes a `tag` that identifies the module or functionality, a `path` that points to the relevant code location, and `owners` who are the maintainers for that part of the codebase. +# - If there are multiple entries, each entry should be separated by a blank line. Within a single entry, there is no need to insert blank lines between the tag, path, and owners. + +# Below are existing maintainer entries, divided by module: + +tag: workflow +path: .github +owners: supper thomas(supperthomas)<78900636@qq.com>, Bingru Zhang(Rbb666)<751061401@qq.com>, Yuqiang Wang(kurisaW)<2053731441@qq.com> + +tag: stm32f407-rt-spark +path: bsp/stm32/stm32f407-rt-spark +owners: Bingru Zhang(Rbb666)<751061401@qq.com>, Yuqiang Wang(kurisaW)<2053731441@qq.com> + +tag: libc +path: components/libc +owners: Meco Jianting Man(mysterywolf)<920369182@qq.com>