Skip to content

Commit e832d07

Browse files
committed
feat: CI script assigns PR reviews based on the list of maintainers
1 parent 3436aa6 commit e832d07

File tree

2 files changed

+349
-0
lines changed

2 files changed

+349
-0
lines changed
+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
#
2+
# Copyright (c) 2006-2025, RT-Thread Development Team
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
# Change Logs:
7+
# Date Author Notes
8+
# 2025-01-21 kurisaW Initial version
9+
# 2025-03-14 hydevcode
10+
11+
# Script Function Description: Assign PR reviews based on the MAINTAINERS list.
12+
13+
name: Auto Review Assistant
14+
15+
on:
16+
pull_request_target:
17+
branches: [ master ]
18+
types: [opened, synchronize, reopened]
19+
20+
jobs:
21+
assign-reviewers:
22+
runs-on: ubuntu-22.04
23+
if: github.repository_owner == 'RT-Thread'
24+
permissions:
25+
issues: read
26+
pull-requests: write
27+
contents: read
28+
steps:
29+
- name: Extract PR number
30+
id: extract-pr
31+
run: |
32+
PR_NUMBER=${{ github.event.pull_request.number }}
33+
echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_OUTPUT
34+
- name: Checkout code
35+
uses: actions/checkout@v4
36+
with:
37+
ref: master
38+
sparse-checkout: MAINTAINERS
39+
persist-credentials: false
40+
- name: Get changed files
41+
id: changed_files
42+
run: |
43+
# 通过 GitHub API 获取 PR 的变更文件列表
44+
changed_files=$(curl -s \
45+
"https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.extract-pr.outputs.PR_NUMBER }}/files" | \
46+
jq -r '.[].filename') # 使用 jq 提取文件名
47+
echo "$changed_files" | grep -v '^MAINTAINERS$' > changed_files.txt
48+
49+
existing_comment=$(curl -s \
50+
"https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \
51+
jq -r '.[] | select(.user.login == "github-actions[bot]") | {body: .body} | @base64')
52+
53+
comment_body=""
54+
if [[ ! -z "$existing_comment" ]]; then
55+
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')
56+
57+
comment_time=$(date -d "$comment_body" +%s)
58+
59+
echo "${comment_body}"
60+
echo "COMMENT_TIME=${comment_time}" >> $GITHUB_OUTPUT
61+
else
62+
comment_time=""
63+
echo "COMMENT_TIME=${comment_time}" >> $GITHUB_OUTPUT
64+
fi
65+
echo "COMMENT_TIME=${comment_time}"
66+
- name: Parse MAINTAINERS file
67+
id: parse_maintainer
68+
run: |
69+
# 使用 AWK 解析 MAINTAINERS 文件格式:
70+
# 提取 tag(标签)、path(路径)和 owners(维护者 GitHub ID)
71+
awk '
72+
/^tag:/ {
73+
tag = substr($0, index($0, $2)) # 提取标签内容
74+
}
75+
/^path:/ {
76+
path = substr($0, index($0, $2)) # 提取路径内容
77+
}
78+
/^owners:/ {
79+
owners = substr($0, index($0, $2)) # 提取维护者信息
80+
split(owners, parts, /[()]/) # 拆分出 GitHub ID(括号内内容)
81+
github_ids = ""
82+
for (i=2; i<=length(parts); i+=2) {
83+
github_ids = github_ids "@" parts[i] " " # 拼接为 @user 格式
84+
}
85+
print tag "|" path "|" github_ids
86+
}
87+
' MAINTAINERS > tag_data.csv
88+
- name: Generate reviewers list
89+
id: generate_reviewers
90+
run: |
91+
# 根据变更文件路径匹配维护者规则
92+
rm -f triggered_reviewers.txt
93+
rm -f triggered_tags.txt
94+
while IFS='|' read -r tag path reviewers; do
95+
# 使用正则匹配路径(支持子目录)
96+
if grep -qE "^$path(/|$)" changed_files.txt; then
97+
echo "$reviewers" | tr ' ' '\n' >> triggered_reviewers.txt
98+
echo "$tag" | tr ' ' '\n' >> triggered_tags.txt
99+
fi
100+
done < tag_data.csv
101+
awk 'NF && !seen[$0]++' triggered_reviewers.txt > unique_reviewers.txt
102+
awk 'NF && !seen[$0]++' triggered_tags.txt > unique_tags.txt
103+
- name: Restore Reviewers Cache
104+
id: reviewers-cache-restore
105+
if: ${{ steps.changed_files.outputs.COMMENT_TIME != '' }}
106+
uses: actions/cache/restore@v4
107+
with:
108+
path: |
109+
unique_tags_bak.txt
110+
unique_reviewers_bak.txt
111+
key: ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.changed_files.outputs.COMMENT_TIME }}
112+
- name: Get approval status
113+
id: get_approval
114+
run: |
115+
current_time=$(date -u +"%Y-%m-%d %H:%M UTC")
116+
reviewers=$(cat unique_reviewers.txt | tr '\n' '|')
117+
118+
# 获取 PR 的所有评论
119+
comments=$(curl -s \
120+
"https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments")
121+
122+
echo '#!/bin/bash' > approval_data.sh
123+
echo 'declare -A approvals=()' >> approval_data.sh
124+
125+
# 使用 jq 解析包含 LGTM 的有效评论
126+
jq -r --arg reviewers "$reviewers" '
127+
.[] |
128+
select(.user.login != "github-actions[bot]") | # 排除 bot 的评论
129+
select(.body | test("^\\s*LGTM\\s*$"; "i")) | # 匹配 LGTM 评论(不区分大小写)
130+
.user.login as $user |
131+
"@\($user)" as $mention |
132+
select($mention | inside($reviewers)) | # 过滤有效审查者
133+
"approvals[\"\($mention)\"]=\"\(.created_at)\"" # 记录审批时间
134+
' <<< "$comments" >> approval_data.sh
135+
136+
# 加载审查数据并生成状态报告
137+
chmod +x approval_data.sh
138+
source ./approval_data.sh
139+
140+
jq -r --arg reviewers "$reviewers" '
141+
.[] |
142+
select(.user.login != "github-actions[bot]") | # 排除 bot 的评论
143+
select(.body | test("^\\s*LGTM\\s*$"; "i")) | # 匹配 LGTM 评论(不区分大小写)
144+
.user.login as $user |
145+
"@\($user)" as $mention |
146+
select($mention | inside($reviewers)) | # 过滤有效审查者
147+
"\($mention) \(.created_at)" # 输出审查者和时间
148+
' <<< "$comments" >> approval_data.txt
149+
150+
notified_users=""
151+
if [[ -f unique_reviewers_bak.txt ]]; then
152+
notified_users=$(cat unique_reviewers_bak.txt | xargs)
153+
else
154+
notified_users=""
155+
fi
156+
157+
{
158+
echo "---"
159+
echo "### 📊 Current Review Status (Last Updated: $current_time)"
160+
while read -r reviewer; do
161+
formatted_reviewers=""
162+
for r in $reviewers; do
163+
if [[ " ${notified_users[@]} " =~ " $reviewer " ]]; then
164+
formatted_reviewers+="${reviewer#@}"
165+
else
166+
formatted_reviewers+="$reviewer"
167+
fi
168+
done
169+
170+
if [[ -n "${approvals[$reviewer]}" ]]; then
171+
timestamp=$(date -d "${approvals[$reviewer]}" -u +"%Y-%m-%d %H:%M UTC")
172+
173+
echo "- ✅ **$formatted_reviewers** Reviewed On $timestamp"
174+
else
175+
echo "- ⌛ **$formatted_reviewers** Pending Review"
176+
fi
177+
done < unique_reviewers.txt
178+
} > review_status.md
179+
180+
echo "CURRENT_TIME=${current_time}" >> $GITHUB_OUTPUT
181+
- name: Generate review data
182+
id: generate_review
183+
run: |
184+
unique_tags=""
185+
unique_tags=$(cat unique_tags.txt | xargs)
186+
unique_tags_bak=""
187+
if [[ -f unique_tags_bak.txt ]]; then
188+
unique_tags_bak=$(cat unique_tags_bak.txt | xargs)
189+
fi
190+
191+
existing_tags=""
192+
for r in $unique_tags; do
193+
if [[ " ${unique_tags_bak[@]} " =~ " $r " ]]; then
194+
echo "$r 不存在于数组中"
195+
else
196+
existing_tags+="$r "
197+
fi
198+
done
199+
200+
current_time=$(date -u +"%Y-%m-%d %H:%M UTC")
201+
{
202+
203+
# 生成审查分配信息
204+
echo "## 📌 Code Review Assignment"
205+
echo ""
206+
207+
while IFS='|' read -r tag path reviewers; do
208+
if grep -qE "^$path(/|$)" changed_files.txt; then
209+
echo "### 🏷️ Tag: $tag"
210+
echo "**Path:** \`$path\` "
211+
212+
if [[ " ${existing_tags[@]} " =~ " $tag " ]]; then
213+
echo "**Reviewers:** $reviewers "
214+
else
215+
formatted_reviewers=""
216+
for r in $reviewers; do
217+
formatted_reviewers+="${r#@} "
218+
done
219+
echo "**Reviewers:** $formatted_reviewers "
220+
fi
221+
222+
echo "<details>"
223+
echo "<summary><b>Changed Files</b> (Click to expand)</summary>"
224+
echo ""
225+
grep -E "^$path(/|$)" changed_files.txt | sed 's/^/- /' # 列出匹配的变更文件
226+
echo ""
227+
echo "</details>"
228+
echo ""
229+
fi
230+
done < tag_data.csv
231+
# 插入审查状态
232+
cat review_status.md
233+
234+
echo "---"
235+
echo "### 📝 Review Instructions"
236+
echo ""
237+
echo "1. **维护者可以通过单击此处来刷新审查状态:** [🔄 刷新状态](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
238+
echo " **Maintainers can refresh the review status by clicking here:** [🔄 Refresh Status](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
239+
echo ""
240+
echo "2. **确认审核通过后评论 \`LGTM/lgtm\`**"
241+
echo " **Comment \`LGTM/lgtm\` after confirming approval**"
242+
echo ""
243+
echo "3. **PR合并前需至少一位维护者确认**"
244+
echo " **PR must be confirmed by at least one maintainer before merging**"
245+
echo ""
246+
echo "> ℹ️ **刷新CI状态操作需要具备仓库写入权限。**"
247+
echo "> ℹ️ **Refresh CI status operation requires repository Write permission.**"
248+
} > review_data.md
249+
- name: Post/Update comment
250+
id: post_comment
251+
run: |
252+
# 查找现有的 bot 评论
253+
existing_comment=$(curl -s \
254+
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
255+
"https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \
256+
jq -r '.[] | select(.user.login == "github-actions[bot]") | {id: .id, body: .body} | @base64')
257+
258+
if [[ -n "$existing_comment" ]]; then
259+
# 更新现有评论
260+
comment_id=$(echo "$existing_comment" | head -1 | base64 -d | jq -r .id)
261+
echo "Updating existing comment $comment_id"
262+
response=$(curl -s -X PATCH \
263+
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
264+
-d "$(jq -n --arg body "$(cat review_data.md)" '{body: $body}')" \
265+
"https://api.github.com/repos/${{ github.repository }}/issues/comments/$comment_id")
266+
else
267+
# 创建新评论
268+
echo "Creating new comment"
269+
response=$(curl -s -X POST \
270+
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
271+
-d "$(jq -n --arg body "$(cat review_data.md)" '{body: $body}')" \
272+
"https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments")
273+
fi
274+
- name: Get Comment Time
275+
id: get_comment_time
276+
run: |
277+
existing_comment=$(curl -s \
278+
"https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \
279+
jq -r '.[] | select(.user.login == "github-actions[bot]") | {body: .body} | @base64')
280+
comment_body="${{ steps.get_approval.outputs.CURRENT_TIME }}"
281+
comment_time=$(date -d "$comment_body" +%s)
282+
echo "CURRENT_TIME=${comment_time}" >> $GITHUB_OUTPUT
283+
cp unique_reviewers.txt unique_reviewers_bak.txt
284+
cp unique_tags.txt unique_tags_bak.txt
285+
- name: Restore Reviewers Save
286+
id: reviewers-cache-save
287+
uses: actions/cache/save@v4
288+
with:
289+
path: |
290+
unique_tags_bak.txt
291+
unique_reviewers_bak.txt
292+
key: ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.get_comment_time.outputs.CURRENT_TIME }}

