79c154f9af
对着试点仓落地时暴露的问题做的口径与模板修正(本轮不推、不打 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。
238 lines
11 KiB
Bash
Executable File
238 lines
11 KiB
Bash
Executable File
#!/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 |