feat(builder): load publish credentials from .env.builder
Keep DEB/Docker publish keys out of the project's .env. Scripts and check.py --ready only read .env.builder; empty values count as missing.
This commit is contained in:
@@ -49,8 +49,8 @@ description: >-
|
|||||||
|
|
||||||
项目状态放在 skill 真正消费的位置(覆盖层、`makefile.builder`、compose),不要为了对齐
|
项目状态放在 skill 真正消费的位置(覆盖层、`makefile.builder`、compose),不要为了对齐
|
||||||
而新建一层没人读的 `.pouch/<skill>/`。`pouch init` 只用于确实有 templates/
|
而新建一层没人读的 `.pouch/<skill>/`。`pouch init` 只用于确实有 templates/
|
||||||
覆盖层的 skill。需要 make 目标的 skill 用自己的文件名(如 `makefile.builder`),
|
覆盖层的 skill。需要 make 目标或发布凭据的 skill 用自己的文件名(如 `makefile.builder`、
|
||||||
不要占用用户的 `Makefile`。
|
`.env.builder`),不要占用用户的 `Makefile` 或 `.env`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -18,19 +18,19 @@
|
|||||||
|
|
||||||
对新项目说「用 builder 初始化」。Agent 会探测轨道、按
|
对新项目说「用 builder 初始化」。Agent 会探测轨道、按
|
||||||
`templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置。
|
`templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置。
|
||||||
不改用户已有的 `Makefile`。
|
不改用户已有的 `Makefile` 或 `.env`。
|
||||||
|
|
||||||
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`,
|
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`,
|
||||||
变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
|
变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
|
||||||
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
|
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
|
||||||
2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。
|
2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。
|
||||||
缺发布键只挡住上传,不挡住构建。
|
缺发布键只挡住上传,不挡住构建。
|
||||||
3. 在项目根 `.env` 配置发布环境变量:
|
3. 在项目根 `.env.builder` 配置发布环境变量(不要写进 `.env`):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
DEB_SERVER_URL=https://deb.example.com
|
DEB_SERVER_URL=https://deb.example.com
|
||||||
DEB_REPOSITORY=main
|
DEB_REPOSITORY=main
|
||||||
DEB_TOKEN=<token> # 只放 .env 或密钥系统,不进 git
|
DEB_TOKEN=<token> # 只放 .env.builder 或密钥系统,不进 git
|
||||||
DOCKER_REGISTRY=registry.example.com
|
DOCKER_REGISTRY=registry.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,6 +104,7 @@ detached HEAD 用 `detached`,CI 可注入 `BUILD_BRANCH` / `CI_COMMIT_BRANCH`
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `scripts/check.py` | 校验契约(`--ready` 含工具链与发布键名,`--build` 实构核对产物) |
|
| `scripts/check.py` | 校验契约(`--ready` 含工具链与发布键名,`--build` 实构核对产物) |
|
||||||
| `templates/makefile.builder` | 初始化用的契约文件骨架,拷到项目根 |
|
| `templates/makefile.builder` | 初始化用的契约文件骨架,拷到项目根 |
|
||||||
|
| `templates/env.builder` | 初始化用的 `.env.builder` 骨架(注释键,不含值) |
|
||||||
| `scripts/version.sh` | 从 Git 祖先稳定 tag 推导规范版本 / Docker tag |
|
| `scripts/version.sh` | 从 Git 祖先稳定 tag 推导规范版本 / Docker tag |
|
||||||
| `scripts/version.mk` | `makefile.builder` `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/upload_deb.sh` | 上传 `.deb` 到 HTTP 包仓库(multipart package/token/repository_name) |
|
||||||
|
|||||||
+15
-12
@@ -15,8 +15,8 @@ description: >-
|
|||||||
复用项目已有发布约定,安全地完成"校验 → 构建 → 检查 → 授权 → 上传 → 验证"。
|
复用项目已有发布约定,安全地完成"校验 → 构建 → 检查 → 授权 → 上传 → 验证"。
|
||||||
|
|
||||||
分工原则:**make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。**
|
分工原则:**make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。**
|
||||||
项目状态是根目录 `makefile.builder` 与发布用 `.env`,不要创建 `.pouch/builder/`,
|
项目状态是根目录 `makefile.builder` 与发布用 `.env.builder`,不要创建
|
||||||
不要改用户的 `Makefile` / `makefile`。
|
`.pouch/builder/`,不要改用户的 `Makefile` / `makefile` / `.env`。
|
||||||
|
|
||||||
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
|
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
|
||||||
`git rev-parse --show-toplevel` 解析项目根。
|
`git rev-parse --show-toplevel` 解析项目根。
|
||||||
@@ -39,9 +39,9 @@ description: >-
|
|||||||
|
|
||||||
## 初始化
|
## 初始化
|
||||||
|
|
||||||
1. 确认项目根。探测 `makefile.builder`、用户 `Makefile`/`makefile`(只当证据,
|
1. 确认项目根。探测 `makefile.builder`、`.env.builder`、用户 `Makefile`/`makefile`
|
||||||
不改)、`Dockerfile`、`debian/`、语言清单、`.env` 键名(只看键是否存在,不读、
|
(只当抄 build 配方的证据,不改)、`Dockerfile`、`debian/`、语言清单。
|
||||||
不打印值)。
|
`.env.builder` 只看键是否存在且非空,不读、不打印值。不要读取用户 `.env`。
|
||||||
2. 判定轨道:有 Dockerfile → docker;有 deb 信号或用户要打 deb → deb;都不清则问。
|
2. 判定轨道:有 Dockerfile → docker;有 deb 信号或用户要打 deb → deb;都不清则问。
|
||||||
不要猜测 registry、token 或仓库名。
|
不要猜测 registry、token 或仓库名。
|
||||||
3. 没有 `makefile.builder`:把 `<skill-dir>/templates/makefile.builder` 拷到项目根。
|
3. 没有 `makefile.builder`:把 `<skill-dir>/templates/makefile.builder` 拷到项目根。
|
||||||
@@ -51,7 +51,9 @@ description: >-
|
|||||||
`push: push-deb push-docker`。不要改用户的 `Makefile` / `makefile`。
|
`push: push-deb push-docker`。不要改用户的 `Makefile` / `makefile`。
|
||||||
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖该文件,除非
|
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖该文件,除非
|
||||||
用户明确要求按契约改。不要调用 create-makefile(其版本规则与本契约冲突)。
|
用户明确要求按契约改。不要调用 create-makefile(其版本规则与本契约冲突)。
|
||||||
5. 不要创建 `.env`。缺发布键时在报告里给出可粘贴示例,并说明 `.env` 不进 Git。
|
5. 没有 `.env.builder`:把 `<skill-dir>/templates/env.builder` 拷到项目根为
|
||||||
|
`.env.builder`(注释键,不含值)。不要改、不要读取用户 `.env`。缺发布键时在
|
||||||
|
报告里给出可粘贴示例,并说明把 `.env.builder` 加入 `.gitignore`,不要提交。
|
||||||
6. 运行:
|
6. 运行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -74,6 +76,7 @@ description: >-
|
|||||||
发布键示例(只示范键名):
|
发布键示例(只示范键名):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
# .env.builder
|
||||||
DEB_SERVER_URL=https://deb.example.com
|
DEB_SERVER_URL=https://deb.example.com
|
||||||
DEB_REPOSITORY=main
|
DEB_REPOSITORY=main
|
||||||
DEB_TOKEN=
|
DEB_TOKEN=
|
||||||
@@ -83,8 +86,8 @@ DOCKER_REGISTRY=registry.example.com
|
|||||||
## 检查
|
## 检查
|
||||||
|
|
||||||
只读。运行 `check.py <project-dir> --ready`,用同一报告格式,标题改为
|
只读。运行 `check.py <project-dir> --ready`,用同一报告格式,标题改为
|
||||||
`## builder 检查:…`。不写 `makefile.builder`、不改用户 Makefile、不创建 `.env`。
|
`## builder 检查:…`。不写 `makefile.builder` / `.env.builder`,不改用户 Makefile
|
||||||
用户明确要求修复后再转入初始化。
|
或 `.env`。用户明确要求修复后再转入初始化。
|
||||||
|
|
||||||
## 工作流
|
## 工作流
|
||||||
|
|
||||||
@@ -108,7 +111,7 @@ python3 -I -S <skill-dir>/scripts/check.py <project-dir> --build # 额外实构
|
|||||||
|
|
||||||
执行上传前确认:
|
执行上传前确认:
|
||||||
|
|
||||||
- 目标服务和仓库来自项目配置(`.env`)或用户输入,不猜测生产端点。
|
- 目标服务和仓库来自项目配置(`.env.builder`)或用户输入,不猜测生产端点。
|
||||||
- 认证令牌已通过环境变量或密钥系统提供;绝不写入命令输出、文件、提交或回复,
|
- 认证令牌已通过环境变量或密钥系统提供;绝不写入命令输出、文件、提交或回复,
|
||||||
不用 `set -x` 执行含凭据的命令。
|
不用 `set -x` 执行含凭据的命令。
|
||||||
- 相同版本是否允许覆盖;无法确认且可能覆盖时,先询问。
|
- 相同版本是否允许覆盖;无法确认且可能覆盖时,先询问。
|
||||||
@@ -152,7 +155,7 @@ DOCKER_REGISTRY=… \
|
|||||||
<skill-dir>/scripts/publish_docker.sh # env 优先,flag 可覆盖
|
<skill-dir>/scripts/publish_docker.sh # env 优先,flag 可覆盖
|
||||||
```
|
```
|
||||||
|
|
||||||
环境变量缺失时脚本会自动向上查找项目 `.env` 加载(shell 显式值优先)。不把 token
|
环境变量缺失时脚本会加载项目 `.env.builder`(shell 显式值优先),不读 `.env`。不把 token
|
||||||
作为命令行参数;不把脚本复制进项目。upload_deb.sh 默认请求 `/api/v2/upload/package`
|
作为命令行参数;不把脚本复制进项目。upload_deb.sh 默认请求 `/api/v2/upload/package`
|
||||||
(multipart 字段 `package`/`token`/`repository_name`,接受 200/201),协议不符时设
|
(multipart 字段 `package`/`token`/`repository_name`,接受 200/201),协议不符时设
|
||||||
`DEB_UPLOAD_PATH` 或改用项目专属逻辑。publish_docker.sh 用 buildx 一步完成构建+推送,
|
`DEB_UPLOAD_PATH` 或改用项目专属逻辑。publish_docker.sh 用 buildx 一步完成构建+推送,
|
||||||
@@ -188,8 +191,8 @@ rg -n -i --hidden --glob '!.git' \
|
|||||||
- 上传/发布脚本是 SSOT:通用行为修改落在 `skills/builder/scripts/`,不同步复制到
|
- 上传/发布脚本是 SSOT:通用行为修改落在 `skills/builder/scripts/`,不同步复制到
|
||||||
业务项目。产物版本只通过 `scripts/version.sh` 推导,不要在 `makefile.builder` 内联
|
业务项目。产物版本只通过 `scripts/version.sh` 推导,不要在 `makefile.builder` 内联
|
||||||
`git describe` 或 `sort -V`。
|
`git describe` 或 `sort -V`。
|
||||||
- 契约变更先改 `scripts/check.py`,再同步 `references/contract.md` 与
|
- 契约变更先改 `scripts/check.py`,再同步 `references/contract.md`、
|
||||||
`templates/makefile.builder`。
|
`templates/makefile.builder` 与 `templates/env.builder`。
|
||||||
- 可用 `bash -n` 检查脚本语法;有 ShellCheck 时一并运行。
|
- 可用 `bash -n` 检查脚本语法;有 ShellCheck 时一并运行。
|
||||||
- 不通过真实生产上传测试脚本,除非用户明确授权并给出测试版本/仓库。
|
- 不通过真实生产上传测试脚本,除非用户明确授权并给出测试版本/仓库。
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ make -f makefile.builder <target>
|
|||||||
不要把 builder 目标写进用户的 `Makefile` 或 `makefile`。`check.py` 只读
|
不要把 builder 目标写进用户的 `Makefile` 或 `makefile`。`check.py` 只读
|
||||||
`makefile.builder`。
|
`makefile.builder`。
|
||||||
|
|
||||||
|
发布凭据固定为项目根 `.env.builder`。不要把这些键写进用户的 `.env`。脚本不读取
|
||||||
|
`.env`。
|
||||||
|
|
||||||
## 1. Make 目标
|
## 1. Make 目标
|
||||||
|
|
||||||
### 必备目标(所有项目)
|
### 必备目标(所有项目)
|
||||||
@@ -113,9 +116,9 @@ include $(HOME)/.pouch/skills/builder/scripts/version.mk
|
|||||||
| `IMAGE_TAG` | 否 | 默认由规范版本渲染:正式为 `X.Y.Z`;测试将 `~` 换成 `-`、`+g` 换成 `.g` |
|
| `IMAGE_TAG` | 否 | 默认由规范版本渲染:正式为 `X.Y.Z`;测试将 `~` 换成 `-`、`+g` 换成 `.g` |
|
||||||
| `PLATFORMS` | 否 | 默认 `linux/amd64`;多平台如 `linux/amd64,linux/arm64` |
|
| `PLATFORMS` | 否 | 默认 `linux/amd64`;多平台如 `linux/amd64,linux/arm64` |
|
||||||
|
|
||||||
配置来源优先级:shell 已显式设置的值 > 项目根 `.env` > 失败并询问用户。
|
配置来源优先级:shell 已显式设置的值 > 项目根 `.env.builder` > 失败并询问用户。
|
||||||
`.env` 由 builder 脚本自动向上查找并加载(不回显任何值);当前 shell 已设置的值
|
`.env.builder` 由 builder 脚本加载(不回显任何值);当前 shell 已设置的值优先。
|
||||||
优先于 `.env`。
|
不要读取或改写用户 `.env`。空值视为未配置。不要提交 `.env.builder`。
|
||||||
|
|
||||||
### 工作区安全
|
### 工作区安全
|
||||||
|
|
||||||
@@ -137,8 +140,8 @@ clone 到 `~/.pouch`。
|
|||||||
`python3 -I -S <builder-scripts>/check.py <project-dir> [--build]` 对本项目逐条检查
|
`python3 -I -S <builder-scripts>/check.py <project-dir> [--build]` 对本项目逐条检查
|
||||||
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构
|
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构
|
||||||
`make -f makefile.builder deb` 并核对产物元数据(默认只静态检查配方)。`--ready`
|
`make -f makefile.builder deb` 并核对产物元数据(默认只静态检查配方)。`--ready`
|
||||||
额外检查轨道工具链,以及 `.env` / 环境中的发布键名是否存在(不读取、不打印值;
|
额外检查轨道工具链,以及 `.env.builder` / 环境中的发布键名是否存在且非空(不读取、
|
||||||
缺键只挡住发布)。校验失败时的修复路径:按 `templates/makefile.builder` 补齐或
|
不打印值;缺键只挡住发布)。校验失败时的修复路径:按 `templates/makefile.builder` 补齐或
|
||||||
修正 `makefile.builder`,再跑 check.py,不要绕过校验器,不要改用户 Makefile,
|
修正 `makefile.builder`,再跑 check.py,不要绕过校验器,不要改用户 Makefile,
|
||||||
不要用 create-makefile(版本推导与本契约冲突)。
|
不要用 create-makefile(版本推导与本契约冲突)。
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
信息来源优先级:
|
信息来源优先级:
|
||||||
|
|
||||||
1. 用户本次请求中明确给出的值。
|
1. 用户本次请求中明确给出的值。
|
||||||
2. 当前项目的 `.env` 与 `AGENTS.md`、发布文档。
|
2. 当前项目的 `.env.builder` 与 `AGENTS.md`、发布文档。不要读用户 `.env`。
|
||||||
3. `makefile.builder`、用户 Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
|
3. `makefile.builder`、用户 Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
|
||||||
4. 询问用户。
|
4. 询问用户。
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ FLOATING_TAGS = (":latest", ":stable")
|
|||||||
DEB_SHAPE = re.compile(r"^[^_\s]+_[^_\s]+_[^_\s]+\.deb$")
|
DEB_SHAPE = re.compile(r"^[^_\s]+_[^_\s]+_[^_\s]+\.deb$")
|
||||||
VALID_SCRIPT_NAMES = ("upload_deb.sh", "publish_docker.sh")
|
VALID_SCRIPT_NAMES = ("upload_deb.sh", "publish_docker.sh")
|
||||||
BUILDER_MAKEFILE = "makefile.builder"
|
BUILDER_MAKEFILE = "makefile.builder"
|
||||||
|
BUILDER_ENV = ".env.builder"
|
||||||
|
|
||||||
PASS = "PASS"
|
PASS = "PASS"
|
||||||
FAIL = "FAIL"
|
FAIL = "FAIL"
|
||||||
@@ -279,12 +280,12 @@ def check_secrets_and_tags(report: Report, project: Path) -> None:
|
|||||||
|
|
||||||
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
|
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
|
||||||
DOCKER_ENV_KEYS = ("DOCKER_REGISTRY",)
|
DOCKER_ENV_KEYS = ("DOCKER_REGISTRY",)
|
||||||
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=")
|
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
|
||||||
|
|
||||||
|
|
||||||
def env_file_keys(project: Path) -> set[str]:
|
def env_file_keys(project: Path) -> set[str]:
|
||||||
"""Return key names defined in project `.env`. Never return or print values."""
|
"""Return nonempty key names in `.env.builder`. Never return or print values."""
|
||||||
path = project / ".env"
|
path = project / BUILDER_ENV
|
||||||
keys: set[str] = set()
|
keys: set[str] = set()
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return keys
|
return keys
|
||||||
@@ -297,7 +298,10 @@ def env_file_keys(project: Path) -> set[str]:
|
|||||||
if not stripped or stripped.startswith("#"):
|
if not stripped or stripped.startswith("#"):
|
||||||
continue
|
continue
|
||||||
match = ENV_KEY_LINE.match(stripped)
|
match = ENV_KEY_LINE.match(stripped)
|
||||||
if match:
|
if not match:
|
||||||
|
continue
|
||||||
|
value = match.group(2).strip().strip("'\"")
|
||||||
|
if value:
|
||||||
keys.add(match.group(1))
|
keys.add(match.group(1))
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
@@ -364,9 +368,9 @@ def check_ready_env_keys(
|
|||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"blocks publish, not build. Put keys in the environment or project `.env`:",
|
f"blocks publish, not build. Put keys in the environment or {BUILDER_ENV}:",
|
||||||
*[f" {key}=" for key in missing],
|
*[f" {key}=" for key in missing],
|
||||||
"Do not commit `.env`. Never print values.",
|
f"Do not commit {BUILDER_ENV}. Do not put these keys in `.env`. Never print values.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "\n".join(lines))
|
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "\n".join(lines))
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ usage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Build and publish a Docker image with buildx. Configuration comes from the
|
# Build and publish a Docker image with buildx. Configuration comes from the
|
||||||
# environment first (optionally loaded from the project root .env); flags
|
# environment first (optionally loaded from the project root .env.builder);
|
||||||
# override.
|
# flags override. Does not read `.env`.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# publish_docker.sh [--registry HOST] [--repository PATH] [--tag TAG] \
|
# publish_docker.sh [--registry HOST] [--repository PATH] [--tag TAG] \
|
||||||
@@ -30,18 +30,19 @@ usage() {
|
|||||||
|
|
||||||
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
|
||||||
# Load project .env without printing values; explicitly exported shell values keep precedence.
|
# Load .env.builder without printing values; shell values win. Do not read `.env`.
|
||||||
if [[ -n "$project_root" && -f "$project_root/.env" ]]; then
|
if [[ -n "$project_root" && -f "$project_root/.env.builder" ]]; then
|
||||||
while IFS='=' read -r key value; do
|
while IFS='=' read -r key value; do
|
||||||
key=${key%%[[:space:]]*}
|
key=${key%%[[:space:]]*}
|
||||||
[[ -z "$key" || "$key" == \#* ]] && continue
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||||
if [[ -n "${!key:-}" ]]; then
|
if [[ -n "${!key:-}" ]]; then
|
||||||
continue # shell value already set: wins over .env
|
continue # shell value already set: wins over .env.builder
|
||||||
fi
|
fi
|
||||||
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
||||||
|
[[ -z "$value" ]] && continue
|
||||||
printf -v "$key" '%s' "$value"
|
printf -v "$key" '%s' "$value"
|
||||||
export "$key"
|
export "$key"
|
||||||
done < <(grep -v '^[[:space:]]*$' "$project_root/.env")
|
done < <(grep -v '^[[:space:]]*$' "$project_root/.env.builder")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git_repo_name=
|
git_repo_name=
|
||||||
@@ -81,7 +82,7 @@ if [[ -n "$registry" && ( "$registry" == *://* || "$registry" == */* ) ]]; then
|
|||||||
fi
|
fi
|
||||||
if [[ -z "$registry" ]]; then
|
if [[ -z "$registry" ]]; then
|
||||||
echo "Error: DOCKER_REGISTRY (or --registry) is required." >&2
|
echo "Error: DOCKER_REGISTRY (or --registry) is required." >&2
|
||||||
echo "Set it in the environment or the project root .env." >&2
|
echo "Set it in the environment or the project root .env.builder." >&2
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ Options:
|
|||||||
-p UPLOAD_PATH Override DEB_UPLOAD_PATH (default: /api/v2/upload/package)
|
-p UPLOAD_PATH Override DEB_UPLOAD_PATH (default: /api/v2/upload/package)
|
||||||
-h Show help
|
-h Show help
|
||||||
|
|
||||||
Environment variables may live in the project root .env; this script walks up
|
Environment variables may live in the project root .env.builder; this script
|
||||||
from the current directory, loads it silently (existing shell values win), and
|
loads it silently (existing shell values win), never echoes values, and does
|
||||||
never echoes variable values. The endpoint must accept multipart fields named
|
not read `.env`. The endpoint must accept multipart fields named
|
||||||
package, token, and repository_name. Authentication is read only from
|
package, token, and repository_name. Authentication is read only from
|
||||||
DEB_TOKEN so it is not exposed in the process command line.
|
DEB_TOKEN so it is not exposed in the process command line.
|
||||||
|
|
||||||
@@ -25,21 +25,22 @@ The working tree must be clean to publish; set ALLOW_UNCOMMITTED=1 to override.
|
|||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# Locate project root (.git) upward from cwd for .env loading and git checks.
|
# Locate project root (.git) upward from cwd for .env.builder loading and git checks.
|
||||||
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
|
||||||
# Load project .env without printing values; explicitly exported shell values keep precedence.
|
# Load .env.builder without printing values; shell values win. Do not read `.env`.
|
||||||
if [[ -n "$project_root" && -f "$project_root/.env" ]]; then
|
if [[ -n "$project_root" && -f "$project_root/.env.builder" ]]; then
|
||||||
while IFS='=' read -r key value; do
|
while IFS='=' read -r key value; do
|
||||||
key=${key%%[[:space:]]*}
|
key=${key%%[[:space:]]*}
|
||||||
[[ -z "$key" || "$key" == \#* ]] && continue
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||||
if [[ -n "${!key:-}" ]]; then
|
if [[ -n "${!key:-}" ]]; then
|
||||||
continue # shell value already set: wins over .env
|
continue # shell value already set: wins over .env.builder
|
||||||
fi
|
fi
|
||||||
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
||||||
|
[[ -z "$value" ]] && continue
|
||||||
printf -v "$key" '%s' "$value"
|
printf -v "$key" '%s' "$value"
|
||||||
export "$key"
|
export "$key"
|
||||||
done < <(grep -v '^[[:space:]]*$' "$project_root/.env")
|
done < <(grep -v '^[[:space:]]*$' "$project_root/.env.builder")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
server_url=${DEB_SERVER_URL:-}
|
server_url=${DEB_SERVER_URL:-}
|
||||||
@@ -61,7 +62,7 @@ shift $((OPTIND - 1))
|
|||||||
|
|
||||||
if [[ -z "$server_url" || -z "$repository" || -z "$token" || $# -eq 0 ]]; then
|
if [[ -z "$server_url" || -z "$repository" || -z "$token" || $# -eq 0 ]]; then
|
||||||
echo "Error: DEB_SERVER_URL, DEB_TOKEN, DEB_REPOSITORY, and at least one file are required." >&2
|
echo "Error: DEB_SERVER_URL, DEB_TOKEN, DEB_REPOSITORY, and at least one file are required." >&2
|
||||||
echo "Set them in the environment or the project root .env." >&2
|
echo "Set them in the environment or the project root .env.builder." >&2
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Builder publish credentials. Copy to the project root as `.env.builder`.
|
||||||
|
# Do not commit this file. Do not put these keys in the project's `.env`.
|
||||||
|
# Uncomment and fill the keys for tracks this project uses.
|
||||||
|
# Shell-exported values override this file.
|
||||||
|
#
|
||||||
|
# DEB_SERVER_URL=
|
||||||
|
# DEB_TOKEN=
|
||||||
|
# DEB_REPOSITORY=
|
||||||
|
# DEB_UPLOAD_PATH=
|
||||||
|
# DOCKER_REGISTRY=
|
||||||
|
# DOCKER_REPOSITORY=
|
||||||
|
# IMAGE_TAG=
|
||||||
|
# PLATFORMS=
|
||||||
@@ -95,10 +95,13 @@ class BuilderCheckTests(unittest.TestCase):
|
|||||||
self.assertIn("check.py", skill)
|
self.assertIn("check.py", skill)
|
||||||
self.assertIn("--ready", skill)
|
self.assertIn("--ready", skill)
|
||||||
self.assertIn("makefile.builder", skill)
|
self.assertIn("makefile.builder", skill)
|
||||||
|
self.assertIn(".env.builder", skill)
|
||||||
self.assertIn("不要改用户的 `Makefile`", skill)
|
self.assertIn("不要改用户的 `Makefile`", skill)
|
||||||
self.assertIn("不要调用 create-makefile", skill)
|
self.assertIn("不要调用 create-makefile", skill)
|
||||||
self.assertIn("不要用 create-makefile", contract)
|
self.assertIn("不要用 create-makefile", contract)
|
||||||
self.assertIn("makefile.builder", contract)
|
self.assertIn("makefile.builder", contract)
|
||||||
|
self.assertIn(".env.builder", contract)
|
||||||
|
self.assertIn("不要读取或改写用户 `.env`", contract)
|
||||||
|
|
||||||
def test_no_makefile_without_ready_is_usage_error(self) -> None:
|
def test_no_makefile_without_ready_is_usage_error(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
@@ -136,7 +139,7 @@ class BuilderCheckTests(unittest.TestCase):
|
|||||||
project = Path(temp)
|
project = Path(temp)
|
||||||
init_repo(project)
|
init_repo(project)
|
||||||
write_contract_makefile(project)
|
write_contract_makefile(project)
|
||||||
(project / ".env").write_text(
|
(project / ".env.builder").write_text(
|
||||||
"DEB_SERVER_URL=https://secret.example.com\n"
|
"DEB_SERVER_URL=https://secret.example.com\n"
|
||||||
"DEB_TOKEN=super-secret-token-value\n"
|
"DEB_TOKEN=super-secret-token-value\n"
|
||||||
"DEB_REPOSITORY=main\n",
|
"DEB_REPOSITORY=main\n",
|
||||||
@@ -151,6 +154,40 @@ class BuilderCheckTests(unittest.TestCase):
|
|||||||
self.assertNotIn("super-secret-token-value", text)
|
self.assertNotIn("super-secret-token-value", text)
|
||||||
self.assertNotIn("https://secret.example.com", text)
|
self.assertNotIn("https://secret.example.com", text)
|
||||||
|
|
||||||
|
def test_user_dotenv_is_ignored(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
project = Path(temp)
|
||||||
|
init_repo(project)
|
||||||
|
write_contract_makefile(project)
|
||||||
|
(project / ".env").write_text(
|
||||||
|
"DEB_SERVER_URL=https://user.example.com\n"
|
||||||
|
"DEB_TOKEN=user-env-secret-token\n"
|
||||||
|
"DEB_REPOSITORY=main\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
code, text = run_check(
|
||||||
|
project, "--ready", which={"dpkg-deb": "/usr/bin/dpkg-deb"}
|
||||||
|
)
|
||||||
|
self.assertEqual(code, 0, text)
|
||||||
|
self.assertIn("DEB_SERVER_URL: MISSING", text)
|
||||||
|
self.assertNotIn("user-env-secret-token", text)
|
||||||
|
self.assertNotIn("https://user.example.com", text)
|
||||||
|
|
||||||
|
def test_empty_env_builder_values_count_as_missing(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
project = Path(temp)
|
||||||
|
init_repo(project)
|
||||||
|
write_contract_makefile(project)
|
||||||
|
(project / ".env.builder").write_text(
|
||||||
|
"DEB_SERVER_URL=\nDEB_TOKEN=\nDEB_REPOSITORY=\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
code, text = run_check(
|
||||||
|
project, "--ready", which={"dpkg-deb": "/usr/bin/dpkg-deb"}
|
||||||
|
)
|
||||||
|
self.assertEqual(code, 0, text)
|
||||||
|
self.assertIn("DEB_TOKEN: MISSING", text)
|
||||||
|
|
||||||
def test_ready_fails_when_docker_track_missing_docker(self) -> None:
|
def test_ready_fails_when_docker_track_missing_docker(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
project = Path(temp)
|
project = Path(temp)
|
||||||
|
|||||||
Reference in New Issue
Block a user