对着试点仓落地时暴露的问题做的口径与模板修正(本轮不推、不打 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。
52 KiB
Git / 提交 / CI-CD 规范(Gitea 版)
性质:系统级规范,适用于本机所有 Gitea 仓库(
Weknora/*、aiball/*)。 正本:本文件。同仓/同级仓库用相对路径引用;跨机请带上路径与本文件版本号。 样板实例:Weknora/knowledge-governance(唯一把"提交闸门 + 编译 + bridge 上报"全套跑通的仓库)。 口径:必须 = 违反即不合格;建议 = 有理由才能偏离,偏离写在代码注释里(本环境的规矩是"注释写为什么,不写是什么")。与附件原稿的关系:原稿是 GitLab / glab 口径,本环境是 Gitea 1.26.2 + act_runner,触发体系、手动作业语义、变量作用域、CLI 全部不同。逐条映射与证据见
04-模板评估与修正.md。
0. 四条地基
- 结论只来自可复现的命令。任何数字、清单、成败,要么由脚本自己打印,要么写成断言 —— 禁止手抄进文档。手抄的东西过期时没有任何人会发现,而过期的表现是"守卫变松"。
- 每个校验都要有反向自检:故意弄坏一处,它必须红。一个"永远返回 0 的校验器"比没有校验更糟 —— 它给人一种已经检查过的错觉。
- 本环境的范例:
knowledge-adapter-service的test/bad-commit分支在 CI 上必须失败(实测已存在这样一条失败 run)。
- 本环境的范例:
- 密钥只从 Gitea
Secrets/ 目标机文件进:不入库、不回显、不进命令行参数(会进ps)。 - 提交前跑本仓门禁命令;CI 不是第一次验证,CI 只是"再来一遍 + 部署"。
1. 仓库、分支、评审
1.1 分支模型
- 单主干:默认分支
main,所有改动直接进main(原稿口径)。本地只保留main这一条长期分支。 - 需要评审时用短命分支 + PR,合完即删。禁止长期分支。
- 现状欠账:
Weknora/Enterprise-Knowledge-Base有 7 条分支(含release/*、develop/*),属于"长期分支"反模式,见03-待配置事项.md。
- 现状欠账:
- 禁止重写
main历史(rebase/push --force)。理由:CI 以main的推送为部署触发,"线上跑的是哪个提交"这个问题靠main的历史回答;历史一改,答案就没了。 - 一切"退路"都必须是远端有的东西:本地分支、本地 tag 都不算退路。真正的退路是"远端有那个提交、且能重跑那条流水线"。
1.2 什么时候开 PR(建议,不是门禁)
默认直推 main。 原稿 §1.1 的口径就是"所有改动直接进 main,需要评审时才用短命分支 + MR"。
下面这张表是建议 —— 判断依据是"错了代价高不高、需不需要留下可追溯的记录":
| 改动落在 | 建议 |
|---|---|
deploy/、.gitea/、db/migrations/、生产配置模板、Dockerfile |
建议开 PR(这类改动错了会动到线上),描述里写清"怎么验的" |
业务代码(internal/、src/、web/) |
可直接提交;改动大、或想让别人能回看时开 PR |
| 纯文档 / 文案 / 注释 | 直接提交 |
⚠️ 这是建议,不是"必须"。 把建议当强制会把 3 人小团队卡在流程上 —— 本规范早期版本正是这么错的(曾写成"业务代码必须 PR、main 禁止直接推送"),已按原稿口径改回。 上一行这句留着,是为了让下一个人不再"顺手加严"。
- PR 描述是交付契约,不是说明文。
gitea-workflows技能要求:写清 已实现 / 安全与数据边界 / 真跑过的测试 / 尚未验证 / "未合并、未部署"。 - 合并前必须读到权威读回:
GET /pulls/{n}→state=closed、merged=true、merge_commit_sha非空。 ⚠️ Gitea 的合并接口成功时可能返回空 body,客户端解析 JSON 会误报失败 —— 永远不要重试合并,改为读回。 - ⚠️
mergeable=true只表示没有冲突,不代表测试、评审、迁移演练、部署门禁已满足。
1.3 分支保护:默认不开
原稿的默认是 main 不受保护(它 §8 甚至写明"不要勾 Protected"),本规范沿用。
- 默认不开。它有一个容易忽略的代价:"必须状态检查通过"只门控 PR 合并 ——
直推
main时管不住(CI 是"推完再跑",红了也拦不住那次推送)。 既然本环境以直推为主,开保护的收益小于它带来的流程成本。 - 什么时候值得开:某段时间改动都要评审,或需要"任何人都不能直推"(交接期、多人并行、外部协作者)。
开的时候按需勾"禁止直接推送 + 必须走 PR",不勾强制状态检查 ——
除非该仓工作流覆盖所有分支(判据见
06-接入既有工程.md§2)。 - 现状:
Weknora/knowledge-governance曾开过一次,已按本节口径关掉;7 个仓现在GET /repos/{r}/branch_protections全为[]。
1.4 .gitignore 是契约(五条)
必须忽略:依赖(node_modules/)、构建产物(dist/、bin/)、运行期数据(data/、*.db)、密钥(secrets/、.env)、发布产物(runtime/、*.tar.gz、version.txt、*.tar)。
.gitignore对已入库的文件无效 ⇒ "规则齐全"不等于"没被追踪"。断言要查两件事:规则在、且git ls-files里没有这些路径。- 无斜杠前缀的规则匹配任意层级。被咬过的三次都是这个:
dist/吞掉src/admin/dist/、data/吞掉src/data/、secrets/吞掉src/secrets/。 处理办法不是加!例外(例外越加越多,规则本身就没人看得懂了),而是别在源码路径里用dist/data/secrets这些名字。 - 症状很轻:
git commit只回一句paths are ignored,文件静默没进提交。看到这句必须回去查。 - 产物名里带 sha:
dist/app-<sha12>.tar这类发布产物一律忽略,绝不入库。 - 反查命令(提交前跑一次,成本一秒):
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:featfixdocsstylerefactorperftestbuildcichorerevert。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:
git commit -F - <<'EOF'
fix(dobotai): 上游不再收到重复的客户端请求头
根因:……(写为什么)
EOF
<<'EOF' 的单引号是"里面的字符都是字面量"的唯一保证。
- ⚠️ 这条不只是提交信息:任何"把文本写进文件"的场合都一样。把含反引号的文本放进双引号交给
python3 -c/node -e,shell 会先执行反引号里的东西 —— 写进文件的句子凭空少片段,而退出码仍是 0、没有任何报错。要写的文本一律走带引号的 heredoc。 - 不要用
--no-verify绕过门禁(CI 侧ci_gate.py会再拦一次)。
2.4 两处闸门(本环境已就位)
- 本机 hook:
~/.githooks/commit-msg(由git config --global core.hooksPath生效)—— 快速反馈。 - 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 verify(首选名字,本环境已统一)/ npm run verify / scripts/verify.sh 都行。
⚠️ 名字可以不同,但 AGENTS、Runbook、CI 必须指向同一个:
两边各写一套步骤的结局是"迟早只改一边",而症状是本地绿、CI 红(或反过来),谁都说不清哪边是对的。
- 必须串行:构建会清空产物目录,而端到端/契约测试跑的是产物 ⇒ 并行会随机报"某条路由 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 |
大多数是测源码:Go 自己编译测试程序、pytest 直读源码、Vitest 默认也跑源码 |
| 5 集成/端到端 | 真起服务、打真实端口 | 单测覆盖不到"TCP 上真的连得上" |
| 6 部署脚本自测 | bash deploy/deploy_test.sh |
真跑一次 release;守的是静默故障 |
3.2 八条通用铁律
- ⚠️ "跑产物"的套件(端到端 / 契约 / 产物自检)必须排在构建之后:它们读的是构建输出,
改了源码不重建,测的就是旧代码(第一次一定会被这个骗一次)。
⚠️ 但别把这条推广成"所有单测都依赖预构建产物" —— 那是错的:
go test自己编译测试程序、pytest直读源码、vitest默认也跑源码。写错这条的代价是给每个测试套件加上假的"先构建"依赖, 白白拖慢门禁,还会制造"没构建所以红了"的假故障。(本规范早期版本就是这么写的,已改回。) - ⚠️ 只在"干净 clone"里出现的缺陷:本地工作区总有"恰好能跑"的残留。要专门造一次干净树(
git clean -xdf后 clone / CI 的干净检出)验证。 - ⚠️ 每个校验都要反向自检:
- 断言"它会红":故意弄坏一处,必须失败。
- 断言"它不会假绿":给它一条对照(正例必须过)。
- 断言"校验器作用在目标上":曾有一条
nginx -t -c静默地在读镜像自带的配置 —— 每次"通过",实际什么都没验。
- ⚠️ 断言要有唯一命中点:负向断言会被别的记录撑成假绿(判据太宽);判据太窄则永远够不着,而且报绿。
- ⚠️ 测试夹具要打扫:只建不删会把
$TMPDIR攒到几万条,之后mktemp -d开始卡住 —— 表现像"某个用例死循环",实际是环境问题。 - ⚠️ 跑之前先确认没有上次遗留的进程(
pgrep -fl "app"):否则新实例EADDRINUSE起不来,探针打到旧实例上 ⇒ 一堆假 401/404。 - ⚠️
set -o pipefail+grep -q= SIGPIPE 谎报失败:grep -q一命中就退出并关闭管道,上游随即 SIGPIPE,整条管道返回 141 —— 明明匹配上了却判失败。用不带-q的 grep,或让它直接读文件。 - ⚠️ 文档里不许出现会漂移的数字(测试条数、断言条数)。要么由脚本打印,要么写成断言(如
EXPECTED_CHECKS)。
4. CI 流水线(自动检查 / 测试 / 构建 / 测试环境部署)
4.1 触发规则(Gitea 口径)
Gitea 没有 workflow: rules,触发写在 on:。本环境只允许三类入口:
| 入口 | 写法 | 用途 |
|---|---|---|
| 推任意分支 | on.push.branches: ["**"] |
校验 + 编译(不部署) |
| 人工发起 | on.workflow_dispatch |
手动重跑校验;生产发布是独立 workflow(见 §5) |
⚠️ CI 与 CD 分离(本环境口径):推送触发的流水线只做 CI —— 检查、测试、构建、证据。
"推 main 就自动部署测试环境"不是通用规范:部署目标、产物形态、迁移与回滚由每个项目自己设计
(§8)。通用模板里默认不含任何部署作业。
反例(本规范早期版本):模板里放一个 deploy:test 占位作业,它打印 TODO 然后返回成功 ——
那不是"还没做",那是假报成功:网页上看起来部署过了。
硬约束:
- ⚠️ 生产部署必须是独立 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 |
校验 | 环境参数不变量、配置语法、规范守门 | 只读仓库、几秒钟,没理由拖到部署阶段才发现 |
| (无) | 部署 | 通用 CI 里没有部署作业 | CD 由项目独立设计(§8);占位作业不许假报成功 |
要点:
- 校验类作业与
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 测试环境部署(项目自己的 CD,不属于通用 CI)
⚠️ 本节描述的是项目 CD 的目标形态,不是通用 CI 的一部分。通用模板不会生成这些作业; 项目要做部署时另写独立 workflow(
workflow_dispatch触发、cancel-in-progress: false)。
- 触发方式由项目自己决定(例如"CI 全绿之后由人工发起")。通用 CI 不替你决定,也不自动部署。
⚠️ 本规范早期版本写的是"推
main→ 自动部署测试环境"—— 那是把某个项目的选择当成了通用规则,已改。 - 部署后必须读回(不是"启动成功"就算):
- 容器镜像 ID == 本次构建产物的镜像 ID(
docker inspect --format '{{.Id}}'); GET /healthz返回{"status":"ok"};GET /version的commit_sha== 本次main的短 sha;- 关键 env 读回(
docker exec <ctr> env | grep -E '^DOBOTAI_')。
- 容器镜像 ID == 本次构建产物的镜像 ID(
- 测试环境部署不等于验收:验收的门是"测试机上一轮真实业务跑通",由人确认,见 §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 才算发出去了,否则会收到一条没有预览链接的通知。
- ⚠️ 通知里的链接基址用非秘密配置
CI_PUBLIC_BASE_URL(由 workflow 传给上报脚本):CI 运行 / 提交 / 变更 三类链接统一用它拼,链接里的仓库、SHA、run_id 仍是真实值。不覆盖GITHUB_SERVER_URL(检出与所有内部调用继续用它)。配置缺失或非法 ⇒ 上报脚本明确回退GITHUB_SERVER_URL,并把基址来源写进link_base_source(便于事后核对)。 bridge 侧只渲染允许来源(ci_base_url,支持多个)的链接;禁止对消息文本做全局 URL 替换 —— 提交标题、日志摘要里的 URL 是事实,一个字符都不许改。 - ⚠️ 报告入口用 Gitea 的运行页:消息里的"查看"给 CI 运行 / 变更(push 才有)/ 预览;
不再附 bridge 自己的
/r/<id>页(重复且在内网)。只有拿不到运行链接(缺失或非可信来源)时 才退回 bridge 报告页 —— 不留"没有任何证据入口"的通知。 - ⚠️ 静态产物打包不要产生
.根成员:tar -czf x.tgz -C dir .会写入.和./index.html;严格解包器可能拒绝或报错,而流水线仍显示成功 —— 通知到了、预览没有。bridge 侧应规范化(剥掉./,丢弃归一化为./空的成员),而不是直接拒绝整个包。
4.8 必需检查清单与结果口径(汇总不许自欺)
每个项目声明自己的必需检查清单,并让清单能被打印出来(如 make list),
CI 与本地都按清单逐项核对:
| ID | 必需检查 | 判据 |
|---|---|---|
| identity | 触发提交 / 检出身份 | 实际 HEAD 等于事件完整 SHA(不是短 sha,也不是"当时的分支 HEAD") |
| commits | 提交信息 | 本次推送范围内的提交全部通过规则 |
| deps-install | 依赖安装 | 按锁文件装;缺锁即失败,不回落 npm install / -mod=mod |
| format | 格式 | 不合规即非零退出;CI 不自动修复 |
| static | 静态检查 / 类型 | 声明的语言模块全部通过 |
| build | 构建 | 声明的模块构建成功 |
| tests | 自动化测试 | 必需套件实际执行且通过;空套件 / 全跳过不算通过 |
| contracts | 项目契约 | 版本与产物/接口契约通过(被 SKIP 的断言不算通过) |
| security | 密钥 / 依赖漏洞 | 无泄漏;无未获授权的适用高危风险;扫描器没跑成也不许算过 |
| manifests | 依赖清单 | 门禁过程中 go.mod / go.sum / 锁文件没有被任何一步改写 |
结果口径(三条,都是踩出来的):
- 失败即停,但汇总照做:第一个必需项失败后,剩下的记为
not_run并如实写进报告, 不许从报告里消失(消失了,下游就会当成"本来就没有这一项")。 not_run/skipped都不算过:汇总只认success; 不适用要在清单里声明(比如"本地没有事件负载,identity 只在 CI 必需"),不能在运行时悄悄跳过。- 汇总本身故障 ⇒ 不能报绿:读不到 / 解析不了报告时,结论是"质量无从判定",不是"没问题"。
预览与通知是旁路:预览只在全部必需项 success 之后发布(并且要断言产物包里真的有入口文件、
报告里给出的每个地址都探测到 200);通知失败不改质量结论,只单独标注 notification_status。
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。人工确认按三层落地:
- 平台层:生产部署是独立 workflow,只有
on.workflow_dispatch,不存在自动触发路径。 - 参数层:dispatch 必须显式传入
sha(要发的短 sha)与confirm(必须等于字面量deploy-production),workflow 第一步就校验;不匹配直接红。防误点、防机器人误触。 - 人层:最后一步"切镜像"由本人在生产机执行(或由本人在生产机确认脚本输出)。CI 只负责"传产物 + 备好命令",不替人按下那一刻。
(若实例已启用 Environment 审批之类能力,可作为额外一道;本规范不假设它存在,需在实例上实测,见
03-待配置事项.md第 8 项。)
5.3 健康检查(读回证据,缺一即不合格)
生产切换后必须取全下列证据,不能只看"容器起来了":
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、跨人手手动执行时挡不住。 - 真正的互斥放在目标机(可靠性只在这一层):
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 必须人工按序执行:
- 幂等门:先数
information_schema.columns里有没有新列 ——0才继续,1直接中止(已应用),其它值中止。 - 版本匹配:RDS 是 PostgreSQL 17.9 ⇒ 用
postgres:17-alpine(postgres:16-alpine会server version mismatch失败)。 - DDL 前先 schema-only dump 到
container/backups/(0600)。 - 暂存迁移文件到
container/migrations/(部署集 = 本地记录)。 - 在一个事务里执行,带
DO $$ ... RAISE EXCEPTION守卫。 - 读回精确契约(如
is_nullable|column_default=NO|false)。 - 凭据不进 argv:写 0600 临时文件
PGURL=<url>,docker run --rm -i --network host --env-file <file> ... 'exec psql -v ON_ERROR_STOP=1 "$PGURL"'。 - 顺序:迁移完成并读回后,才切镜像。
回滚:加列型迁移(新列可空/有默认)不必跑 .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. 版本号与发版
本节 2026-10-12 重写(原稿 §6 只有"构建注入"与"默认不打 tag"两条): ① 权威来源从笼统的"构建时注入"改成每个项目显式声明一个来源; ② 正式发版改为注释 tag + Gitea Release(原稿写的是"默认不打 tag"); ③ 新增"CI 成功 / 正式发版 / 部署"三态区分; ④ 新增 CI 自动检查与只读的发布预检入口。
本节 2026-10-13 二次修正(对着真实试点仓
knowledge-governance落地时发现的问题): ① 版本条目形状由"四个自定义必填节(改了什么 / 兼容性与迁移影响 / 怎么验收 / 已知问题)" 改为"固定四类小节(Added / Changed / Fixed / Security,分类可省略、不许空栏目)"—— 原来的自定义节会把审查过程、测试命令、待办都塞进发布记录;过程记录改放docs/archive/、待办放docs/BACKLOG.md; ② 破坏性变化改为固定写在### Changed里、以**破坏性**开头并必须链接发布文档(链接目标会被对账,不存在即红); ③ 新增构建身份(release_version/build_kind/commit_sha/display_version/built_at): 开发构建显示X.Y.Z-dev+g<短SHA>或dev+g<短SHA>,不许只显示上一正式版本号,也不许自称正式版; ④ 还没有正式版本(新仓常态)必须放行:version检查不判红、构建落入dev+g<短SHA>、 首个正式版本号由人指定(预检把它列为阻塞项,不替你编一个号); ⑤ 没核对远端 tag 就不许说"校验成功":CI 用--require-remote,拿不到远端清单即红; ⑥ 修正"CI 取不到 tag 所以不用 tag"的过时口径:正式版本就是要用注释 tag, 只是构建时不要用git describe取版本号(浅克隆里它会静默退化)。 ⑦ 测试 tag 只能在临时测试仓库里创建,不许在真实仓里造 tag 试检查器。操作步骤见同级
07-发版操作说明(tag与Release).md。
6.1 版本号只有一个权威来源(每个项目显式声明它)
- 每个项目必须声明自己的版本号权威来源,并写进该仓的
AGENTS.md; 其他位置一律读取或生成,禁止多处手工同步。 - 本环境已声明的:
knowledge-governance—— 仓库根CHANGELOG.md顶部那条已发布版本 (scripts/version.py读它;构建时-ldflags -X …/internal/version.Number=<该值>注入)。- 其他 Go 项目 —— 默认同样以
CHANGELOG.md为权威来源;要换成别的来源,必须在AGENTS.md里写明。
- ⚠️ 禁止:仓库根再放一个
VERSION文件、把版本号写进源码常量、用git describe取版本 (CI 检出是git fetch --depth 50,取不到 tag ⇒git describe静默给错值)。 - 前端子工程(
knowledge-governance/web)的package.json.version只表示前端包版本,不是服务版本。 - 版本号只增不减;回退走部署层回滚(§5.6),不改版本历史。
- 运行时
/version报出的version/commit_sha/build_time是"线上跑的是哪个提交"的权威答案; CI 断言"注入进二进制的号 == 权威来源"。 - 没有 HTTP 面的项目(纯脚本/内部服务)至少要能用
python3 scripts/version.py --identity(或make version)打印构建身份 JSON;把"用户可见标识"与"仓库/提交事实"一起给出来。
6.2 版本号格式、日常改动与构建身份
-
三段十进制
主版本.次版本.修订版本;不许前导零(01.2.3不合格);只增不减。 -
日常改动记入
CHANGELOG.md的[Unreleased](按 §6.3 的四类小节); 不要求每个提交都改本文件 —— 有使用者可见的变化才记。 -
[Unreleased]段若有,必须在文件最上面;发版时把它改成## [x.y.z] - YYYY-MM-DD。 -
构建身份(开发构建也要能说清"这是哪个开发版"):
字段 含义 release_version正式版本基线;还没有正式发布过就是空 build_kinddevelopment/release—— 只有显式发布上下文(如BUILD_KIND=release)才是 releasecommit_sha完整 40 位提交 SHA display_version用户可见标识: X.Y.Z/X.Y.Z-dev+g<短SHA>/dev+g<短SHA>/devbuilt_at构建时间 ⚠️ 开发构建不能只显示上一正式版本号;普通
main构建不许自称正式版 (判据是显式上下文,不是"CHANGELOG 顶部有版本号")。
6.3 条目形状:固定四类小节(不写过程记录)
标题形如 ## [x.y.z] - YYYY-MM-DD。小节只认四类:
| 小节 | 写什么 |
|---|---|
### Added |
新增能力 |
### Changed |
行为调整(破坏性变化写在这里) |
### Fixed |
修复 |
### Security |
安全相关变化 |
-
分类可省略(有哪类写哪类),但不要空栏目 —— 写了就得有内容,不需要就整节删掉。
-
面向使用者描述变化,不逐条复制提交;不写"第几轮审查"、不贴命令与测试输出。
-
过程记录(审查轮次、命令输出、逐条修正明细)放
docs/archive/; 未合并事项放docs/BACKLOG.md,不作为已完成变更写进变更记录。 -
破坏性变化 / 数据迁移:写在
### Changed里,以**破坏性**开头,并链接发布文档 (升级/迁移步骤与回退办法写在那份文档里):### Changed - **破坏性**:接口 `/api/old` 下线,升级步骤见 [迁移步骤](docs/RELEASE_RUNBOOK.md)⚠️ 检查器会对账仓内链接:
docs/….md不存在 ⇒ 红(防止链接到一个不存在的 runbook)。
6.4 正式发版 = 注释 tag + Gitea Release(要人批准)
- 正式发布使用注释 tag(
git tag -a),名称v<版本号>,指向验收过的准确提交(完整 40 位 SHA)。 - Gitea Release 关联该 tag;Release 说明来自 CHANGELOG,不维护第二份独立正文。
- ⚠️ 只有主人明确批准后才打 tag / 建 Release。CI 不自动升版本、不打 tag、不发布。
- ⚠️ 不接 tag 触发流水线(要把某个 tag 上线,就在
workflow_dispatch的ref里选它)。 - ⚠️ 已发布的 tag 不移动、不覆盖(需要"重发"就发一个新版本号);回滚恢复旧产物,不改版本历史。
6.5 三个状态必须分开说(不许合并成一个"绿")
| 状态 | 含义 | 证据(缺一即不算) |
|---|---|---|
| CI 成功 | 这次提交通过了必需检查 | 提交上的 status context + run 链接 + 完整 SHA |
| 正式发版 | 批准后打了 tag / 建了 Release | 远端 tag 的 SHA == 验收过的提交;Release 的 tag_name 读回一致 |
| 部署 | 某个环境真的在跑这个版本 | /version 的 commit_sha + 部署读回证据(§5.3) |
"CI 绿"不等于"发过版","发过版"不等于"部署了" —— 汇报时说清是哪一个。
6.6 自动检查(CI 必查,失败即红)
CHANGELOG.md格式与版本号合法:三段十进制、无前导零、日期合法、顺序严格递减、不重复;- 小节只属四类、没有空栏目、破坏性变化在 Changed 里且带可用的发布文档链接(§6.3);
- 构建身份 == 权威来源:发布构建的
release_version必须等于 CHANGELOG 顶部; 开发构建必须是dev形状且不许自称正式版(这条要有一条"自称正式版 ⇒ 必须红"的负例); 注入的断言被 SKIP 也算不通过; - 存在正式 tag 时:tag 版本 == 它指到的那个提交里的版本记录;正式 tag 必须是注释 tag
(轻量 tag 不合格);核对必须真的问远端(
--require-remote)—— 拿不到远端清单 ⇒ 红(没核对就不许说"校验成功");非正式 tag(如本地x)不当作发布版本; - 缺失 / 格式错 / 版本漂移 ⇒ 失败;
- 还没有正式版本(新仓常态)⇒ 放行:不许因为"没有已发布版本"判红,首个版本号由人指定。
⚠️ 没有 tag 不算失败:开发版本不打 tag 是常态,日常 CI 不许因此变红。
参考实现:scripts/check_changelog.py(--self-test 逐条证明会红;--tags [--require-remote] 带 tag 一致性)
与 scripts/version.py --identity(构建身份);
清单里的检查项 id 叫 version(与 contracts 分开:一个是"记录是否合规",一个是"注入是否等于来源")。
6.7 发布预检入口(只读,不产生副作用)
正式发版前先跑预检,它只列出事实:
- 待发布版本(默认按
--bump给建议值,也可--version显式指定); - 完整提交 SHA(HEAD 或
--sha); - 对应的 CI 成功证据(该提交上的 status context + 最近一次运行);
- CHANGELOG 里对应内容(
[Unreleased]原样打印); - 同名远端 tag / Gitea Release 是否已存在;
- 遗留风险与未验证项(指向 CHANGELOG 的『已知问题』与本次声明的未验证项 —— 预检不替你判断可否接受)。
预检有阻塞项(工作区脏 / 检查不过 / 没有 Unreleased / 版本已存在 / tag 或 Release 已存在 / 没有 CI 成功证据 / 拿不到凭据 ⇒ 只能写"未核对")时,不给"可以发"的结论。
批准后才执行:--execute --approve-version X.Y.Z --approve-sha <完整SHA>(两个批准参数必须逐字符等于预检结果),
顺序是打注释 tag → 推送 tag → 建 Release → 读回(读取不到 tag SHA、或 Release 的 tag_name 不一致 ⇒ 不算完成;
tag 推送成功但建 Release 失败时不要重复推 tag)。
参考实现:scripts/release_preflight.py(--self-test 断言"写类 git 命令与 POST/PUT/DELETE/PATCH
在预检路径上被拒")。
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 verify(首选;scripts/verify.sh亦可)—— 一条命令跑完、串行、失败即停、末尾汇总;跳过项显式警告"不算全绿"。本地与 CI 必须是同一条命令。 - 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. 分支保护(可选,默认不开):见 §1.3。只在"需要任何人都不许直推"时开,且不要勾强制状态检查(除非该仓工作流覆盖所有分支)。
- 8.
AGENTS.md指针:仓库AGENTS.md写一行指回本规范;陷阱库拆AGENTS.md(索引)+docs/agents/traps.md(全文)。 - 9. 自检一遍:推一个故意违规的提交,确认
gate红且build没跑(反向自检,§3.2-3)。 - 10. 首次推
main因为缺变量而红 —— 这是设计:变量检查在任何上传动作之前,目标机一个字节都不会被碰。
跨仓引用:正本仓库
zheromiku/eng-standards-main(Gitea);本机工作副本~/eng-standards/。 ⚠️ 指令文件不会跨仓库自动加载 —— 每个仓库要在自己的AGENTS.md里写一行指针,否则那个仓库里的代理看不到这份规范。
9. 速查表
# ── 提交前 ────────────────────────────────────────────────
make verify # 本仓门禁(本地与 CI 同一条),串行、失败即停
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> # 重跑(不推新提交即可验证整条链路)
# ── 发版(人工,CI 不代劳;详见 §6 与 07-发版操作说明)──────
# 0. CHANGELOG:把 [Unreleased] 改成 ## [x.y.z] - 日期,按四类小节写(破坏性变化带发布文档链接)
python3 scripts/check_changelog.py --tags --require-remote # 结构 + 正式 tag 与提交内版本一致(真查远端)
python3 scripts/version.py --identity # 构建身份(开发/发布、显示版本、完整 SHA)
python3 scripts/release_preflight.py # **只读**预检:版本/完整SHA/CI证据/CHANGELOG/远端tag与Release/遗留项
# 1. 主人批准(版本号 + 完整 SHA + 那次 CI 的 run 链接)之后才执行:
python3 scripts/release_preflight.py --execute --version <x.y.z> --approve-version <x.y.z> --approve-sha <完整SHA>
# 内部:打注释 tag → 推 tag → 建 Release(说明来自 CHANGELOG)→ **读回** tag SHA 与 tag_name
# 2. 部署是另一件事:构建注入版本 → 写 release-<sha12>.json → 部署测试 → 验收 → dispatch 生产
# ── 上线与回退 ───────────────────────────────────────────
# 推任意分支 → gate + build(**只做 CI,不部署**)
# 生产 → 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 不符就拒跑) | 本环境 |