- e2e:修死选择器与蜜罐误命中、补断言、GA4/security-headers 用例诚实标注 skip 边界;新增 assert-helpers/fixtures/hydrated/primary-nav/touch-targets 等复用桩。 - 集成层:config/test/jest.integration.config.js + tests-integration/ 真库 一次性 SQLite 用例,teardown 守卫开发库指纹。 - 门禁工具链:jest.setup 归位到根、scripts/accessibility 全站 axe 节点计数、 check-brand-text-token/check-motion-constraints 机械守卫、stryker/lighthouse 配置收敛。
169 lines
6.6 KiB
Bash
Executable File
169 lines
6.6 KiB
Bash
Executable File
#!/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 而非 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-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
|
||
echo "✘ PR 目录不存在: ${SCAN_DIR} —— 门禁根本没跑。退出 2 表示脚本自身/入参故障,不等于通过" >&2
|
||
exit 2
|
||
fi
|
||
scanned=0
|
||
while IFS= read -r f; do
|
||
[ -f "$f" ] || continue
|
||
scanned=$((scanned + 1))
|
||
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)
|
||
if [ "$scanned" -eq 0 ]; then
|
||
echo "✘ ${SCAN_DIR} 内没有任何 *.md —— 0 个文件被检查不等于 0 个违规。退出 2" >&2
|
||
exit 2
|
||
fi
|
||
[ "$QUIET" -eq 0 ] && echo "(已扫描 ${scanned} 个 PR 描述文件)"
|
||
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 模板结构门禁未通过"
|