MAINTAINERS

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# List of maintainers of the RT-Thread open-source community
2+
#
3+
# This file manages the maintainers and their associated sections in the repository.
4+
# Developers should update this file according to project needs.
5+
# The following are explanations of each field and guidelines for adding new maintainer entries.
6+
#
7+
# When adding new entries, please follow the format:
8+
#
9+
# 1. **tag** - Assign a unique tag to each entry for identifying the code module or functionality.
10+
# - The tag should be concise and descriptive, such as `workflow`, `libc`...
11+
# - **Rule for Adding**: Use a new tag when adding a new functionality or module to ensure it clearly describes the area of responsibility.
12+
#
13+
# 2. **path** - Specify the directory or file path that the maintainer is responsible for.
14+
# - The path must be relative to the repository's root directory and can refer to either a single file or a folder.
15+
# - 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.
16+
# - **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.
17+
#
18+
# 3. **owners** - List the maintainers responsible for the section, including their GitHub usernames and contact information.
19+
# - The owners should be listed as a comma-separated list if there are multiple maintainers.
20+
# - Format: `Name(GitHub username)<email address>`.
21+
# - **Rule for Adding**: Ensure that the listed GitHub usernames are correct, and the maintainers are aware of their responsibilities and duties.
22+
#
23+
# Example: How to Add a Maintainer Entry
24+
#
25+
# The following is a template for adding new entries in the MAINTAINER file:
26+
#
27+
# tag: <module-name>
28+
# path: <file-or-directory-path>
29+
# owners: <maintainer1>, <maintainer2>, ...
30+
#
31+
# When adding entries, please follow these guidelines:
32+
# - Ensure the `tag` is unique and descriptive.
33+
# - Ensure the `path` points to the correct location in the repository.
34+
# - Ensure the `owners` are accurate and that all new maintainers are aware of their responsibilities.
35+
#
36+
# Example Entry:
37+
# tag: example-module
38+
# path: example/module/path
39+
# owners: John Doe(johndoe)<[email protected]>, Jane Smith(janesmith)<[email protected]>
40+
41+
# Note:
42+
# - 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.
43+
# - 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.
44+
45+
# Below are existing maintainer entries, divided by module:
46+
47+
tag: workflow
48+
path: .github
49+
owners: supper thomas(supperthomas)<[email protected]>
50+
51+
tag: stm32f407-rt-spark
52+
path: bsp/stm32/stm32f407-rt-spark
53+
owners: Bingru Zhang(Rbb666)<[email protected]>, Yuqiang Wang(kurisaW)<[email protected]>
54+
55+
tag: libc
56+
path: components/libc
57+
owners: Meco Jianting Man(mysterywolf)<[email protected]>

0 commit comments

Comments
 (0)