fix(gen): 生成器落齐门禁/预览/检出/安全脚本与策略骨架

新增落盘:check_secrets.py、ci_preview.py、ci_checkout.sh、test-scope.json(合法但空的骨架)、.gitea/required-contexts.txt(故意留空并说明填法 —— 不预填猜出来的 context,否则会把没跑过必需检查的 PR 放过去)。
This commit is contained in:
姚章浩
2026-10-12 11:15:32 +08:00
parent 05c8395bc0
commit 27db54c44e
+61 -66
View File
@@ -34,81 +34,73 @@ echo " $DIR"
say "2/7 .gitignore / 门禁入口"
cp "$TPL/gitignore" .gitignore
# verify.sh:本地门禁 —— 必须与 CI 的 build 作业同一条命令、串行、失败即停(正本 §3.1)
# 门禁入口:**只有一份实现**(scripts/ci_checks.py),verify.sh 只是转发。
# 为什么不再生成一套 bash 探测逻辑:那会和模板/CI 形成第二套步骤,迟早只改一边。
cat > scripts_verify.tmp <<'SH'
#!/usr/bin/env bash
# 本地门禁:一条命令跑完、串行、失败即停、末尾汇总(正本 §3.1)。
# 与 CI 的 build 作业**同一套步骤**,只允许两处刻意差异(正本 §3.1):外部环境对拍在 CI 跳过、对拍单独一步。
# 本地门禁入口:转发给 scripts/ci_checks.py —— **不要**在这里再写一套检查。
# 为什么:CI 调用的是同一条命令(make verify),两边只有一份实现才不会漂移。
set -uo pipefail
LOG="$(mktemp)"; fail=0; n=0; ran=""
step() { n=$((n+1)); echo ""; echo "── [$n] $* ──"; "$@" 2>&1 | tee -a "$LOG"; return "${PIPESTATUS[0]}"; }
export PATH="/opt/citools/go/bin:/opt/citools/bin:$PATH"
if [ -f go.mod ]; then
ran="go"
check_gofmt() { # ⚠️ gofmt -l 列出文件时**退出码仍是 0**:判输出,不判退出码
U="$(gofmt -l $(git ls-files '*.go' 2>/dev/null) 2>/dev/null)"
[ -z "$U" ] || { echo "未格式化的文件:"; printf '%s\n' "$U"; return 1; }
}
step check_gofmt || fail=1 # 格式:CI 会因为这一条红
step go vet ./... || fail=1 # 静态检查:类型错了后面全是噪声
step go build ./... || fail=1 # 构建
step go test ./... || fail=1 # 单测(Go 会自己编译测试程序;不是"测构建产物")
fi
if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f setup.py ]; then
ran="$ran python"
step ruff format --check . || fail=1
step ruff check . || fail=1
step uv run pytest -q || fail=1
fi
if [ -f package.json ]; then
ran="$ran node"
if [ -f package-lock.json ]; then
step npm ci || fail=1 # 按锁装;缺锁**不**退化成 npm install(那会重写锁文件)
else
echo "❌ 没有 package-lock.json:不接受无约束安装(先提交锁文件)"; fail=1
fi
if grep -q '"format:check"[[:space:]]*:' package.json 2>/dev/null; then
step npm run format:check || fail=1
else
echo "❌ package.json 未声明 format:check —— 前端格式没有门禁(加 prettier --check)"; fail=1
fi
step npm run build || fail=1
step npm test || fail=1
fi
if [ -z "$ran" ]; then echo ""; echo "⚠️ 未识别到构建入口(go.mod / pyproject.toml / package.json 都没有)—— 入口待补,不算全绿"; fail=1; fi
echo ""; echo "════════════════════════════════════════"
if [ "$fail" -eq 0 ]; then
echo "✅ 本地门禁全过"
rm -f "$LOG" # 夹具要打扫(正本 §3.2-5):成功时不留日志,否则 $TMPDIR 会攒到几万条
else
echo "❌ 门禁未通过 —— 先修它,再提交"
echo "(完整日志留在:$LOG)"
fi
exit "$fail"
exec python3 "$(dirname "$0")/ci_checks.py" "$@"
SH
mkdir -p scripts && mv scripts_verify.tmp scripts/verify.sh && chmod +x scripts/verify.sh
cat > Makefile <<'MK'
# 本地门禁入口:一条命令跑完(正本 §3.1)。CI 的 build 作业跑同一套步骤。
# 名字统一用 `verify`(如本环境另有一套叫 `check` 的历史习惯,让它成为别名,别起第二套逻辑)。
.PHONY: verify check
# 本地门禁入口:一条命令跑完(正本 §3.1)。CI 跑的是**同一条命令**。
# 实现只有一份:scripts/ci_checks.py(由 templates/ci-gate.py 生成)。
.PHONY: verify check list
verify:
bash scripts/verify.sh
python3 scripts/ci_checks.py
check: verify
list:
python3 scripts/ci_checks.py --list
MK
say "3/7 CI 工作流"
say "3/7 CI 工作流(只做 CI;**默认不生成 CD 工作流**)"
mkdir -p .gitea/workflows .gitea/scripts docs
sed "s|<PROJECT>|$NAME|g" "$TPL/gitea-build.yml" > .gitea/workflows/build.yml
sed "s|<PROJECT>|$NAME|g" "$TPL/gitea-prod-deploy.yml" > .gitea/workflows/deploy-production.yml
sed "s|<PROJECT>|$NAME|g" "$TPL/gitea-build.yml" > .gitea/workflows/build.yml
echo " build.yml(只此一个:CI 不含部署)"
echo " 要 CD 时**显式**拷 templates/gitea-prod-deploy.yml 过去 —— 它的未实现步骤故意是红的"
say "4/7 闸门与上报脚本(从正本仓逐字节复制)"
cp "$TPL/ci_gate.py" .gitea/scripts/ci_gate.py
cp "$TPL/ci_report.py" .gitea/scripts/ci_report.py
chmod +x .gitea/scripts/*.py
say "4/7 门禁实现、上报、预览、检出脚本"
cp "$TPL/ci-gate.py" scripts/ci_checks.py # 权威门禁:唯一一份检查实现
cp "$TPL/check-secrets.py" scripts/check_secrets.py # 密钥扫描(门禁的 security 项会调它)
cp "$TPL/ci_gate.py" .gitea/scripts/ci_gate.py # 提交信息闸门(按准确 Git 范围校验)
cp "$TPL/ci_report.py" .gitea/scripts/ci_report.py # 核对报告 + 上报(--verify-only 可单独核对)
cp "$TPL/ci_preview.py" .gitea/scripts/ci_preview.py # **先核对完整报告,再发预览**
cp "$TPL/ci_checkout.sh" .gitea/scripts/ci_checkout.sh # 检出:固定事件 SHA、协议不降级、URL 不放凭据
chmod +x scripts/ci_checks.py scripts/check_secrets.py .gitea/scripts/*.py .gitea/scripts/*.sh
# 测试范围声明:门禁的 tests 项要读它("允许跳过哪些用例""前端用什么跑测试")。
# 给一份**合法但空**的骨架 —— 空声明不等于"随便跳",必需用例被跳过照样红。
cat > scripts/test-scope.json <<'JS'
{
"_doc": [
"测试范围与前端测试框架的声明(门禁的 tests 项读它)。strict schema:",
"· optional 条目必须给 package 或 test(至少一个)—— 两个都不给会通配任意跳过,不允许;",
"· 每条必须有 reason 与 owner;package 级条目还要给 expected_skips(跳过数不许无声明地涨);",
"· review_by 是到期日(YYYY-MM-DD),过期 ⇒ 整份声明失效。",
"frontend_runner:前端用什么跑测试(vitest / jest)。不写就按 package.json 依赖认;认不出来 ⇒ 明确失败。"
],
"frontend_runner": "vitest",
"optional": []
}
JS
echo " scripts/test-scope.json(空声明骨架:允许跳过哪些用例要在这里写明)"
# 合并策略文件:**必须由你填**,不填就等于没有"要求哪些检查" ⇒ 合并会被拒绝。
# 这里不预填一个猜出来的 context —— 猜错了会把没跑过必需检查的 PR 放过去。
cat > .gitea/required-contexts.txt <<'CTX'
# 合并前必须在 PR 头提交上 success 的 status context(一行一个,`#` 后是注释)。
#
# ⚠️ 这份文件是"要求哪些检查"的**唯一来源**:合并脚本只认它或显式 --require-context,
# 不接受"观察到的状态"。文件为空 / 只有注释 ⇒ 视为没有策略 ⇒ 拒绝合并。
#
# 怎么填:workflow 跑过一次之后,看那次提交的 status context(Gitea 界面或
# GET /api/v1/repos/<owner>/<repo>/commits/<sha>/status ),把它逐行抄下来。
# 例如:build / build (push)
CTX
echo " ⚠️ .gitea/required-contexts.txt 还是空的:填上真实 context 之前,--merge-pr 会拒绝合并"
say "5/7 AGENTS.md(从 templates/README.md 的骨架抽取并替换)"
python3 - "$TPL/README.md" "$NAME" "$REPO" > AGENTS.md <<'PY'
@@ -193,11 +185,14 @@ cat <<EOF
4) (可选)需要"谁都不能直推"时才开分支保护;**默认不开**(正本 §1.3)
5) **反向自检**(正本 §3.2-3):推一个故意违规的提交,确认 gate 红、且 build 没跑
5) **反向自检**(正本 §3.2-3):
python3 scripts/ci_checks.py --list # 看必需检查清单
python3 scripts/ci_checks.py --self-test # 证明汇总逻辑真的会红
推一个故意违规的提交,确认 gate 红、且 build 没跑
6) 部署(CD)**不在这里**:本骨架只做 CI。要部署时**另写一个**独立 workflow
(只由 workflow_dispatch 触发,且 cancel-in-progress 必须为 false),
别把 deploy 作业塞回 build.yml ——占位作业会打印 TODO 然后返回成功,那是假报成功(正本 §8)。
6) 部署(CD)**不在这里,默认也不生成**:本骨架只做 CI。要部署时**显式**把
templates/gitea-prod-deploy.yml 拷成 .gitea/workflows/deploy-production.yml,
并接受它"未实现的步骤就是红的"(占位不许返回成功)。别把 deploy 作业塞回 build.yml。
⚠️ 这段说明写在这里、不写进那个 heredoc 里的原因:外层 heredoc 没加引号,
反引号会被 shell 先执行 —— 上面这行原先带反引号,输出里那段字直接消失了(退出码仍是 0)。
EOF