237 lines
15 KiB
Markdown
237 lines
15 KiB
Markdown
# 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。
|