99# 2025-03-14 hydevcode
1010# 2025-05-10 kurisaW Fixed file existence, cache, and comment time issues
1111# 2025-05-11 kurisaW Fixed missing unique files creation and cache logic
12+ # 2025-07-14 kurisaW Merge same tag with different paths, remove Path display from CI comment
1213
1314# Script Function Description: Assign PR reviews based on the MAINTAINERS list.
1415
@@ -54,97 +55,160 @@ jobs:
5455 "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \
5556 jq -r '.[] | select(.user.login == "github-actions[bot]") | {body: .body} | @base64')
5657
57- echo "=== Changed Files ==="
58- cat changed_files.txt
59- echo "====================="
60-
6158 comment_body=""
6259 if [[ ! -z "$existing_comment" ]]; then
63- 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')
64- comment_time=$(date -d "$comment_body" +%s)
65- echo "${comment_body}"
60+ 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} CST ).*/\1/p')
61+ comment_time=$(TZ='Asia/Shanghai' date -d "$comment_body" +%s)
62+ echo "CACHE_TIMESTAMP=${comment_time}" >> $GITHUB_OUTPUT # 统一使用这个变量名
6663 echo "COMMENT_TIME=${comment_time}" >> $GITHUB_OUTPUT
6764 else
6865 comment_time=""
69- echo "COMMENT_TIME=${comment_time}" >> $GITHUB_OUTPUT
66+ echo "CACHE_TIMESTAMP=" >> $GITHUB_OUTPUT
67+ echo "COMMENT_TIME=" >> $GITHUB_OUTPUT
7068 fi
71- echo "COMMENT_TIME=${ comment_time} "
69+ echo "Debug - CACHE_TIMESTAMP: $ comment_time"
7270
7371 - name : Parse MAINTAINERS file
7472 id : parse_maintainer
7573 run : |
76- # 使用 AWK 解析 MAINTAINERS 文件格式:
77- # 提取 tag(标签)、path(路径)和 owners(维护者 GitHub ID)
74+ set -euo pipefail
7875 awk '
76+ BEGIN{ tag=""; paths=""; owners="" }
7977 /^tag:/ {
80- tag = substr($0, index($0, $2)) # 提取标签内容
78+ tag = substr($0, index($0, $2));
79+ paths=""; owners=""
8180 }
8281 /^path:/ {
83- # 提取 path 字段并去除前后空格
84- path = substr($0, index($0, $2) )
85- gsub(/^[ \t]+|[ \t]+$/, "", path) # 清理前后空格和制表符
82+ path = substr($0, index($0, $2))
83+ gsub(/^[ \t]+|[ \t]+$/, "", path )
84+ paths = (paths == "" ? path : paths "|" path)
8685 }
8786 /^owners:/ {
88- owners = substr($0, index($0, $2)) # 提取维护者信息
89- split(owners, parts, /[()]/) # 拆分出 GitHub ID(括号内内容)
90- github_ids = ""
91- for (i=2; i<=length(parts); i+=2) {
92- github_ids = github_ids "@" parts[i] " " # 拼接为 @user 格式
93- }
94- print tag "|" path "|" github_ids
87+ owners = substr($0, index($0, $2))
88+ n = split(owners, parts, /[()]/)
89+ github_ids=""
90+ for (i=2; i<=n; i+=2) {
91+ id=parts[i]
92+ gsub(/^[ \t@]+|[ \t]+$/, "", id)
93+ if(id != "") github_ids=github_ids "@" id " "
94+ }
95+ print tag "|" paths "|" github_ids
96+ tag=""; paths=""; owners=""
9597 }
9698 ' MAINTAINERS > tag_data.csv
9799
98- - name : Generate reviewers list
100+ - name : Generate reviewers list and tag-file mapping
99101 id : generate_reviewers
100102 run : |
101- rm -f triggered_reviewers.txt triggered_tags.txt unique_reviewers.txt unique_tags.txt
103+ rm -f triggered_reviewers.txt triggered_tags.txt unique_reviewers.txt unique_tags.txt tag_files_map.json tag_reviewers_map.txt
102104 touch triggered_reviewers.txt triggered_tags.txt unique_reviewers.txt unique_tags.txt
103105
104- while IFS='|' read -r tag path reviewers; do
105- # 转义路径中的正则特殊字符
106- escaped_path=$(sed 's/[.[\*^$]/\\&/g' <<< "$path")
107-
108- # 使用增强型正则匹配路径及其所有子目录
109- if grep -qE "^$escaped_path(/.*)*" changed_files.txt; then
110- echo "$reviewers" | tr -s ' ' '\n' | sed '/^$/d' >> triggered_reviewers.txt
111- echo "$tag" >> triggered_tags.txt
112- echo "Matched: $path → $tag"
113- fi
106+ # 1. 读取 tag_data.csv,建立 tag -> [paths], tag -> reviewers
107+ declare -A tag_paths_map
108+ declare -A tag_reviewers_map
109+
110+ while IFS='|' read -r tag paths reviewers; do
111+ IFS='|' read -ra path_arr <<< "$paths"
112+ for p in "${path_arr[@]}"; do
113+ tag_paths_map["$tag"]+="$p;"
114+ done
115+ # 合并 reviewers,去重,只保留合法格式
116+ existing_reviewers="${tag_reviewers_map["$tag"]}"
117+ all_reviewers="$existing_reviewers $reviewers"
118+ # 只保留 @xxx 格式,去重
119+ all_reviewers=$(echo "$all_reviewers" | grep -o '@[A-Za-z0-9_-]\+' | sort -u | tr '\n' ' ')
120+ tag_reviewers_map["$tag"]="$all_reviewers"
114121 done < tag_data.csv
115122
123+ # 2. 针对每个 tag,找出它所有 path 匹配的变更文件
124+ declare -A tag_changedfiles_map
125+ while IFS= read -r changed; do
126+ for tag in "${!tag_paths_map[@]}"; do
127+ IFS=';' read -ra tpaths <<< "${tag_paths_map[$tag]}"
128+ for tpath in "${tpaths[@]}"; do
129+ [[ -z "$tpath" ]] && continue
130+ if [[ -f "$tpath" ]]; then
131+ # 精确文件名
132+ [[ "$changed" == "$tpath" ]] && tag_changedfiles_map["$tag"]+="$changed;"
133+ else
134+ # 目录前缀
135+ [[ "$changed" == $tpath* ]] && tag_changedfiles_map["$tag"]+="$changed;"
136+ fi
137+ done
138+ done
139+ done < changed_files.txt
140+
141+ # 3. 输出合并后的 tag reviewers、tag、并去重
142+ for tag in "${!tag_changedfiles_map[@]}"; do
143+ reviewers="${tag_reviewers_map[$tag]}"
144+ echo "$reviewers" | tr -s ' ' '\n' | sed '/^$/d' >> triggered_reviewers.txt
145+ echo "$tag" >> triggered_tags.txt
146+ done
147+
116148 # 生成去重的 unique_reviewers.txt 和 unique_tags.txt
117149 sort -u triggered_reviewers.txt > unique_reviewers.txt
118150 sort -u triggered_tags.txt > unique_tags.txt
119151
120- # 检查是否有匹配的 reviewers
152+ # 4. 输出 tag_files_map.json,格式 { "tag1": ["file1","file2"], ... }
153+ {
154+ echo "{"
155+ first_tag=1
156+ for tag in "${!tag_changedfiles_map[@]}"; do
157+ [[ $first_tag -eq 0 ]] && echo ","
158+ echo -n " \"${tag}\": ["
159+ IFS=';' read -ra files <<< "${tag_changedfiles_map[$tag]}"
160+ file_list=""
161+ for f in "${files[@]}"; do
162+ [[ -z "$f" ]] && continue
163+ [[ -n "$file_list" ]] && file_list+=", "
164+ file_list+="\"$f\""
165+ done
166+ echo -n "$file_list"
167+ echo -n "]"
168+ first_tag=0
169+ done
170+ echo ""
171+ echo "}"
172+ } > tag_files_map.json
173+
174+ # 5. 保存聚合去重后的 reviewers 到 tag_reviewers_map.txt
175+ {
176+ for tag in "${!tag_reviewers_map[@]}"; do
177+ echo "$tag|${tag_reviewers_map[$tag]}"
178+ done
179+ } > tag_reviewers_map.txt
180+
181+ # 6. 标记是否有 reviewer
121182 if [[ -s unique_reviewers.txt ]]; then
122183 echo "HAS_REVIEWERS=true" >> $GITHUB_OUTPUT
123184 else
124185 echo "HAS_REVIEWERS=false" >> $GITHUB_OUTPUT
125186 fi
126187
127- echo "=== Matched Paths ==="
188+ echo "=== Matched Tags ==="
128189 cat unique_tags.txt
129190 echo "=== Matched Reviewers ==="
130191 cat unique_reviewers.txt
192+ echo "=== Tag-ChangedFiles Map ==="
193+ cat tag_files_map.json
131194
132195 - name : Restore Reviewers Cache
133- id : reviewers-cache-restore
134- if : ${{ steps.changed_files.outputs.COMMENT_TIME != '' }}
196+ id : reviewers-cache-restore
197+ if : ${{ steps.changed_files.outputs.CACHE_TIMESTAMP != '' }}
135198 uses : actions/cache/restore@v4
136- with :
199+ with :
137200 path : |
138201 unique_tags_bak.txt
139202 unique_reviewers_bak.txt
140- key : ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.changed_files.outputs.COMMENT_TIME }}
203+ key : ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.changed_files.outputs.CACHE_TIMESTAMP }}-${{ github.run_id }}
204+ restore-keys : |
205+ ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.changed_files.outputs.CACHE_TIMESTAMP }}-
206+ ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-
141207
142208 - name : Get approval status
143209 id : get_approval
144210 run : |
145- current_time=$(date -u +"%Y-%m-%d %H:%M UTC")
146-
147- # 检查 unique_reviewers.txt 是否存在且非空
211+ current_time=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M CST")
148212 if [[ ! -s unique_reviewers.txt ]]; then
149213 echo "No reviewers found, creating empty unique_reviewers.txt"
150214 touch unique_reviewers.txt
@@ -183,39 +247,35 @@ jobs:
183247 select($mention | inside($reviewers)) | # 过滤有效审查者
184248 "\($mention) \(.created_at)" # 输出审查者和时间
185249 ' <<< "$comments" >> approval_data.txt
186-
187250 notified_users=""
188251 if [[ -f unique_reviewers_bak.txt ]]; then
189252 notified_users=$(cat unique_reviewers_bak.txt | xargs)
190253 else
191254 notified_users=""
192255 fi
193-
194256 {
195257 echo "---"
196258 echo "### 📊 Current Review Status (Last Updated: $current_time)"
197259 while read -r reviewer; do
198260 formatted_reviewers=""
199261 for r in $reviewers; do
200262 if [[ " ${notified_users[@]} " =~ " $reviewer " ]]; then
201- formatted_reviewers+="${reviewer#@}"
263+ formatted_reviewers+="${reviewer#@}"
202264 else
203- formatted_reviewers+="$reviewer"
265+ formatted_reviewers+="$reviewer"
204266 fi
205267 done
206-
207268 if [[ -n "${approvals[$reviewer]}" ]]; then
208- timestamp=$(date -d "${approvals[$reviewer]}" -u +"%Y-%m-%d %H:%M UTC ")
269+ timestamp=$(TZ='Asia/Shanghai' date -d "${approvals[$reviewer]}" +"%Y-%m-%d %H:%M CST ")
209270 echo "- ✅ **$formatted_reviewers** Reviewed On $timestamp"
210271 else
211272 echo "- ⌛ **$formatted_reviewers** Pending Review"
212273 fi
213274 done < unique_reviewers.txt
214275 } > review_status.md
215-
216276 echo "CURRENT_TIME=${current_time}" >> $GITHUB_OUTPUT
217277
218- - name : Generate review data
278+ - name : Generate review data (tag merge, no path in comment, changed files summary per tag)
219279 id : generate_review
220280 if : steps.generate_reviewers.outputs.HAS_REVIEWERS == 'true'
221281 run : |
@@ -227,49 +287,55 @@ jobs:
227287 if [[ -f unique_tags_bak.txt ]]; then
228288 unique_tags_bak=$(cat unique_tags_bak.txt | xargs)
229289 fi
230-
231- existing_tags=""
232- for r in $unique_tags; do
233- if [[ " ${unique_tags_bak[@]} " =~ " $r " ]]; then
234- echo "$r 不存在于数组中"
235- else
236- existing_tags+="$r "
237- fi
238- done
239-
240- current_time=$(date -u +"%Y-%m-%d %H:%M UTC")
290+ # 读取 tag->files 映射
291+ declare -A tag_files_map
292+ eval "$(jq -r 'to_entries[] | "tag_files_map[\"\(.key)\"]=\"\(.value | join(";"))\"" ' tag_files_map.json)"
293+ # 读取 tag->reviewers(只读聚合去重后的结果)
294+ declare -A tag_reviewers_map
295+ while IFS='|' read -r tag reviewers; do
296+ tag_reviewers_map["$tag"]="$reviewers"
297+ done < tag_reviewers_map.txt
298+ # 获取已通知的 reviewers
299+ notified_users=""
300+ if [[ -f unique_reviewers_bak.txt ]]; then
301+ notified_users=$(cat unique_reviewers_bak.txt | xargs)
302+ fi
303+ current_time=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M CST")
241304 {
242- # 生成审查分配信息
243305 echo "## 📌 Code Review Assignment"
244306 echo ""
245-
246- while IFS='|' read -r tag path reviewers; do
247- if grep -qE "^$path(/|$)" changed_files.txt; then
248- echo "### 🏷️ Tag: $tag"
249- echo "**Path:** \`$path\` "
250-
251- if [[ " ${existing_tags[@]} " =~ " $tag " ]]; then
252- echo "**Reviewers:** $reviewers "
307+ for tag in $unique_tags; do
308+ reviewers="${tag_reviewers_map[$tag]}"
309+ # 移除尾部空格并提取有效的@username格式
310+ reviewers=$(echo "$reviewers" | sed 's/[[:space:]]*$//' | grep -o '@[A-Za-z0-9_-]\+' | sort -u | tr '\n' ' ')
311+
312+ # 格式化reviewers显示(仅对已通知用户去掉@)
313+ formatted_reviewers=""
314+ for reviewer in $reviewers; do
315+ if [[ " ${notified_users[@]} " =~ " $reviewer " ]]; then
316+ formatted_reviewers+="${reviewer#@} " # 已通知用户去掉@
253317 else
254- formatted_reviewers=""
255- for r in $reviewers; do
256- formatted_reviewers+="${r#@} "
257- done
258- echo "**Reviewers:** $formatted_reviewers "
318+ formatted_reviewers+="$reviewer " # 未通知用户保留@
259319 fi
260-
261- echo "<details>"
262- echo "<summary><b>Changed Files</b> (Click to expand)</summary>"
263- echo ""
264- grep -E "^$path(/|$)" changed_files.txt | sed 's/^/- /' # 列出匹配的变更文件
265- echo ""
266- echo "</details>"
267- echo ""
268- fi
269- done < tag_data.csv
320+ done
321+
322+ echo "### 🏷️ Tag: $tag"
323+ echo ""
324+ echo "**Reviewers:** $formatted_reviewers" # 确保显示Reviewers
325+ echo "<details>"
326+ echo "<summary><b>Changed Files</b> (Click to expand)</summary>"
327+ echo ""
328+ IFS=';' read -ra files <<< "${tag_files_map[$tag]}"
329+ for file in "${files[@]}"; do
330+ [[ -z "$file" ]] && continue
331+ echo "- $file"
332+ done
333+ echo ""
334+ echo "</details>"
335+ echo ""
336+ done
270337 # 插入审查状态
271338 cat review_status.md
272-
273339 echo "---"
274340 echo "### 📝 Review Instructions"
275341 echo ""
@@ -321,17 +387,30 @@ jobs:
321387 "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \
322388 jq -r '.[] | select(.user.login == "github-actions[bot]") | {body: .body} | @base64')
323389 comment_body="${{ steps.get_approval.outputs.CURRENT_TIME }}"
324- comment_time=$(date -d "$comment_body" +%s)
325- echo "CURRENT_TIME=${comment_time}" >> $GITHUB_OUTPUT
326- cp unique_reviewers.txt unique_reviewers_bak.txt
327- cp unique_tags.txt unique_tags_bak.txt
390+ comment_time=$(TZ='Asia/Shanghai' date -d "$comment_body" +%s)
391+ echo "CACHE_TIMESTAMP=${comment_time}" >> $GITHUB_OUTPUT # 统一使用这个变量名
392+ echo "Debug - Saving cache with timestamp: $comment_time"
393+
394+ mkdir -p $(dirname unique_reviewers_bak.txt)
395+ if [[ -s unique_reviewers.txt ]]; then
396+ cp unique_reviewers.txt unique_reviewers_bak.txt
397+ else
398+ touch unique_reviewers_bak.txt
399+ fi
400+
401+ if [[ -s unique_tags.txt ]]; then
402+ cp unique_tags.txt unique_tags_bak.txt
403+ else
404+ touch unique_tags_bak.txt
405+ fi
328406
329407 - name : Save Reviewers Cache
330408 id : reviewers-cache-save
331409 if : steps.generate_reviewers.outputs.HAS_REVIEWERS == 'true'
410+ continue-on-error : true
332411 uses : actions/cache/save@v4
333412 with :
334413 path : |
335414 unique_tags_bak.txt
336415 unique_reviewers_bak.txt
337- key : ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.get_comment_time.outputs.CURRENT_TIME }}
416+ key : ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.get_comment_time.outputs.CACHE_TIMESTAMP }}-${{ github.run_id }}
0 commit comments