chore(repo): 规范仓库化 —— 从项目目录移到独立仓
原先把规范放在 /srv/aiball/ci/standards/ 是分类错误:规范是跨仓的 (Weknora 三个仓 + aiball),却挂在单个项目(AI球)的目录下, Weknora 侧的 AGENTS.md 要引用一个 aiball 路径,读起来就是错的。 改法:正本移到独立仓 ~/eng-standards(Gitea 远端 zheromiku/eng-standards 待本人网页创建),文档内 4 处旧路径引用一并更新,并补 .gitignore。 验证:git status 干净;grep 文档已无旧路径;提交信息闸门(~/.githooks/commit-msg)放行。
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
# 本仓库是纯文档(Markdown + 模板),没有构建产物。
|
||||
# 但按规范自己的规矩,关键忽略规则仍照写 —— 与其为个例把判据写宽
|
||||
# (那正是"守卫变松"的起点),不如照写一条零成本的规则。
|
||||
node_modules/
|
||||
dist/
|
||||
runtime/
|
||||
*.tar.gz
|
||||
*.tar
|
||||
version.txt
|
||||
release-*.json
|
||||
|
||||
# 本机与临时
|
||||
.DS_Store
|
||||
*.log
|
||||
.tmp/
|
||||
*.bak.*
|
||||
@@ -0,0 +1,493 @@
|
||||
# Git / 提交 / CI-CD 规范(Gitea 版)
|
||||
|
||||
> **性质**:系统级规范,适用于本机所有 Gitea 仓库(`Weknora/*`、`aiball/*`)。
|
||||
> **正本**:本文件。同仓/同级仓库用相对路径引用;跨机请带上路径与本文件版本号。
|
||||
> **样板实例**:`Weknora/knowledge-governance`(唯一把"提交闸门 + 编译 + bridge 上报"全套跑通的仓库)。
|
||||
> **口径**:**必须** = 违反即不合格;**建议** = 有理由才能偏离,偏离写在代码注释里(本环境的规矩是"注释写为什么,不写是什么")。
|
||||
>
|
||||
> 与附件原稿的关系:原稿是 **GitLab / glab** 口径,本环境是 **Gitea 1.26.2 + act_runner**,触发体系、手动作业语义、变量作用域、CLI 全部不同。逐条映射与证据见 `04-模板评估与修正.md`。
|
||||
|
||||
---
|
||||
|
||||
## 0. 四条地基
|
||||
|
||||
1. **结论只来自可复现的命令**。任何数字、清单、成败,要么由脚本自己打印,要么写成断言 —— 禁止手抄进文档。手抄的东西过期时**没有任何人会发现**,而过期的表现是"守卫变松"。
|
||||
2. **每个校验都要有反向自检**:故意弄坏一处,它必须红。一个"永远返回 0 的校验器"比没有校验更糟 —— 它给人一种已经检查过的错觉。
|
||||
- 本环境的范例:`knowledge-adapter-service` 的 `test/bad-commit` 分支在 CI 上**必须**失败(实测已存在这样一条失败 run)。
|
||||
3. **密钥只从 Gitea `Secrets` / 目标机文件进**:不入库、不回显、不进命令行参数(会进 `ps`)。
|
||||
4. **提交前跑本仓门禁命令**;CI 不是第一次验证,CI 只是"再来一遍 + 部署"。
|
||||
|
||||
---
|
||||
|
||||
## 1. 仓库、分支、评审
|
||||
|
||||
### 1.1 分支模型
|
||||
|
||||
- **单主干**:默认分支 `main`;需要评审时用**短命分支 + PR**,合完即删。**禁止长期分支**。
|
||||
- 现状欠账:`Weknora/Enterprise-Knowledge-Base` 有 7 条分支(含 `release/*`、`develop/*`),属于"长期分支"反模式,见 `03-待配置事项.md`。
|
||||
- **禁止重写 `main` 历史**(`rebase` / `push --force`)。理由:CI 以 `main` 的推送为部署触发,"线上跑的是哪个提交"这个问题靠 `main` 的历史回答;历史一改,答案就没了。
|
||||
- **一切"退路"都必须是远端有的东西**:本地分支、本地 tag 都不算退路。真正的退路是"远端有那个提交、且能重跑那条流水线"。
|
||||
|
||||
### 1.2 评审(按风险分级,不搞一刀切)
|
||||
|
||||
本环境是 3 人小团队 + AI 代理,多加一道"人人评审"会直接卡死流程;所以按**改动路径**分级:
|
||||
|
||||
| 改动落在 | 评审要求 |
|
||||
|---|---|
|
||||
| `deploy/`、`.gitea/`、`db/migrations/`、生产配置模板、`Dockerfile` | **必须**异人评审(不得自评合并),必须在 PR 描述里写"怎么验的" |
|
||||
| 业务代码(`internal/`、`src/`、`web/`) | **必须** PR;自评合并允许,但 PR 描述必须含"改动范围 + 已跑的门禁 + 未验证项" |
|
||||
| 纯文档 / 文案 / 注释 | 可直接提交,**不**要求 PR |
|
||||
|
||||
- PR 描述是**交付契约**,不是说明文。`gitea-workflows` 技能要求:写清 **已实现 / 安全与数据边界 / 真跑过的测试 / 尚未验证 / "未合并、未部署"**。
|
||||
- 合并前必须读到**权威读回**:`GET /pulls/{n}` → `state=closed`、`merged=true`、`merge_commit_sha` 非空。
|
||||
⚠️ Gitea 的合并接口**成功时可能返回空 body**,客户端解析 JSON 会误报失败 —— **永远不要重试合并**,改为读回。
|
||||
- ⚠️ `mergeable=true` 只表示**没有冲突**,不代表测试、评审、迁移演练、部署门禁已满足。
|
||||
|
||||
### 1.3 分支保护(Gitea 侧)
|
||||
|
||||
`main` **必须**设分支保护,至少勾:禁止直接推送、必须走 PR、必须 CI 状态通过。
|
||||
现状:**四个仓库全部没有分支保护**(`GET /repos/{r}/branch_protections` 返回 `[]`),见 `03-待配置事项.md` 第 1 项。
|
||||
|
||||
### 1.4 `.gitignore` 是契约(五条)
|
||||
|
||||
必须忽略:依赖(`node_modules/`)、构建产物(`dist/`、`bin/`)、运行期数据(`data/`、`*.db`)、密钥(`secrets/`、`.env`)、发布产物(`runtime/`、`*.tar.gz`、`version.txt`、`*.tar`)。
|
||||
|
||||
1. **`.gitignore` 对已入库的文件无效** ⇒ "规则齐全"不等于"没被追踪"。断言要查**两件事**:规则在、且 `git ls-files` 里没有这些路径。
|
||||
2. **无斜杠前缀的规则匹配任意层级**。被咬过的三次都是这个:`dist/` 吞掉 `src/admin/dist/`、`data/` 吞掉 `src/data/`、`secrets/` 吞掉 `src/secrets/`。
|
||||
处理办法**不是**加 `!` 例外(例外越加越多,规则本身就没人看得懂了),而是**别在源码路径里用 `dist`/`data`/`secrets` 这些名字**。
|
||||
3. 症状很轻:`git commit` 只回一句 `paths are ignored`,文件**静默没进提交**。看到这句必须回去查。
|
||||
4. **产物名里带 sha**:`dist/app-<sha12>.tar` 这类发布产物一律忽略,绝不入库。
|
||||
5. 反查命令(提交前跑一次,成本一秒):
|
||||
|
||||
```bash
|
||||
git status --short --ignored | grep '^!!' | head
|
||||
git ls-files | grep -E '(^|/)(dist|node_modules|data|runtime|bin)/|\.tar\.gz$|^version\.txt$' # 应输出为空
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 提交规范
|
||||
|
||||
### 2.1 格式
|
||||
|
||||
**Conventional Commits,标题用中文**(本环境已有两处闸门强制,见 §2.4):
|
||||
|
||||
```
|
||||
<type>(<scope>): 一句话说清"改了什么"
|
||||
|
||||
正文:为什么这么改 / 现象 / 根因 / 怎么验的。
|
||||
```
|
||||
|
||||
- `type`:`feat` `fix` `docs` `style` `refactor` `perf` `test` `build` `ci` `chore` `revert`。
|
||||
- `scope` 取**本仓库的真实模块名**(如 `dobotai-platform`:`card`、`weknora`、`coverage`、`dingtalk`、`deploy`;`knowledge-governance`:`api`、`release`、`web`、`monitor`、`deploy`)。
|
||||
- 冒号后**一个空格**;描述 **≥3 字符**;标题 **≤72 字符**、**结尾不加句号**;破坏性变更在 type 后加 `!` 并在正文写 `BREAKING CHANGE:`。
|
||||
- 放行自动生成的提交:`Merge ` / `Revert "` / `fixup! ` / `squash! ` / `amend! ` 开头。
|
||||
- **一件事一个提交**,且**每个提交都要能各自编译/构建通过**(否则 `git bisect` 失效)。
|
||||
|
||||
### 2.2 正文写"为什么",不写"是什么"
|
||||
|
||||
diff 已经写了"是什么"。正文要回答:**当时为什么这么改、试过什么、怎么确认它对了**。四种要素可直接照抄形状:
|
||||
|
||||
| 要素 | 作用 |
|
||||
|---|---|
|
||||
| 现象(用户原话 / 报错原文) | 让后来的人对上号:我遇到的是不是同一件事 |
|
||||
| 根因(具体到文件 / 行 / 机制) | 正文核心;"没渲染"和"按纯文本渲染"是两回事 |
|
||||
| 取舍(为什么不用那个更自然的写法) | 防止下一个人"顺手现代化"改回去 |
|
||||
| 证据(跑了什么、量到了什么) | 把结论钉在可复现的命令上 |
|
||||
|
||||
### 2.3 提交信息的写法禁忌
|
||||
|
||||
- ⚠️ **不要在提交信息里用反引号与 `$()`**:shell 会先做命令替换,轻则信息被改写,重则**执行**了里面的东西。一律用带引号的 heredoc:
|
||||
|
||||
```bash
|
||||
git commit -F - <<'EOF'
|
||||
fix(dobotai): 上游不再收到重复的客户端请求头
|
||||
|
||||
根因:……(写为什么)
|
||||
EOF
|
||||
```
|
||||
|
||||
`<<'EOF'` 的单引号是"里面的字符都是字面量"的唯一保证。
|
||||
- ⚠️ 这条**不只**是提交信息:任何"把文本写进文件"的场合都一样。把**含反引号的文本**放进双引号交给 `python3 -c` / `node -e`,shell 会先执行反引号里的东西 —— 写进文件的句子凭空少片段,而**退出码仍是 0、没有任何报错**。要写的文本一律走**带引号的 heredoc**。
|
||||
- 不要用 `--no-verify` 绕过门禁(CI 侧 `ci_gate.py` 会再拦一次)。
|
||||
|
||||
### 2.4 两处闸门(本环境已就位)
|
||||
|
||||
1. **本机 hook**:`~/.githooks/commit-msg`(由 `git config --global core.hooksPath` 生效)—— 快速反馈。
|
||||
2. **CI 权威闸门**:`.gitea/scripts/ci_gate.py` —— 在**编译之前**执行,非法提交**不编译**。它优先读 `GITHUB_EVENT_PATH` 里的本次推送提交,读不到再退回 `git log`。
|
||||
|
||||
⚠️ 闸门失败时报告必须写明"**提交信息规范校验未通过**",否则一次被拦的推送会被误当成编译错误。
|
||||
|
||||
### 2.5 陷阱库(每个工程都该有的结构)
|
||||
|
||||
踩到的坑**不要只写在提交信息里**(会被埋掉)。结构:
|
||||
|
||||
- `AGENTS.md` §4 = **索引**:一行一条,编号 | 一句话 | 全文链接。
|
||||
- `docs/agents/traps.md` = **全文**:事故经过、判据、反例。
|
||||
- **为什么拆两份**:`AGENTS.md` 会被自动加载,而**指令有字节预算**,超出的部分**静默截断**。索引留在指令文件里,全文放旁边。
|
||||
- 编号**只追加、不重排、不复用**:代码注释、测试、文档里会写"见 4.46"。
|
||||
|
||||
---
|
||||
|
||||
## 3. 本地门禁(提交前的最低要求)
|
||||
|
||||
### 3.1 一条命令,串行,失败即停
|
||||
|
||||
每个仓库**必须**有一条"跑完本地门禁"的入口(`make check` / `npm run verify` / `scripts/verify.sh`)。
|
||||
|
||||
- **必须串行**:构建会清空产物目录,而端到端/契约测试跑的是**产物** ⇒ 并行会随机报"某条路由 404"。这类假失败会把你带到完全错误的方向。
|
||||
- **失败即停**:后面很多步骤依赖前面的产物,继续跑只会产生一堆同因假失败,把真正的线索埋掉。
|
||||
- 跳过项要**显式警告"不算全绿"**,并以非 0 退出。
|
||||
- 与 CI 的 `build` 作业**一一对应**,只允许两处刻意差异:① 依赖外部环境的对拍/联调在 CI 上是"环境跳过",本地**必须真跑**;② 对拍单独一步,好让"门禁全过、只有对拍没跑"一眼看得出来。
|
||||
|
||||
建议步骤形状(换语言/工具即可,**形状**才是要点):
|
||||
|
||||
| 步 | 示例 | 为什么在这一步 |
|
||||
|---|---|---|
|
||||
| 1 格式 | `gofmt -l` / `prettier --check` / `ruff format --check` | CI 会因为这一条红 |
|
||||
| 2 静态检查 | `go vet` / `tsc --noEmit` / `ruff check` | 类型/静态错,后面全是噪声 |
|
||||
| 3 构建 | `go build ./...` / `npm run build` | **会清空产物目录**,必须排在测试之前 |
|
||||
| 4 单元测试 | `go test ./...` / `pytest -q` / `npm test` | 跑的是构建产物,不是源码 |
|
||||
| 5 集成/端到端 | 真起服务、打真实端口 | 单测覆盖不到"TCP 上真的连得上" |
|
||||
| 6 部署脚本自测 | `bash deploy/deploy_test.sh` | 真跑一次 release;守的是**静默故障** |
|
||||
|
||||
### 3.2 八条通用铁律
|
||||
|
||||
1. ⚠️ **单测跑的是构建产物,不是源码**:改了源码不重建,测的是**旧代码**(第一次一定会被这个骗一次)。
|
||||
2. ⚠️ **只在"干净 clone"里出现的缺陷**:本地工作区总有"恰好能跑"的残留。要专门造一次干净树(`git clean -xdf` 后 clone / CI 的干净检出)验证。
|
||||
3. ⚠️ **每个校验都要反向自检**:
|
||||
- 断言"它会红":故意弄坏一处,必须失败。
|
||||
- 断言"它不会假绿":给它一条**对照**(正例必须过)。
|
||||
- 断言"校验器作用在目标上":曾有一条 `nginx -t -c` 静默地在读镜像自带的配置 —— 每次"通过",实际什么都没验。
|
||||
4. ⚠️ **断言要有唯一命中点**:负向断言会被**别的记录**撑成假绿(判据太宽);判据太窄则永远够不着,而且报绿。
|
||||
5. ⚠️ **测试夹具要打扫**:只建不删会把 `$TMPDIR` 攒到几万条,之后 `mktemp -d` 开始卡住 —— 表现像"某个用例死循环",实际是环境问题。
|
||||
6. ⚠️ **跑之前先确认没有上次遗留的进程**(`pgrep -fl "app"`):否则新实例 `EADDRINUSE` 起不来,探针打到旧实例上 ⇒ 一堆**假 401/404**。
|
||||
7. ⚠️ `set -o pipefail` + `grep -q` = **SIGPIPE 谎报失败**:`grep -q` 一命中就退出并关闭管道,上游随即 SIGPIPE,整条管道返回 141 —— 明明匹配上了却判失败。用**不带 `-q` 的 grep**,或让它直接读文件。
|
||||
8. ⚠️ **文档里不许出现会漂移的数字**(测试条数、断言条数)。要么由脚本打印,要么写成断言(如 `EXPECTED_CHECKS`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. CI 流水线(自动检查 / 测试 / 构建 / 测试环境部署)
|
||||
|
||||
### 4.1 触发规则(Gitea 口径)
|
||||
|
||||
Gitea 没有 `workflow: rules`,触发写在 `on:`。本环境只允许三类入口:
|
||||
|
||||
| 入口 | 写法 | 用途 |
|
||||
|---|---|---|
|
||||
| 推任意分支 | `on.push.branches: ["**"]` | 校验 + 编译(**不部署**) |
|
||||
| 推默认分支 | `on.push.branches: [main]` | 校验 + 编译 + **自动部署测试环境** |
|
||||
| 人工发起 | `on.workflow_dispatch` | 生产发布(**独立 workflow**,见 §5) |
|
||||
|
||||
**硬约束**:
|
||||
|
||||
- ⚠️ **生产部署必须是独立 workflow,只由 `workflow_dispatch` 触发**,不得塞进 push 流水线里做成一个"永远等不到人点"的作业。
|
||||
原稿那条"永不结束的流水线"教训在 Gitea 下的等价形式:Gitea 里 `if:` 不成立的作业是 **skipped**(不是 GitLab 的 blocked),不会把流水线挂死;**但** `concurrency.cancel-in-progress` 会把正在跑的部署**取消**,而**「取消流水线」不是回滚** —— 取消不改任何已经发生的事,测试环境/生产上跑着的还是那个版本。这两点都必须在文档里说清,否则一定会有人拿它当回滚用。
|
||||
- **不要接 tag 触发**。要按 tag 上线,就在 `workflow_dispatch` 的 `ref` 里选那个 tag。
|
||||
- 部署类作业**必须**显式等齐校验作业(Gitea 用 `needs:`)。
|
||||
|
||||
### 4.2 作业划分
|
||||
|
||||
| 作业 | 阶段 | 做什么 | 为什么单独占一个 |
|
||||
|---|---|---|---|
|
||||
| `gate` | 校验 | `.gitea/scripts/ci_gate.py` 提交信息闸门 | 失败就**不编译**,一次被拦的推送不会被误当编译错误 |
|
||||
| `build` | 构建 | 装依赖 → 静态检查 → 单测 → 构建 → **产物自检** → 打包/送产物 | 唯一产出交付物的作业 |
|
||||
| `validate` | 校验 | 环境参数不变量、配置语法、规范守门 | 只读仓库、几秒钟,没理由拖到部署阶段才发现 |
|
||||
| `deploy:test` | 部署 | 推 `main` 自动部署测试环境 | 见 §4.1 |
|
||||
|
||||
要点:
|
||||
- **校验类作业与 `gate` 归"校验/构建"**,用 `needs:` 显式等齐再进部署。
|
||||
- 每个作业显式声明 `runs-on:` 标签(本环境是 `ubuntu-latest`);**Runner 侧不要勾 "Run untagged jobs"**。
|
||||
- 单 Runner `capacity: 1` ⇒ **同一标签的作业串行**。这是天然的编译互斥,但**不能**替代部署互斥(见 §5.4)。
|
||||
- **必须自己 clone**:Weknora 三仓的 runner 配置下,act_runner **不会**自动检出代码,工作区初始为空 ⇒ 工作流里要 `git init` + 加带令牌的 remote + `fetch --depth 50` + `checkout -B`。(见 `templates/gitea-build.yml`)
|
||||
|
||||
### 4.3 第三方 action 的取舍(原稿这条要修正)
|
||||
|
||||
原稿写"**不用任何第三方 action**"。本环境实测**不成立**:`aiball/software-pc` 的 `validate.yml` 用了 `actions/checkout@v4` + `actions/setup-node@v4`,run#1 结论 **success**(该 runner 能取到 action)。
|
||||
|
||||
所以规则改成**按 Runner 配置区分**:
|
||||
|
||||
- **默认(Weknora 三仓 runner)**:**不用**第三方 action,全部逻辑写在 `run:` 里,用镜像自带工具 + 主机挂载的 `/opt/citools`。理由是该 runner 上 `github_mirror` 为空、且实测过 `uses:` 拉取不稳定;宁可慢,不要随机红。
|
||||
- **例外(aiball 的 `runner-software-01`)**:允许 `uses:`,但必须在 workflow 顶部注释里写明"该 runner 支持取 action",并**不得**用需要凭据/网络的 marketplace action 做发布步骤。
|
||||
- ⚠️ 判据:如果一个作业**时红时绿、报错是拉取失败**,先怀疑 `uses:` 而不是代码。
|
||||
|
||||
### 4.4 act 执行语义(本环境的硬事实)
|
||||
|
||||
- 每步以 `bash -e -o pipefail` 执行 ⇒ 禁止 `cmd | head` 这种会 SIGPIPE 提前退出的管道。需要拿退出码时用 `set +e; set +o pipefail` + `${PIPESTATUS[0]}`(现有模板已这么做)。
|
||||
- 进镜像先补齐工具并**一次性把缺的都报出来**(`bash`/`git`/`curl`/`tar`…),别等到某一行才 `command not found`。alpine 默认只有 busybox 的 `sh`,**没有 `bash`**。
|
||||
- `LANG=C.UTF-8` / `LC_ALL=C.UTF-8`:默认 C locale,中文提示会乱码。
|
||||
- **校验/上报逻辑放 `.gitea/scripts/` 脚本里,YAML 只负责调用**:深层内嵌 Python 在 YAML 里很脆,被编辑器重排缩进就静默坏掉。
|
||||
- 日志格式统一:`── 小标题 ──`、`✓ / ✗ / ⚠️ / ❌`。**失败信息必须包含"下一步怎么办"**(看哪个文件、跑哪条命令、找谁)。
|
||||
|
||||
### 4.5 工具链与缓存
|
||||
|
||||
- 工具链来自主机**只读挂载**:容器内 `/opt/citools`(真实路径 `/srv/aiball/ci/toolcache`)。
|
||||
- 缓存写**可写挂载**:容器内 `/opt/citools-cache`(真实路径 `/srv/aiball/ci/toolcache/cache`),导出 `GOCACHE` / `GOMODCACHE` / `UV_CACHE_DIR` / `GOPATH`。
|
||||
- 网络参数:`GOPROXY=https://goproxy.cn,direct`、`GOTOOLCHAIN=local`、npm 走国内源。
|
||||
- ⚠️ **不缓存 `node_modules`**(或等价的语言级依赖目录):它是平台相关的产物,缓存里混进另一套架构/运行时版本的二进制时,症状是"某次构建突然报原生模块加载失败",而那次构建的 diff 里什么都没有。
|
||||
|
||||
### 4.6 测试环境部署
|
||||
|
||||
- 推 `main` → `gate` + `build` 通过 → 自动部署测试环境。
|
||||
- 部署后**必须读回**(不是"启动成功"就算):
|
||||
1. 容器镜像 ID == 本次构建产物的镜像 ID(`docker inspect --format '{{.Id}}'`);
|
||||
2. `GET /healthz` 返回 `{"status":"ok"}`;
|
||||
3. `GET /version` 的 `commit_sha` == 本次 `main` 的短 sha;
|
||||
4. 关键 env 读回(`docker exec <ctr> env | grep -E '^DOBOTAI_'`)。
|
||||
- 测试环境部署**不等于**验收:验收的门是"测试机上一轮真实业务跑通",由人确认,见 §5.1。
|
||||
|
||||
### 4.7 上报(通知 / 预览)
|
||||
|
||||
- 主机侧 `ci-bridge` 负责落报告 + 推微信 + 托管静态预览;作业容器里没有 CLI、没有微信凭据,**通知只能由主机侧发**。
|
||||
- 上报步骤用 `if: always()`(失败的构建也要报),通知失败**不能**让作业失败。
|
||||
- ⚠️ **兜底地址不要只写一个**:作业里按 `CI_BRIDGE_URL` → `http://172.17.0.1:8899` → `http://192.168.50.134:8899` → `http://100.92.49.78:8899` 顺序探测 `/healthz`,取第一个可达(现有模板已这么做,保留)。
|
||||
- ⚠️ 报告里给出的每个 URL(报告页、预览页)**都要探测到 200** 才算发出去了,否则会收到一条没有预览链接的通知。
|
||||
- ⚠️ 静态产物打包**不要产生 `.` 根成员**:`tar -czf x.tgz -C dir .` 会写入 `.` 和 `./index.html`;严格解包器可能拒绝或报错,而流水线**仍显示成功** —— 通知到了、预览没有。bridge 侧应**规范化**(剥掉 `./`,丢弃归一化为 `.`/空的成员),而不是直接拒绝整个包。
|
||||
|
||||
---
|
||||
|
||||
## 5. 生产发布(人工确认 / 健康检查 / 互斥 / 回滚 / 同一产物)
|
||||
|
||||
### 5.1 生产使用测试验收过的**同一份构建产物**(本环境新增硬约束)
|
||||
|
||||
> ⚠️ 原稿只讲"产物分层 + 回滚",没有把"同一份产物"写成硬约束。而本环境现状是**在目标机上各自 build**(测试机 build 一次、生产机再 clone 源码 build 一次),**两份二进制并不逐字节相同**。这必须改。
|
||||
|
||||
**铁律**:构建**只做一次**,产出**不可变产物**;测试环境与生产环境部署的是**同一份**(同 `sha`、同镜像 ID、同 tar sha256)。
|
||||
|
||||
落地流程:
|
||||
|
||||
```
|
||||
① 构建(只此一次,在仓库 Runner 上)
|
||||
docker build --build-arg VERSION=<v> --build-arg COMMIT_SHA=<sha12> --build-arg BUILD_TIME=<ts> \
|
||||
-t dobotai-platform:sha-<sha12> <SRC>
|
||||
IMAGE_ID=$(docker image inspect --format '{{.Id}}' dobotai-platform:sha-<sha12>) # sha256:...
|
||||
docker save dobotai-platform:sha-<sha12> -o dist/dobotai-platform-<sha12>.tar
|
||||
TAR_SHA=$(sha256sum dist/dobotai-platform-<sha12>.tar | cut -d' ' -f1)
|
||||
# 写发布清单(唯一真值,后续两个环境都对着它核对)
|
||||
→ release-<sha12>.json { sha12, commit_sha, version, build_time, image_id, tar_sha256 }
|
||||
② 部署测试环境:docker load < tar → 断言 image_id == 清单 → 起容器 → 读回 /version
|
||||
③ 人工验收:在测试环境跑一轮真实业务,通过后在 release issue/PR 里留下验收记录
|
||||
④ 部署生产:取**同一个 tar**(按 tar_sha256 校验收到的文件)→ docker load → 断言 image_id == 清单 → 切镜像
|
||||
```
|
||||
|
||||
**证据链(缺一即不合格)**:
|
||||
|
||||
| 环节 | 断言的命令 | 通过判据 |
|
||||
|---|---|---|
|
||||
| 测试机加载 | `docker image inspect --format '{{.Id}}' <tag>` | == 清单 `image_id` |
|
||||
| 测试机运行 | `curl -fsS 127.0.0.1:8090/version` | `commit_sha` == 清单 `sha12` |
|
||||
| 生产机加载 | `sha256sum <tar>` 然后 `docker image inspect --format '{{.Id}}'` | 分别 == 清单 `tar_sha256` / `image_id` |
|
||||
| 生产机运行 | `curl -fsS 127.0.0.1:8090/version` | `commit_sha` == 清单 `sha12` |
|
||||
| 两端一致 | 比较两次 `image_id` | **完全相同** |
|
||||
|
||||
- **例外(必须写明)**:数据库迁移**不属于**镜像产物,是独立步骤,顺序是"**先迁移、后切镜像**"(见 §5.5)。
|
||||
- **回退用镜像回退**,**不是**把源码改回去重编。
|
||||
|
||||
### 5.2 生产发布的人工确认
|
||||
|
||||
Gitea 1.26.2 的 Actions **没有** GitLab 的 `when: manual` + `manual_confirmation`。人工确认按三层落地:
|
||||
|
||||
1. **平台层**:生产部署是**独立 workflow**,只有 `on.workflow_dispatch`,不存在自动触发路径。
|
||||
2. **参数层**:dispatch 必须显式传入 `sha`(要发的短 sha)与 `confirm`(必须等于字面量 `deploy-production`),workflow 第一步就校验;不匹配直接红。防误点、防机器人误触。
|
||||
3. **人层**:**最后一步"切镜像"由本人在生产机执行**(或由本人在生产机确认脚本输出)。CI 只负责"传产物 + 备好命令",不替人按下那一刻。
|
||||
(若实例已启用 Environment 审批之类能力,可作为额外一道;**本规范不假设它存在**,需在实例上实测,见 `03-待配置事项.md` 第 8 项。)
|
||||
|
||||
### 5.3 健康检查(读回证据,缺一即不合格)
|
||||
|
||||
生产切换后**必须**取全下列证据,不能只看"容器起来了":
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:8090/healthz # {"status":"ok"}
|
||||
curl -fsS http://127.0.0.1:8090/version # version/commit_sha/build_time
|
||||
docker inspect dobotai-dingtalk --format '{{.Config.Image}}' # 新 tag
|
||||
docker inspect dobotai-dingtalk --format '{{.Id}}' # == 清单 image_id
|
||||
docker inspect dobotai-dingtalk --format '{{.State.Status}}' # running(该容器无 healthcheck 段落,别读 .State.Health)
|
||||
docker exec dobotai-dingtalk env | grep -E '^DOBOTAI_(DEFAULT_MODE|REASONING_AGENT_ID|AGENT_BINDINGS)='
|
||||
docker exec dobotai-dingtalk grep -aqF '<字面量>' /proc/1/root/usr/local/bin/dobotai # 只 grep 字符串字面量
|
||||
docker logs dobotai-dingtalk --since 60s | tail
|
||||
```
|
||||
|
||||
- ⚠️ 构建用了 `-ldflags "-s -w"`,Go 符号被剥离 ⇒ **grep 符号证明不了任何事**,只能 grep **字符串字面量**。
|
||||
- ⚠️ **loopback 健康 ≠ 业务验收**:最后的门是**本人在生产钉钉机器人上发一条真实消息**,再回读 `turns.agent_id`。
|
||||
|
||||
### 5.4 部署互斥(原稿的 `resource_group` 在本环境不存在)
|
||||
|
||||
- Gitea 1.26.2 **没有** `resource_group`。可用的第一层是 `concurrency:`,但它只对"同一 workflow 的同一 group"生效,**跨 workflow、跨人手手动执行时挡不住**。
|
||||
- **真正的互斥放在目标机**(可靠性只在这一层):
|
||||
|
||||
```bash
|
||||
exec 9>/var/lock/dobotai-deploy.lock
|
||||
flock -n 9 || { echo "❌ 另一个部署正在进行,退出"; exit 1; }
|
||||
```
|
||||
|
||||
- ⚠️ **不要**用 `concurrency.cancel-in-progress` 去"保证互斥":它取消的是**正在进行的部署**,而**取消不是回滚** —— 镜像可能已经换了。生产的 concurrency 组**必须** `cancel-in-progress: false`。
|
||||
- ⚠️ 测试与生产**两个锁是两个文件**;而且两个环境共用同一套阿里云 RDS ⇒ **压测/并发高的动作不要同时在两边跑**(会扰动生产)。
|
||||
|
||||
### 5.5 数据库迁移(与镜像分开发布)
|
||||
|
||||
本环境(DobotAI)**应用不会在启动时自动迁移**;新的 `db/migrations/0000NN_*.sql` 必须人工按序执行:
|
||||
|
||||
1. **幂等门**:先数 `information_schema.columns` 里有没有新列 —— `0` 才继续,`1` 直接中止(已应用),其它值中止。
|
||||
2. 版本匹配:RDS 是 **PostgreSQL 17.9** ⇒ 用 `postgres:17-alpine`(`postgres:16-alpine` 会 `server version mismatch` 失败)。
|
||||
3. DDL 前先 schema-only dump 到 `container/backups/`(0600)。
|
||||
4. 暂存迁移文件到 `container/migrations/`(部署集 = 本地记录)。
|
||||
5. 在**一个事务**里执行,带 `DO $$ ... RAISE EXCEPTION` 守卫。
|
||||
6. 读回精确契约(如 `is_nullable|column_default` = `NO|false`)。
|
||||
7. **凭据不进 argv**:写 0600 临时文件 `PGURL=<url>`,`docker run --rm -i --network host --env-file <file> ... 'exec psql -v ON_ERROR_STOP=1 "$PGURL"'`。
|
||||
8. 顺序:**迁移完成并读回后**,才切镜像。
|
||||
|
||||
**回滚**:加列型迁移(新列可空/有默认)**不必**跑 `.down.sql` 就能回滚应用(应用回退后旧代码不看新列)。
|
||||
|
||||
### 5.6 回滚
|
||||
|
||||
- 切换前先把**当前在跑的镜像**打回滚标签:`docker tag <current> dobotai-platform:rollback-<old12>-before-<new>`。
|
||||
- 回滚 = 恢复回滚标签 + `docker compose up -d --force-recreate` + 读回 health/image/env(脚本 `rollback_dobotai_prod.sh`)。
|
||||
- 回退到几天前的版本 = 找到那个 commit,用 `workflow_dispatch` 把 `ref` 指向那个 commit(或直接用它的产物 tar)。
|
||||
- ⚠️ **「取消流水线」不是回滚**(重复三遍也不多):取消不改任何已经发生的事。
|
||||
- ⚠️ 回滚**不改** `version` 常量/源码;回滚发生在部署层。
|
||||
|
||||
### 5.7 目标机布局(三层,是回滚/清理/排障的前提)
|
||||
|
||||
| 层 | 内容 | 说明 |
|
||||
|---|---|---|
|
||||
| `state/` | 数据库、密钥、备份、日志 | ★ **唯一不可再生**,任何自动化脚本都不许碰 |
|
||||
| `runtime/` | 程序本体 + 启动器 | 可随时重装;回退靠它 |
|
||||
| `deploy/` | compose、模板、脚本、env、密钥中转 | CI **每次都会重写**;整个删掉不会丢任何东西 |
|
||||
|
||||
(DobotAI 实际落在 `/opt/dobotai/{config,container,secrets}`:`secrets/` 是不可再生的 `state/`,`container/backups/` 是备份层。)
|
||||
|
||||
### 5.8 目标机操作细节(都是踩出来的)
|
||||
|
||||
- **目标机上只跑一个自包含的部署脚本**,把这一串放在同一个脚本里、**顺序不许挪**:
|
||||
`① 校验产物 ② 备份当前版本 ③ 加载并原子替换 ④ 渲染配置 ⑤ 升级前备份数据库 ⑥ 体检 ⑦ 起容器 → 等健康 → 读回`
|
||||
把"停容器、再换目录"分在两个地方写,**一定会有人改错一处**。CI 只负责"传文件 + 调它"。
|
||||
- **幂等**:每一步都是可重跑的(`mv`/`load`/`tag`),中途失败重跑一次即可。(脚本要**带门**:迁移列已存在就拒跑、镜像 tar sha 不符就拒跑,防止部分失败后重跑造成双重应用。)
|
||||
- ⚠️ **非交互 ssh 的 PATH 与交互登录不同**:`ssh host "命令"` 是非交互、非登录 shell,**不读 `/etc/profile`** ⇒ 会出现"机器上敲 `docker compose -v` 正常,流水线里却 `docker: command not found`"。两条保险:远端命令显式补 PATH;脚本内部把 docker 解析成绝对路径(`sudo` 也会重置 PATH)。
|
||||
- ⚠️ **连通性预检要分诊**:按 stderr 特征分别提示"公钥被拒 / 解析不了主机 / 连不上 / 主机密钥变了 / 私钥格式坏了",并打印**在 Runner 那台机器上**复现的命令(强调 `-F /dev/null` —— 忽略本机 `~/.ssh/config`,否则会连到别的地址,报出来的错与真实原因无关)。
|
||||
- ⚠️ **长脚本不要走 SSH heredoc**:嵌套引号会塌。本地写好脚本 → `scp` → 按绝对路径执行。多步远程作业一律如此。
|
||||
|
||||
---
|
||||
|
||||
## 6. 版本号与发版
|
||||
|
||||
### 6.1 版本号只有一个来源
|
||||
|
||||
本环境主语言是 **Go**(`dobotai-platform`、`knowledge-governance`),版本**不是** `package.json`:
|
||||
|
||||
- 唯一来源 = **构建时注入**:`-ldflags "-X main.Version=<v> -X main.CommitSHA=<sha12> -X main.BuildTime=<ts>"`,值取自 `git describe --tags --always` / 短 sha / CI 时间。
|
||||
- 运行时由 `/version` 端点报出(`version` / `commit_sha` / `build_time`)—— 这是"线上跑的是哪个提交"的**权威答案**。
|
||||
- ⚠️ **不要再放一个仓库根的 `VERSION` 文件**:那是第二份副本,发版要改两处,忘一处就漂移。
|
||||
- 前端子工程(`knowledge-governance/web`)的 `package.json.version` 只表示前端包版本,**不是**服务版本。
|
||||
- 版本号**只增不减**。回退用部署层回滚(§5.6),不是把 version 改回去。
|
||||
- CI 里**断言**:`/version` 的 `commit_sha` == 本次构建的短 sha(少这条,`version` 会静默显示 `dev`)。
|
||||
|
||||
### 6.2 打不打 tag
|
||||
|
||||
- 本环境**默认不打 tag**:部署由 `main` 推送驱动,版本靠构建注入 + `/version` 回读锚定。
|
||||
- 一旦引入 tag,必须同时定下三件事:**注释 tag 还是轻量 tag、谁负责打、CI 怎么用**。否则会出现"tag 指着的提交从没部署过"。
|
||||
- ⚠️ **不要接 tag 触发流水线**。
|
||||
|
||||
---
|
||||
|
||||
## 7. 密钥、变量与配置
|
||||
|
||||
### 7.1 分类放置
|
||||
|
||||
| 类别 | 放哪 | 为什么 |
|
||||
|---|---|---|
|
||||
| 非密钥参数(目录、端口、容器名、镜像 tag) | 版本库 `deploy/env/*.env` 或目标机 `/opt/.../config/`(视项目而定,见下) | 可 review、有历史、有 diff |
|
||||
| 密钥与连接信息 | Gitea 仓库 **Secrets** | 不入库、可轮换 |
|
||||
| 全项目共用的可选值(播报地址等) | Gitea 仓库 **Variables** | 与环境无关 |
|
||||
|
||||
- ⚠️ **Gitea 1.26.2 没有 GitLab 的"environment scope"**:同名变量**不能**按环境取不同值。要用**命名后缀**区分:
|
||||
`DEPLOY_TEST_HOST` / `DEPLOY_PROD_HOST`、`DEPLOY_TEST_SSH_KEY` / `DEPLOY_PROD_SSH_KEY`。
|
||||
- ⚠️ **Gitea 1.26.2 也没有 "File" 类型 secret**:私钥内容整体存为 secret,运行时由脚本写文件并 `chmod 600`。
|
||||
- ⚠️ 变量/密钥的**上报接口形状**(实测):变量用 `POST/PUT /repos/{r}/actions/variables/{name}`,body `{"value": …}`;密钥只有 `PUT /repos/{r}/actions/secrets/{name}`(upsert),body `{"data": …}`。**集合级 POST 返回 405 且空 body**,那看起来像权限问题、实际不是。写完必须 `GET` 读回。
|
||||
(`/srv/aiball/runner/attach_repo.sh` 已封装这个形状,复用它。)
|
||||
|
||||
### 7.2 启动时先校验变量清单,再上传任何字节
|
||||
|
||||
缺必需变量就直接红 —— 否则 CI 会把 `${XXX}` 替换成空串,容器照样起得来但日志里全是"配置为空",排查很费劲。
|
||||
检查时**只打印变量名与 ✓/✗,绝不回显值**。
|
||||
|
||||
### 7.3 把密钥写到目标机的规矩
|
||||
|
||||
- 走 **stdin 重定向**写文件(`ssh host "cat > path" < file`),**不要塞进 ssh 命令行** —— 那会让密钥出现在远端的 `ps` 输出里。
|
||||
- 落地即 `chmod 600`;权限**显式**设置,**不依赖目标机 umask**。
|
||||
- 私钥文本先 `sed -i 's/\r$//'` 去 CRLF、并补结尾换行,否则 ssh 会报误导性的 `error in libcrypto` / `invalid format`。
|
||||
- 密钥写入与"渲染成配置"**只在一处加引号**:两边都加会变成 `''value''`,引号本身成了值的一部分。
|
||||
|
||||
---
|
||||
|
||||
## 8. 新仓库接入清单
|
||||
|
||||
照着做一遍(`Weknora/Enterprise-Knowledge-Base` 就是下一个要接入的):
|
||||
|
||||
- [ ] **1. 仓库与默认分支**:默认分支定 `main`;分支收敛到 `main` + 短命分支(长期分支要么合要么删)。
|
||||
- [ ] **2. `.gitignore`**:依赖 / 构建产物 / 运行期数据 / 密钥 / 发布产物各一条;再用 §1.4 的两条反查命令确认**没有已入库的漏网文件**。
|
||||
- [ ] **3. 门禁入口**:`make check` 或 `scripts/verify.sh` —— 一条命令跑完、**串行**、失败即停、末尾汇总;跳过项显式警告"不算全绿"。
|
||||
- [ ] **4. CI 骨架**:复制 `templates/gitea-build.yml` → `.gitea/workflows/build.yml`;随仓带 `.gitea/scripts/{ci_gate.py,ci_report.py}`(**逐字节复制**已验证的版本,不要重写)。
|
||||
- [ ] **5. 仓库级 Runner**:`bash /srv/aiball/runner/attach_repo.sh <owner/repo> <runner-name>`(幂等;注册令牌走文件挂载,不落容器 env)。
|
||||
- [ ] **6. bridge 变量/密钥**:`CI_BRIDGE_URL`(variable)、`CI_BRIDGE_TOKEN`(secret),由上面的脚本写入;写完 `GET` 读回。
|
||||
- [ ] **7. 分支保护**:`main` 禁止直接推送 + 必须 PR + 必须 CI 通过。
|
||||
- [ ] **8. `AGENTS.md` 指针**:仓库 `AGENTS.md` 写一行指回本规范;陷阱库拆 `AGENTS.md`(索引)+ `docs/agents/traps.md`(全文)。
|
||||
- [ ] **9. 自检一遍**:推一个**故意违规**的提交,确认 `gate` 红且 `build` 没跑(反向自检,§3.2-3)。
|
||||
- [ ] **10. 首次推 `main` 因为缺变量而红 —— 这是设计**:变量检查在任何上传动作**之前**,目标机一个字节都不会被碰。
|
||||
|
||||
> **跨仓引用**:正本仓库 `zheromiku/eng-standards`(Gitea);本机工作副本 `~/eng-standards/`。
|
||||
> ⚠️ **指令文件不会跨仓库自动加载** —— 每个仓库要在自己的 `AGENTS.md` 里写一行指针,否则那个仓库里的代理看不到这份规范。
|
||||
|
||||
---
|
||||
|
||||
## 9. 速查表
|
||||
|
||||
```bash
|
||||
# ── 提交前 ────────────────────────────────────────────────
|
||||
make check # 或 npm run verify —— 本仓门禁,串行、失败即停
|
||||
git status --short --ignored # 确认没有该入库却被忽略的文件
|
||||
git commit -F - <<'EOF' # ⚠️ 不用 -m:正文里的反引号/$() 会被 shell 吃掉
|
||||
feat(scope): 标题(中文)
|
||||
|
||||
为什么这么改、试过什么、怎么验的。
|
||||
EOF
|
||||
|
||||
# ── 看 CI(只读)──────────────────────────────────────────
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET "/api/v1/repos/<ns>/<repo>/actions/tasks?limit=5"
|
||||
giteactl runs <ns>/<repo> # 最近运行
|
||||
giteactl logs <ns>/<repo> <runId> # 单个作业日志
|
||||
giteactl rerun <ns>/<repo> <runId> # 重跑(不推新提交即可验证整条链路)
|
||||
|
||||
# ── 发版 ─────────────────────────────────────────────────
|
||||
# 构建注入版本 → 写 release-<sha12>.json → 部署测试 → 验收 → dispatch 生产
|
||||
|
||||
# ── 上线与回退 ───────────────────────────────────────────
|
||||
# 推 main → gate + build + deploy:test 自动跑
|
||||
# 生产 → Gitea「Actions → 生产 workflow → Run workflow」选 ref=main,填 sha 与 confirm
|
||||
# 回退(生产机上) → bash /opt/dobotai/container/ops/rollback_dobotai_prod.sh
|
||||
# ⚠️「取消流水线」不是回滚
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 附录:通用陷阱(继承原稿 + 本环境新增)
|
||||
|
||||
| # | 一句话 | 通用性 |
|
||||
|---|---|---|
|
||||
| T1 | `set -o pipefail` + `grep -q` ⇒ SIGPIPE 谎报失败 | CI 脚本通用 |
|
||||
| T2 | 测试夹具太小 / 自出自判 ⇒ 假绿 | 通用 |
|
||||
| T3 | 只在"干净 clone"里出现的缺陷 —— 本地必须专门造一次干净树 | 通用 |
|
||||
| T4 | 手抄的清单会过期,而过期的表现是守卫变松 | 文档通用 |
|
||||
| T5 | 断言"我这条守卫会红"之外,还要断言**不会假绿**:给它一条对照 | 通用 |
|
||||
| T6 | 测试里的**负向断言**会被"别的记录"撑成假绿(判据太宽) | 通用 |
|
||||
| T7 | 测试夹具只建不删 ⇒ `$TMPDIR` 攒到几万条,`mktemp -d` 开始卡住 | 通用 |
|
||||
| T8 | 单测跑的是构建产物,不是源码 —— 改了源码不重建,测的是旧代码 | 通用 |
|
||||
| **G1** | **act_runner 不自动检出代码**(Weknora 三仓)⇒ 工作流必须自 clone | 本环境 |
|
||||
| **G2** | **act 以 `bash -e -o pipefail` 执行每步** ⇒ `cmd \| head` 会假红 | 本环境 |
|
||||
| **G3** | **`uses:` 第三方 action 在部分 runner 上不可用**(实测 aiball runner 可用) | 本环境 |
|
||||
| **G4** | **Gitea 无 `resource_group`、无 environment scope、无 File secret** ⇒ 互斥/环境区分要自己实现 | 本环境 |
|
||||
| **G5** | **`cancel-in-progress` 会取消正在进行的部署,而取消 ≠ 回滚** | 本环境 |
|
||||
| **G6** | **Gitea 合并成功可能返回空 body** ⇒ 解析 JSON 会误报失败,**永不重试**,改读回 | 本环境 |
|
||||
| **G7** | **目标机各不相同地 build 会得到不同二进制** ⇒ 必须"构建一次、两个环境部署同一份" | 本环境 §5.1 |
|
||||
| **G8** | **作业容器走 docker 网关 `172.17.0.1:8899` 才到得了 bridge** | 本环境 |
|
||||
| **G9** | **静态产物包不要带 `.` 根成员** ⇒ 看似成功、实际没有预览 | 本环境 |
|
||||
| **G10** | **生产脚本必须幂等带门**(迁移列已存在就拒跑、镜像 sha 不符就拒跑) | 本环境 |
|
||||
@@ -0,0 +1,266 @@
|
||||
# 流程说明与 Runbook
|
||||
|
||||
> 配套 `01-规范-Git与CI-CD(Gitea版).md`。这里只写**怎么做**:逐步命令 + 每一步的通过判据。
|
||||
> 所有涉及凭据的命令都**从文件读**,不出现在命令行参数里。
|
||||
|
||||
## A. 日常开发循环
|
||||
|
||||
### A1. 开工前
|
||||
|
||||
```bash
|
||||
cd <repo>
|
||||
git fetch origin --prune
|
||||
git status --short --branch # 必须干净
|
||||
git log --oneline origin/main..HEAD # 本地领先的提交 = 本次要发的范围
|
||||
```
|
||||
|
||||
**判据**:工作区干净;`origin/main..HEAD` 的每一条都能说清属于本次需求(不能把"顺手带上"的历史提交混进 PR 范围)。
|
||||
|
||||
### A2. 改完、提交前
|
||||
|
||||
```bash
|
||||
make check # 或 npm run verify / scripts/verify.sh —— 本仓门禁,串行、失败即停
|
||||
```
|
||||
|
||||
**判据**:末尾汇总表全绿;有跳过项会显式警告"不算全绿"且非 0 退出 —— 那就不是绿。
|
||||
|
||||
```bash
|
||||
git status --short --ignored | grep '^!!' | head
|
||||
git ls-files | grep -E '(^|/)(dist|node_modules|data|runtime|bin)/|\.tar\.gz$|^version\.txt$' # 必须为空
|
||||
```
|
||||
|
||||
**判据**:第二条命令**输出为空**。
|
||||
|
||||
### A3. 提交
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -F - <<'EOF'
|
||||
<type>(<scope>): 一句话(中文,≤72 字符,结尾不加句号)
|
||||
|
||||
现象 / 根因 / 取舍 / 证据(写"为什么",不写"是什么")。
|
||||
EOF
|
||||
```
|
||||
|
||||
**判据**:本机 hook `~/.githooks/commit-msg` 未报错。报错就照它的提示改,**不要** `--no-verify`(CI 侧会再拦一次)。
|
||||
|
||||
### A4. 推分支、开 PR
|
||||
|
||||
```bash
|
||||
git push -u origin HEAD
|
||||
# PR:网页或 API
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET "/api/v1/repos/<ns>/<repo>/pulls?state=open"
|
||||
# 创建用 POST /api/v1/repos/<ns>/<repo>/pulls body: {title, head, base:"main", body}
|
||||
```
|
||||
|
||||
**PR 描述必须含**:已实现 / 安全与数据边界 / 真跑过的测试 / 尚未验证 / 明确写"未部署"。
|
||||
|
||||
**判据(读回,不看 POST 的返回就当成功)**:
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET "/api/v1/repos/<ns>/<repo>/pulls/<n>"
|
||||
# state=open, merged=false, head.ref/base.ref 正确, head.sha == git rev-parse HEAD
|
||||
```
|
||||
|
||||
### A5. 看 CI(失败先读日志,再改代码)
|
||||
|
||||
```bash
|
||||
giteactl runs <ns>/<repo>
|
||||
giteactl logs <ns>/<repo> <runId> # 或 giteacli action job view --repo <ns>/<repo> --index <jobId>
|
||||
```
|
||||
|
||||
Gitea API 等价路径(`gitea_api.py` 只拼 `/api/v1` 前缀):
|
||||
|
||||
```
|
||||
GET /repos/{r}/actions/tasks?limit=5 # 最近运行(注意:是 per-run task,可能多行同一条)
|
||||
GET /repos/{r}/actions/runs/{id}/jobs # 某次运行的作业
|
||||
GET /repos/{r}/actions/jobs/{jobId}/logs # 单个作业日志
|
||||
```
|
||||
|
||||
**判据**:追到第一处 `❌`,读它给出的"下一步怎么办"。
|
||||
|
||||
⚠️ 两个易混点:
|
||||
- `status=pending` 且 `status_count=0` **不是**失败 —— 是"这个提交没有任何 status 检查"。别当成 CI 挂了。
|
||||
- 重跑整条链路(不推新提交)用 `POST /repos/{r}/actions/runs/{id}/rerun`;**先筛 `path` 是真工作流的那条 run**,诊断/临时工作流重跑出来的绿或红不代表任何事。
|
||||
|
||||
### A6. 合并
|
||||
|
||||
只在**评审(§1.2)**与 **CI 状态**都满足后合并。合并后**立即读回**:
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET "/api/v1/repos/<ns>/<repo>/pulls/<n>"
|
||||
# 判据:state=closed, merged=true, merge_commit_sha 非空
|
||||
git fetch origin main
|
||||
git merge-base --is-ancestor <reviewed_sha> origin/main # 必须成功
|
||||
```
|
||||
|
||||
⚠️ 合并 POST 可能返回**空 body** → 客户端解析 JSON 会误报失败。**永不重试**,改读回。
|
||||
|
||||
---
|
||||
|
||||
## B. 接入一个新仓库(照 `01-规范` §8)
|
||||
|
||||
```bash
|
||||
bash /srv/aiball/runner/attach_repo.sh <owner/repo> runner-<...>-01
|
||||
```
|
||||
|
||||
它会(幂等地):校验仓库存在 → 起仓库级 Runner 容器 → 写 `CI_BRIDGE_URL`(variable)/`CI_BRIDGE_TOKEN`(secret) → 从模板仓拷 `.gitea/workflows/build.yml` + `.gitea/scripts/{ci_gate.py,ci_report.py}` → 提交推送 → 读回第一次运行。
|
||||
|
||||
**判据**:
|
||||
```bash
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET "/api/v1/repos/<ns>/<repo>/actions/tasks?limit=3"
|
||||
docker ps --format '{{.Names}}' | grep runner
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET "/api/v1/repos/<ns>/<repo>/actions/secrets" # CI_BRIDGE_TOKEN 在
|
||||
```
|
||||
⚠️ 若 run 一直 `queued`,先查**该仓库是否真的拥有一个 Runner**(标签是否匹配、Runner 是不是**这个仓库**的),再怀疑工作流或提交。
|
||||
|
||||
---
|
||||
|
||||
## C. 构建与测试环境部署
|
||||
|
||||
### C1. 自动(推 main)
|
||||
|
||||
```
|
||||
git push origin main
|
||||
→ gate(提交信息闸门)→ build(依赖/静态/单测/构建/产物自检)→ deploy:test
|
||||
```
|
||||
|
||||
### C2. 测试环境读回(每次部署都做全)
|
||||
|
||||
```bash
|
||||
# 在测试机 120.76.217.51(key ~/.ssh/weknora-aliyun-test-ed25519)
|
||||
curl -fsS http://127.0.0.1:8090/healthz
|
||||
curl -fsS http://127.0.0.1:8090/version
|
||||
docker inspect <ctr> --format '{{.Config.Image}}'
|
||||
docker inspect <ctr> --format '{{.Id}}'
|
||||
```
|
||||
|
||||
**判据**:`/version.commit_sha` == 本次 main 的短 sha;容器镜像 ID == 本次构建产物的镜像 ID。
|
||||
|
||||
### C3. 人工验收
|
||||
|
||||
在测试环境跑**一轮真实业务**(DobotAI:在测试钉钉机器人上真实发一条消息,再回读 `turns.agent_id`)。
|
||||
通过后在 release issue / PR 里写下:验收人、时间、观察到的现象、结论。
|
||||
|
||||
**判据**:有这条记录。没有这条记录就不算验收过 —— 后面的生产发布不允许开始。
|
||||
|
||||
---
|
||||
|
||||
## D. 生产发布(人工确认 + 同一产物)
|
||||
|
||||
> 前提:§C3 的人工验收已完成、release 清单 `release-<sha12>.json` 已生成、迁移(若有)已单独走完 §E。
|
||||
|
||||
### D1. 发起(人工确认第一、二层)
|
||||
|
||||
Gitea 网页 → 仓库 → **Actions → 生产 workflow → Run workflow**:
|
||||
- `ref` = `main`(或要发布的那条 commit/tag)
|
||||
- `sha` = 要发布的短 sha(12 位)
|
||||
- `confirm` = 字面量 `deploy-production`
|
||||
|
||||
workflow 第一步就断言 `confirm` 与 `sha` 合法,不匹配直接红。
|
||||
|
||||
### D2. 生产机上的动作(人工确认第三层:最后一步由本人执行)
|
||||
|
||||
```bash
|
||||
# 在生产机 120.76.218.137(key ~/.ssh/weknora-aliyun-prod-ed25519)
|
||||
# 1) 互斥锁(防两人/两条流水线同时改同一目录)
|
||||
exec 9>/var/lock/dobotai-deploy.lock
|
||||
flock -n 9 || { echo "❌ 另一个部署正在进行"; exit 1; }
|
||||
|
||||
# 2) 校验产物(按清单核对,不核对不加载)
|
||||
sha256sum /opt/dobotai/container/incoming/dobotai-platform-<sha12>.tar
|
||||
# 判据:== release-<sha12>.json 的 tar_sha256
|
||||
|
||||
# 3) 备份当前版本(回滚用)
|
||||
docker tag <current> dobotai-platform:rollback-<old12>-before-<sha12>
|
||||
|
||||
# 4) 加载并断言镜像 ID
|
||||
docker load -i /opt/dobotai/container/incoming/dobotai-platform-<sha12>.tar
|
||||
docker image inspect --format '{{.Id}}' dobotai-platform:sha-<sha12>
|
||||
# 判据:== release-<sha12>.json 的 image_id(且与测试机读到的相同)
|
||||
|
||||
# 5) 切镜像(唯一动线上流量的动作)
|
||||
cd /opt/dobotai/container
|
||||
# 用 awk 计数替换 DOBOTAI_IMAGE=,替换数必须为 1,否则中止;改前先备份 0600
|
||||
docker compose --env-file runtime.env -f compose.yaml up -d --no-deps --force-recreate dobotai-dingtalk
|
||||
|
||||
# 6) 读回(§5.3 全套证据,见下)
|
||||
```
|
||||
|
||||
### D3. 健康检查(缺一即不合格)
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:8090/healthz # {"status":"ok"}
|
||||
curl -fsS http://127.0.0.1:8090/version # commit_sha == <sha12>
|
||||
docker inspect dobotai-dingtalk --format '{{.Config.Image}}'
|
||||
docker inspect dobotai-dingtalk --format '{{.Id}}' # == image_id
|
||||
docker inspect dobotai-dingtalk --format '{{.State.Status}}' # running
|
||||
docker exec dobotai-dingtalk env | grep -E '^DOBOTAI_(DEFAULT_MODE|REASONING_AGENT_ID|AGENT_BINDINGS)='
|
||||
docker exec dobotai-dingtalk grep -aqF '<改动引入的字符串字面量>' /proc/1/root/usr/local/bin/dobotai
|
||||
docker logs dobotai-dingtalk --since 60s | tail
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET ... # (可选)RDS 侧读回:turns.agent_id 等
|
||||
```
|
||||
|
||||
⚠️ 只 grep **字符串字面量**(构建剥了符号)。
|
||||
⚠️ 最后的门:**本人在生产钉钉机器人上发一条真实消息**,再回读 `turns.agent_id`。
|
||||
|
||||
### D4. 回滚
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/weknora-aliyun-prod-ed25519 root@120.76.218.137 \
|
||||
'bash /opt/dobotai/container/ops/rollback_dobotai_prod.sh'
|
||||
```
|
||||
判据:health ok + 镜像 tag 回到 `<old12>` + `/version.commit_sha` == `<old12>` + env 读回。
|
||||
|
||||
⚠️ **「取消流水线」不是回滚**。取消不改任何已经发生的事。
|
||||
|
||||
---
|
||||
|
||||
## E. 数据库迁移(与镜像分开发布)
|
||||
|
||||
```bash
|
||||
# 0) 幂等门:数新列是否存在(0=继续;1=已应用,中止;其它=中止)
|
||||
# 1) DDL 前 schema-only dump 到 /opt/dobotai/container/backups/(0600)
|
||||
# 2) 暂存迁移到 /opt/dobotai/container/migrations/
|
||||
# 3) 一个事务内执行,带 DO $$ ... RAISE EXCEPTION 守卫
|
||||
# 凭据走 0600 env 文件,不进 argv:
|
||||
# docker run --rm -i --network host --env-file <0600 file> postgres:17-alpine \
|
||||
# sh -ec 'exec psql -v ON_ERROR_STOP=1 "$PGURL"'
|
||||
# 4) 读回精确契约(如 is_nullable|column_default = NO|false)
|
||||
# 5) 迁移读回通过后,才做 D2 的切镜像
|
||||
```
|
||||
|
||||
**顺序铁律:先迁移、后切镜像。** 加列型迁移回滚应用时**不必**跑 `.down.sql`。
|
||||
|
||||
---
|
||||
|
||||
## F. bridge / 通知排障
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8899/healthz # {"ok":true,"service":"hermes-ci-bridge",...}
|
||||
ls /srv/aiball/ci/reports/ | tail # 报告落盘
|
||||
ls /srv/aiball/ci/reports/pending/ /srv/aiball/ci/reports/undelivered/
|
||||
tail -n 50 /srv/aiball/ci/logs/bridge.log
|
||||
```
|
||||
|
||||
- 通知发不出去时:先看 `undelivered/`。它是**入队即落盘**的设计,进程重启不丢;`pending/` 是重试中。
|
||||
- ⚠️ 微信侧只能在"用户最近发过消息"的**会话窗口内**主动推送;窗口外发送会 `ret=-2 prepare failed`(**伪限流**,不是本机故障)。恢复办法是**本人给 bot 发一句**;队列会由 20 分钟一次的补投自己清空。**不要**为此重启任何服务。
|
||||
- ⚠️ 通知正文要**空行分隔**:单换行会被客户端的 Markdown 折叠成一段。
|
||||
- ⚠️ bridge 暴露的地址**每个都要能 200**(LAN `192.168.50.134:8899` 与 Tailscale `100.92.49.78:8899`),否则收到的是打不开的链接。
|
||||
|
||||
---
|
||||
|
||||
## G. 常见故障 → 先怀疑什么
|
||||
|
||||
| 现象 | 先怀疑 | 查法 |
|
||||
|---|---|---|
|
||||
| run 一直 `queued` | 该仓库**没有**匹配标签的 Runner | `GET /repos/{r}/actions/runners`、`docker ps \| grep runner` |
|
||||
| 作业时红时绿,报拉取失败 | 用了 `uses:` 第三方 action,该 runner 取不到 | 改成 `run:` 内联(§4.3) |
|
||||
| `command not found: bash/docker` | 非交互 ssh 的 PATH / alpine 无 bash | 显式补 PATH、用绝对路径 |
|
||||
| 同上一条,但本地正常 | `ssh host "cmd"` 不读 `/etc/profile` | 见 §5.8 |
|
||||
| 明明匹配却报失败 | `pipefail` + `grep -q` 的 SIGPIPE | 去掉 `-q` |
|
||||
| 合并报错但 PR 其实合并了 | Gitea 成功返回空 body | **不要重试**,`GET /pulls/{n}` 读回 |
|
||||
| 通知到了但预览 404 | 产物包的 `.` 根成员被拒绝 | 规范化成员名(§4.7) |
|
||||
| 构建随机报原生模块加载失败 | 缓存里混了平台相关依赖 | 不缓存 `node_modules` |
|
||||
| "登录成功却又要重新登录" | 两个环境共用「父域 + Cookie 名」 | 改其中一个 DOMAIN 或换 COOKIE_NAME |
|
||||
| 境外请求全 000 | mihomo 节点被钉死 | `PUT /proxies/三毛机场 {"name":"<活节点>"}` |
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
# 待配置事项清单
|
||||
|
||||
> 每项:**现状(已核实)→ 要做什么 → 为什么 → 怎么验证 → 谁来做**。
|
||||
> 优先级:**P0** = 不做则规范形同虚设;**P1** = 影响发布可靠性;**P2** = 一致性/卫生。
|
||||
> 现状全部来自本次实测(命令在括注里)。
|
||||
|
||||
---
|
||||
|
||||
## P0
|
||||
|
||||
### 1. 分支保护(4 个仓库全无)
|
||||
|
||||
- **现状**:`GET /api/v1/repos/{r}/branch_protections` 对 `Weknora/dobotai-platform`、`Weknora/knowledge-governance`、`Weknora/knowledge-adapter-service`、`aiball/software-pc` 全部返回 `[]`。
|
||||
- **要做什么**:Gitea → 仓库 → Settings → Branches → 给 `main` 加规则:禁止直接推送、必须走 PR、必须状态检查通过。
|
||||
- **为什么**:没有保护,"提交规范 + 评审分级"就全是自觉;`main` 被 force-push 一次,"线上跑的是哪个提交"就永久失去答案。
|
||||
- **怎么验证**:重跑 `GET /repos/{r}/branch_protections`,返回非空且 `enable_push=false`、`required_approvals` 符合 §1.2。
|
||||
- **谁来做**:本人(网页操作,需仓库管理权限)。
|
||||
|
||||
### 2. `knowledge-adapter-service` 的工作流不在 `main` 上
|
||||
|
||||
- **现状**:`main` 的 `.gitea/workflows/` **不存在**;`build.yml` 只在 `ci/setup` 分支(`GET .../contents/.gitea/workflows?ref=ci/setup` → 200,`ref=main` → 404)。仓库已有 `runner-weknora-ka-01`。
|
||||
- **要做什么**:把 `.gitea/workflows/build.yml` + `.gitea/scripts/{ci_gate.py,ci_report.py}` 合到 `main`;关掉/删掉 `ci/setup` 分支。
|
||||
- **为什么**:现在推 `main` **不触发任何校验**——这个仓库的 `main` 处于"裸奔"状态。
|
||||
- **怎么验证**:`GET /repos/Weknora/knowledge-adapter-service/contents/.gitea/workflows` 在默认分支返回 `build.yml`;推一个提交,`actions/tasks` 出现新 run。
|
||||
- **谁来做**:开发(走正常 PR);合并前用 `attach_repo.sh` 的模板拷贝逻辑。
|
||||
|
||||
### 3. 生产发布的"人工确认"落地方式二选一(需在实例上实测)
|
||||
|
||||
- **现状**:Gitea **没有** GitLab 的 `when: manual` + `manual_confirmation`;生产部署目前**完全不经过 CI**(DobotAI 是人工在生产机 clone + build)。
|
||||
- **要做什么**:
|
||||
- 方案 A:确认 Gitea 1.26.2 的 Actions 是否支持 `environment:` 及其审批;支持就在生产 workflow 上用。
|
||||
- 方案 B(不依赖平台):生产 workflow 只 `on.workflow_dispatch`,必填 `sha` + `confirm=deploy-production`,最后一步"切镜像"由本人在生产机执行。
|
||||
- **为什么**:规范 §5.2 的三层确认里,第二层(参数)与第三层(人)已可落地;第一层要不要平台能力必须先实测,**不能假设**。
|
||||
- **怎么验证**:在**非生产**仓库建一个 `environment: test-protect` 的最小 workflow 试跑;或直接采用方案 B 并留一条演练记录。
|
||||
- **谁来做**:本人确认 + 开发实现。
|
||||
|
||||
### 4. "同一份构建产物"的产物落地方式(本环境当前**不满足**)
|
||||
|
||||
- **现状**:DobotAI 的做法是**在目标机上各自 build**(测试机 build 一次、生产机再 clone 源码 build 一次)——**两份二进制并不逐字节相同**,违反 §5.1。
|
||||
- **要做什么**:
|
||||
1. 构建移进 CI(仓库 Runner 上 `docker build`)或至少**只在一台机器 build 一次**;
|
||||
2. `docker save` 出 `dist/<repo>-<sha12>.tar` + 写 `release-<sha12>.json`(含 `image_id` / `tar_sha256`);
|
||||
3. 产物放到一个**两个环境都取得到**的托管点(见第 5 项);
|
||||
4. 测试与生产都 `docker load` 同一份 tar,并各自断言 `image_id` 相同。
|
||||
- **为什么**:这是本次规范最核心的新增要求。"测试验收过"必须等于"生产跑的就是那份字节",否则验收结论对生产**无效**。
|
||||
- **怎么验证**:测试机与生产机各跑 `docker image inspect --format '{{.Id}}'`,两个值**相同**;且都 == 清单 `image_id`。
|
||||
- **谁来做**:开发(改造构建 + 部署脚本);本人验收。
|
||||
|
||||
---
|
||||
|
||||
## P1
|
||||
|
||||
### 5. 构建产物的托管点
|
||||
|
||||
- **现状**:`ci-bridge` 的 `/api/ci/publish` 是给**静态网页预览**(tar.gz 解包 + URL)用的,不适合放镜像 tar / 大文件;`/srv/aiball/ci/previews/` 下面只有 3 个目录。DobotAI 的 tar 目前不在任何统一位置。
|
||||
- **要做什么**:定一个产物目录(建议 `/srv/aiball/ci/artifacts/<ns>/<repo>/<sha12>/`,与 bridge 复用同机同 token,按 sha 分目录 + 保留 N 份),或走对象存储/OSS。
|
||||
- **为什么**:§5.1 要求"生产取同一个 tar",就必须有一个**两个环境都能取到、且可按 sha 寻址**的地方。
|
||||
- **怎么验证**:从测试机和生产机各 `curl` 一次同一个 `tar_sha256` 地址,都能下到且 `sha256sum` 与清单一致。
|
||||
- **谁来做**:本人定位置 + 开发接。
|
||||
|
||||
### 6. 生产/测试的部署凭据尚不存在
|
||||
|
||||
- **现状**:只有 3 个 Weknora 仓库配了 `CI_BRIDGE_URL`(variable) / `CI_BRIDGE_TOKEN`(secret),**没有任何部署凭据**(`GET /repos/{r}/actions/secrets`)。DobotAI 的部署靠本人在生产机手动执行。
|
||||
- **要做什么**:按命名后缀建变量/密钥(Gitea 无 environment scope,见规范 §7.1):
|
||||
- Variables:`DEPLOY_TEST_HOST` `DEPLOY_TEST_PORT` `DEPLOY_TEST_USER` `DEPLOY_PROD_HOST` `DEPLOY_PROD_PORT` `DEPLOY_PROD_USER` `ARTIFACT_BASE_URL`
|
||||
- Secrets:`DEPLOY_TEST_SSH_KEY` `DEPLOY_PROD_SSH_KEY` `ARTIFACT_TOKEN`
|
||||
- **为什么**:CI 要有能力"传产物"到目标机;同时 Gitea 无 File 类型 secret,私钥内容整体存 secret、由脚本落盘 0600。
|
||||
- **怎么验证**:`GET /repos/{r}/actions/variables` 与 `/actions/secrets` 列出全部名字(值不显示);在一个 dry-run workflow 里只在**变量名 + ✓/✗** 层面校验(绝不回显值)。
|
||||
- **谁来做**:本人在网页/用 `attach_repo.sh` 的接口写入;凭据环节**必须**由本人操作。
|
||||
|
||||
### 7. `Weknora/Enterprise-Knowledge-Base` 接入(含分支收敛)
|
||||
|
||||
- **现状**:7 条分支(`main` + `release/graph-guard-v0.7.1-r1`、`release/server-running-v0.7.1`、`develop/enterprise-kb`、`chore/db-pool-tuning`、`feature/dingtalk-richtext-image`、`feature/dingtalk-image-only`);**无 workflow、无 Runner**;语言 Go+Vue。
|
||||
- **要做什么**:分支收敛(该合的合、该删的删)→ 跑规范 §8 的 10 步接入清单。
|
||||
- **为什么**:长期分支 + `release/*` 正是规范 §1.1 的反模式;且它没有任何校验。
|
||||
- **怎么验证**:`GET /repos/{r}/branches` 只剩 `main` + 短命分支;`actions/tasks` 有新 run。
|
||||
- **谁来做**:开发 + 本人确认分支删除范围(删分支不可逆)。
|
||||
|
||||
### 8. Runner 覆盖盘点
|
||||
|
||||
- **现状**:Weknora 三仓**各一个**仓库级 Runner(kg/dp/ka);`aiball` 组织级 1 个(`runner-software-01`,覆盖 `protocol-docs` 与 `software-pc`);`Weknora` 组织级 **0 个**。
|
||||
- **要做什么**:每个新接入仓库都要有**归属该仓库**的 Runner(否则 run 永远 `queued`)。用 `deploy_repo_runner.sh` 起。
|
||||
- **为什么**:规范 §4.2 —— "run 一直 queued" 的头号原因是仓库没有自己的 Runner,而不是工作流写错。
|
||||
- **怎么验证**:`GET /repos/{r}/actions/runners` 的 `runners` 非空且 `status=online`。
|
||||
- **谁来做**:开发(`bash /srv/aiball/runner/deploy_repo_runner.sh <owner/repo> <name>`)。
|
||||
|
||||
### 9. `aiball/software-pc` 的 runner 依赖第三方 action
|
||||
|
||||
- **现状**:`validate.yml` 用 `actions/checkout@v4` + `actions/setup-node@v4`,在 `runner-software-01` 上 run#1 = success(**该 runner 能取 action**);但 Weknora 三仓 runner 上刻意不用。
|
||||
- **要做什么**:在 `validate.yml` 顶部加注释说明"本 runner 支持取 action";并补上 `npm ci`(现在用 `npm install`,不可重现)。
|
||||
- **为什么**:规范 §4.3 要求"按 Runner 配置区分"且要写明理由,否则下一个人会照抄到不支持 `uses:` 的 runner 上。
|
||||
- **怎么验证**:把 `npm install` 改成 `npm ci` 后重跑 run,仍 green。
|
||||
- **谁来做**:开发。
|
||||
|
||||
### 10. 生产迁移与回滚脚本的"幂等门"复核
|
||||
|
||||
- **现状**:DobotAI 已有 `ops/` 下的 `deploy_dobotai_prod.sh` / `rollback_dobotai_prod.sh` / `prod_verify.sh` / `prod_last_turn.sh`(在生产机 `/opt/dobotai/container/ops/`)。
|
||||
- **要做什么**:对照规范 §5.8 / §5.5 逐条核对:产物 sha 校验、迁移列 COUNT 门、agent 配置 hash CAS、`flock` 互斥、0600 备份。
|
||||
- **为什么**:规范要求"部分失败后重跑不会双重应用",这是**脚本级**保证,必须在脚本里看到。
|
||||
- **怎么验证**:每个门都**故意触发**一次(喂一个已存在的列 / 错的 sha),确认脚本**拒绝执行**而不是静默继续。
|
||||
- **谁来做**:开发(只读复核 + 演练);演练在测试机做。
|
||||
|
||||
---
|
||||
|
||||
## P2
|
||||
|
||||
### 11. `aiball/upper-tool`、`zheromiku/protocol-docs` 是空仓
|
||||
|
||||
- **现状**:两个仓库 `empty=true`。
|
||||
- **要做什么**:要么初始化(README + `.gitignore` + 门禁骨架),要么归档。
|
||||
- **为什么**:空仓出现在仓库列表里,会让人"以为这里该有东西"。
|
||||
- **怎么验证**:`GET /repos/{r}` 的 `empty=false`,或仓库已归档。
|
||||
- **谁来做**:本人决定去留。
|
||||
|
||||
### 12. `docs/提交规范.md` 缺失
|
||||
|
||||
- **现状**:`~/.githooks/commit-msg` 与 `.gitea/scripts/ci_gate.py` 的提示文案都写着"详见 `docs/提交规范.md`",但仓库里**没有这个文件**。
|
||||
- **要做什么**:把规范 §2 的内容落成各仓的 `docs/提交规范.md`(或把提示文案改成指向本规范)。
|
||||
- **为什么**:一条指不到地方的指路,比不指路更费时间(原稿 §2.2 的原话)。
|
||||
- **怎么验证**:`test -f docs/提交规范.md` 通过;或 grep 提示文案确认已改为可达路径。
|
||||
- **谁来做**:开发(顺手改)。
|
||||
|
||||
### 13. 规范自身的仓库化(进行中)
|
||||
|
||||
- **现状**:本地 git 仓已建:`~/eng-standards/`(默认分支 `main`,内容已提交)。**Gitea 远端尚未创建**。
|
||||
- **要做什么**:① 本人在网页建仓 `zheromiku/eng-standards`(空仓,**不要**勾"初始化仓库");② 我加 remote 并 push;③ 各仓 `AGENTS.md` 加一行指针。
|
||||
- **为什么**:规范要"不用每次重复说一遍",靠的是可被引用的单一来源(原稿 §11)。此前放在 `/srv/aiball/ci/standards/` 下是分类错误 —— 跨仓规范不该挂在单个项目(AI球)的目录里。
|
||||
- **怎么验证**:`git ls-remote origin` 能看到 `main`;各仓 `AGENTS.md` 里的指针路径可达。
|
||||
- **谁来做**:**建仓 = 本人**(本机 repo-scoped token 建仓会 403);push 与指针 = 开发。
|
||||
|
||||
### 14. `aiball` 固件(ESP32-P4)的流水线尚未定义
|
||||
|
||||
- **现状**:`/srv/aiball/firmware` 有固件工程,但 Gitea 上**没有对应仓库**;`aiball` 组织下只有 `protocol-docs` / `software-pc` / `upper-tool`。
|
||||
- **要做什么**:固件入仓后,按同一套 §5.1 建固件流水线:`idf.py build` 一次 → 记录 `.bin` 的 sha256 → 测试机烧录测试版 → **量产/发布烧同一份 `.bin`**。
|
||||
- **为什么**:固件同样适用"同一份产物"铁律;烧录"只写不回读",所以**产物哈希是唯一可信的同一性证据**(AIxProbe 经验)。
|
||||
- **怎么验证**:`sha256sum` 两次产物文件一致;烧录日志里的字节数与 `OpenOCD wrote` 一致。
|
||||
- **谁来做**:本人定仓库归属 + 开发建流水线。
|
||||
|
||||
### 15. `ci-bridge` 的公开地址与实际可达性核对
|
||||
|
||||
- **现状**:`config.json` 的 `public_base=http://192.168.50.134:8899`、`public_base_alt=http://100.92.49.78:8899`;本次实测 `127.0.0.1:8899/healthz` 正常(`reports:19, previews:7, notify:true`)。
|
||||
- **要做什么**:核对 `192.168.50.134` 是否仍是本机当前 LAN 地址(网段变化会让所有通知里的链接失效)。
|
||||
- **为什么**:报告里给的每个 URL 都必须能 200,否则通知等于没发。
|
||||
- **怎么验证**:`curl -m 5 http://192.168.50.134:8899/healthz` 与 `curl -m 5 http://100.92.49.78:8899/healthz` 都返回 `ok:true`。
|
||||
- **谁来做**:本人/开发。
|
||||
|
||||
### 16. 规范守门脚本的 Gitea 版(可选)
|
||||
|
||||
- **现状**:附件里的 `spec-check.mjs` 是 GitLab 口径(判据含 `glab` 占位符、`.gitlab-ci.yml`、`workflow.rules`),直接拿来自Gitea 环境跑会**假红**。
|
||||
- **要做什么**:改写判据:`.gitlab-ci.yml` → `.gitea/workflows/*.yml`;`workflow.rules` → `on:` 三类入口;`glab-placeholder` → `giteactl/gitea_api.py` 等价约束;`resource_group` → `flock`。保留 `--self-test` 的反向自检。
|
||||
- **为什么**:规范的"可执行"部分最好变成退出码(原稿 §11.2),但不改写就会假红,而假红和假绿一样要修。
|
||||
- **怎么验证**:`--self-test` 造两个夹具(合规全绿、违规每条都红)。
|
||||
- **谁来做**:开发(本次未做,见任务边界)。
|
||||
|
||||
---
|
||||
|
||||
## 不建议做的事(写下来防止被"顺手加上")
|
||||
|
||||
- ❌ 不要为了能 push 去改 Gitea 凭据/token scope —— 权限没配好要**问人**,自己换 remote/换账号会把"权限没配好"变成"动作做了一半"。
|
||||
- ❌ 不要把 `deploy:test` 也挂到 `workflow_dispatch` 上"顺便重部署测试环境"(生产 workflow 就是生产 workflow)。
|
||||
- ❌ 不要给生产 workflow 设 `concurrency.cancel-in-progress: true`(会取消正在进行的部署,而取消不是回滚)。
|
||||
- ❌ 不要在生产与测试**同时**跑压测(两服共用同一套 RDS,会扰动生产)。
|
||||
- ❌ 不要在源码路径里用 `dist` / `data` / `secrets` 这些名字(`.gitignore` 会静默吞掉)。
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
# 模板评估与修正(附件 `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. 保留的(原稿最有价值的部分,不要因为"平台换了"就丢掉)
|
||||
|
||||
1. **§0 四条地基**:结论只来自可复现命令 / 每个校验都要反向自检 / 密钥不进命令行 / 提交前跑门禁。
|
||||
2. **§4.2 六条铁律**:单测跑产物不跑源码、干净 clone、反向自检三问(会红吗 / 会假绿吗 / 真的作用在目标上吗)、断言要有唯一命中点、夹具要打扫、先确认无遗留进程。
|
||||
3. **§2.2 正文写"为什么"** 的四种要素(现象/根因/取舍/证据)与示例形状。
|
||||
4. **§2.3 heredoc 禁忌**(反引号 `$()` 会被 shell 先执行,且退出码仍是 0)。
|
||||
5. **§1.3 `.gitignore` 是契约** 与两条反查命令。
|
||||
6. **§5.4 密钥只走 stdin 写文件**、`chmod 600`、去 CRLF。
|
||||
7. **§5.7 目标机三层布局** 与"停容器/换目录必须在同一个脚本里、顺序不许挪"。
|
||||
8. **§5.6 不缓存 `node_modules`**。
|
||||
9. **§2.4 陷阱库拆两份**(指令文件放索引、旁边放全文,因为指令有字节预算)。
|
||||
10. **§11 规范自己怎么被管理**(单一来源、投影、可检验、有版本)—— 见本目录的 `README.md` 与 `03-待配置事项.md` 第 13 项。
|
||||
@@ -0,0 +1,63 @@
|
||||
# Gitea 版工程规范(eng-standards 适配版)
|
||||
|
||||
> **性质**:跨仓库的系统级规范,按**本机真实环境**(自建 Gitea 1.26.2 + act_runner + ci-bridge + 阿里云测试/生产)改写。
|
||||
> **来源**:评估并适配 `eng-standards-main.zip`(原稿是 GitLab/glab 口径),模板问题与修正见 `04-模板评估与修正.md`。
|
||||
> **状态**:本次**只制定规范**。不部署生产、不运行附件原脚本、不改任何仓库。
|
||||
> **正本仓库**:`zheromiku/eng-standards`(Gitea);本机工作副本 `~/eng-standards/`。
|
||||
>
|
||||
> **怎么读**:`README` 三十秒版 → `01-规范` 全文 → `02-流程说明与Runbook`(怎么操作)→ `03-待配置事项`(还差什么)→ `04-模板评估与修正`(原稿哪里不对)。
|
||||
|
||||
## 交付物
|
||||
|
||||
| 文件 | 是什么 |
|
||||
|---|---|
|
||||
| `README.md` | 本文件:索引 + 三十秒版 + 本环境**唯一取值表** |
|
||||
| `01-规范-Git与CI-CD(Gitea版).md` | **正本**:分支/提交/评审/触发、自动检查与测试环境部署、生产发布与回滚、版本与产物 |
|
||||
| `02-流程说明与Runbook.md` | 日常动作的**逐步命令**(提交前、看 CI、接入新仓、发布、回滚) |
|
||||
| `03-待配置事项.md` | 逐项:现状 / 要做什么 / 为什么 / 怎么验证 / 谁来做 |
|
||||
| `04-模板评估与修正.md` | 附件 15 处问题的证据与改法(含实测反证) |
|
||||
| `templates/` | 修正后的可直接落仓骨架:`gitea-build.yml`、`gitignore`、`commit-msg`、`AGENTS.md` |
|
||||
|
||||
## 0. 三十秒版(只记五句)
|
||||
|
||||
1. **结论只来自可复现的命令**。数字、清单、成败一律由脚本打印或断言,禁止手抄进文档(原稿 §0-1,保留)。
|
||||
2. **每个校验都要有反向自检**:故意弄坏一处,它必须红。永远返回 0 的校验器比没有校验更糟。
|
||||
3. **密钥只从 Gitea Secrets / 目标机文件进**,不入库、不回显、不进命令行参数(会进 `ps`)。
|
||||
4. **提交前跑本仓门禁命令**;CI 不是第一次验证,CI 只是"再来一遍 + 部署"。
|
||||
5. **生产部署只部署"测试验收过的那一份产物"**(同 sha、同镜像 ID)。这条是本环境新增的硬约束,见 §6.1。
|
||||
|
||||
## 1. 本环境事实(**唯一取值表**;以命令输出为准,不要手抄到别处)
|
||||
|
||||
```bash
|
||||
# 取这张表的命令(改完环境重跑,别靠记忆)
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET /api/v1/version
|
||||
python3 ~/.hermes/scripts/gitea_api.py GET /api/v1/repos/search?limit=50
|
||||
docker ps --format '{{.Names}}\t{{.Image}}' | grep -E 'runner|bridge'
|
||||
curl -s http://127.0.0.1:8899/healthz
|
||||
```
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| Gitea | 自建 **1.26.2**;LAN `http://192.168.50.223:3000`;公网别名 `https://gitea.zheromiku.top` |
|
||||
| 账号 / 组织 | `zheromiku`;组织 `Weknora`、`aiball` |
|
||||
| 仓库 | `Weknora/{dobotai-platform, knowledge-governance, knowledge-adapter-service, Enterprise-Knowledge-Base}`;`aiball/{protocol-docs, software-pc, upper-tool(空)}`;`zheromiku/{hello, protocol-docs(空)}` |
|
||||
| CI 引擎 | Gitea Actions(act_runner,**仓库级** Runner 容器) |
|
||||
| Runner | `runner-weknora-kg-01`→knowledge-governance、`runner-weknora-dp-01`→dobotai-platform、`runner-weknora-ka-01`→knowledge-adapter-service、`runner-software-01`(aiball 组织级,覆盖 protocol-docs/software-pc) |
|
||||
| Runner 配置 | `capacity: 1`(同时只跑 1 个作业)、`force_pull: false`、labels `ubuntu-latest/24.04/22.04` → `docker://docker.gitea.com/runner-images:*`、注册令牌**文件挂载**(`/run/secrets/registration_token:ro`)、挂 `docker.sock` |
|
||||
| 工具链/缓存 | 主机 `/srv/aiball/ci/toolcache` 只读挂载为容器内 `/opt/citools`;可写缓存 `/srv/aiball/ci/toolcache/cache` → `/opt/citools-cache` |
|
||||
| ci-bridge | `/srv/aiball/ci/bridge/bridge.py`,监听 `0.0.0.0:8899`;容器内可达地址 `http://172.17.0.1:8899`;局域网 `http://192.168.50.134:8899`;Tailscale `http://100.92.49.78:8899` |
|
||||
| bridge 接口 | `POST /api/ci/report`(作业结果→落盘+微信通知)、`POST /api/ci/publish`(静态产物 tar.gz→URL)、`GET /p/<ns>/<repo>/<sha>/…`、`GET /r/<id>`、`GET /healthz` |
|
||||
| bridge 落盘 | 报告 `/srv/aiball/ci/reports/`(含 `pending/`、`undelivered/`);预览 `/srv/aiball/ci/previews/` |
|
||||
| 仓库变量/密钥 | `CI_BRIDGE_URL`(variable)、`CI_BRIDGE_TOKEN`(secret)——仅 Weknora 三仓已配 |
|
||||
| 提交闸门 | 本机 hook `~/.githooks/commit-msg`(`core.hooksPath`)+ CI 侧 `.gitea/scripts/ci_gate.py`(同一套规则,绕不过) |
|
||||
| 仓库接入脚本 | `/srv/aiball/runner/attach_repo.sh <owner/repo> <runner-name>`、`/srv/aiball/runner/deploy_repo_runner.sh` |
|
||||
| Gitea API 客户端 | `~/.hermes/scripts/gitea_api.py`(库/CLI)、`~/.local/bin/giteactl`(`runs/show/logs/watch/rerun`)、`giteacli`(**只读用途,本机 token 无 read:user**) |
|
||||
| 测试服务器 | 阿里云 ECS `120.76.217.51`(key `~/.ssh/weknora-aliyun-test-ed25519`) |
|
||||
| 生产服务器 | 阿里云 ECS `120.76.218.137`(key `~/.ssh/weknora-aliyun-prod-ed25519`) |
|
||||
| 生产布局 | `/opt/dobotai/{config/dobotai.env, container/{compose.yaml,runtime.env,migrations/,backups/,ops/}, secrets/}`;镜像由 `container/runtime.env` 的 `DOBOTAI_IMAGE=` 选择 |
|
||||
| 健康/版本 | `http://127.0.0.1:8090/healthz`(`{"status":"ok"}`)、`http://127.0.0.1:8090/version`(`version/commit_sha/build_time`) |
|
||||
| 数据库 | **永远是阿里云 RDS(PostgreSQL 17.9)**;目标机 Docker 只跑应用容器,禁止本地库 |
|
||||
| 构建受限 | 目标机 **不可达 Docker Hub** → 用 daocloud 镜像源;Go 用 `GOPROXY=https://goproxy.cn,direct` |
|
||||
| 本机网络 | 出网走 mihomo;境外全 000 时先查 `~/.clashctl` 节点(`PUT /proxies/三毛机场 {"name":"<活节点>"}`) |
|
||||
|
||||
> ⚠️ 上表里**只有一个数字是"规范"**(如"生产路径 /opt/dobotai"),其余都是"现状快照"。现状会漂;所以每项都给了取数命令。
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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` | 提交触发的校验+编译+预览+上报;`deploy:test` 是待填骨架 |
|
||||
| `gitea-prod-deploy.yml` | `.gitea/workflows/deploy-production.yml` | **生产发布唯一入口**,只 `workflow_dispatch`,不构建 |
|
||||
| `gitignore` | `.gitignore` | 去掉原稿自相矛盾的 `!src/secrets/` 例外 |
|
||||
| `commit-msg` | `~/.githooks/commit-msg` | 本机提交闸门;规则必须与 `ci_gate.py` 一致 |
|
||||
| `AGENTS.md` 骨架 | `AGENTS.md` | 见下(agent 指令文件有写入保护,故内联在此) |
|
||||
| —— 随仓携带 | `.gitea/scripts/ci_gate.py`<br>`.gitea/scripts/ci_report.py` | **逐字节复制已验证版本**,不要重写 |
|
||||
|
||||
## 落仓后的自检(反向自检,规范 §3.2-3)
|
||||
|
||||
```bash
|
||||
# 1) 故意违规的提交必须被闸门拦住,且 build 不跑
|
||||
git commit -F - <<'EOF'
|
||||
update stuff
|
||||
EOF
|
||||
# 判据:本地 hook 报 ❌;push 后 CI 的 gate 红、build 未执行
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
## `AGENTS.md` 骨架(复制到仓库根,按本工程改写 TODO)
|
||||
|
||||
````markdown
|
||||
# AGENTS.md — 给在 <PROJECT> 里干活的 AI 代理
|
||||
|
||||
> 这是**操作手册**,不是给用户看的介绍。改代码前先读「硬约束」与「已知陷阱」。
|
||||
>
|
||||
> **系统级规范(本机所有 Gitea 仓库都适用)**:仓库 `zheromiku/eng-standards`(本机工作副本 `~/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 check # 或 npm run verify —— 一条命令跑完本地门禁;跳过项会显式警告"不算全绿"
|
||||
```
|
||||
|
||||
- 门禁**串行**、失败即停:构建会清空产物目录,而端到端测的是构建产物 —— 并行会制造假失败。
|
||||
- 改到部署脚本或产物形状时,必须另外跑一次真实的装配/启动自检。
|
||||
|
||||
## 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. 提交与推送
|
||||
|
||||
- conventional commits,正文中文,**说清"为什么"**;一件事一个提交。
|
||||
- 提交信息不要用反引号 / `$()`(会触发命令替换):用 `git commit -F -` + heredoc。
|
||||
- 推之前跑完第 2 节;推送后用第 3 节确认流水线,失败就**先读 trace 再改代码**。
|
||||
- 生产发布**不走 push**:它是独立的 `deploy-production` workflow,只由人手动发起(见规范 §5.2)。
|
||||
````
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# Conventional Commits 提交信息校验闸门(本机 hook)
|
||||
# 安装:cp templates/commit-msg ~/.githooks/commit-msg && chmod +x ~/.githooks/commit-msg
|
||||
# git config --global core.hooksPath ~/.githooks
|
||||
# 绕过:git commit --no-verify(不建议);CI 侧 .gitea/scripts/ci_gate.py 会再拦一次,绕不过。
|
||||
# 规则必须与 .gitea/scripts/ci_gate.py 完全一致 —— 两处不一致 = 本地绿、CI 红(或反之)。
|
||||
set -uo pipefail
|
||||
|
||||
msg_file="${1:-}"
|
||||
[ -n "$msg_file" ] || { echo "commit-msg: 缺少提交信息文件参数" >&2; exit 1; }
|
||||
[ -f "$msg_file" ] || exit 0
|
||||
|
||||
# 取第一条非注释行作为标题
|
||||
subject_line=""
|
||||
while IFS= read -r line; do
|
||||
case "$line" in ''|'#'*) continue ;; esac
|
||||
subject_line="$line"; break
|
||||
done < "$msg_file"
|
||||
[ -n "$subject_line" ] || exit 0
|
||||
|
||||
# 放行自动生成的提交
|
||||
case "$subject_line" in
|
||||
Merge\ *|Revert\ \"*|fixup!\ *|squash!\ *|amend!\ *) exit 0 ;;
|
||||
esac
|
||||
|
||||
TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert"
|
||||
RE='^('"$TYPES"')(\([a-zA-Z0-9._/-]+\))?!?: .{3,}$'
|
||||
|
||||
fail() {
|
||||
cat >&2 <<EOF
|
||||
|
||||
❌ 提交信息不合规,已拦截。
|
||||
|
||||
你写的 : $subject_line
|
||||
原因 : $1
|
||||
|
||||
规范 : <类型>(<范围>): <简短描述>
|
||||
类型 : feat 新功能 | fix 修缺陷 | docs 文档 | style 格式(不改逻辑)
|
||||
refactor 重构 | perf 性能 | test 测试 | build 构建/依赖
|
||||
ci CI 配置 | chore 杂务 | revert 回滚
|
||||
范围 : 可选,小写,如 (card) (weknora) (deploy)
|
||||
|
||||
正确示例
|
||||
feat(asset): 新增按歌曲生成应援序列
|
||||
fix(parser): 修复表格解析越界导致的 500
|
||||
ci: 接入 Gitea Actions 自动编译
|
||||
|
||||
要点
|
||||
· 类型小写,冒号后必须有一个空格,描述 ≥3 个字符
|
||||
· 标题 ≤72 字符,结尾不要加句号
|
||||
· 说明「为什么」写正文(空一行后),一行一件事
|
||||
· 破坏性变更:类型后加 !,并在正文写 BREAKING CHANGE: 说明
|
||||
· ⚠️ 正文不要直接在 -m 里写反引号 / \$()(会被 shell 先执行):
|
||||
git commit -F - <<'EOF'
|
||||
|
||||
临时绕过(不推荐,CI 仍会拦截): git commit --no-verify
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ ${#subject_line} -le 72 ] || fail "标题 ${#subject_line} 字符 > 72"
|
||||
case "$subject_line" in
|
||||
*. | *。) fail "标题结尾不要加句号" ;;
|
||||
esac
|
||||
printf '%s' "$subject_line" | grep -Eq "$RE" || fail "不符合 <类型>(<范围>): <描述>"
|
||||
@@ -0,0 +1,192 @@
|
||||
# <PROJECT> —— 提交触发的 CI(Gitea Actions)
|
||||
# 来自 zheromiku/eng-standards 的 templates/gitea-build.yml;正本见 01-规范-Git与CI-CD(Gitea版).md §4
|
||||
#
|
||||
# ⚠️ 四处形状不许动(动了会被规范判为不合规):
|
||||
# · 触发只有三类入口:推任意分支(校验+编译,不部署)、推 main(+自动部署测试环境)、workflow_dispatch
|
||||
# · 提交信息闸门(gate)必须在编译之前
|
||||
# · 生产部署**不在这里** —— 它是独立的 prod-deploy.yml,只由 workflow_dispatch 触发(§5.2)
|
||||
# · 不用第三方 action(`uses:`)—— 本 runner 取不到就随机红;全部逻辑写在 run: 里
|
||||
#
|
||||
# ⚠️ 本 runner 实测特性(act 语义,见正本 §4.4):
|
||||
# · act_runner 不会自动检出代码 ⇒ 必须自己 git init + fetch + checkout
|
||||
# · 每步以 `bash -e -o pipefail` 执行 ⇒ 禁止 `cmd | head`(SIGPIPE 假红)
|
||||
# · 工具链来自主机只读挂载:容器内 /opt/citools;可写缓存 /opt/citools-cache
|
||||
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BRIDGE_URL: ${{ vars.CI_BRIDGE_URL }}
|
||||
BRIDGE_TOKEN: ${{ secrets.CI_BRIDGE_TOKEN }}
|
||||
GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LANG: C.UTF-8
|
||||
LC_ALL: C.UTF-8
|
||||
|
||||
# 同一 ref 的新推送取消旧的**校验/编译**运行(注意:生产部署 workflow 不许这么设,见 §5.4)
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: 检出代码
|
||||
run: |
|
||||
set -uo pipefail
|
||||
SERVER="${GITHUB_SERVER_URL:-http://192.168.50.223:3000}"
|
||||
HOSTPORT="${SERVER#*://}"
|
||||
BRANCH="${GITHUB_REF_NAME:-${GITHUB_REF#refs/heads/}}"
|
||||
cd "${GITHUB_WORKSPACE:-$(pwd)}"
|
||||
rm -rf ./* .git 2>/dev/null || true
|
||||
git init -q .
|
||||
git remote add origin "http://x-access-token:${GIT_TOKEN}@${HOSTPORT}/${GITHUB_REPOSITORY}.git"
|
||||
git fetch -q --depth 50 origin "$BRANCH"
|
||||
git checkout -q -B "$BRANCH" FETCH_HEAD
|
||||
echo "已检出 ${GITHUB_REPOSITORY} 分支 ${BRANCH}"
|
||||
git log -1 --format=' %h %an %s'
|
||||
|
||||
- name: 提交信息规范校验(权威闸门,失败不编译)
|
||||
run: python3 .gitea/scripts/ci_gate.py
|
||||
|
||||
build:
|
||||
needs: [gate]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- name: 检出代码
|
||||
run: |
|
||||
set -uo pipefail
|
||||
SERVER="${GITHUB_SERVER_URL:-http://192.168.50.223:3000}"
|
||||
HOSTPORT="${SERVER#*://}"
|
||||
BRANCH="${GITHUB_REF_NAME:-${GITHUB_REF#refs/heads/}}"
|
||||
cd "${GITHUB_WORKSPACE:-$(pwd)}"
|
||||
rm -rf ./* .git 2>/dev/null || true
|
||||
git init -q .
|
||||
git remote add origin "http://x-access-token:${GIT_TOKEN}@${HOSTPORT}/${GITHUB_REPOSITORY}.git"
|
||||
git fetch -q --depth 50 origin "$BRANCH"
|
||||
git checkout -q -B "$BRANCH" FETCH_HEAD
|
||||
git rev-parse HEAD > /tmp/ci-sha
|
||||
git rev-parse --short=12 HEAD > /tmp/ci-sha12
|
||||
date +%s > /tmp/ci-start
|
||||
: > /tmp/ci-preview-url
|
||||
echo skipped > /tmp/ci-build-status
|
||||
echo skipped > /tmp/ci-gate-status
|
||||
git rev-list --count FETCH_HEAD > /tmp/ci-commits-count 2>/dev/null || echo 1 > /tmp/ci-commits-count
|
||||
|
||||
- name: 编译与自检
|
||||
run: |
|
||||
set +e; set +o pipefail
|
||||
LOG=/tmp/ci-build.log; : > "$LOG"
|
||||
run() { echo "-- $* --"; "$@" 2>&1 | tee -a "$LOG"; return "${PIPESTATUS[0]}"; }
|
||||
runin() { local d="$1"; shift; echo "-- ($d) $* --"; ( cd "$d" && "$@" ) 2>&1 | tee -a "$LOG"; return "${PIPESTATUS[0]}"; }
|
||||
|
||||
export PATH=/opt/citools/go/bin:/opt/citools/bin:$PATH
|
||||
export GOCACHE=/opt/citools-cache/gocache GOMODCACHE=/opt/citools-cache/gomod
|
||||
export UV_CACHE_DIR=/opt/citools-cache/uv GOPATH=/opt/citools-cache/gopath
|
||||
export GOPROXY=https://goproxy.cn,direct GOTOOLCHAIN=local GOFLAGS=-mod=mod
|
||||
mkdir -p "$GOCACHE" "$GOMODCACHE" "$UV_CACHE_DIR" "$GOPATH" 2>/dev/null
|
||||
|
||||
echo "工具链: $(go version 2>&1) | $(uv --version 2>&1) | $(node -v 2>&1) | $(python3 -V 2>&1)"
|
||||
fail=0; ran=""
|
||||
|
||||
if [ -f go.mod ]; then
|
||||
ran="$ran go"
|
||||
run go build ./... || fail=1
|
||||
run go vet ./... || fail=1
|
||||
run go test ./... || fail=1
|
||||
fi
|
||||
if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f setup.py ]; then
|
||||
ran="$ran python"
|
||||
if grep -qE '^check:' Makefile 2>/dev/null; then
|
||||
run make UV=uv check || fail=1
|
||||
else
|
||||
run uv sync || fail=1
|
||||
run uv run pytest -q || fail=1
|
||||
fi
|
||||
fi
|
||||
WEBPKG=""
|
||||
for p in package.json web/package.json frontend/package.json ui/package.json; do
|
||||
if [ -f "$p" ]; then WEBPKG="$p"; break; fi
|
||||
done
|
||||
if [ -n "$WEBPKG" ]; then
|
||||
ran="$ran node"; D="$(dirname "$WEBPKG")"
|
||||
if [ -f "$D/package-lock.json" ]; then runin "$D" npm ci || fail=1; else runin "$D" npm install || fail=1; fi
|
||||
if grep -q '"build"[[:space:]]*:' "$WEBPKG" 2>/dev/null; then runin "$D" npm run build || fail=1; fi
|
||||
fi
|
||||
if [ -z "$ran" ] && [ -f Makefile ]; then ran="$ran make"; run make || fail=1; fi
|
||||
if [ -z "$ran" ]; then echo "未识别到构建入口(go.mod / pyproject.toml / package.json / Makefile 都没有),跳过编译"; fi
|
||||
|
||||
if [ "$fail" -eq 0 ]; then echo success > /tmp/ci-build-status; else echo failure > /tmp/ci-build-status; fi
|
||||
tail -c 4000 "$LOG" > /tmp/ci-build-tail
|
||||
echo "构建结果: $(cat /tmp/ci-build-status)(识别:$ran)"
|
||||
[ "$fail" -eq 0 ] || exit 1
|
||||
|
||||
- name: 发布网页预览
|
||||
if: always()
|
||||
run: |
|
||||
set +e; set +o pipefail
|
||||
: > /tmp/ci-preview-url
|
||||
if [ "$(cat /tmp/ci-build-status 2>/dev/null)" != "success" ]; then
|
||||
echo "构建未成功,跳过预览发布"; exit 0
|
||||
fi
|
||||
WEB=""
|
||||
for d in web/dist dist build out public web/build frontend/dist; do
|
||||
if [ -f "$d/index.html" ]; then WEB="$d"; break; fi
|
||||
done
|
||||
if [ -z "$WEB" ]; then echo "未发现静态网页产物目录,跳过预览发布"; exit 0; fi
|
||||
# ⚠️ 不要在打包时产生 `.` 根成员:先 cd 再只打目录内容(正本 §4.7)
|
||||
tar -czf /tmp/ci-web.tgz -C "$WEB" . || { echo "打包失败"; exit 0; }
|
||||
B="${BRIDGE_URL:-}"
|
||||
for cand in "$B" "http://172.17.0.1:8899" "http://192.168.50.134:8899" "http://100.92.49.78:8899"; do
|
||||
if [ -n "$cand" ]; then curl -s -m 5 -o /dev/null "$cand/healthz" && { B="$cand"; break; }; fi
|
||||
done
|
||||
SHA="$(cat /tmp/ci-sha)"
|
||||
echo "发布预览到 $B(产物目录 $WEB)"
|
||||
curl -sS -m 90 -X POST "$B/api/ci/publish?repo=${GITHUB_REPOSITORY}&sha=${SHA}" \
|
||||
-H "X-CI-Token: ${BRIDGE_TOKEN}" -H 'Content-Type: application/gzip' \
|
||||
--data-binary @/tmp/ci-web.tgz -o /tmp/ci-pub.json
|
||||
cat /tmp/ci-pub.json; echo
|
||||
URL="$(python3 -c "import json;print(json.load(open('/tmp/ci-pub.json')).get('url',''))" 2>/dev/null)"
|
||||
if [ -n "$URL" ]; then echo "$URL" > /tmp/ci-preview-url; echo "网页预览: $URL"; fi
|
||||
|
||||
- name: 上报 ci-bridge(通知 + 报告)
|
||||
if: always()
|
||||
run: |
|
||||
set +e; set +o pipefail
|
||||
B="${BRIDGE_URL:-}"
|
||||
for cand in "$B" "http://172.17.0.1:8899" "http://192.168.50.134:8899" "http://100.92.49.78:8899"; do
|
||||
if [ -n "$cand" ]; then curl -s -m 5 -o /dev/null "$cand/healthz" && { B="$cand"; break; }; fi
|
||||
done
|
||||
echo "使用 ci-bridge: $B"
|
||||
S=$(date +%s); T=$(cat /tmp/ci-start 2>/dev/null); [ -n "$T" ] || T=$S
|
||||
echo $((S - T)) > /tmp/ci-duration
|
||||
CI_BRIDGE_RESOLVED="$B" python3 .gitea/scripts/ci_report.py
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# 测试环境部署:只在推 main 时自动跑(正本 §4.6)
|
||||
# TODO:本作业只是骨架。填 <TEST_HOST> 与下面的读回断言后再启用。
|
||||
# 发布**必须**是"构建一次、两个环境部署同一份产物"(正本 §5.1)—— 见 prod-deploy.yml 的注释。
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
deploy:test:
|
||||
needs: [gate, build]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
concurrency:
|
||||
group: deploy-test
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: 占位(未启用)
|
||||
run: |
|
||||
echo "TODO: ① 取本次构建产物(按 sha12 寻址)② 校验 tar_sha256 ③ scp 到测试机"
|
||||
echo " ④ docker load → 断言 image_id == release-<sha12>.json"
|
||||
echo " ⑤ 切镜像 → 读回 healthz / version / env"
|
||||
echo " ⑥ /version.commit_sha 必须 == $(cat /tmp/ci-sha12 2>/dev/null || echo '<sha12>')"
|
||||
echo " 见正本 §5.1 证据链表与 02-流程说明与Runbook.md §C"
|
||||
@@ -0,0 +1,96 @@
|
||||
# <PROJECT> —— 生产发布(Gitea Actions,**只由人工发起**)
|
||||
# 正本见 01-规范-Git与CI-CD(Gitea版).md §5
|
||||
#
|
||||
# ⚠️ 这个文件是"生产发布"的**唯一入口**。四条不许动的形状:
|
||||
# 1. 触发**只有** workflow_dispatch —— 不存在任何自动触发路径(§4.1)
|
||||
# 2. 必填 sha + confirm,第一步就校验,不合法直接红(§5.2 第二层)
|
||||
# 3. concurrency.cancel-in-progress **必须** false —— 取消不是回滚,会留下换了一半的镜像(§5.4)
|
||||
# 4. **不构建**。只搬运"测试验收过的那一份产物"(同 tar_sha256 / 同 image_id,§5.1)
|
||||
#
|
||||
# 人工确认三层(§5.2):
|
||||
# 第一层 平台:本 workflow 只能手动发起
|
||||
# 第二层 参数:confirm 必须等于字面量 deploy-production
|
||||
# 第三层 人:最后一步"切镜像"由本人在生产机执行(或本人确认脚本输出)
|
||||
|
||||
name: deploy-production
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sha:
|
||||
description: '要发布的短 sha(12 位,必须是已在测试环境验收过的那一份)'
|
||||
required: true
|
||||
confirm:
|
||||
description: '确认执行生产发布,请输入字面量 deploy-production'
|
||||
required: true
|
||||
note:
|
||||
description: '发布说明(对应 release issue / 验收记录链接)'
|
||||
required: false
|
||||
|
||||
# ⚠️ 不要 cancel-in-progress
|
||||
concurrency:
|
||||
group: deploy-production
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
LC_ALL: C.UTF-8
|
||||
|
||||
jobs:
|
||||
guard:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: 人工确认 + 参数校验
|
||||
run: |
|
||||
set -eu
|
||||
SHA="${{ github.event.inputs.sha }}"
|
||||
CONFIRM="${{ github.event.inputs.confirm }}"
|
||||
echo "请求发布 sha=$SHA"
|
||||
[ "$CONFIRM" = "deploy-production" ] || { echo "❌ confirm 不匹配,拒绝执行"; exit 1; }
|
||||
printf '%s' "$SHA" | grep -Eq '^[0-9a-f]{12}$' || { echo "❌ sha 必须是 12 位小写十六进制"; exit 1; }
|
||||
echo "$SHA" > /tmp/ci-sha12
|
||||
echo "✅ 参数合法"
|
||||
|
||||
publish:
|
||||
needs: [guard]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: 取产物并校验(不构建!)
|
||||
run: |
|
||||
set -eu
|
||||
echo "TODO: 从产物托管点取 release-<${{ github.event.inputs.sha }}>.json 与 <repo>-<sha12>.tar"
|
||||
echo " ① sha256sum <tar> 必须 == 清单 tar_sha256"
|
||||
echo " ② 清单 sha12 必须 == 输入 sha"
|
||||
echo " 见 02-流程说明与Runbook.md §D"
|
||||
|
||||
- name: 交付到生产机(只传文件,不切镜像)
|
||||
run: |
|
||||
set -eu
|
||||
echo "TODO: scp 到 120.76.218.137:/opt/dobotai/container/incoming/"
|
||||
echo " ⚠️ 凭据只走 stdin / 文件,绝不进命令行参数(会进 ps)"
|
||||
echo " ⚠️ ssh 是非交互 shell,不读 /etc/profile:远端命令要显式补 PATH"
|
||||
|
||||
- name: 输出给本人的"下一步"(第三层确认在这里)
|
||||
run: |
|
||||
cat <<'EOF'
|
||||
|
||||
✅ 产物已送达生产机。**最后一步由本人在生产机执行**:
|
||||
|
||||
exec 9>/var/lock/dobotai-deploy.lock
|
||||
flock -n 9 || { echo "另一个部署正在进行"; exit 1; }
|
||||
sha256sum /opt/dobotai/container/incoming/<repo>-<sha12>.tar # == 清单 tar_sha256
|
||||
docker tag <current> <repo>:rollback-<old12>-before-<sha12> # 回滚用
|
||||
docker load -i /opt/dobotai/container/incoming/<repo>-<sha12>.tar
|
||||
docker image inspect --format '{{.Id}}' <repo>:sha-<sha12> # == 清单 image_id
|
||||
# 迁移(若有)必须在切镜像**之前**完成并读回(正本 §5.5)
|
||||
cd /opt/dobotai/container && docker compose --env-file runtime.env -f compose.yaml \
|
||||
up -d --no-deps --force-recreate <ctr>
|
||||
curl -fsS http://127.0.0.1:8090/healthz # {"status":"ok"}
|
||||
curl -fsS http://127.0.0.1:8090/version # commit_sha == <sha12>
|
||||
|
||||
最后的门:**本人在生产机器人上发一条真实消息**,再回读 turns.agent_id。
|
||||
回滚:bash /opt/dobotai/container/ops/rollback_dobotai_prod.sh
|
||||
⚠️「取消流水线」不是回滚。
|
||||
EOF
|
||||
@@ -0,0 +1,34 @@
|
||||
# 依赖与构建产物
|
||||
node_modules/
|
||||
dist/
|
||||
bin/
|
||||
build/
|
||||
|
||||
# 运行期数据(唯一不可再生的东西,绝不入库)
|
||||
data/
|
||||
secrets/
|
||||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
backups/
|
||||
|
||||
# ⚠️ 无斜杠前缀的规则匹配任意层级:`secrets/` 会连 `src/secrets/` 一起忽略。
|
||||
# 处理办法**不是**加 `!` 例外(例外越加越多,规则本身就没人看得懂了),
|
||||
# 而是**别在源码路径里用 `dist`/`data`/`secrets` 这些名字**。
|
||||
# (附件原稿的模板在这里给了 !src/secrets/,与它自己的正本条款矛盾,已删。)
|
||||
|
||||
# 发布产物(CI 生成,绝不能入库;产物名带 sha,见正本 §1.4)
|
||||
runtime/
|
||||
out/
|
||||
*.tar.gz
|
||||
*.tar
|
||||
version.txt
|
||||
release-*.json
|
||||
|
||||
# 本机与密钥
|
||||
.DS_Store
|
||||
*.log
|
||||
.tmp/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
Reference in New Issue
Block a user