Files
eng-standards-main/scripts/new-project.sh
T
姚章浩 79c154f9af docs: 版本条目改固定四类 + 破坏性变化固定写法 + 构建身份(口径变更)
对着试点仓落地时暴露的问题做的口径与模板修正(本轮不推、不打 tag、不部署):

- 条目形状:四个自定义必填节 → **固定四类** Added/Changed/Fixed/Security
  (分类可省略、不许空栏目);过程记录进 docs/archive/,未合并事项进 docs/BACKLOG.md。
- 破坏性变化/迁移:固定写在 Changed 里、以 `**破坏性**` 开头并**必须链接发布文档**,
  仓内 `docs/….md` 链接**对账**(不存在即红)。
- 构建身份:release_version / build_kind / commit_sha / display_version / built_at;
  开发构建显示 X.Y.Z-dev+g<短SHA>(无基线 dev+g<短SHA>),**不许只显示上一正式版本号、
  不许自称正式版**;release 只认显式发布上下文。
- **还没有正式版本一律放行**:不判红;首个正式版本号**由人指定**(预检把它列成阻塞项)。
- **没核对远端就不许说校验成功**:CI/预检用 `--require-remote`(本地模式明确写「未核对远端」)。
- 修正"CI 取不到 tag 所以不用 tag"的过时口径:正式版本**就是**用注释 tag,
  只是**构建时不要用 git describe 取版本号**;**测试 tag 只在临时测试仓库里创建**。
- 模板同步:templates/check-changelog.py、templates/release-preflight.py、
  新增 templates/version.py;ci-gate.py 的 version 项在 CI 下强制核对远端;
  new-project.sh 落 scripts/version.py 并按四类重写 CHANGELOG 骨架;
  README/07/模板 README 同步;04 增加 §I(V-1~V-4 四条,含负例在哪)。

验证:templates 两个检查器 --self-test 全过;new-project.sh bash -n 通过;
生成的新工程里 check_changelog --tags 绿、--identity 给出 dev+g<短SHA>、
CI=true 下 version 项因"未核对远端"判红(预期);试点仓反向夹具 80/0。
2026-10-13 14:59:48 +08:00

