Files
t 3b26457165 docs: 抬档门禁 + 1.0.0 前置(稳定契约承诺)
- **抬哪一档成为硬门禁**:破坏性条数取**目标提交里候选那一段** ⇒ `0.x` 有破坏性必须抬**次版本**(或主版本),
  `≥1.0.0` 有破坏性必须抬**主版本**;只抬修订/抬次不够都阻断。无破坏性时抬哪档不限,
  但候选号必须严格大于上一正式版与当前开发号(号只增不减)。
- **`1.0.0` 首见的前置**:《稳定契约承诺》必须写实(接口 / 数据模型 / 配置 / CLI 输出与退出码,
  不留 `待填`/`TODO`/`TBD`)并在 `## [1.0.0]` 记录里链接它;`1.x → 2.0.0` 不重复要求。
- 同步:正本 §6 五次修正(⑭⑮)、07 发版判据表、"抬哪一档"对照表、templates/release-preflight.py、
  templates/README.md、04 §M(V-14~V-17)。

验证:templates/release-preflight.py --self-test 32 分支全过;试点仓夹具 91/0、make verify 15/15。
2026-10-13 17:13:44 +08:00
..

templates/ —— 可直接落仓的骨架

每个文件都替换掉附件原稿的 GitLab 口径;改法见 ../04-模板评估与修正.md。 ⚠️ 复制到仓库后不要就地重写:ci_gate.py / ci_report.py 请从已验证的仓库逐字节复制 (Weknora/knowledge-governance/.gitea/scripts/ 或 /srv/aiball/ci/attach/)。

文件 落仓位置 说明
gitea-build.yml .gitea/workflows/build.yml 提交触发的校验+编译+预览+上报。只做 CI:里面没有任何部署作业(CD 由项目另写独立 workflow,见正本 §8)
gitea-prod-deploy.yml .gitea/workflows/deploy-production.yml 生产发布唯一入口,只 workflow_dispatch,不构建。⚠️ 未实现的步骤故意 exit 1:占位不许返回成功
gitignore .gitignore 去掉原稿自相矛盾的 !src/secrets/ 例外
commit-msg ~/.githooks/commit-msg 本机提交闸门;规则必须与 ci_gate.py 一致
AGENTS.md 骨架 AGENTS.md 见下(agent 指令文件有写入保护,故内联在此)
—— 随仓携带 .gitea/scripts/ci_gate.py
.gitea/scripts/ci_report.py
逐字节复制已验证版本,不要重写

落仓后的自检(反向自检,规范 §3.2-3)

# 1) 故意违规的提交必须被闸门拦住,且 build 不跑
git commit -F - <<'EOF'
update stuff
EOF
#   判据:本地 hook 报 ❌;push 后 CI 的 gate 红、build 未执行
#   ⚠️ 口径别说过头:CI 的 gate 是**推完再跑**的,它拦不住那次推送本身。
#      要拦推送只有两条路:本机 hook,或开分支保护(禁直推)。gate 的价值是
#      "把违规提交标红、挡住编译与预览",不是"没进 main"。

# 2) 合规提交必须过(正例对照:证明不是"永远红")
git commit -F - <<'EOF'
ci(deploy): 接入 Gitea Actions 生产发布 workflow

生产发布改成独立 workflow(只 workflow_dispatch),避免塞进 push 流水线;
concurrency 明确 cancel-in-progress: false —— 取消不是回滚。
验证:手动发起一次,confirm 不匹配时 guard 作业必须红。
EOF

# 3) 产物地址每个都要 200
curl -m 5 -o /dev/null -w '%{http_code}\n' http://192.168.50.134:8899/healthz
curl -m 5 -o /dev/null -w '%{http_code}\n' http://100.92.49.78:8899/healthz

上报脚本的非秘密配置(写在 workflow 里,便于审查)

