auto_assign_reviewers.yml 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  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. # 2025-05-10 kurisaW Fixed file existence, cache, and comment time issues
  11. # 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
  13. # Script Function Description: Assign PR reviews based on the MAINTAINERS list.
  14. name: Auto Review Assistant
  15. on:
  16. pull_request_target:
  17. branches: [ master ]
  18. types: [opened, synchronize, reopened]
  19. concurrency:
  20. group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  21. cancel-in-progress: true
  22. jobs:
  23. assign-reviewers:
  24. runs-on: ubuntu-22.04
  25. if: github.repository_owner == 'RT-Thread'
  26. permissions:
  27. issues: read
  28. pull-requests: write
  29. contents: read
  30. steps:
  31. - name: Extract PR number
  32. id: extract-pr
  33. run: |
  34. PR_NUMBER=${{ github.event.pull_request.number }}
  35. echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_OUTPUT
  36. - name: Checkout code
  37. uses: actions/checkout@v4
  38. with:
  39. ref: master
  40. sparse-checkout: MAINTAINERS
  41. persist-credentials: false
  42. - name: Get changed files
  43. id: changed_files
  44. run: |
  45. # 通过 GitHub API 获取 PR 的变更文件列表(带重试机制和错误处理)
  46. max_retries=3
  47. retry_count=0
  48. changed_files=""
  49. api_response=""
  50. echo "Fetching changed files for PR #${{ steps.extract-pr.outputs.PR_NUMBER }}..."
  51. while [ $retry_count -lt $max_retries ]; do
  52. api_response=$(curl -s \
  53. -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
  54. -H "Accept: application/vnd.github.v3+json" \
  55. "https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.extract-pr.outputs.PR_NUMBER }}/files")
  56. # 验证响应是否为有效JSON且包含文件数组
  57. if jq -e 'if type=="array" then .[0].filename else empty end' <<<"$api_response" >/dev/null 2>&1; then
  58. changed_files=$(jq -r '.[].filename' <<<"$api_response")
  59. break
  60. else
  61. echo "Retry $((retry_count+1)): API response not ready or invalid format"
  62. echo "API Response: $api_response"
  63. sleep 5
  64. ((retry_count++))
  65. fi
  66. done
  67. if [ -z "$changed_files" ]; then
  68. echo "Error: Failed to get changed files after $max_retries attempts"
  69. echo "Final API Response: $api_response"
  70. exit 1
  71. fi
  72. echo "$changed_files" > changed_files.txt
  73. echo "Successfully fetched $(wc -l < changed_files.txt) changed files"
  74. # 以下是原有的评论处理逻辑(保持不变)
  75. existing_comment=$(curl -s \
  76. -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
  77. "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments")
  78. # Check if response is valid JSON
  79. if jq -e . >/dev/null 2>&1 <<<"$existing_comment"; then
  80. existing_comment=$(jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("<!-- Auto Review Assistant Comment -->"))) | {body: .body} | @base64' <<< "$existing_comment")
  81. else
  82. existing_comment=""
  83. echo "Warning: Invalid JSON response from GitHub API for comments"
  84. echo "Response: $existing_comment"
  85. fi
  86. comment_body=""
  87. if [[ ! -z "$existing_comment" ]]; then
  88. 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')
  89. comment_time=$(TZ='Asia/Shanghai' date -d "$comment_body" +%s)
  90. echo "CACHE_TIMESTAMP=${comment_time}" >> $GITHUB_OUTPUT # 统一使用这个变量名
  91. echo "COMMENT_TIME=${comment_time}" >> $GITHUB_OUTPUT
  92. else
  93. comment_time=""
  94. echo "CACHE_TIMESTAMP=" >> $GITHUB_OUTPUT
  95. echo "COMMENT_TIME=" >> $GITHUB_OUTPUT
  96. fi
  97. echo "Debug - CACHE_TIMESTAMP: $comment_time"
  98. - name: Parse MAINTAINERS file
  99. id: parse_maintainer
  100. run: |
  101. set -euo pipefail
  102. awk '
  103. BEGIN{ tag=""; paths=""; owners="" }
  104. /^tag:/ {
  105. tag = substr($0, index($0, $2));
  106. paths=""; owners=""
  107. }
  108. /^path:/ {
  109. path = substr($0, index($0, $2))
  110. gsub(/^[ \t]+|[ \t]+$/, "", path)
  111. paths = (paths == "" ? path : paths "|" path)
  112. }
  113. /^owners:/ {
  114. owners = substr($0, index($0, $2))
  115. n = split(owners, parts, /[()]/)
  116. github_ids=""
  117. for (i=2; i<=n; i+=2) {
  118. id=parts[i]
  119. gsub(/^[ \t@]+|[ \t]+$/, "", id)
  120. if(id != "") github_ids=github_ids "@" id " "
  121. }
  122. print tag "|" paths "|" github_ids
  123. tag=""; paths=""; owners=""
  124. }
  125. ' MAINTAINERS > tag_data.csv
  126. - name: Generate reviewers list and tag-file mapping
  127. id: generate_reviewers
  128. run: |
  129. rm -f triggered_reviewers.txt triggered_tags.txt unique_reviewers.txt unique_tags.txt tag_files_map.json tag_reviewers_map.txt
  130. touch triggered_reviewers.txt triggered_tags.txt unique_reviewers.txt unique_tags.txt
  131. # 1. 读取 tag_data.csv,建立 tag -> [paths], tag -> reviewers
  132. declare -A tag_paths_map
  133. declare -A tag_reviewers_map
  134. while IFS='|' read -r tag paths reviewers; do
  135. IFS='|' read -ra path_arr <<< "$paths"
  136. for p in "${path_arr[@]}"; do
  137. tag_paths_map["$tag"]+="$p;"
  138. done
  139. # 合并 reviewers,去重,只保留合法格式
  140. existing_reviewers="${tag_reviewers_map["$tag"]}"
  141. all_reviewers="$existing_reviewers $reviewers"
  142. # 只保留 @xxx 格式,去重
  143. all_reviewers=$(echo "$all_reviewers" | grep -o '@[A-Za-z0-9_-]\+' | sort -u | tr '\n' ' ')
  144. tag_reviewers_map["$tag"]="$all_reviewers"
  145. done < tag_data.csv
  146. # 2. 针对每个 tag,找出它所有 path 匹配的变更文件
  147. declare -A tag_changedfiles_map
  148. while IFS= read -r changed; do
  149. for tag in "${!tag_paths_map[@]}"; do
  150. IFS=';' read -ra tpaths <<< "${tag_paths_map[$tag]}"
  151. for tpath in "${tpaths[@]}"; do
  152. [[ -z "$tpath" ]] && continue
  153. if [[ -f "$tpath" ]]; then
  154. # 精确文件名
  155. [[ "$changed" == "$tpath" ]] && tag_changedfiles_map["$tag"]+="$changed;"
  156. else
  157. # 目录前缀
  158. [[ "$changed" == $tpath* ]] && tag_changedfiles_map["$tag"]+="$changed;"
  159. fi
  160. done
  161. done
  162. done < changed_files.txt
  163. # 3. 输出合并后的 tag reviewers、tag、并去重
  164. for tag in "${!tag_changedfiles_map[@]}"; do
  165. reviewers="${tag_reviewers_map[$tag]}"
  166. echo "$reviewers" | tr -s ' ' '\n' | sed '/^$/d' >> triggered_reviewers.txt
  167. echo "$tag" >> triggered_tags.txt
  168. done
  169. # 生成去重的 unique_reviewers.txt 和 unique_tags.txt
  170. sort -u triggered_reviewers.txt > unique_reviewers.txt
  171. sort -u triggered_tags.txt > unique_tags.txt
  172. # 4. 输出 tag_files_map.json,格式 { "tag1": ["file1","file2"], ... }
  173. {
  174. echo "{"
  175. first_tag=1
  176. for tag in "${!tag_changedfiles_map[@]}"; do
  177. [[ $first_tag -eq 0 ]] && echo ","
  178. echo -n " \"${tag}\": ["
  179. IFS=';' read -ra files <<< "${tag_changedfiles_map[$tag]}"
  180. file_list=""
  181. for f in "${files[@]}"; do
  182. [[ -z "$f" ]] && continue
  183. [[ -n "$file_list" ]] && file_list+=", "
  184. file_list+="\"$f\""
  185. done
  186. echo -n "$file_list"
  187. echo -n "]"
  188. first_tag=0
  189. done
  190. echo ""
  191. echo "}"
  192. } > tag_files_map.json
  193. # 5. 保存聚合去重后的 reviewers 到 tag_reviewers_map.txt
  194. {
  195. for tag in "${!tag_reviewers_map[@]}"; do
  196. echo "$tag|${tag_reviewers_map[$tag]}"
  197. done
  198. } > tag_reviewers_map.txt
  199. # 6. 标记是否有 reviewer
  200. if [[ -s unique_reviewers.txt ]]; then
  201. echo "HAS_REVIEWERS=true" >> $GITHUB_OUTPUT
  202. else
  203. echo "HAS_REVIEWERS=false" >> $GITHUB_OUTPUT
  204. fi
  205. echo "=== Matched Tags ==="
  206. cat unique_tags.txt
  207. echo "=== Matched Reviewers ==="
  208. cat unique_reviewers.txt
  209. echo "=== Tag-ChangedFiles Map ==="
  210. cat tag_files_map.json
  211. - name: Restore Reviewers Cache
  212. id: reviewers-cache-restore
  213. if: ${{ steps.changed_files.outputs.CACHE_TIMESTAMP != '' }}
  214. uses: actions/cache/restore@v4
  215. with:
  216. path: |
  217. unique_tags_bak.txt
  218. unique_reviewers_bak.txt
  219. key: ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.changed_files.outputs.CACHE_TIMESTAMP }}-${{ github.run_id }}
  220. restore-keys: |
  221. ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.changed_files.outputs.CACHE_TIMESTAMP }}-
  222. ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-
  223. - name: Get approval status
  224. id: get_approval
  225. run: |
  226. current_time=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M CST")
  227. if [[ ! -s unique_reviewers.txt ]]; then
  228. echo "No reviewers found, creating empty unique_reviewers.txt"
  229. touch unique_reviewers.txt
  230. fi
  231. reviewers=$(cat unique_reviewers.txt | tr '\n' '|' | sed 's/|$//')
  232. # 获取 PR 的所有评论
  233. comments=$(curl -s \
  234. "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments")
  235. echo '#!/bin/bash' > approval_data.sh
  236. echo 'declare -A approvals=()' >> approval_data.sh
  237. # 使用 jq 解析包含 LGTM 的有效评论
  238. jq -r --arg reviewers "$reviewers" '
  239. .[] |
  240. select(.user.login != "github-actions[bot]") | # 排除 bot 的评论
  241. select(.body | test("^\\s*LGTM\\s*$"; "i")) | # 匹配 LGTM 评论(不区分大小写)
  242. .user.login as $user |
  243. "@\($user)" as $mention |
  244. select($mention | inside($reviewers)) | # 过滤有效审查者
  245. "approvals[\"\($mention)\"]=\"\(.created_at)\"" # 记录审批时间
  246. ' <<< "$comments" >> approval_data.sh
  247. # 加载审查数据并生成状态报告
  248. chmod +x approval_data.sh
  249. source ./approval_data.sh
  250. jq -r --arg reviewers "$reviewers" '
  251. .[] |
  252. select(.user.login != "github-actions[bot]") | # 排除 bot 的评论
  253. select(.body | test("^\\s*LGTM\\s*$"; "i")) | # 匹配 LGTM 评论(不区分大小写)
  254. .user.login as $user |
  255. "@\($user)" as $mention |
  256. select($mention | inside($reviewers)) | # 过滤有效审查者
  257. "\($mention) \(.created_at)" # 输出审查者和时间
  258. ' <<< "$comments" >> approval_data.txt
  259. notified_users=""
  260. if [[ -f unique_reviewers_bak.txt ]]; then
  261. notified_users=$(cat unique_reviewers_bak.txt | xargs)
  262. else
  263. notified_users=""
  264. fi
  265. {
  266. echo "---"
  267. echo "### 📊 Current Review Status (Last Updated: $current_time)"
  268. while read -r reviewer; do
  269. formatted_reviewers=""
  270. for r in $reviewers; do
  271. if [[ " ${notified_users[@]} " =~ " $reviewer " ]]; then
  272. formatted_reviewers+="${reviewer#@}"
  273. else
  274. formatted_reviewers+="$reviewer"
  275. fi
  276. done
  277. if [[ -n "${approvals[$reviewer]}" ]]; then
  278. timestamp=$(TZ='Asia/Shanghai' date -d "${approvals[$reviewer]}" +"%Y-%m-%d %H:%M CST")
  279. echo "- ✅ **$formatted_reviewers** Reviewed On $timestamp"
  280. else
  281. echo "- ⌛ **$formatted_reviewers** Pending Review"
  282. fi
  283. done < unique_reviewers.txt
  284. } > review_status.md
  285. echo "CURRENT_TIME=${current_time}" >> $GITHUB_OUTPUT
  286. - name: Generate review data (tag merge, no path in comment, changed files summary per tag)
  287. id: generate_review
  288. if: steps.generate_reviewers.outputs.HAS_REVIEWERS == 'true'
  289. run: |
  290. unique_tags=""
  291. if [[ -s unique_tags.txt ]]; then
  292. unique_tags=$(cat unique_tags.txt | xargs)
  293. fi
  294. unique_tags_bak=""
  295. if [[ -f unique_tags_bak.txt ]]; then
  296. unique_tags_bak=$(cat unique_tags_bak.txt | xargs)
  297. fi
  298. # 读取 tag->files 映射
  299. declare -A tag_files_map
  300. eval "$(jq -r 'to_entries[] | "tag_files_map[\"\(.key)\"]=\"\(.value | join(";"))\"" ' tag_files_map.json)"
  301. # 读取 tag->reviewers(只读聚合去重后的结果)
  302. declare -A tag_reviewers_map
  303. while IFS='|' read -r tag reviewers; do
  304. tag_reviewers_map["$tag"]="$reviewers"
  305. done < tag_reviewers_map.txt
  306. # 获取已通知的 reviewers
  307. notified_users=""
  308. if [[ -f unique_reviewers_bak.txt ]]; then
  309. notified_users=$(cat unique_reviewers_bak.txt | xargs)
  310. fi
  311. current_time=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M CST")
  312. {
  313. echo "<!-- Auto Review Assistant Comment -->"
  314. echo "## 📌 Code Review Assignment"
  315. echo ""
  316. for tag in $unique_tags; do
  317. reviewers="${tag_reviewers_map[$tag]}"
  318. # 移除尾部空格并提取有效的@username格式
  319. reviewers=$(echo "$reviewers" | sed 's/[[:space:]]*$//' | grep -o '@[A-Za-z0-9_-]\+' | sort -u | tr '\n' ' ')
  320. # 格式化reviewers显示(仅对已通知用户去掉@)
  321. formatted_reviewers=""
  322. for reviewer in $reviewers; do
  323. if [[ " ${notified_users[@]} " =~ " $reviewer " ]]; then
  324. formatted_reviewers+="${reviewer#@} " # 已通知用户去掉@
  325. else
  326. formatted_reviewers+="$reviewer " # 未通知用户保留@
  327. fi
  328. done
  329. echo "### 🏷️ Tag: $tag"
  330. echo ""
  331. echo "**Reviewers:** $formatted_reviewers" # 确保显示Reviewers
  332. echo "<details>"
  333. echo "<summary><b>Changed Files</b> (Click to expand)</summary>"
  334. echo ""
  335. IFS=';' read -ra files <<< "${tag_files_map[$tag]}"
  336. for file in "${files[@]}"; do
  337. [[ -z "$file" ]] && continue
  338. echo "- $file"
  339. done
  340. echo ""
  341. echo "</details>"
  342. echo ""
  343. done
  344. # 插入审查状态
  345. cat review_status.md
  346. echo "---"
  347. echo "### 📝 Review Instructions"
  348. echo ""
  349. echo "1. **维护者可以通过单击此处来刷新审查状态:** [🔄 刷新状态](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
  350. echo " **Maintainers can refresh the review status by clicking here:** [🔄 Refresh Status](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
  351. echo ""
  352. echo "2. **确认审核通过后评论 \`LGTM/lgtm\`**"
  353. echo " **Comment \`LGTM/lgtm\` after confirming approval**"
  354. echo ""
  355. echo "3. **PR合并前需至少一位维护者确认**"
  356. echo " **PR must be confirmed by at least one maintainer before merging**"
  357. echo ""
  358. echo "> ℹ️ **刷新CI状态操作需要具备仓库写入权限。**"
  359. echo "> ℹ️ **Refresh CI status operation requires repository Write permission.**"
  360. } > review_data.md
  361. - name: Post/Update comment
  362. id: post_comment
  363. if: steps.generate_reviewers.outputs.HAS_REVIEWERS == 'true'
  364. run: |
  365. # 查找现有的 bot 评论
  366. existing_comment=$(curl -s \
  367. -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
  368. "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments" | \
  369. jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("<!-- Auto Review Assistant Comment -->"))) | {id: .id, body: .body} | @base64')
  370. if [[ -n "$existing_comment" ]]; then
  371. # 更新现有评论
  372. comment_id=$(echo "$existing_comment" | head -1 | base64 -d | jq -r .id)
  373. echo "Updating existing comment $comment_id"
  374. response=$(curl -s -X PATCH \
  375. -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
  376. -d "$(jq -n --arg body "$(cat review_data.md)" '{body: $body}')" \
  377. "https://api.github.com/repos/${{ github.repository }}/issues/comments/$comment_id")
  378. else
  379. # 创建新评论
  380. echo "Creating new comment"
  381. response=$(curl -s -X POST \
  382. -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
  383. -d "$(jq -n --arg body "$(cat review_data.md)" '{body: $body}')" \
  384. "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments")
  385. fi
  386. - name: Get Comment Time
  387. id: get_comment_time
  388. if: steps.generate_reviewers.outputs.HAS_REVIEWERS == 'true'
  389. run: |
  390. existing_comment=$(curl -s \
  391. -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
  392. "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.extract-pr.outputs.PR_NUMBER }}/comments")
  393. # Check if response is valid JSON
  394. if jq -e . >/dev/null 2>&1 <<<"$existing_comment"; then
  395. existing_comment=$(jq -r '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("<!-- Auto Review Assistant Comment -->"))) | {body: .body} | @base64' <<< "$existing_comment")
  396. else
  397. existing_comment=""
  398. echo "Warning: Invalid JSON response from GitHub API"
  399. echo "Response: $existing_comment"
  400. fi
  401. comment_body="${{ steps.get_approval.outputs.CURRENT_TIME }}"
  402. comment_time=$(TZ='Asia/Shanghai' date -d "$comment_body" +%s)
  403. echo "CACHE_TIMESTAMP=${comment_time}" >> $GITHUB_OUTPUT # 统一使用这个变量名
  404. echo "Debug - Saving cache with timestamp: $comment_time"
  405. mkdir -p $(dirname unique_reviewers_bak.txt)
  406. if [[ -s unique_reviewers.txt ]]; then
  407. cp unique_reviewers.txt unique_reviewers_bak.txt
  408. else
  409. touch unique_reviewers_bak.txt
  410. fi
  411. if [[ -s unique_tags.txt ]]; then
  412. cp unique_tags.txt unique_tags_bak.txt
  413. else
  414. touch unique_tags_bak.txt
  415. fi
  416. - name: Save Reviewers Cache
  417. id: reviewers-cache-save
  418. if: steps.generate_reviewers.outputs.HAS_REVIEWERS == 'true'
  419. continue-on-error: true
  420. uses: actions/cache/save@v4
  421. with:
  422. path: |
  423. unique_tags_bak.txt
  424. unique_reviewers_bak.txt
  425. key: ${{ runner.os }}-auto-assign-reviewers-${{ steps.extract-pr.outputs.PR_NUMBER }}-${{ steps.get_comment_time.outputs.CACHE_TIMESTAMP }}-${{ github.run_id }}