Files
zhangxiang 03ed066c6c 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 等负路径,均带正控制以防「匹配零的假绿」。
2026-09-20 11:50:58 +08:00

162 lines
6.3 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 模板结构门禁未通过"