chore(repo): 移植 PR-First 提交流程门禁(脚本 + Gitea PR 模板)

对齐 novavis AGENTS.md §27,逐项按本仓实测改写,不照抄不存在的命令:
- scripts/check-pr-checklist.sh:三种模式(模板结构 / 单 PR 文件 / --pr-dir 扫描)
  与退出码 0/1/2 保持同源;模板根改为按脚本自身位置解析并保留 AGENT_PROJECT_DIR
  覆写;修上游 --pr-dir 的参数解析缺陷(for arg in "$@" 内 shift 不消费值,
  目录会被再当成 PR 文件),改 while+shift
- .gitea/PULL_REQUEST_TEMPLATE.md:30 项 checklist 全部换成本仓真实门禁命令
  (type-check / lint / test:unit / test:coverage 阈值 / Playwright / visual /
  check:contrast / check:headings / lighthouse / test:security:headers),并补
  CMS 字段-需重跑 seed 声明、:3000 旧预览不作数、暗黑 token、动效 180–280ms、
  品牌红触达、数字 basis 口径等本仓专有约束
- 文档同源:docs/development/quality-gates.md 新增「提交与 PR 流程」权威副本
  (AGENTS.md 被 .gitignore 排除,不可依赖)、CLAUDE.md 补 Submission Flow 节

门禁自检:脚本 bash -n 通过;11 个用例覆盖三模式 + 缺节/缺文件/子项不足/非 PR
文件/未知选項/--quiet 等负路径,均带正控制以防「匹配零的假绿」。
This commit is contained in:
2026-09-20 11:50:58 +08:00
parent 4dcc15ebd0
commit 03ed066c6c
4 changed files with 251 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
#!/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-description-file> # 检查 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 而非 forfor 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-file> # 检查 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 模板结构门禁未通过"