238 lines
11 KiB
Bash
Executable File
Raw 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
# 起一个「默认就合规」的新工程(正本 §8 的自动化部分)。
#
# 用法: new-project.sh <目标目录> <owner/repo> [runner-name]
# 例: new-project.sh ~/proj/my-svc Weknora/my-svc runner-weknora-my-01
#
# 刻意**不覆盖**任何已有文件:目标目录非空就直接拒绝(半途改主意不会留下半成品)。
# 依赖:正本仓本身(templates/ 与已验证的闸门脚本都在里面)—— 不在正本仓里跑会拒。
set -euo pipefail
STANDARDS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TPL="$STANDARDS_DIR/templates"
TARGET="${1:?用法: new-project.sh <目标目录> <owner/repo> [runner-name]}"
REPO="${2:?缺少 owner/repo,例: Weknora/my-svc}"
RUNNER="${3:-${REPO##*/}-runner-01}"
[ -d "$TPL" ] || { echo "❌ 找不到 templates/($TPL)—— 本脚本必须在正本仓里运行" >&2; exit 1; }
case "$REPO" in */*) ;; *) echo "❌ owner/repo 格式不对: $REPO" >&2; exit 1 ;; esac
NAME="${REPO##*/}"
DIR="$(cd "$(dirname "$TARGET")" 2>/dev/null && pwd)/$(basename "$TARGET")"
if [ -e "$DIR" ] && [ -n "$(ls -A "$DIR" 2>/dev/null)" ]; then
echo "❌ 目标目录非空:$DIR(不覆盖任何东西)" >&2; exit 1
fi
say() { printf '\n== %s ==\n' "$*"; }
say "1/7 目录"
mkdir -p "$DIR"
cd "$DIR"
echo " $DIR"
say "2/7 .gitignore / 门禁入口"
cp "$TPL/gitignore" .gitignore
# 门禁入口:**只有一份实现**(scripts/ci_checks.py),verify.sh 只是转发。
# 为什么不再生成一套 bash 探测逻辑:那会和模板/CI 形成第二套步骤,迟早只改一边。
cat > scripts_verify.tmp <<'SH'
#!/usr/bin/env bash
# 本地门禁入口:转发给 scripts/ci_checks.py —— **不要**在这里再写一套检查。
# 为什么:CI 调用的是同一条命令(make verify),两边只有一份实现才不会漂移。
set -uo pipefail
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 跑的是**同一条命令**。
# 实现只有一份:scripts/ci_checks.py(由 templates/ci-gate.py 生成)。
.PHONY: verify check list
verify:
python3 scripts/ci_checks.py
check: verify
list:
python3 scripts/ci_checks.py --list
MK
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
echo " build.yml(只此一个:CI 不含部署)"
echo " 要 CD 时**显式**拷 templates/gitea-prod-deploy.yml 过去 —— 它的未实现步骤故意是红的"
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 不放凭据
cp "$TPL/check-changelog.py" scripts/check_changelog.py # 版本记录检查(正本 §6.6,门禁的 version 项调它)
cp "$TPL/version.py" scripts/version.py # 版本与构建身份(正本 §6.2,唯一来源 CHANGELOG.md)
cp "$TPL/release-preflight.py" scripts/release_preflight.py # 发版预检(**只读**;批准后才 --execute)
chmod +x scripts/ci_checks.py scripts/check_secrets.py scripts/check_changelog.py \
scripts/release_preflight.py .gitea/scripts/*.py .gitea/scripts/*.sh
# 变更记录骨架:**版本号的唯一权威来源就是本文件顶部那条已发布版本**(正本 §6.1)。
# 首次发版前只有 `[Unreleased]` 是正常的(检查会放行;构建拿不到版本号则是另一回事)。
cat > CHANGELOG.md <<'CL'
# Changelog
> 版本号的**唯一权威来源** = 本文件顶部那条已发布版本(正本 §6.1);不放 `VERSION` 文件、不写进源码常量。
> 日常改动写进 `## [Unreleased]`(**不要求每个提交都写**,有使用者可见的变化才记)。
> 正式发版时把它改成 `## [x.y.z] - YYYY-MM-DD`(版本号由人指定:**首次发版前没有已发布版本是正常的**)。
>
> 小节**只认四类**(分类可省略,但不要空栏目):`### Added` / `### Changed` / `### Fixed` / `### Security`。
> **破坏性变化**写在 `### Changed` 里、以 `**破坏性**` 开头,并链接发布文档(升级/迁移与回退步骤)。
>
> 构建身份:`python3 scripts/version.py --identity`(release_version / build_kind / commit_sha /
> display_version / built_at)。正式发版流程见规范仓 `07-发版操作说明(tag与Release).md`;
> 本仓只读预检:`python3 scripts/release_preflight.py`。
## [Unreleased]
### Added
- (这次新增了什么 —— 写使用者能感知的变化,不逐条抄提交)
### Changed
- (行为调整)
### Fixed
- (修了什么)
### Security
- (安全相关;没有就整节删掉,别留空栏目)
CL
echo " CHANGELOG.md(版本权威来源骨架)"
# 测试范围声明:门禁的 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'
import re, sys
src, name, repo = sys.argv[1:4]
t = open(src, encoding="utf-8").read()
m = re.search(r"^````markdown\n(.*?)^````", t, re.S | re.M)
if not m:
sys.exit("❌ templates/README.md 里找不到 ```markdown 骨架块")
body = m.group(1)
body = body.replace("<PROJECT>", name).replace("<ns>/<repo>", repo)
sys.stdout.write(body)
PY
echo " AGENTS.md($(wc -c < AGENTS.md) 字节)"
say "6/7 docs/提交规范.md"
cat > docs/提交规范.md <<MD
# 提交规范
正本:Git / 提交 / CI-CD 规范(Gitea 版)—— 仓库 \`zheromiku/eng-standards-main\`,本机工作副本 \`~/eng-standards/\`。
## 格式
\`\`\`
<type>(<scope>): 一句话说清"改了什么"
正文:为什么这么改 / 现象 / 根因 / 怎么验的。
\`\`\`
- \`type\`:feat fix docs style refactor perf test build ci chore revert
- \`scope\`:本仓库的真实模块名,可选,小写
- 冒号后一个空格;描述 ≥3 字符;标题 **≤72 字符、结尾不加句号**
- 放行自动生成的提交:\`Merge \` / \`Revert "\` / \`fixup! \` / \`squash! \` / \`amend! \`
- **一件事一个提交**,且每个提交都要能各自编译/构建通过
## 正文写"为什么",不写"是什么"
diff 已经写了"是什么"。正文回答:当时为什么这么改、试过什么、怎么确认它对了。
四种要素:**现象** / **根因**(具体到文件·行·机制)/ **取舍**(为什么不用那个更自然的写法)/ **证据**(跑了什么、量到了什么)。
## 两条闸门
1. 本机 hook \`~/.githooks/commit-msg\`(\`core.hooksPath\` 全局生效)—— 快速反馈
2. CI \`.gitea/scripts/ci_gate.py\` —— 权威闸门,在**编译之前**执行
⚠️ \`--no-verify\` 只骗得过第一条。
## 写法禁忌
不要在提交信息里用反引号与 \`\$()\`:shell 会先做命令替换,轻则信息被改写,重则**执行**了里面的东西,
而退出码仍是 0、没有任何报错。一律用带引号的 heredoc:
\`\`\`bash
git commit -F - <<'EOF'
fix(parser): 修复表格解析越界导致的 500
根因:……
EOF
\`\`\`
MD
say "7/7 git 仓库"
git init -q -b main
echo " 已初始化(默认分支 main),未提交"
cat <<EOF
== 下一步 ==
1) cd "$DIR" && git add -A
git commit -F - <<'MSG'
chore(init): 初始化工程骨架(CI + 门禁 + 提交规范)
由 ~/eng-standards/scripts/new-project.sh 生成:提交信息闸门、编译自检、
静态产物预览与微信上报(ci-bridge)。正本见 zheromiku/eng-standards-main。
MSG
2) 建远端并在网页建仓:$REPO(**不要**勾"初始化仓库")
git remote add origin https://gitea.zheromiku.top/$REPO.git && git push -u origin main
3) 挂仓库级 Runner + bridge 变量(幂等):
bash /srv/aiball/runner/attach_repo.sh $REPO $RUNNER
4) (可选)需要"谁都不能直推"时才开分支保护;**默认不开**(正本 §1.3)
5) **反向自检**(正本 §3.2-3):
python3 scripts/ci_checks.py --list # 看必需检查清单
python3 scripts/ci_checks.py --self-test # 证明汇总逻辑真的会红
推一个故意违规的提交,确认 gate 红、且 build 没跑
6) 部署(CD)**不在这里,默认也不生成**:本骨架只做 CI。要部署时**显式**把
templates/gitea-prod-deploy.yml 拷成 .gitea/workflows/deploy-production.yml,
并接受它"未实现的步骤就是红的"(占位不许返回成功)。别把 deploy 作业塞回 build.yml。
⚠️ 这段说明写在这里、不写进那个 heredoc 里的原因:外层 heredoc 没加引号,
反引号会被 shell 先执行 —— 上面这行原先带反引号,输出里那段字直接消失了(退出码仍是 0)。
EOF