对着试点仓落地时暴露的问题做的口径与模板修正(本轮不推、不打 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。
21 KiB
模板评估与修正(附件 eng-standards-main.zip)
结论先说:原稿的"地基"(四条铁律、反向自检、"为什么"写在正文、门禁串行失败即停)全部保留; 但它整体是 GitLab / glab 口径,而本环境是 Gitea 1.26.2 + act_runner + 自建 ci-bridge + 阿里云双机。 逐条评估结果如下。每条的"证据"都是本次在本机/实例上实测到的,不是推断。
一、结论
| 分类 | 数量 | 处理 |
|---|---|---|
| A. 平台错配(GitLab 语义在本环境不存在) | 6 | 全部改写为 Gitea 口径 |
| B. 与实测事实矛盾(原稿断言不成立) | 3 | 修正断言,写清成立条件 |
| C. 模板内部自相矛盾 / 数值不可信 | 3 | 改掉矛盾点 |
| D. 具体取值是他人环境的 | 3 | 全表换成"本环境事实"(README.md §1) |
| E. 覆盖缺口(原稿根本没约束) | 1 | 新增铁律(同一份构建产物,规范 §5.1) |
A. 平台错配(6 处)
A1. 触发体系:workflow: rules / $CI_* 全部不存在
- 原稿:
.gitlab-ci.yml的workflow.rules用$CI_PIPELINE_SOURCE == "web"与$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH两条件。 - 实测:本实例是 Gitea Actions,
.gitea/workflows/*.yml,触发在on:;可用变量是GITHUB_*(GITHUB_REF_NAME、GITHUB_REPOSITORY、GITHUB_SERVER_URL)。 - 修正:见规范 §4.1,三类入口写成
on.push.branches与on.workflow_dispatch。原稿那条"刻意不接 tag"的判断保留,理由改写(见 A2)。
A2. 手动作业语义:when: manual 的"阻塞型"论证是 GitLab 专有
- 原稿:手动作业没有
allow_failure: true时流水线永远到不了success(停在blocked),取消会把已成功作业一起标canceled。 - 实测:Gitea 里
if:不成立的作业是 skipped,不会把运行挂死;生产发布的正确形态是独立的workflow_dispatch运行。 - 修正:保留原稿的真问题("取消 ≠ 回滚"、"不要重写会抹掉证据"),但改挂到 Gitea 的真实风险上:
concurrency.cancel-in-progress会取消正在进行的部署。生产 workflow 必须cancel-in-progress: false。
A3. resource_group 在 Gitea 1.26.2 不存在
- 原稿:同一环境的部署用
resource_group: <env>防并发。 - 实测:Gitea 没有
resource_group;concurrency:只对同 workflow 同 group 生效,跨 workflow / 人手手动执行挡不住。 - 修正:互斥下移到目标机(
flock -n,规范 §5.4)。这是"可靠性只在这一层"的取舍,必须写明。
A4. 变量作用域:Gitea 没有 environment scope,也没有 File 类型 secret,勾选坑不同
- 原稿:GitLab CI/CD Variables 用
environment scope区分环境;有 "Expand variable reference" / "Masked" / "Protected" 三个勾选坑;私钥用 File 类型(因为多行勾不了 Masked)。 - 实测:Gitea 仓库变量/密钥没有环境作用域;私钥只能整体存 secret(无 File 类型);三个勾选坑不存在。
- 修正:环境区分改用命名后缀(
DEPLOY_TEST_*/DEPLOY_PROD_*);私钥由运行时脚本落盘 0600(规范 §7.1 / §7.3)。 另有接口形状要写对(实测):变量POST/PUT …/actions/variables/{name}body{"value":…};密钥只有PUT …/actions/secrets/{name}body{"data":…};集合级 POST 返回 405 空 body(像权限问题,实际不是)。
A5. CLI:glab → giteactl / gitea_api.py
- 原稿:§6 整章讲
glab,含projects/<ns>%2F<repo>显式路径坑(细粒度只读 token 下:id占位符会 403)。 - 实测:本机没有
glab;有~/.local/bin/giteactl(runs/show/logs/watch/rerun)、~/.hermes/scripts/gitea_api.py、giteacli(本机 token 缺read:user,whoami/login会失败但仓库功能可用)。 - 修正:§9 速查表与 §02 Runbook 换成 giteactl/gitea_api.py 的真实命令。原稿"读日志用显式路径、不用交互式 TUI、trace 要清洗"的经验保留,但换成 Gitea 的日志接口地址。
A6. GIT_DEPTH: 1 / after_script + $CI_JOB_STATUS 通知 / tags: [docker]
- 原稿:CI 变量
GIT_DEPTH: 1;通知用after_script拿$CI_JOB_STATUS;Runner 标签docker。 - 实测:本环境是自 clone 模型(
git fetch --depth 50);通知走.gitea/scripts/ci_report.py+if: always()步骤 + 主机侧 bridge;Runner 标签是ubuntu-latest/24.04/22.04。 - 修正:全部替换。原稿"通知失败不能让作业失败"、"可能的话两条通知链路分开"的判断保留。
B. 与实测事实矛盾(3 处)
B1. "不用任何第三方 action" —— 本环境不成立
- 原稿:文件头写"不用任何第三方 action(作业容器不联网拉 action)",并把它当铁律。
- 实测:
aiball/software-pc的.gitea/workflows/validate.yml用了actions/checkout@v4+actions/setup-node@v4,run#1 = success(GET /repos/aiball/software-pc/actions/runs显示completed success)。而 Weknora 三仓的 runner 上工作流刻意自 clone。 - 修正:规则改成按 Runner 配置区分(规范 §4.3):默认不用
uses:;例外要在文件头写明该 runner 支持取 action。并给出一条判据:"时红时绿且报拉取失败"先怀疑uses:。
B2. .gitignore 模板与正本条款自相矛盾
- 原稿:正本 §1.3 明说"处理办法不是加
!例外(例外越加越多,规则本身就没人看得懂了)";但templates/gitignore里就给了!src/secrets/和!src/secrets/**。 - 实测:原稿自己的
spec-check.mjs的ARTIFACT_PREFIX正则包含secrets/—— 于是"按模板加了例外、又真把src/secrets/入库"的工程会被tracked-artifacts判红。 - 修正:模板去掉
!例外,改为一句话:"别在源码路径里用dist/data/secrets这些名字"(见templates/gitignore)。
B3. spec-check.mjs 的 AGENTS_MAX_BYTES = 32768 与正本数字不一致
- 原稿:正本 §2.4 写"实测 65983 字节时尾部 739 字节被静默截断";而守门判据硬编码
AGENTS_MAX_BYTES = 32768。 - 矛盾点:正本自己立了规矩"文档里不许出现会漂移的数字,要么由脚本打印要么写成断言"(§4.3),却在这里放了一个来源不明的常量。
- 修正:本次不引入该脚本(任务边界是"只制定规范"),但在
03-待配置事项.md第 16 项写明:改写 Gitea 版守门时,上限值必须由一次真实测量得出并写成断言,不许手抄。
C. 模板内部问题(3 处)
C1. templates/verify.mjs 写死跨机路径
- 原稿:门禁第 5 步
cmd: [process.execPath, '__STANDARDS_DIR__/scripts/spec-check.mjs', ...]—— 一个绝对路径,在 CI 容器里根本不存在(原稿自己的validate:repo注释也承认了这一点)。 - 修正:门禁入口只跑本仓的东西;规范守门要么随仓携带(
.gitea/scripts/),要么在 CI 里先git clone规范仓库到固定路径。见templates/gitea-build.yml的validate作业注释。
C2. spec-check.mjs 两条判据过窄/无对象
ci-cache-node-modules:只匹配- node_modules/这一种写法;本环境的缓存是主机挂载目录(/opt/citools-cache),判据完全够不着。glab-placeholder:在 Gitea 环境里没有glab,判据无对象(永远是 ok)。- 修正:说明见
03-待配置事项.md第 16 项。同时保留原稿两条好经验:判据要认"这件事"而不是"那种写法"(原稿自己在 v1.0.4 里踩过)、并且"报错文案里也不能出现针的字面量"(会被下一次扫描命中)。
C3. check_env.sh 的必需键是按"网关 + Cookie"项目定的
- 原稿:
REQUIRED="ENV_NAME APP_DIR DOMAIN COOKIE_NAME",并断言"两个环境的父域 + Cookie 名不能同时相同"。 - 实测:该不变量属于原稿样板项目(带 nginx + Cookie 的网关)。本环境的项目是 Go 服务(DobotAI / knowledge-governance / adapter),没有这一对不变量;DobotAI 的环境参数在生产机
/opt/dobotai/config/dobotai.env(不入库),也不是deploy/env/*.env。 - 修正:本规范不照搬该脚本。保留它的形状("仓库层面的不变量放在 CI 校验,在传任何字节之前失败"),但要按项目定键(规范 §7.2)。
D. 具体取值是他人环境的(3 处 → 全表替换)
| 原稿取值 | 本环境实际 |
|---|---|
https://gitlab.artificialheart.cn/liuzhufu/...、默认组 liuzhufu |
http://192.168.50.223:3000(https://gitea.zheromiku.top);组 Weknora / aiball |
Runner 标签 docker;镜像 node:24-alpine |
标签 ubuntu-latest/24.04/22.04 → docker://docker.gitea.com/runner-images:* |
APP_DIR=/volume1/docker/...(群晖 NAS)、域名 *.artificialheart.cn / *.dobotoa.cn |
阿里云 ECS:测试 120.76.217.51、生产 120.76.218.137,路径 /opt/dobotai/... |
以上全部换成了 README.md §1 的唯一取值表,并给了取数命令(避免手抄漂移)。
E. 覆盖缺口(1 处 → 新增铁律)
E1. 「生产使用测试验收过的同一份构建产物」原稿没有约束
- 原稿:§5.5 讲产物按生命周期拆包、§5.7 讲目标机三层布局与回滚,但没有要求"测试与生产部署同一份产物"。
- 实测:本环境现状恰恰相反 —— DobotAI 的既有流程是在目标机上各自 build(生产机 clone 源码 +
docker build),测试与生产不是同一份二进制。 - 修正:作为本次规范的第一优先级新增(规范 §5.1):构建只做一次 → 产出
image_id+tar_sha256写进发布清单 → 测试与生产都docker load同一份 tar 并各自断言image_id相同。并给出完整的证据链表(§5.1)与"迁移不属于镜像、先迁移后切镜像"的例外。
F. 保留的(原稿最有价值的部分,不要因为"平台换了"就丢掉)
- §0 四条地基:结论只来自可复现命令 / 每个校验都要反向自检 / 密钥不进命令行 / 提交前跑门禁。
- §3.2 八条通用铁律:只有"跑产物"的套件要排在构建之后(不是"所有单测都依赖预构建产物")、干净 clone、反向自检三问(会红吗 / 会假绿吗 / 真的作用在目标上吗)、断言要有唯一命中点、夹具要打扫、先确认无遗留进程。
- §2.2 正文写"为什么" 的四种要素(现象/根因/取舍/证据)与示例形状。
- §2.3 heredoc 禁忌(反引号
$()会被 shell 先执行,且退出码仍是 0)。 - §1.3
.gitignore是契约 与两条反查命令。 - §5.4 密钥只走 stdin 写文件、
chmod 600、去 CRLF。 - §5.7 目标机三层布局 与"停容器/换目录必须在同一个脚本里、顺序不许挪"。
- §5.6 不缓存
node_modules。 - §2.4 陷阱库拆两份(指令文件放索引、旁边放全文,因为指令有字节预算)。
- §11 规范自己怎么被管理(单一来源、投影、可检验、有版本)—— 见本目录的
README.md与03-待配置事项.md第 13 项。
G. 第二轮审查修正(模板/生成器/接入器,2026-10-12)
审查(GPT,第二轮)用模拟项目逐个打穿了模板的假检查。逐条修法与判据:
| 编号 | 问题(实际证据) | 修法 | 负例在哪 |
|---|---|---|---|
| T-1 | 通用 Python 检查空跑:模拟 Python 项目,依赖/格式/构建/测试全返回成功,实际没执行 | templates/ci-gate.py 的 Python 分支真跑 uv sync --frozen / uv run ruff format --check / compileall / uv run pytest;工具缺失 ⇒ FAILURE(need() 抛错) |
夹具 gate-py-neg:抹掉 uv 后五项全红;补上工具后五项真绿;把一个用例改失败 ⇒ pytest 退出码 1 红 |
| T-2 | 前端测试忽略进程失败:测试进程退出 1、JSON 写 pass,门禁仍绿 | judge_frontend() 把退出码当判据;空套件 / skip / todo / 没报告 ⇒ 红;框架读 test-scope.json 声明,不写死 vitest |
夹具 gate-node-neg:A 例(rc=1 + 报告写 pass)红、B 例(rc=0 + 报告全过)绿 |
| T-3 | 通用清单没有安全项 | 清单新增 security(密钥扫描 + 依赖漏洞,工具缺失/输出解析不了 ⇒ 红);模板带 templates/check-secrets.py,生成器落到 scripts/check_secrets.py |
--self-test 含清单比对 |
| T-4 | test-scope 可用空对象逃逸:条目缺 package/test 时会通配任意跳过 | 严格 schema:必须给 package 或 test 至少一个 + reason + owner;package 级还要 expected_skips(跳过数涨了要重新声明);review_by 过期 ⇒ 整份失效 |
--self-test 分支「范围声明有问题 ⇒ 红」 |
| T-5 | 预览早于报告核对(模板里还 cat /tmp/ci-sha、跨作业 /tmp 假契约) |
模板 workflow 收成单作业;预览改调 .gitea/scripts/ci_preview.py(先 verify(),不过则 0 请求) |
项目侧 scripts/ci_preview_negative_test.py(假 bridge 数请求,10 分支) |
| T-6 | 协议被拼成 http | 检出抽成 templates/ci_checkout.sh:remote 用 GITHUB_SERVER_URL 原样拼 + 断言不降级 + URL 不带凭据 + 顺带取事件 before |
ci_checkout.sh --print-remote:https/http 保留、ftp:// 拒绝;https 真跑一次检出验证 |
| T-7 | 合并必需 context 可为空 | adopt-existing.py:必需 context 只认显式策略(--require-context / .gitea/required-contexts.txt),空/缺失 ⇒ 拒绝;并核对 base/state/head 仓库 |
--self-test-chain(假 Gitea + 真实 merge_pr(),13 分支) |
| T-8 | 提交范围可能漏查 | .gitea/scripts/ci_gate.py 以 Git 范围为准(before..after / 新分支 after --not --remotes);声明与范围不一致、空范围、浅克隆缺 before、列表提交不在检出 ⇒ 拒绝;手工运行只核 HEAD 并写明 |
--self-test 11 分支(含"声明 6 条 / 列表 1 条 / Git 2 条 ⇒ 报错") |
| T-9 | 通知里的链接指向内网地址(群里点不开/换网就失效) | 上报脚本新增链接基址 CI_PUBLIC_BASE_URL(非秘密,workflow 传给上报脚本):run/提交/变更链接统一用它拼;缺失/非法明确回退 GITHUB_SERVER_URL 并写 link_base_source;bridge 可信来源(ci_base_url,支持多个)同步登记;不做消息文本的全局 URL 替换 |
项目侧真实链路用例:配了公网域名 ⇒ 三类链接都用它且保留真实 repo/SHA/run_id、链接目标只有可信来源;未配/非法(ftp://、带 userinfo、带空白)⇒ 回退且消息里不出现非法域名;提交标题里的 URL 仍是文本;真实对象的链接可达(匿名与带 token 各测) |
生成器同步:new-project.sh 现在会落 ci_preview.py、ci_checkout.sh、check_secrets.py、
test-scope.json(合法但空的骨架)、.gitea/required-contexts.txt(故意留空并要求你填 —— 不预填猜出来的 context,
否则会把没跑过必需检查的 PR 放过去)。
口径修正(文档):模板不再声称"CI 能拦住直推";"所有单测都依赖预构建产物"改成"只有跑产物的套件才排在构建之后";
通用模板默认不携带任何部署步骤,占位一律 exit 1(不许假报成功)。
H. 第三轮审查修正(启动阻断与范围,2026-10-12)
| 编号 | 问题(实际复现) | 修法 | 负例在哪 |
|---|---|---|---|
| U-1 | 空工作区起不来:模板 workflow 第一步 bash .gitea/scripts/ci_checkout.sh —— Runner 不自动检出,工作区为空时脚本不存在,实测 127 |
模板 workflow 第一步改成最小 bootstrap(只用 runner 环境变量:协议/凭据校验 → git init+remote add → 取事件完整 SHA → checkout → 断言 HEAD==事件 SHA);检出后才调 ci_checkout.sh --post-checkout |
kg 的 scripts/ci_chain_fixture.py:把模板/YAML 里那段 run: 原样抽出来在空目录执行;并复现旧写法的 127 |
| U-2 | 按任意 SHA fetch 不可用:分支前进时 git fetch origin $SHA → 服务器不允许请求未公开的对象 |
改为"取分支历史 + --deepen 再核事件 SHA",仍找不到才失败(提示提高深度) |
同上(C 例:取不到 SHA 必须非 0) |
| U-3 | 新分支范围恒为空:after --not --remotes 把 origin/<本分支>(= after)当排除集,排除自身 ⇒ 0 条并失败 |
新分支基准 = 远端默认分支:refs/remotes/origin/<default>..<after>;取不到基准 ⇒ 明确失败 |
ci_gate.py --self-test 加了回归用例(origin/<本分支> == after 时仍须算出那几条)+ 调用链夹具 D 例(合规 0 / 违规 1,且红在那条违规提交上) |
| U-4 | Python 未锁定/未隔离:缺 uv.lock 回退 uv sync;requirements 路径用 uv pip install --system;uv run 未冻结 |
必须 uv.lock ⇒ uv sync --frozen;所有检查 uv run --frozen;断言 sys.prefix != sys.base_prefix(项目内 .venv);只有 requirements.txt ⇒ 明确失败并要求先生成锁 |
夹具 gate-py-neg(无锁红 / 有锁绿 + 隔离打印)+ 夹具脚本第 21 节(静态查 --system 必须不存在) |
顺序口径(模板注释里也钉住了):最小 bootstrap → 仓内脚本 → 环境 → 门禁入口 → 权威门禁 → 核对报告 → 预览 → 上报。
不能在检出前依赖仓内文件;预览仍然排在"报告核对"之后。
| U-5 | 把 total_commits 当声明条数:真实新分支推送时 Gitea 给 total_commits=0、列表被截断(5 条),Git 范围 12 条 ⇒ 严判让门禁在真实 CI 首跑就红(与代码质量无关) | Git 范围是唯一权威:total_commits > 0 才交叉核对;<=0/缺失视为"未声明",以 Git 范围为准并把"负载条数不可信"写进来源;--self-test 加 total_commits=0 分支 | 真实 run #16(ci-trial-20261012);自检 15 分支 |
I. 第四轮修正(版本与变更记录规范化,2026-10-13)
按试点仓 knowledge-governance 落地时暴露的三个问题(CHANGELOG 混入审查流水与命令;
文档说"不使用 tag"但 CI 已在核验 tag;开发构建沿用上一正式版本号)做的口径与模板修正。
| 编号 | 问题 | 修法 | 负例在哪 |
|---|---|---|---|
| V-1 | 变更记录变成过程日志:[Unreleased] 里塞着审查轮次、测试命令、待办 ⇒ 发布记录读不出"这次发了什么" |
条目形状由"四个自定义必填节"改为固定四类(Added/Changed/Fixed/Security,分类可省略、不许空栏目);过程记录迁 docs/archive/,未合并事项迁 docs/BACKLOG.md;不要求每个提交都改 |
check-changelog.py --self-test:自创栏目 / 空栏目 / 重复 / 顺序倒置 / 非法日期 / 前导零 逐条报红;项目夹具真改文件再恢复 |
| V-2 | 破坏性变化无处安放:混在"兼容性与迁移影响"里,且可能链接到不存在的文档 | 固定写在 ### Changed 里、以 **破坏性** 开头,必须链接发布文档;仓内 docs/….md 链接对账(不存在即红) |
夹具:无链接 ⇒ 红;链接 docs/NO_SUCH_DOC.md ⇒ 红;写在 ### Added 里 ⇒ 红(位置固定) |
| V-3 | 开发构建沿用上一正式版本号,分不清开发版与正式版;且"还没有正式版本"会被当成错误 | 新增构建身份(release_version / build_kind / commit_sha / display_version / built_at):开发显示 X.Y.Z-dev+g<短SHA>、无基线 dev+g<短SHA>、发布才是 X.Y.Z;build_kind=release 只认显式上下文;还没有正式版本一律放行(不判红,首个版本号由人指定) |
internal/version 契约:开发构建带 dev 标识且不等于正式版本号;负例"自称正式版 ⇒ 必须红"(门禁里真跑这条负例);夹具另有 --identity --release 无基线 ⇒ rc=2 |
| V-4 | "没核对远端"却声称校验成功(拿不到远端 tag 清单时照打✅) | check_changelog.py --tags --require-remote:CI 与预检都强制真查远端(.ci-remote-tags 或 ls-remote);查不到 ⇒ 红;本地模式明确写「未核对远端」,不再打印"校验成功" |
夹具:临时仓库里 6 个场景(无 tag 绿 / 注释 tag 一致绿 / 版本不符红 / 轻量 tag 红 / 拿不到远端+require-remote 红 / 未发布段写日期红);测试 tag 只在临时仓库创建 |
生成器同步:new-project.sh 现在也落 scripts/version.py(构建身份),CHANGELOG 骨架改成四类小节 +
构建身份说明;templates/ci-gate.py 的 version 项改用 --tags --require-remote。
口径变更(01-规范 §6 二次修正):① 条目形状四节 → 四类;② 破坏性变化固定位置 + 必须带文档链接;
③ 新增构建身份与"开发构建不许自称正式版";④ "还没有正式版本"放行、首个版本号由人指定;
⑤ 拿不到远端 tag ⇒ 红;⑥ 修正"CI 取不到 tag 所以不用 tag"的过时口径(正式版本就是用注释 tag,
只是构建时不要用 git describe 取版本号);⑦ 测试 tag 只在临时仓库创建。