auto-assign-reviewers.yml 14 KB

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