feat: add skill init/check and isolate builder makefile
Give ack, builder, and deployer an explicit init/check mode that reports missing project config instead of failing mid-work. Point builder at makefile.builder so its contract targets do not collide with an existing Makefile.
This commit is contained in:
@@ -6,19 +6,25 @@
|
||||
|
||||
## 什么时候使用
|
||||
|
||||
- "用 builder 初始化这个项目"
|
||||
- "帮我构建这个项目的 DEB / Docker 镜像"
|
||||
- "把 1.2.3 发布到包仓库 / 镜像仓库"
|
||||
- "检查这个项目的 Makefile 是否符合 builder 契约"
|
||||
- "检查这个项目的 makefile.builder 是否符合 builder 契约"
|
||||
- "看看项目现在的发布流程"
|
||||
|
||||
只构建不上传时明确说明即可;上传永远需要你显式授权。
|
||||
|
||||
## 项目接入契约
|
||||
|
||||
1. 用 create-makefile skill 生成或修正 Makefile(目标 `help/build/clean/version`
|
||||
+ 条件 `deb/docker/push*`,变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`)。
|
||||
对新项目说「用 builder 初始化」。Agent 会探测轨道、按
|
||||
`templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置。
|
||||
不改用户已有的 `Makefile`。
|
||||
|
||||
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`,
|
||||
变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
|
||||
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
|
||||
2. 运行 `python3 -I -S <builder>/scripts/check.py .` 直到全部 PASS。
|
||||
2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。
|
||||
缺发布键只挡住上传,不挡住构建。
|
||||
3. 在项目根 `.env` 配置发布环境变量:
|
||||
|
||||
```text
|
||||
@@ -28,7 +34,9 @@ DEB_TOKEN=<token> # 只放 .env 或密钥系统,不进 git
|
||||
DOCKER_REGISTRY=registry.example.com
|
||||
```
|
||||
|
||||
4. 日常发布就是两条命令:`make deb && make push-deb`、`make push-docker`。
|
||||
4. 日常发布就是两条命令:
|
||||
`make -f makefile.builder deb && make -f makefile.builder push-deb`、
|
||||
`make -f makefile.builder push-docker`。
|
||||
|
||||
## 版本号
|
||||
|
||||
@@ -40,7 +48,7 @@ DOCKER_REGISTRY=registry.example.com
|
||||
| --- | --- |
|
||||
| manage-release | 选定下一个正式 SemVer,打 annotated tag `vX.Y.Z` |
|
||||
| builder | 读 HEAD:落在稳定 tag 上则打正式产物,否则打测试产物 |
|
||||
| `make version` | 输出一行规范版本(无 `v` 前缀),DEB / Docker 都从它渲染 |
|
||||
| `make -f makefile.builder version` | 输出一行规范版本(无 `v` 前缀),DEB / Docker 都从它渲染 |
|
||||
|
||||
**不要**用 `git tag \| sort -V \| tail -1` 取全仓库最大号,也不要把
|
||||
`git describe --dirty` 的原始字符串写进 DEB 或镜像 tag。
|
||||
@@ -83,7 +91,8 @@ detached HEAD 用 `detached`,CI 可注入 `BUILD_BRANCH` / `CI_COMMIT_BRANCH`
|
||||
## 使用示例
|
||||
|
||||
```text
|
||||
用 builder 检查这个项目的 Makefile 是否符合契约。
|
||||
用 builder 初始化这个项目。
|
||||
用 builder 检查这个项目的 makefile.builder 是否符合契约。
|
||||
用 builder 构建当前版本的 DEB 和镜像,先不要上传。
|
||||
用 builder 把当前 commit 的产物发布到项目已配置的仓库。
|
||||
用 builder 发布多平台 linux/amd64,linux/arm64 镜像。
|
||||
@@ -93,9 +102,10 @@ detached HEAD 用 `detached`,CI 可注入 `BUILD_BRANCH` / `CI_COMMIT_BRANCH`
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| `scripts/check.py` | 校验项目 Makefile 是否符合契约(`--build` 实构核对产物) |
|
||||
| `scripts/check.py` | 校验契约(`--ready` 含工具链与发布键名,`--build` 实构核对产物) |
|
||||
| `templates/makefile.builder` | 初始化用的契约文件骨架,拷到项目根 |
|
||||
| `scripts/version.sh` | 从 Git 祖先稳定 tag 推导规范版本 / Docker tag |
|
||||
| `scripts/version.mk` | 项目 Makefile `include`,设置 `VERSION` 与 `IMAGE_TAG` |
|
||||
| `scripts/version.mk` | `makefile.builder` `include`,设置 `VERSION` 与 `IMAGE_TAG` |
|
||||
| `scripts/upload_deb.sh` | 上传 `.deb` 到 HTTP 包仓库(multipart package/token/repository_name) |
|
||||
| `scripts/publish_docker.sh` | buildx 构建 + 推送镜像,远端 digest 验证 |
|
||||
| `scripts/verify_deb.sh` | 核对包元数据、内容与 SHA-256 |
|
||||
|
||||
+84
-18
@@ -1,12 +1,13 @@
|
||||
---
|
||||
name: builder
|
||||
description: >-
|
||||
按统一契约构建并发布项目的 DEB 包与 Docker 镜像:先校验项目 Makefile 是否符合
|
||||
builder 契约(check.py),再 make 构建产物,经授权后用 skill 自带脚本上传并验证。
|
||||
触发词:构建 deb、发布 deb、上传 deb、推送 apt 仓库、打 Debian 包、构建镜像、
|
||||
发布镜像、推送 Docker 镜像、make push、检查 Makefile 是否符合规范。仅分析打包
|
||||
逻辑或只构建不上传时也可使用;不会在未获授权时执行任何上传。Docker 轨道保持
|
||||
显式触发:用户点名(builder/publish docker)时才走镜像发布。
|
||||
初始化或检查项目是否满足 DEB/Docker 构建发布契约,再按契约构建并发布:
|
||||
先校验 makefile.builder(check.py),再 make -f makefile.builder 构建产物,
|
||||
经授权后用 skill 自带脚本上传并验证。触发词:初始化 builder、接入 builder、
|
||||
检查 makefile.builder、构建 deb、发布 deb、上传 deb、推送 apt 仓库、打 Debian
|
||||
包、构建镜像、发布镜像、推送 Docker 镜像、make push。仅分析打包逻辑或只构建
|
||||
不上传时也可使用;不会在未获授权时执行任何上传。Docker 轨道保持显式触发:
|
||||
用户点名(builder/publish docker)时才走镜像发布。
|
||||
---
|
||||
|
||||
# Builder:DEB / Docker 构建发布
|
||||
@@ -14,28 +15,91 @@ description: >-
|
||||
复用项目已有发布约定,安全地完成"校验 → 构建 → 检查 → 授权 → 上传 → 验证"。
|
||||
|
||||
分工原则:**make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。**
|
||||
项目状态是根目录 `makefile.builder` 与发布用 `.env`,不要创建 `.pouch/builder/`,
|
||||
不要改用户的 `Makefile` / `makefile`。
|
||||
|
||||
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
|
||||
`git rev-parse --show-toplevel` 解析项目根。
|
||||
|
||||
## 选择模式
|
||||
|
||||
- 用户要求初始化、接入 builder,或新项目还没有 `makefile.builder`:执行“初始化”。
|
||||
- 用户要求检查 builder 契约或发布配置是否齐全:执行“检查”。
|
||||
- 用户要求构建、发布、上传:执行“工作流”。不要静默初始化。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 用户要求初始化或接入 builder。
|
||||
- 用户要求构建、发布、上传 `.deb` 包或 Docker/OCI 镜像。
|
||||
- 用户要求检查项目 Makefile 是否符合 builder 契约。
|
||||
- 用户要求检查 `makefile.builder` 是否符合 builder 契约。
|
||||
- 用户要求梳理或接通项目现有的 DEB/镜像发布流程。
|
||||
|
||||
不适用:本地安装/卸载 DEB;RPM/APK/语言包管理器;从零设计全新打包体系(先出方案);
|
||||
普通编码与 Dockerfile 编辑。
|
||||
|
||||
## 初始化
|
||||
|
||||
1. 确认项目根。探测 `makefile.builder`、用户 `Makefile`/`makefile`(只当证据,
|
||||
不改)、`Dockerfile`、`debian/`、语言清单、`.env` 键名(只看键是否存在,不读、
|
||||
不打印值)。
|
||||
2. 判定轨道:有 Dockerfile → docker;有 deb 信号或用户要打 deb → deb;都不清则问。
|
||||
不要猜测 registry、token 或仓库名。
|
||||
3. 没有 `makefile.builder`:把 `<skill-dir>/templates/makefile.builder` 拷到项目根。
|
||||
按轨道删掉未使用的 deb/docker/push* 段,把 `build` 的 TODO 换成仓库里已有的
|
||||
真实编译命令(可从用户 Makefile 抄配方,但不要 `include` 或递归调用它)。
|
||||
`include` builder 的 `scripts/version.mk`。双产物把 `push` 改成
|
||||
`push: push-deb push-docker`。不要改用户的 `Makefile` / `makefile`。
|
||||
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖该文件,除非
|
||||
用户明确要求按契约改。不要调用 create-makefile(其版本规则与本契约冲突)。
|
||||
5. 不要创建 `.env`。缺发布键时在报告里给出可粘贴示例,并说明 `.env` 不进 Git。
|
||||
6. 运行:
|
||||
|
||||
```bash
|
||||
python3 -I -S <skill-dir>/scripts/check.py <project-dir> --ready
|
||||
```
|
||||
|
||||
7. 按下面格式报告。结构校验通过且当前轨道能构建时才能称「完成」;只缺发布
|
||||
键是「部分完成」(可构建,不可发布)。契约 FAIL 或轨道工具缺失是「阻塞」。
|
||||
除非用户明确要求,不提交、不推送、不上传。
|
||||
|
||||
```text
|
||||
## builder 初始化:完成 | 部分完成 | 阻塞
|
||||
|
||||
已具备: …
|
||||
待配置: 路径 + 字段 + 可粘贴示例 + 缺了会挡住哪步
|
||||
工具链: make / docker / dpkg-deb(缺则怎么装,不擅自安装)
|
||||
下一步: 一句话
|
||||
```
|
||||
|
||||
发布键示例(只示范键名):
|
||||
|
||||
```text
|
||||
DEB_SERVER_URL=https://deb.example.com
|
||||
DEB_REPOSITORY=main
|
||||
DEB_TOKEN=
|
||||
DOCKER_REGISTRY=registry.example.com
|
||||
```
|
||||
|
||||
## 检查
|
||||
|
||||
只读。运行 `check.py <project-dir> --ready`,用同一报告格式,标题改为
|
||||
`## builder 检查:…`。不写 `makefile.builder`、不改用户 Makefile、不创建 `.env`。
|
||||
用户明确要求修复后再转入初始化。
|
||||
|
||||
## 工作流
|
||||
|
||||
### 0. 校验契约
|
||||
|
||||
```bash
|
||||
python3 -I -S <skill-dir>/scripts/check.py <project-dir> # 静态检查
|
||||
python3 -I -S <skill-dir>/scripts/check.py <project-dir> --ready # 含工具链与发布键
|
||||
python3 -I -S <skill-dir>/scripts/check.py <project-dir> --build # 额外实构 deb 并核对产物
|
||||
```
|
||||
|
||||
任一 FAIL:停下修复(引导用 create-makefile skill 补齐),不要绕过校验继续发布。
|
||||
完整要求见 [contract.md](references/contract.md)。存量项目未接契约时走第 6 节
|
||||
fallback;成功交付一次后引导用户迁移到契约。
|
||||
契约 FAIL 或轨道工具缺失:停下,转入「初始化」按契约补齐 `makefile.builder`,
|
||||
不要绕过校验继续发布,不要改用户 Makefile。只缺发布键:允许构建,禁止上传。
|
||||
完整要求见 [contract.md](references/contract.md)。存量项目未接契约时走
|
||||
「存量项目 fallback」;成功交付一次后引导用户迁到 `makefile.builder`。
|
||||
|
||||
### 1. 确认发布边界
|
||||
|
||||
@@ -56,8 +120,8 @@ fallback;成功交付一次后引导用户迁移到契约。
|
||||
### 2. 构建
|
||||
|
||||
```bash
|
||||
make build ARCH=<amd64|arm64> VERSION=<version> # 主产物
|
||||
make deb ARCH=<amd64|arm64> # DEB 项目
|
||||
make -f makefile.builder build ARCH=<amd64|arm64> VERSION=<version> # 主产物
|
||||
make -f makefile.builder deb ARCH=<amd64|arm64> # DEB 项目
|
||||
```
|
||||
|
||||
版本缺省按契约「版本号」从 Git 祖先稳定 tag 推导(正式 = HEAD exact-match
|
||||
@@ -78,7 +142,7 @@ verify_deb.sh 输出元数据、关键内容清单和 SHA-256。匹配到多个
|
||||
|
||||
### 4. 发布
|
||||
|
||||
优先 `make push[-deb|-docker]`(契约要求的薄包装);直接调用等价:
|
||||
优先 `make -f makefile.builder push[-deb|-docker]`(契约要求的薄包装);直接调用等价:
|
||||
|
||||
```bash
|
||||
DEB_SERVER_URL=… DEB_TOKEN=… DEB_REPOSITORY=… \
|
||||
@@ -115,21 +179,23 @@ rg -n -i --hidden --glob '!.git' \
|
||||
'build-deb|upload-deb|publish-deb|dpkg-deb|debuild|curl.*deb|\.deb\b|aptly|reprepro'
|
||||
```
|
||||
|
||||
重点检查 Makefile、CI 配置、`debian/`、构建脚本和发布文档中的入口、变量传递方式、
|
||||
端点与认证方式。优先复用已有构建入口;上传仍用 builder 脚本。交付后引导迁移到契约
|
||||
(create-makefile + check.py 通过为准)。
|
||||
重点检查用户 Makefile、CI 配置、`debian/`、构建脚本和发布文档中的入口、变量传递方式、
|
||||
端点与认证方式。优先复用已有构建入口;上传仍用 builder 脚本。交付后引导迁移到
|
||||
`makefile.builder`(`templates/makefile.builder` + `check.py` 通过为准)。
|
||||
|
||||
## 修改 builder 自身时
|
||||
|
||||
- 上传/发布脚本是 SSOT:通用行为修改落在 `skills/builder/scripts/`,不同步复制到
|
||||
业务项目。产物版本只通过 `scripts/version.sh` 推导,不要在项目 Makefile 内联
|
||||
业务项目。产物版本只通过 `scripts/version.sh` 推导,不要在 `makefile.builder` 内联
|
||||
`git describe` 或 `sort -V`。
|
||||
- 契约变更先改 `scripts/check.py`,再同步 `references/contract.md`。
|
||||
- 契约变更先改 `scripts/check.py`,再同步 `references/contract.md` 与
|
||||
`templates/makefile.builder`。
|
||||
- 可用 `bash -n` 检查脚本语法;有 ShellCheck 时一并运行。
|
||||
- 不通过真实生产上传测试脚本,除非用户明确授权并给出测试版本/仓库。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 初始化/检查:报告为完成、部分完成或阻塞;待配置项含文件、字段和示例。
|
||||
- 仅分析:入口、调用链、配置来源和风险已被准确说明。
|
||||
- 仅校验:check.py 结果逐条可解释,修复建议明确。
|
||||
- 仅构建:产物已生成并通过 verify_deb.sh,未发生上传。
|
||||
|
||||
@@ -7,6 +7,15 @@ builder 脚本只做发布,不做项目特定的构建逻辑。
|
||||
分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT),
|
||||
Agent 只保留授权判断和歧义处理。**
|
||||
|
||||
契约文件固定为项目根 `makefile.builder`,调用方式:
|
||||
|
||||
```bash
|
||||
make -f makefile.builder <target>
|
||||
```
|
||||
|
||||
不要把 builder 目标写进用户的 `Makefile` 或 `makefile`。`check.py` 只读
|
||||
`makefile.builder`。
|
||||
|
||||
## 1. Make 目标
|
||||
|
||||
### 必备目标(所有项目)
|
||||
@@ -30,7 +39,7 @@ Agent 只保留授权判断和歧义处理。**
|
||||
|
||||
规则:
|
||||
|
||||
1. 项目有 DEB 产物的判据:Makefile 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`。
|
||||
1. 项目有 DEB 产物的判据:`makefile.builder` 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`。
|
||||
有镜像的判据:项目根存在 `Dockerfile`。
|
||||
2. 双产物项目必须拆 `push-deb`/`push-docker`,`push` 依序聚合两者;单产物项目一个
|
||||
`push` 即可。
|
||||
@@ -49,9 +58,9 @@ Agent 只保留授权判断和歧义处理。**
|
||||
|
||||
### 版本号
|
||||
|
||||
`make version` 输出一行规范版本(无 `v` 前缀)。DEB 的 `Version` 与文件名
|
||||
直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh`(`make` 通过
|
||||
`scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用
|
||||
`make -f makefile.builder version` 输出一行规范版本(无 `v` 前缀)。DEB 的
|
||||
`Version` 与文件名直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh`
|
||||
(`makefile.builder` 通过 `scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用
|
||||
manage-release,不猜测下一个正式 SemVer。正式 tag 由 manage-release 事先打好。
|
||||
`version.sh` 不执行 `git fetch`。
|
||||
|
||||
@@ -77,7 +86,7 @@ manage-release,不猜测下一个正式 SemVer。正式 tag 由 manage-release
|
||||
`--dirty` 写入版本;脏树发布仍走既有门禁。显式 `VERSION=` / `IMAGE_TAG=`
|
||||
可覆盖推导,但不得把非 exact-match 的 commit 标成正式 `X.Y.Z`。
|
||||
|
||||
项目 Makefile 不要内联 `git describe` 或 `sort -V`,include 本 skill 的
|
||||
`makefile.builder` 不要内联 `git describe` 或 `sort -V`,include 本 skill 的
|
||||
`scripts/version.mk`:
|
||||
|
||||
```makefile
|
||||
@@ -126,12 +135,16 @@ clone 到 `~/.pouch`。
|
||||
## 5. 校验
|
||||
|
||||
`python3 -I -S <builder-scripts>/check.py <project-dir> [--build]` 对本项目逐条检查
|
||||
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构 `make deb` 并核对
|
||||
产物元数据(默认只静态检查配方)。校验失败时的修复路径:用 create-makefile skill
|
||||
补齐或修正 Makefile,不要绕过校验器。
|
||||
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构
|
||||
`make -f makefile.builder deb` 并核对产物元数据(默认只静态检查配方)。`--ready`
|
||||
额外检查轨道工具链,以及 `.env` / 环境中的发布键名是否存在(不读取、不打印值;
|
||||
缺键只挡住发布)。校验失败时的修复路径:按 `templates/makefile.builder` 补齐或
|
||||
修正 `makefile.builder`,再跑 check.py,不要绕过校验器,不要改用户 Makefile,
|
||||
不要用 create-makefile(版本推导与本契约冲突)。
|
||||
|
||||
## 6. 存量项目(legacy fallback)
|
||||
|
||||
未接入契约的项目:builder 仍可按发现流程工作——从 `Makefile`、CI 配置、`debian/`
|
||||
未接入契约的项目:builder 仍可按发现流程工作——从用户 `Makefile`、CI 配置、`debian/`
|
||||
与发布文档中找已有构建/上传入口,优先复用;上传仍使用 builder 脚本。完成一次成功
|
||||
交付后应引导用户用 create-makefile 把项目迁移到本契约,之后以 check.py 为准。
|
||||
交付后应引导用户按 `templates/makefile.builder` 写入项目根 `makefile.builder`,之后以
|
||||
check.py 为准。不把契约目标合并进用户 Makefile。
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
1. 用户本次请求中明确给出的值。
|
||||
2. 当前项目的 `.env` 与 `AGENTS.md`、发布文档。
|
||||
3. Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
|
||||
3. `makefile.builder`、用户 Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
|
||||
4. 询问用户。
|
||||
|
||||
不要从其他项目、shell history 或无关的本地配置中猜测发布目标。
|
||||
|
||||
+186
-20
@@ -1,14 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Executable form of the builder contract (references/contract.md).
|
||||
|
||||
Checks a project's Makefile against the contract by probing make itself with
|
||||
dry runs (`make -n`) instead of parsing Makefile text: includes, conditionals,
|
||||
and variable expansion are resolved by make, so behavior is what gets judged.
|
||||
Checks a project's makefile.builder against the contract by probing make
|
||||
itself with dry runs (`make -f makefile.builder -n`) instead of parsing
|
||||
makefile text: includes, conditionals, and variable expansion are resolved
|
||||
by make, so behavior is what gets judged. The default Makefile/makefile is
|
||||
not read.
|
||||
|
||||
Usage:
|
||||
python3 -I -S check.py <project-dir> [--build]
|
||||
python3 -I -S check.py <project-dir> [--build] [--ready]
|
||||
|
||||
Exit codes: 0 = all PASS, 1 = at least one FAIL, 2 = usage/environment error.
|
||||
`--ready` 额外检查轨道工具链和发布环境变量键名(只看键是否存在,永不打印值)。
|
||||
无 makefile.builder 时普通模式退出 2;`--ready` 输出结构化 FAIL 并继续工具链/发布键检查。
|
||||
|
||||
Change the contract here first, then mirror the change into contract.md.
|
||||
"""
|
||||
@@ -17,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -37,6 +42,7 @@ SECRET_PATTERNS = (
|
||||
FLOATING_TAGS = (":latest", ":stable")
|
||||
DEB_SHAPE = re.compile(r"^[^_\s]+_[^_\s]+_[^_\s]+\.deb$")
|
||||
VALID_SCRIPT_NAMES = ("upload_deb.sh", "publish_docker.sh")
|
||||
BUILDER_MAKEFILE = "makefile.builder"
|
||||
|
||||
PASS = "PASS"
|
||||
FAIL = "FAIL"
|
||||
@@ -58,10 +64,15 @@ class Report:
|
||||
self.skips += 0 if self.skips else 1
|
||||
|
||||
|
||||
def run_make(project: Path, *args: str, timeout: int = 60) -> subprocess.CompletedProcess[str]:
|
||||
def run_make(
|
||||
project: Path, *args: str, timeout: int = 60, dry_run: bool = True
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
cmd = ["make", "-C", str(project), "-f", BUILDER_MAKEFILE]
|
||||
if dry_run:
|
||||
cmd.append("-n")
|
||||
cmd.extend(args)
|
||||
return subprocess.run(
|
||||
["make", "-C", str(project), "-n", *args],
|
||||
capture_output=True, text=True, timeout=timeout, check=False,
|
||||
cmd, capture_output=True, text=True, timeout=timeout, check=False
|
||||
)
|
||||
|
||||
|
||||
@@ -244,8 +255,7 @@ def check_push_delegates(report: Report, project: Path, dual_artifact: bool) ->
|
||||
|
||||
|
||||
def check_secrets_and_tags(report: Report, project: Path) -> None:
|
||||
makefile = project / "Makefile"
|
||||
included_text = ""
|
||||
makefile = project / BUILDER_MAKEFILE
|
||||
problems = []
|
||||
files = [makefile]
|
||||
if makefile.exists():
|
||||
@@ -267,9 +277,104 @@ def check_secrets_and_tags(report: Report, project: Path) -> None:
|
||||
report.add(FAIL if problems else PASS, 8, "无内联机密、无隐式 latest/stable", "\n".join(problems) or "clean")
|
||||
|
||||
|
||||
def check_script_paths(report: Report) -> None:
|
||||
import os
|
||||
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
|
||||
DOCKER_ENV_KEYS = ("DOCKER_REGISTRY",)
|
||||
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=")
|
||||
|
||||
|
||||
def env_file_keys(project: Path) -> set[str]:
|
||||
"""Return key names defined in project `.env`. Never return or print values."""
|
||||
path = project / ".env"
|
||||
keys: set[str] = set()
|
||||
if not path.is_file():
|
||||
return keys
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return keys
|
||||
for raw in text.splitlines():
|
||||
stripped = raw.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
match = ENV_KEY_LINE.match(stripped)
|
||||
if match:
|
||||
keys.add(match.group(1))
|
||||
return keys
|
||||
|
||||
|
||||
def key_present(key: str, env_keys: set[str]) -> bool:
|
||||
return bool(os.environ.get(key)) or key in env_keys
|
||||
|
||||
|
||||
def check_ready_toolchain(
|
||||
report: Report, *, deb_project: bool, docker_project: bool
|
||||
) -> None:
|
||||
lines = [f"make: {'found' if shutil.which('make') else 'MISSING'}"]
|
||||
problems = []
|
||||
if docker_project:
|
||||
docker_ok = shutil.which("docker") is not None
|
||||
lines.append(f"docker: {'found' if docker_ok else 'MISSING (blocks docker track)'}")
|
||||
if not docker_ok:
|
||||
problems.append("install docker to build/publish images")
|
||||
else:
|
||||
lines.append("docker: skipped (no docker track)")
|
||||
if deb_project:
|
||||
dpkg_ok = shutil.which("dpkg-deb") is not None
|
||||
lines.append(
|
||||
f"dpkg-deb: {'found' if dpkg_ok else 'MISSING (blocks make deb / --build)'}"
|
||||
)
|
||||
if not dpkg_ok:
|
||||
problems.append("install dpkg-dev (or equivalent) to build .deb packages")
|
||||
else:
|
||||
lines.append("dpkg-deb: skipped (no deb track)")
|
||||
report.add(
|
||||
FAIL if problems else PASS,
|
||||
10,
|
||||
"轨道工具链",
|
||||
"\n".join(lines + ([""] + problems if problems else [])),
|
||||
)
|
||||
|
||||
|
||||
def check_ready_env_keys(
|
||||
report: Report, project: Path, *, deb_project: bool, docker_project: bool
|
||||
) -> None:
|
||||
env_keys = env_file_keys(project)
|
||||
lines = []
|
||||
missing: list[str] = []
|
||||
if not deb_project and not docker_project:
|
||||
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "no deb/docker track")
|
||||
return
|
||||
if deb_project:
|
||||
for key in DEB_ENV_KEYS:
|
||||
found = key_present(key, env_keys)
|
||||
lines.append(f"{key}: {'present' if found else 'MISSING'}")
|
||||
if not found:
|
||||
missing.append(key)
|
||||
else:
|
||||
lines.append("DEB_*: skipped (no deb track)")
|
||||
if docker_project:
|
||||
for key in DOCKER_ENV_KEYS:
|
||||
found = key_present(key, env_keys)
|
||||
lines.append(f"{key}: {'present' if found else 'MISSING'}")
|
||||
if not found:
|
||||
missing.append(key)
|
||||
else:
|
||||
lines.append("DOCKER_*: skipped (no docker track)")
|
||||
if missing:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"blocks publish, not build. Put keys in the environment or project `.env`:",
|
||||
*[f" {key}=" for key in missing],
|
||||
"Do not commit `.env`. Never print values.",
|
||||
]
|
||||
)
|
||||
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "\n".join(lines))
|
||||
return
|
||||
report.add(PASS, 11, "发布环境变量键名(不读取值)", "\n".join(lines))
|
||||
|
||||
|
||||
def check_script_paths(report: Report) -> None:
|
||||
candidates = []
|
||||
env_dir = os.environ.get("BUILDER_SKILL_DIR")
|
||||
if env_dir:
|
||||
@@ -289,26 +394,80 @@ def check_script_paths(report: Report) -> None:
|
||||
|
||||
|
||||
def build_project(project: Path) -> Path | None:
|
||||
"""Run `make deb` for real and return the produced .deb, or None."""
|
||||
result = subprocess.run(["make", "-C", str(project), "deb"], capture_output=True, text=True, timeout=1800, check=False)
|
||||
"""Run `make -f makefile.builder deb` for real and return the produced .deb, or None."""
|
||||
result = run_make(project, "deb", timeout=1800, dry_run=False)
|
||||
if result.returncode != 0:
|
||||
print(f"--build: `make deb` failed:\n{result.stderr[-2000:]}", file=sys.stderr)
|
||||
print(
|
||||
f"--build: `make -f {BUILDER_MAKEFILE} deb` failed:\n{result.stderr[-2000:]}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
debs = sorted((p for p in (project / "dist").glob("*.deb") if p.is_file()), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
return debs[0] if debs else None
|
||||
|
||||
|
||||
MAKEFILE_HINT = (
|
||||
f"Fix: copy <builder-skill>/templates/{BUILDER_MAKEFILE} to the project "
|
||||
f"root as {BUILDER_MAKEFILE}. Do not put builder targets in Makefile or "
|
||||
"makefile. Keep help/build/clean/version, include builder "
|
||||
"scripts/version.mk, and enable deb/docker/push* for the tracks this "
|
||||
"project actually uses. Then re-run check.py. Do not use create-makefile."
|
||||
)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("project", type=Path, help="project directory containing the Makefile")
|
||||
parser.add_argument("--build", action="store_true", help="actually run `make deb` and verify the artifact")
|
||||
parser.add_argument(
|
||||
"project",
|
||||
type=Path,
|
||||
help="project directory containing makefile.builder",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build",
|
||||
action="store_true",
|
||||
help="actually run `make -f makefile.builder deb` and verify the artifact",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ready",
|
||||
action="store_true",
|
||||
help="also check toolchain and publish env key names (init/check mode)",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
project = args.project.resolve()
|
||||
makefile = project / "Makefile"
|
||||
makefile = project / BUILDER_MAKEFILE
|
||||
if not makefile.is_file():
|
||||
print(f"Error: no Makefile in {project}", file=sys.stderr)
|
||||
return 2
|
||||
if not args.ready:
|
||||
print(f"Error: no {BUILDER_MAKEFILE} in {project}", file=sys.stderr)
|
||||
return 2
|
||||
report = Report()
|
||||
report.add(
|
||||
FAIL,
|
||||
1,
|
||||
f"{BUILDER_MAKEFILE} 存在",
|
||||
f"no {BUILDER_MAKEFILE} in {project}\n{MAKEFILE_HINT}",
|
||||
)
|
||||
skip_detail = f"(no {BUILDER_MAKEFILE})"
|
||||
for number, title, detail in (
|
||||
(2, "ARCH 守卫与缺省值", skip_detail),
|
||||
(3, "version 输出一行非空规范版本(无 v 前缀)", skip_detail),
|
||||
(4, "build 不含上传动作", skip_detail),
|
||||
(5, "deb 目标产物形状与纯构建", skip_detail),
|
||||
(6, "docker 目标为本地单平台构建", skip_detail),
|
||||
(7, "push 仅委托 builder 脚本(薄包装)", skip_detail),
|
||||
(8, "无内联机密、无隐式 latest/stable", skip_detail),
|
||||
(9, "builder 脚本路径可达", skip_detail),
|
||||
):
|
||||
report.add(SKIP, number, title, detail)
|
||||
docker_project = detect_docker_project(project)
|
||||
deb_project = any(project.glob("debian/*"))
|
||||
check_ready_toolchain(report, deb_project=deb_project, docker_project=docker_project)
|
||||
check_ready_env_keys(
|
||||
report, project, deb_project=deb_project, docker_project=docker_project
|
||||
)
|
||||
print()
|
||||
print(f"RESULT: FAILED ({report.failures} check(s) failed)")
|
||||
return 1
|
||||
if shutil.which("make") is None:
|
||||
print("Error: make is required.", file=sys.stderr)
|
||||
return 2
|
||||
@@ -340,7 +499,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
if deb_project:
|
||||
if args.build:
|
||||
print("--build: running `make deb` ...")
|
||||
print(f"--build: running `make -f {BUILDER_MAKEFILE} deb` ...")
|
||||
built_deb = build_project(project)
|
||||
if built_deb is None:
|
||||
print("--build: no .deb produced; artifact checks degrade to recipe-only.", file=sys.stderr)
|
||||
@@ -357,6 +516,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
check_push_delegates(report, project, dual)
|
||||
check_secrets_and_tags(report, project)
|
||||
check_script_paths(report)
|
||||
if args.ready:
|
||||
check_ready_toolchain(
|
||||
report, deb_project=deb_project, docker_project=docker_project
|
||||
)
|
||||
check_ready_env_keys(
|
||||
report, project, deb_project=deb_project, docker_project=docker_project
|
||||
)
|
||||
|
||||
total_fail = report.failures
|
||||
print()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Include from a project Makefile. Sets VERSION (canonical, no leading v)
|
||||
# Include from makefile.builder. Sets VERSION (canonical, no leading v)
|
||||
# and IMAGE_TAG (Docker rendering) via builder version.sh.
|
||||
#
|
||||
# Command-line / environment VERSION= is passed as --version (official X.Y.Z
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
|
||||
# 不要把这些目标写进用户的 Makefile / makefile。
|
||||
# 初始化时按项目轨道删掉未使用的 deb / docker 段。
|
||||
# 把 build 的 TODO 换成真实编译命令;禁止在 build / deb / docker 里上传。
|
||||
#
|
||||
# include 路径见 references/contract.md。测试或非标准安装可设 BUILDER_SKILL_DIR。
|
||||
|
||||
ARCH ?= amd64
|
||||
ifneq ($(filter $(ARCH),amd64 arm64),$(ARCH))
|
||||
$(error ARCH must be amd64 or arm64)
|
||||
endif
|
||||
|
||||
PROJECT_NAME ?= $(notdir $(CURDIR))
|
||||
DIST_DIR ?= dist
|
||||
|
||||
ifneq ($(BUILDER_SKILL_DIR),)
|
||||
include $(BUILDER_SKILL_DIR)/scripts/version.mk
|
||||
else
|
||||
include $(HOME)/.pouch/skills/builder/scripts/version.mk
|
||||
endif
|
||||
|
||||
BUILDER_SCRIPTS := $(or $(BUILDER_SKILL_DIR),$(HOME)/.pouch/skills/builder)/scripts
|
||||
|
||||
.PHONY: help version clean build deb docker push push-deb push-docker
|
||||
|
||||
help:
|
||||
@echo "$(PROJECT_NAME) $(VERSION)"
|
||||
@echo " make -f makefile.builder build [ARCH=amd64|arm64] - 构建主产物 / build"
|
||||
@echo " make -f makefile.builder version - 打印规范版本 / canonical version"
|
||||
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
|
||||
@echo " make -f makefile.builder deb - 打 DEB(只构建不上传)"
|
||||
@echo " make -f makefile.builder docker - 本地单平台镜像"
|
||||
@echo " make -f makefile.builder push / push-deb / push-docker"
|
||||
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
|
||||
clean:
|
||||
rm -rf $(DIST_DIR)
|
||||
|
||||
# 替换为项目真实构建命令。不得 curl/scp/docker push。
|
||||
build:
|
||||
mkdir -p $(DIST_DIR)
|
||||
@echo "TODO: build $(PROJECT_NAME) for linux/$(ARCH)"
|
||||
|
||||
deb: build
|
||||
mkdir -p $(DIST_DIR)
|
||||
dpkg-deb --build packaging $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb
|
||||
|
||||
docker:
|
||||
docker build --platform=linux/$(ARCH) -t $(PROJECT_NAME):$(IMAGE_TAG) .
|
||||
|
||||
push-deb:
|
||||
$(BUILDER_SCRIPTS)/upload_deb.sh $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb
|
||||
|
||||
push-docker:
|
||||
$(BUILDER_SCRIPTS)/publish_docker.sh
|
||||
|
||||
# 单产物项目只保留一条 push。双产物改为:push: push-deb push-docker
|
||||
push: push-deb
|
||||
Reference in New Issue
Block a user