Files
.pouch/skills/builder/references/contract.md
T
2026-10-13 20:17:26 +08:00

237 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Builder 构建发布契约 v1
本契约是 builder skill 的规范本体。`scripts/check.py` 是它的可执行形态:改契约先改
check.py,本文档跟随。所有接入项目按同一套 make 目标、产物形状和环境变量执行,
builder 脚本只做发布,不做项目特定的构建逻辑。
配置边界按 Git 项目根生效:`makefile.builder` 所在仓库就是 builder 项目根。
如果父仓库通过 submodule 引入另一个项目,submodule 的构建、`.env.builder` 和
`.pouch/builder/` 分发脚本属于 submodule;父仓库不得替它承载这些配置。
分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT),
Agent 只保留授权判断和歧义处理。** 构建在哪台机器执行、使用哪个 CLI/model 和
session 如何保存,由共享 `agent-runtime` 的 binding 决定;makefile 不能承载主机或
Agent 选择。
`builder.android`、`builder.ios`、`builder.harmony` 都是合法的可配置 binding。新增平台
只增加 binding 和对应 make target,不改 builder skill 的固定枚举。
契约文件固定为项目根 `makefile.builder`,调用方式:
```bash
make -f makefile.builder <target>
```
不要把 builder 目标写进用户的 `Makefile` 或 `makefile`。`check.py` 只读
`makefile.builder`。
发布凭据固定为项目根 `.env.builder`。不要把这些键写进用户的 `.env`。脚本不读取
`.env`。
## 1. Make 目标
### 必备目标(所有项目)
| 目标 | 要求 |
|------|------|
| `help` | 分组列出全部目标;首屏含当前版本 |
| `version` | 输出一行版本号,适合脚本消费 |
| `clean` | 只删除明确、受限的构建产物目录 |
| `build` | 编译/打包主产物;尊重 `ARCH`(单架构原语);**不得内含任何上传动作** |
### 条件目标
| 目标 | 适用 | 要求 |
|------|------|------|
| `deb` | 有 DEB 产物的项目 | 产出 `$(DIST_DIR)/<name>_<version>_<arch>.deb`;只构建不上传。未在命令行/环境指定 `ARCH=` 且 `ARCHES` 多于一项时,按清单逐个递归构建;指定了 `ARCH=` 则只打那一个 |
| `docker` | 有镜像的项目 | 构建本地单平台镜像 `linux/$(ARCH)`;**禁止 `--push`、禁止多平台** |
| `android` | 有 Android APK/AAB 的项目 | 可与 `build` 相同。产出 `$(DIST_DIR)/<name>_<version>_<arch>.apk`(或 `.aab`);只构建不上传。`ARCHES` 是构建机 ISA;fat APK 只留本机一项 |
| `ios` | 有 iOS IPA 的项目 | 可与 `build` 相同。产出 `$(DIST_DIR)/<name>_<version>_<arch>.ipa`;只构建不上传 |
| `arches` | 可选(新模板必备) | 打印一行空格分隔的 `ARCHES`;无此目标视为存量 makefile,隐式单架构 |
| `push-deb` | 同时有多种发布轨道的项目 | 仅调 builder 的 `upload_deb.sh`。未指定 `ARCH=` 时上传 `ARCHES` 中每一项对应的 `.deb`;指定了则只上传那一个 |
| `push-docker` | 同时有多种发布轨道的项目 | 仅调 builder 的 `publish_docker.sh` |
| `push-copy` | 同时有多种发布轨道的项目 | 仅调 builder 的 `copy_artifact.sh`;由它按配置选择内置拷贝或自定义脚本 |
| `push` | 单一产物类型时必备;多产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 |
| `obtain` | 有产物的项目 | 调用 `obtain.sh`,对精确 commit SHA 拿到已存在的产物。`BUILD_VENUE=ci` 时等待该 SHA 的 forge 流水线再 inspect,不在本机 `docker push` |
| `venue` | 可选(新模板必备) | 打印一行 `BUILD_VENUE`(`local` 或 `ci`) |
规则:
1. 项目有 DEB 产物的判据:`makefile.builder` 配方以独立命令调用 `dpkg-deb`/`debuild` 或产出 `.deb`;
不能把 `xcodebuild` 中包含的字符误判成 `debuild`。
有镜像的判据:项目根存在 `Dockerfile`。
Android 判据:配方产出 `.apk` / `.aab`。iOS 判据:配方产出 `.ipa`。
拷贝发布判据:配方调用 `copy_artifact.sh`。`copy_artifact.sh` 是稳定入口,具体分发可由
`COPY_SCRIPT` 接管。
2. 多于一种发布轨道必须拆 `push-deb` / `push-docker` / `push-copy`,`push` 依序聚合;
单轨道项目一个 `push` 即可。
3. `docker` 目标只能本地构建。多平台镜像无法拆成"make 构建 + 单独推送"
(`buildx --push` 是一步),因此多平台发布只能走 `publish_docker.sh`。
未指定 `ARCH=` / `PLATFORMS` 时,`publish_docker.sh` 把 `ARCHES` 映射为
`linux/<arch>,…`。
4. push 类目标必须是薄包装:解析脚本路径后委托,不内联 curl/token/端点/`docker cp`。
5. `build` 保持单架构,供 `deb` 循环调用。不要让 `build` 自己遍历 `ARCHES`。
`build` 禁止 `docker cp`、`copy_artifact.sh`、curl/scp。
## 2. 变量
配置来源按变量的稳定性分层:`makefile.builder` 是项目构建与发布轨道的可迁移事实,
包括 `ARCHES`、产物命名、目标和项目内 `COPY_SCRIPT`;`.pouch/builder/local.mk` 是
不提交的机器工具链覆盖;`.env.builder` 是不提交的凭据、发布端点、目标目录和本轮别名。
命令行/环境变量可以覆盖前两者,但不应把项目契约依赖在本机环境上。
| 变量 | 默认 | 说明 |
|------|------|------|
| `BUILD_VENUE` | `local` | 产物在哪构建:`local` 本机 make;`ci` 由 GitLab/GitHub 流水线构建。这不是新轨道。写在 `makefile.builder` 并提交。`ci` 时 `push-docker` 委托 `obtain.sh`(等流水线 + inspect),不跑 `publish_docker.sh` |
| `ARCHES` | `amd64` | 本项目要发布的架构清单,空格分隔,子集只能是 `amd64` `arm64`。写在 `makefile.builder` 里并提交;不要放进 `.env.builder`。缺省或无 `arches` 目标时视为单架构 `amd64`。不要默认写成双架构 |
| `ARCH` | `$(firstword $(ARCHES))` | 这一次构建的架构。必须落在 `ARCHES` 里;非法值必须 `$(error)` 并提示合法值。命令行/环境传入时只构建/上传这一项 |
| `VERSION` | `` (空) | 为空时按「版本号」节从 Git 祖先稳定 tag 推导;禁止 `sort -V` 取全局最新 |
| `DIST_DIR` | `dist` | 产物目录(deb / apk / ipa) |
| `PROJECT_NAME` | git 仓库名 | 包名/镜像名主体 |
`ARCHES` 是「这个项目发哪些架构」;`ARCH` 是「这一次编哪个」。宇宙集合仍是 `amd64` \| `arm64`,不开放其它 ISA。
`deb` 配方在打包前必须把 `packaging/DEBIAN/control`(或等价文件)的 `Architecture:` 写成当前 `$(ARCH)`,不要写死 `amd64`。推荐先拷到 `$(DIST_DIR)/packaging-$(ARCH)` 再改,避免弄脏源树。
`build` 配方必须使用 `$(ARCH)`(例如 `GOARCH=$(ARCH)`、`--platform=linux/$(ARCH)`)。配方若忽略 `ARCH`,双架构会打出两个文件名不同、内容却都是宿主机架构的包。Builder 不安装 qemu 或交叉编译器;看不出交叉编法时 `ARCHES` 只留本机一项。
### 版本号
`make -f makefile.builder version` 输出一行规范版本(无 `v` 前缀)。DEB 的
`Version` 与文件名直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh`
(`makefile.builder` 通过 `scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用
releaser 选定下一正式 SemVer。测试号把祖先基线的 patch 机械 +1 作为排序键
(`v1.4.2` 之后 → `1.4.3~…`),不是在宣布下一正式号就是 1.4.3。正式 tag 由
releaser 事先打好。`version.sh` 不执行 `git fetch`。
推导前 `git fetch --tags`(本地 linked worktree 共享 tags,不必再 fetch 才
能看见其它 worktree 新打的 tag)。基线是 **HEAD 祖先上最近的稳定 tag**,
不是全仓库 `sort -V` 的最大号。稳定 tag 仅 `v<major>.<minor>.<patch>`。
| 判定 | 规范版本 | DEB 文件 | Docker tag |
| --- | --- | --- | --- |
| HEAD exact-match 稳定 tag `v1.4.2` | `1.4.2` | `name_1.4.2_<arch>.deb` | `1.4.2` |
| 其它 commit;祖先最近稳定 tag `v1.4.2` | `1.4.3~<date>.<branch>.<n>+g<sha>` | `name_1.4.3~<date>.<branch>.<n>+g<sha>_<arch>.deb` | `1.4.3-<date>.<branch>.<n>.g<sha>` |
| 祖先中没有稳定 tag | `0.0.0~<date>.<branch>+g<sha>` | 同上替换规范版本 | 同上映射 |
测试与正式进入同一 apt / Docker 仓库。测试 DEB 必须用 `~` 挂在基线的下一
patch 上:`1.4.2` < `1.4.3~…` < `1.4.3`。已装上一正式版的机器可以升到
bugfix 测试包;同号正式版仍盖住测试。不要写成 `1.4.2~…`(小于已发布的
`1.4.2`),不要写成 `1.4.2+<date>` / `1.4.3-beta`(Debian 里 `-beta` 大于
`1.4.3`)。日期必须写在 `~` 之后。Docker tag 不得含 `~`、`/`、`:`,由规范
版本把 `~` → `-`、`+g` → `.g`。
`<date>`:`YYYYMMDDTHHMM`,构建的日期加分钟。加分钟是为了同一天重复构建
也能排序:只写到日期时,同分支同日的版本前缀完全相同,只剩 `+g<sha>` 可
区分,而 Debian 把哈希当普通文本比较,新构建可能排到旧构建下面(
`apt upgrade` 因此看不到最新包)。已设 `SOURCE_DATE_EPOCH` 时用该时刻的
UTC 时间,否则用本机当前时间。正式 `X.Y.Z` 不加日期。切换格式当天注意:
Debian 把字母排在标点之前,`YYYYMMDDT…` 小于旧格式的 `YYYYMMDD.…`,同日
已上传的旧格式包会压住新包,需要把它们在仓库里移除或停用;次日恢复正常。
`<branch>`:`git rev-parse --abbrev-ref HEAD`,detached 时用
`BUILD_BRANCH` / `CI_COMMIT_BRANCH` / `GITHUB_REF_NAME`,再没有则
`detached`。清洗:小写;`/`、`_` → `-`;去掉非 `[a-z0-9-]`;压缩连续
`-`;过长截断(给 base、日期、距离、SHA 留位置;Docker tag 上限 128)。
`<n>` 为基线 tag 到 HEAD 的 commit 数;`<sha>` 为 7 位短哈希。不要把
`--dirty` 写入版本;脏树发布仍走既有门禁。显式 `VERSION=` / `IMAGE_TAG=`
可覆盖推导,但不得把非 exact-match 的 commit 标成正式 `X.Y.Z`。
`makefile.builder` 不要内联 `git describe` 或 `sort -V`,include 本 skill 的
`scripts/version.mk`:
```makefile
include $(HOME)/.pouch/skills/builder/scripts/version.mk
```
## 3. 发布环境变量
### DEB 轨道
| 变量 | 必填 | 说明 |
|------|------|------|
| `DEB_SERVER_URL` | 是 | 仓库服务地址 |
| `DEB_TOKEN` | 是 | 认证令牌;只从环境读取,绝不进 argv/日志/git |
| `DEB_REPOSITORY` | 是 | 目标仓库名 |
| `DEB_UPLOAD_PATH` | 否 | 覆盖默认上传路径 `/api/v2/upload/package` |
### 拷贝轨道
| 变量 | 必填 | 说明 |
|---|---|---|
| `COPY_DEST` | 与 `COPY_SCRIPT` 至少配置一个 | 内置后备目标:本地目录,或 Docker 目标 `container:/absolute/dir`(`docker cp`) |
| `COPY_SCRIPT` | 与 `COPY_DEST` 至少配置一个 | 项目内 `.pouch/builder/` 脚本的路径优先写在 `makefile.builder` 并提交;也可用绝对路径或环境/.env.builder 覆盖;必须是可执行文件,同时配置时优先于 `COPY_DEST` |
| `COPY_AS` | 否 | 第一份文件的额外 basename,逗号或空格分隔 |
`copy_artifact.sh FILE [FILE ...]` 是 APK/IPA 等产物的统一分发入口。不读 `.env`,脏工作树默认拒绝,
`ALLOW_UNCOMMITTED=1` 放行。
未配置 `COPY_SCRIPT` 时,入口把文件拷到 `COPY_DEST`,校验目标存在后打印 SHA-256。
配置 `COPY_SCRIPT=.pouch/builder/publish-apk.sh` 时(项目内脚本通常由 `makefile.builder` 传入),入口按项目根解析、先校验每个文件存在,然后直接执行:
```text
/absolute/path/to/publish-script FILE [FILE ...]
```
相对路径只允许位于项目的 `.pouch/builder/` 下;绝对路径仍支持外部脚本。不经过 shell 字符串求值,
不把 token 放进参数。脚本通过位置参数接收原始产物绝对路径,自行负责重命名、上传、远端校验和通知;
当前项目根以 `BUILDER_PROJECT_ROOT` 环境变量提供。
脚本返回非零即视为分发失败;成功后入口只报告脚本成功和本地 SHA-256,不宣称远端已校验。
`--dry-run` 只打印将调用的脚本与文件,不执行自定义脚本。
### Docker 轨道
| 变量 | 必填 | 说明 |
|------|------|------|
| `DOCKER_REGISTRY` | 是 | registry 主机,无 scheme |
| `DOCKER_REPOSITORY` | 否 | 默认取 git 仓库名 |
| `IMAGE_TAG` | 否 | 默认由规范版本渲染:正式为 `X.Y.Z`;测试将 `~` 换成 `-`、`+g` 换成 `.g` |
| `PLATFORMS` | 否 | 本机/本轮覆盖。未设置时由 `ARCHES` 映射为 `linux/<arch>,…`;再没有则 `linux/amd64`。不要把项目的架构清单只写在这里(`.env.builder` 不进 git) |
`PLATFORMS` 优先级(高 → 低):用户这轮 `--platform` 或已 export 的 `PLATFORMS` → 命令行 `ARCH=` 映射的 `linux/$(ARCH)`(由 `push-docker` 传入)→ `.env.builder` 的 `PLATFORMS` → `makefile.builder` 的 `ARCHES` → `linux/amd64`。
`BUILD_VENUE=ci` 时 Docker 发布路径是 `obtain.sh`:只认 **该 commit SHA** 的流水线。没有流水线、job 失败、超时、或 Harbor inspect 不到 `version.sh --docker` 算出的 tag,一律非零退出。不触发流水线。合进默认分支若项目声明不构建,这是正确失败;调用方应打 tag 或推送到项目会构建的 ref(例如 `dev/*`)。CI job 若已经 `docker push`,obtain 成功即视为产物已在 registry,不要再本机 push 一次。
配置来源优先级:shell 已显式设置的值 > 项目根 `.env.builder` > 失败并询问用户。
`.env.builder` 由 builder 脚本加载(不回显任何值);当前 shell 已设置的值优先。
不要读取或改写用户 `.env`。空值视为未配置。不要提交 `.env.builder`。
### 工作区安全
脏工作树(有未提交修改)默认拒绝发布;`ALLOW_UNCOMMITTED=1` 显式放行并在汇报中
注明镜像/包包含哪些未提交修改。该门在 builder 脚本层实现,不在 make 层。
## 4. 脚本解析顺序
push 目标与 `version.sh` 定位 builder 脚本时按以下顺序,命中即用,不做静默兜底:
1. `$BUILDER_SKILL_DIR/scripts/`(特殊安装位置)
2. `$HOME/.pouch/skills/builder/scripts/`(标准 clone 位)
3. `$HOME/.agents/skills/builder/scripts/`(本机 agent 安装位)
两个位置都不可用时必须失败并提示:设置 `BUILDER_SKILL_DIR`,或把 pouch 仓库
clone 到 `~/.pouch`。
## 5. 校验
`python3 -I -S <builder-scripts>/check.py <project-dir> [--build]` 对本项目逐条检查
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构
`make -f makefile.builder deb` 并核对产物元数据(默认只静态检查配方)。`--ready`
额外检查轨道工具链,以及 `.env.builder` / 环境中的发布键名是否存在且非空(不读取、
不打印值;缺键只挡住发布)。校验失败时的修复路径:按 `templates/makefile.builder` 补齐或
修正 `makefile.builder`,再跑 check.py,不要绕过校验器,不要改用户 Makefile,
不要用 create-makefile(版本推导与本契约冲突)。
## 6. 存量项目(legacy fallback)
未接入契约的项目:builder 仍可按发现流程工作——从用户 `Makefile`、CI 配置、`debian/`
与发布文档中找已有构建/上传入口,优先复用;上传仍使用 builder 脚本。从项目根查找,
不预设文件位置:
```bash
rg -n -i --hidden --glob '!.git' \
'build-deb|upload-deb|publish-deb|dpkg-deb|debuild|curl.*deb|\.deb\b|aptly|reprepro'
```
完成一次成功交付后应引导用户按 `templates/makefile.builder` 写入项目根
`makefile.builder`,之后以 check.py 为准。不把契约目标合并进用户 Makefile。