#!/usr/bin/env bash # check-pr-checklist.sh — PR 提交门禁检查(对齐 AGENTS.md §5 质量门禁 / §6 文档同步清单) # # 检查 PR 描述文件中 checklist 子项的勾选完成情况,以及 PR 模板的结构完整性。 # 规则: # ① PR 模板必须包含「全链路检查」「测试分层检查」「质量门禁」三节 # ② PR 描述文件中所有 checklist 子项(- [ ])必须已勾选(不适用项勾选并注明 N/A 理由) # # 用法: # bash scripts/check-pr-checklist.sh # 检查模板结构 # bash scripts/check-pr-checklist.sh # 检查 PR 描述文件 # bash scripts/check-pr-checklist.sh --pr-dir <目录> # 扫描目录下所有 PR 文件 # bash scripts/check-pr-checklist.sh --quiet # 仅违规时输出 # bash scripts/check-pr-checklist.sh --help # # 退出码: 0=全部通过 1=发现违规 2=模板不存在/格式错误 set -euo pipefail # 说明:AGENT_PROJECT_DIR 供测试与多工作树场景覆写模板根目录;默认取脚本所在仓库根, # 因此从任意子目录调用都能定位到 .gitea/PULL_REQUEST_TEMPLATE.md。 REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TEMPLATE_FILE="${AGENT_PROJECT_DIR:-$REPO_ROOT}/.gitea/PULL_REQUEST_TEMPLATE.md" QUIET=0 PR_FILE="" SCAN_DIR="" # 采用 while+shift 而非 for:for arg in "$@" 里 shift 不会跳过已消费的值, # `--pr-dir <目录>` 会把 <目录> 再当成一次位置参数误判为 PR 文件。 while [ $# -gt 0 ]; do case "$1" in --help | -h) echo "check-pr-checklist.sh — PR 提交门禁检查" echo "" echo "用法:" echo " bash scripts/check-pr-checklist.sh # 检查模板结构" echo " bash scripts/check-pr-checklist.sh # 检查 PR 描述文件" echo " bash scripts/check-pr-checklist.sh --pr-dir <目录> # 扫描目录下所有 PR 文件" echo " bash scripts/check-pr-checklist.sh --quiet # 仅违规时输出" echo "" echo "检查项:" echo " - PR 模板结构完整性(三节必填 + checklist 子项 ≥ 20)" echo " - PR 描述文件中所有 checklist 子项均已勾选" exit 0 ;; --quiet | -q) QUIET=1 ;; --pr-dir) if [ $# -lt 2 ]; then echo "❌ --pr-dir 需要一个目录参数" >&2 exit 2 fi SCAN_DIR="$2" shift ;; -*) echo "❌ 未知选项: $1" >&2 exit 2 ;; *) if [ -n "$PR_FILE" ]; then echo "❌ 只接受一个 PR 描述文件,多余参数: $1" >&2 exit 2 fi PR_FILE="$1" ;; esac shift done violations=() # ── 读取 PR 描述正文:仅在文件确实以 YAML frontmatter 开头时才剥离 ── # 若无 frontmatter 也无条件剥离,awk 的 fm 永远 <2 → 正文为空 → 三个标准节全部 # 「未发现」→ 任何普通描述都被误判为不是 PR 描述文件。故按首行判别并加空正文兜底。 read_pr_body() { local file="$1" body="" if [ "$(head -1 "$file" 2>/dev/null)" = "---" ]; then body=$(awk 'BEGIN{fm=0} /^---$/{fm++; next} fm>=2{print}' "$file" 2>/dev/null || true) fi [ -n "$body" ] || body=$(cat "$file") printf '%s\n' "$body" } report_and_exit() { local ok_msg="$1" if [ "${#violations[@]}" -gt 0 ]; then [ "$QUIET" -eq 0 ] && echo "❌ ${2}:" printf '%s\n' "${violations[@]}" >&2 exit 1 fi [ "$QUIET" -eq 0 ] && echo "✅ ${ok_msg}" exit 0 } # ── 模式 1: 扫描目录 ── if [ -n "$SCAN_DIR" ]; then if [ ! -d "$SCAN_DIR" ]; then [ "$QUIET" -eq 0 ] && echo "⚠ PR 目录不存在: ${SCAN_DIR}(跳过)" >&2 exit 0 fi while IFS= read -r f; do [ -f "$f" ] || continue body=$(read_pr_body "$f") unchecked=$(printf '%s\n' "$body" | grep -c '^- \[ \]' || true) if [ "$unchecked" -gt 0 ]; then violations+=("$(basename "$f"): 存在 ${unchecked} 个未勾选子项——所有 checklist 子项须勾选后才可提交 PR") fi done < <(find "$SCAN_DIR" -name '*.md' -type f | sort) report_and_exit "PR 描述文件门禁通过:所有文件子项均已勾选" "PR 描述文件门禁未通过" fi # ── 模式 2: 检查单个 PR 描述文件 ── if [ -n "$PR_FILE" ]; then if [ ! -f "$PR_FILE" ]; then echo "❌ PR 描述文件不存在: $PR_FILE" >&2 exit 2 fi body=$(read_pr_body "$PR_FILE") unchecked=$(printf '%s\n' "$body" | grep -c '^- \[ \]' || true) has_section1=$(printf '%s\n' "$body" | grep -c '全链路检查' || true) has_section2=$(printf '%s\n' "$body" | grep -c '测试分层检查' || true) has_section3=$(printf '%s\n' "$body" | grep -c '质量门禁' || true) if [ "$has_section1" -eq 0 ] && [ "$has_section2" -eq 0 ] && [ "$has_section3" -eq 0 ]; then violations+=("$(basename "$PR_FILE"): 未发现 PR 模板标准节(全链路检查/测试分层检查/质量门禁)——可能不是 PR 描述文件,或模板结构被修改") fi if [ "$unchecked" -gt 0 ]; then violations+=("$(basename "$PR_FILE"): 存在 ${unchecked} 个未勾选子项——所有 checklist 子项须勾选后才可提交 PR") fi report_and_exit "PR 描述文件门禁通过:所有子项均已勾选" "PR 描述文件门禁未通过" fi # ── 模式 3: 检查 PR 模板结构完整性(默认模式)── if [ ! -f "$TEMPLATE_FILE" ]; then echo "❌ PR 模板文件不存在: $TEMPLATE_FILE" >&2 exit 2 fi required_sections=("全链路检查" "测试分层检查" "质量门禁") missing_sections=() for section in "${required_sections[@]}"; do if ! grep -q "^##.*${section}" "$TEMPLATE_FILE"; then missing_sections+=("${section}") fi done if [ "${#missing_sections[@]}" -gt 0 ]; then sections_str=$(IFS=, ; echo "${missing_sections[*]}") violations+=("PR 模板缺失必要节: ${sections_str}(PR 模板须包含全链路检查/测试分层检查/质量门禁三节)") fi checklist_count=$(grep -c '^- \[ \]' "$TEMPLATE_FILE" || true) if [ "$checklist_count" -lt 20 ]; then violations+=("PR 模板中 checklist 子项数量异常(仅 ${checklist_count} 项,预期 ≥ 20 项)——模板可能被意外修改") fi report_and_exit "PR 模板结构门禁通过:${checklist_count} 项子项,三节完整" "PR 模板结构门禁未通过"