变量 作用 缺失时
CI_PUBLIC_BASE_URL 通知里 CI 运行 / 提交 / 变更 三类链接的基址(对外域名,如 https://gitea.example.com) 上报脚本明确回退 GITHUB_SERVER_URL(内网地址),并把来源写进上报的 link_base_source
  • 不覆盖 GITHUB_SERVER_URL:检出、gate、内部调用继续用它;只影响通知里的三类链接。
  • 非法值(非 http(s)、带 userinfo、带空白)⇒ 明确回退并在 CI 日志里写明原因,不生成垃圾链接。
  • 消息里"查看"的报告入口就是 Gitea 运行页(不附 bridge 自己的 /r/<id> 内网页); 只有拿不到运行链接时才退回 bridge 报告页。
  • bridge 侧 ci_base_url 是链接的可信来源(字符串或数组): 公网域名与(回退时的)内网地址都应登记,否则会被当成"非可信链接"丢掉。

AGENTS.md 骨架(复制到仓库根,按本工程改写 TODO)

# AGENTS.md — 给在 <PROJECT> 里干活的 AI 代理

> 这是**操作手册**,不是给用户看的介绍。改代码前先读「硬约束」与「已知陷阱」。
>
> **系统级规范(本机所有 Gitea 仓库都适用)**:仓库 `zheromiku/eng-standards-main`(本机工作副本 `~/eng-standards/`)
> —— `01-规范-Git与CI-CD(Gitea版).md`(正本)+ `02-流程说明与Runbook.md`。
> 做 git / 提交 / CI / 发布 / 回滚 / 密钥 这些事之前先读它;本文件只写**本工程特有**的东西。

## 0. 这个工程是什么

<PROJECT>(一句话说清做什么、给谁用、部署在哪)。

## 1. 硬约束(违反其中任何一条,这次改动就不合格)

1. TODO:不许改变对外可观测行为(接口、报文、退出码、配置语义)
2. TODO:本工程最硬的那条证据是什么(对拍 / 契约 / 字节一致 / 快照)
3. **同一含义的代码只准有一份**(禁止同义复制)
4. **注释写"为什么"**,不写"是什么"
5. 提交按"一件事一个提交",conventional commits,正文用中文,标题 ≤72 字符
6. **生产使用测试验收过的同一份构建产物**(同 sha / 同镜像 ID)—— 不许在生产机上另行 build

## 2. 本地验证闭环(提交前的最低要求)

```bash
make verify       # 本仓唯一门禁入口(本地与 CI 同一条命令);跳过项会显式警告「不算全绿」
```

- 门禁**串行**、失败即停;**汇总按必需清单逐项核对**:失败即停后剩下的记 `not_run`,
  而 `not_run` / `skipped` 都不算通过(别把「没跑到」当成「没问题」)。
- 只有「跑产物」的套件(端到端 / 契约 / 产物自检)才必须排在构建之后;
  单测(`go test` / `pytest` / `vitest`)各自跑源码,不需要先构建。
- 改到部署脚本或产物形状时,必须另外跑一次真实的装配/启动自检。

## 3. CI 闭环

```bash
giteactl runs <ns>/<repo>                 # 最近运行
giteactl logs <ns>/<repo> <runId>         # 作业日志
python3 ~/.hermes/scripts/gitea_api.py GET "/api/v1/repos/<ns>/<repo>/actions/tasks?limit=5"
```

- 失败时**先读日志再改代码**;追到第一处 `❌`。
- `status=pending` 且 `status_count=0` **不是**失败(那是"没有配置任何状态检查")。
- run 一直 `queued` ⇒ 先查**本仓库有没有自己的 Runner**,别怀疑工作流。
- 合并读回:`GET /pulls/{n}` → `state=closed` + `merged=true` + `merge_commit_sha` 非空。
  ⚠️ 合并接口成功时可能返回**空 body** → **永不重试**,改读回。

## 4. 已知陷阱(索引)

全文在 `docs/agents/traps.md`。**只追加、不重排、不复用旧号**;编号会被代码注释与测试引用。
⚠️ 新增陷阱要改两处(traps.md 加全文 + 这张表加一行)。

| # | 一句话 | 全文 |
| --- | ------ | ---- |
| 1 | TODO | [读](docs/agents/traps.md#trap-1) |

## 5. 提交与推送

- **默认直推 `main`**(原稿口径,正本 §1.1);**需要评审时**才开短命分支 + PR(正本 §1.2,那是**建议**、不是门禁)。
- conventional commits,正文中文,**说清"为什么"**;一件事一个提交。
- 提交信息不要用反引号 / `$()`(会触发命令替换):用 `git commit -F -` + heredoc。
- 推之前跑完第 2 节;推送后用第 3 节确认流水线,失败就**先读 trace 再改代码**。
- 生产发布**不走 push**:它是独立的 `deploy-production` workflow,只由人手动发起(见规范 §5.2)。

## 6. 版本与发版(**CI 不代劳,必须人批准**)

- **开工前先读系统级规范的正本 §6**(`~/eng-standards/01-规范-Git与CI-CD(Gitea版).md`)
  与 `07-发版操作说明(tag与Release).md`。
- 版本号的**唯一权威来源**:本仓 `CHANGELOG.md` 顶部那条已发布版本(正本 §6.1)。
  禁止再放 `VERSION` 文件、禁止写进源码常量、禁止在 CI 里用 `git describe`(检出取不到 tag)。
- **完成一个功能**时,把**使用者可见**的变化写进 `CHANGELOG.md` 的 `## [Unreleased]`
  (四类小节 `Added` / `Changed` / `Fixed` / `Security`,分类可省略、别留空栏目;
  **不要求每个提交升版本**)。过程记录放 `docs/archive/`,未合并事项放 `docs/BACKLOG.md`。
- **构建身份**:`python3 scripts/version.py --identity` —— **开发号自动递增**:
  `<主>.<次>.<自 v<正式版本> 以来的提交数>`(如 `0.1.12`;无基线/取不到 tag 时 `dev+g<短SHA>`),
  **不写进 CHANGELOG**、数字只增;**不许只显示上一正式版本号、不许自称正式版**。
  正式发布时请把**次版本加一**(`0.1.x` → `0.2.0`),预检会挡住"候选 <= 当前开发号"。
  **抬档也是硬门禁**:候选发布记录里有破坏性变化 ⇒ `0.x` 必须抬**次版本**、`≥1.0.0` 必须抬**主版本**
  (只抬修订 ⇒ 阻断);**发 `1.0.0` 前**必须在 `docs/RELEASE_RUNBOOK.md` 里把《稳定契约承诺》
  (接口 / 数据模型 / 配置 / CLI 输出与退出码)逐项写实、去掉占位,并在发布记录里链接它。
  ⚠️ 浅克隆里拒绝算号(退回 `dev+g<短SHA>`),检出步骤在有 tag 时会 `--unshallow` 补全历史。
- ⚠️ **正式构建要核证据**:`--identity --release` 必须能核到 **完整 40 位 SHA + 指向该提交的注释 tag
  `v<版本号>` + 该提交里 CHANGELOG 顶部版本一致**;缺一条 ⇒ **拒绝标为正式版**(rc=2)。
  自查:`python3 scripts/version.py --self-test`(正例 + 无 tag / 短 SHA / tag 不符 / 轻量 tag 负例)。
- **准备发版**时,先给用户这五样,批准后才动:
  1. **版本建议**(`python3 scripts/release_preflight.py` 给的候选值;
     **还没有正式发布过时版本号必须由用户显式指定**,预检会把它列成阻塞项);
  2. **变更说明**(四类小节;破坏性变化写在 `### Changed` 里、以 `**破坏性**` 开头并链接发布文档);
  3. **准确提交的完整 40 位 SHA**;
  4. 那次提交的 **CI 成功证据**(status context + run 链接)—— 不是"本地跑过了";
  5. **未验证项与遗留风险**。
- ⛔ **未获用户明确批准,不创建、不推送正式 tag,不发布 Gitea Release。**
  CI **不**自动升版本、**不**打 tag、**不**发布。
- 正式发版用**注释 tag**(`v<版本号>`)指向验收过的提交;Gitea Release 关联该 tag,
  说明**来自 CHANGELOG**,不另写一份。
- 自查命令:`python3 scripts/check_changelog.py --tags --require-remote`(结构 + tag 一致性,**真查远端**);
  `python3 scripts/release_preflight.py`(**只读**预检:`准备建议` + `发布就绪判定`;
  **不给"就绪"就是不能发**;**自动发布路径已禁用** —— `--execute` 会被明确拒绝(rc=2,零写操作),
  输出里也不推荐自动路径)。
- ⚠️ **就绪判据**(缺一条都不算):候选版本**已写进目标提交**(工作区里写了没提交不算)、
  完整 SHA 指向准确提交、记录检查通过、**必需 context 集合全部 success**
  (集合在 `.gitea/required-contexts.txt`,缺失/为空 ⇒ 没有策略 ⇒ 阻断;失败/缺项/pending/查询异常都阻断)、
  远端 tag 已核对未占用、**Release 查询只有 HTTP 404 才算不存在**(403/5xx/超时算异常 ⇒ 阻断)。
  `.gitea/required-contexts.txt` **故意留空**:请按本仓真实的 status context 逐行填好,否则不能判就绪。
- ⚠️ **开发版本没打 tag 是正常的**:日常 CI 不许因此变红。