Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ jobs:
--data-urlencode "scope=repository:${image_repository}:pull" \
https://ghcr.io/token | jq -er '.token // .access_token')
status=$(curl -sS --retry 3 --proto '=https' --tlsv1.2 \
--request HEAD --output /dev/null --write-out '%{http_code}' \
--head --output /dev/null --write-out '%{http_code}' \
--header "Authorization: Bearer ${registry_token}" \
--header 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
"https://ghcr.io/v2/${image_repository}/manifests/${RELEASE_TAG}")
Expand Down Expand Up @@ -367,7 +367,7 @@ jobs:
--data-urlencode "scope=repository:${image_repository}:pull" \
https://ghcr.io/token | jq -er '.token // .access_token')
status=$(curl -sS --retry 3 --proto '=https' --tlsv1.2 \
--request HEAD --output /dev/null --write-out '%{http_code}' \
--head --output /dev/null --write-out '%{http_code}' \
--header "Authorization: Bearer ${registry_token}" \
--header 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
"https://ghcr.io/v2/${image_repository}/manifests/${RELEASE_TAG}")
Expand Down
8 changes: 7 additions & 1 deletion .trellis/spec/core/infra/build-and-generated-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ assets, or production installation/upgrade instructions.
- `contents: write` and `packages: write` remain job-scoped. Image builds keep
the OCI source/version/revision/license labels, SBOM, provenance, and
per-target cache scopes.
- Registry manifest-existence probes use curl's real HEAD mode (`--head`) and
never `--request HEAD`. The latter changes the method but leaves curl
expecting a response body; GHCR can then produce exit 18 for a valid
bodyless HEAD response before the workflow can classify 200 versus 404.

### 4. Validation & Error Matrix

Expand All @@ -191,6 +195,7 @@ assets, or production installation/upgrade instructions.
| an existing asset has different bytes | fail; never clobber it |
| an existing image tag has different commit/platform/OCI metadata | fail before push; never move it |
| a version tag appears after digest staging | fail before promotion; never overwrite the tag |
| a registry existence probe uses `--request HEAD` instead of `--head` | contract tests fail; the release workflow must not depend on curl body-transfer semantics for HEAD |
| one image build/push fails | Release remains a Pre-release and the digest manifest is not published |
| digest artifact is missing, duplicated, malformed, or not `sha256:<64-lowercase-hex>` | manifest publication fails |
| first GHCR packages are still private | do not claim anonymous installation; owner must make all three public and visibility cannot be reverted to private |
Expand All @@ -216,7 +221,8 @@ assets, or production installation/upgrade instructions.
inputs.
- Container workflow contracts assert the upstream/strict-tag gate,
PR/manual `push: false`, source-before-image dependency, amd64, OCI labels,
SBOM/provenance, and immutable digest manifest shape.
SBOM/provenance, immutable digest manifest shape, exactly two real curl
`--head` probes, and absence of `--request HEAD`.
- Run `make check-container-files`, `go test ./internal/containercontract`,
`make container-config CONTAINER_IMAGE_TAG=dev`, `make check-docs`,
`make lint`, `make test`, `make security`, and `git diff --check` before the
Expand Down
15 changes: 12 additions & 3 deletions .trellis/tasks/08-21-ghcr-release-install/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,18 @@ bundle README 仅给出 SHA 校验后的显式命令,不提供 `curl | sh` 或

## First release and visibility

代码经 PR 合并后,仅给合并提交创建 `v0.1.0`。GitHub 首次创建的个人 GHCR package 默认 private,workflow 无法替代所有者的不可逆 visibility 决策;三个镜像完成后由所有者逐个改为 public,再从未登录 registry 的环境执行 inspect/pull 验证。

基础设施瞬时错误可对同一 tag/commit 重跑。若修复需要改代码,不移动 `v0.1.0`,而是在 main 修复后使用 `v0.1.1`。首发保持 Pre-release,clean-VM 验收由独立任务完成。
代码经 PR #5 合并后,给合并提交创建了不可移动的 `v0.1.0`。部署/来源资产成功进入
Pre-release,但两个 registry manifest HEAD 调用错误使用 `curl --request HEAD`,三个
矩阵项均在任何镜像构建或推送前以 exit 18 失败,digest manifest 未生成。该 tag 和
Pre-release 保持不完整,不重跑或补写;修复合并后用新 tag `v0.1.1` 完成首发。

GitHub 首次创建的个人 GHCR package 默认 private,workflow 无法替代所有者的不可逆
visibility 决策;`v0.1.1` 三个镜像完成后由所有者逐个改为 public,再从未登录 registry
的环境执行 inspect/pull 验证。

基础设施瞬时错误可对同一 tag/commit 重跑。此次失败需要改代码,所以不移动或复用
`v0.1.0`,而是在 main 修复后使用 `v0.1.1`。首发保持 Pre-release,clean-VM 验收由
独立任务完成。

同 ref concurrency 消除 workflow 自身重跑竞争;发布期间其他主体不得持有或使用
`packages: write` 改写同 tag。GHCR 在本流程中没有 compare-and-set 接口,因此版本 tag
Expand Down
14 changes: 9 additions & 5 deletions .trellis/tasks/08-21-ghcr-release-install/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@
- [x] 运行 focused bundle/container tests、`make container-config CONTAINER_IMAGE_TAG=dev` 和 Actions lint。
- [x] 运行 `make check-container-files`、`go test ./internal/containercontract`、`make check-docs`、`make lint`、`make test`、`make security`、`git diff --check`。
- [x] 对工作树、历史和生成 bundle 运行启用 redaction 的独立 secret scan;人工复核资产清单/许可证且不保存扫描输出。
- [ ] Trellis check 通过后更新规范、提交分支、推送并创建以 `main` 为 base 的 PR;等待 CI 成功后合并。
- [x] Trellis check 通过后更新规范、提交分支、推送并创建 PR #5;7 个执行检查成功后以 merge commit `d7aaa34` 合并。

## 5. v0.1.0 Pre-release
## 5. v0.1.0 failure and v0.1.1 Pre-release

- [ ] 对合并提交创建/推送不可移动的 `v0.1.0`,等待 containers workflow 完成。
- [ ] 验证 Pre-release 部署包/checksum、strongSwan/Mihomo 来源资产、三个 GHCR tag 和 digest manifest。
- [x] 对合并提交 `d7aaa34` 创建/推送不可移动的 `v0.1.0`;部署/来源资产发布成功,三个镜像 job 在构建/推送前失败,Release 保持不完整 Pre-release。
- [x] 从 Actions 日志确认 `curl --request HEAD` 导致 exit 18;不重跑、移动或补写 `v0.1.0`,添加 `--head` 回归合同。
- [ ] 修复分支经 PR/CI 合并后,对新合并提交创建并推送 `v0.1.1`,等待 containers workflow 完成。
- [ ] 验证 `v0.1.1` Pre-release 部署包/checksum、strongSwan/Mihomo 来源资产、三个 GHCR tag 和 digest manifest。
- [ ] 由仓库所有者把三个首次 package 改为 public;在无 GHCR 登录环境 inspect/pull 并核对 OCI metadata/digest。
- [ ] 解包到临时目录,运行 checksum、`docker compose config --quiet` 和 `docker compose pull`;不得执行 up、宿主准备或 HIL。
- [ ] 若需代码修复,不移动 v0.1.0;在 main 修复后发布 v0.1.1。

## Rollback points

Expand All @@ -53,3 +54,6 @@
用校验和固定的 `make dev-toolchain` 刷新后,`make security` 报告可达漏洞 0、pnpm 漏洞 0。
- `SIMPLUS_DEB_VERSION=0.1.0-1` 的 strongSwan 包、对应源码、manifest 和 checksum 已完成
本地隔离构建与校验;未执行宿主准备、Compose 启动或任何 HIL。
- `v0.1.0` Actions run `32479227277` 证明来源资产阶段成功且镜像阶段失败关闭;修复后的
`v0.1.1` bundle checksum/解包/Compose render、合同测试、lint、完整 `make test`、
`make security` 和脱敏工作树/历史扫描均通过。
28 changes: 21 additions & 7 deletions .trellis/tasks/08-21-ghcr-release-install/prd.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,37 @@
- 当前安装文档仍要求在源码 checkout 中取得 Compose/宿主脚本,并保留首发前本地构建回退;这不是目标生产安装接口。
- clean Debian VM 的完整安装、升级、停止和卸载生命周期仍未验收;首发不得提升为稳定 Runtime 结论。

## Release attempt outcome

PR #5 在全部检查成功后以 `d7aaa34` 合并,并创建了不可移动的 `v0.1.0`。部署包和对应
源码资产成功发布;随后三个镜像 job 因 registry HEAD 探测的 curl 语义错误,在任何
镜像构建/推送前失败,digest manifest 未生成。按既定失败处理,不移动、重跑或补写
`v0.1.0`;代码修复后以 `v0.1.1` 完成剩余验收。

## Requirements

### R1 — Release bundle installation

- 生产安装只依赖 Docker Engine、Compose、下载/校验/解包工具及版本化部署包,不依赖 Git、Go、Node、pnpm、Make 或本地 `docker build`。
- `v0.1.0` 资产名固定为 `simplus-compose-v0.1.0-linux-amd64.tar.gz` 及同名 `.sha256`。
- 每个版本资产名固定为 `simplus-compose-<tag>-linux-amd64.tar.gz` 及同名 `.sha256`;
`v0.1.0` 已生成并保留,最终候选改用对应的 `v0.1.1` 资产。
- 解包目录只包含:写死同版本镜像 tag 的 `compose.yaml`、`.env.example`、宿主准备和检查脚本、简明安装说明、`VERSION`、`LICENSE`、`THIRD_PARTY_NOTICES.md`。
- `.env.example` 只暴露 `SIMPLUS_HTTP_PORT`、`SIMPLUS_CONTROLLER_PORT`、`SIMPLUS_DEVICE_GID`;生产用户不通过环境变量切换镜像版本。

### R2 — Versioned GHCR publication

- 只允许严格 `vX.Y.Z` Git tag 发布 `linux/amd64` 镜像;PR 和 `workflow_dispatch` 只构建验证,不推送。
- 不发布或引用 `latest`、`main` 或分支滚动标签。
- 正式引用为 `ghcr.io/leonfox28/simplus-{control,agent,netd}:<tag>`;首发为 `v0.1.0`。
- 正式引用为 `ghcr.io/leonfox28/simplus-{control,agent,netd}:<tag>`;`v0.1.0` 是失败关闭的
首次尝试,首个完整候选改为 `v0.1.1`。
- 继续保留 OCI source/version/revision/license 标签、SBOM、provenance 和按 target 分离的缓存。

### R3 — Release ordering and evidence

- strongSwan 包/对应源码、Mihomo 对应源码和部署包必须先作为同 tag GitHub Pre-release 资产公开,随后才允许推送镜像。
- 三镜像成功后发布 `simplus-images-v0.1.0.json`,记录版本、Git commit、`linux/amd64` 平台、完整 tag reference 和实际 `sha256` digest。
- 三镜像成功后发布 `simplus-images-<tag>.json`,记录版本、Git commit、`linux/amd64`
平台、完整 tag reference 和实际 `sha256` digest;本任务的完整清单应为
`simplus-images-v0.1.1.json`。
- 任一来源、资产或镜像步骤失败时不得把 GitHub Release 标记为稳定可用。
- GitHub 首次生成的三个 private package 由仓库所有者显式改为 public;完成前不得宣告匿名安装可用。

Expand All @@ -39,7 +50,8 @@
- README 和 `docs/installation.md` 以 Release 下载、SHA-256 校验、解包、宿主检查、`docker compose pull/up` 为唯一生产安装路径。
- `make container-build` 和参数化源码 Compose 只保留在 `docs/development.md` 作为开发验证入口。
- 升级通过用新版本部署包替换受管文件并 pull/up;不得承诺仅切回旧镜像即可安全降级数据库。
- 更新 ADR 0021、相关 Trellis infra/docs 规范和必要的公开状态摘要,但继续明确 `v0.1.0` 是部署候选/Pre-release。
- 更新 ADR 0021、相关 Trellis infra/docs 规范和必要的公开状态摘要,明确 `v0.1.0` 是
不完整 Pre-release、`v0.1.1` 仍只是部署候选。

### R5 — Safety and publication

Expand All @@ -52,11 +64,13 @@

- [x] `make container-release-bundle RELEASE_TAG=v0.1.0` 生成规定名称、内容、权限、确定性 tar.gz 和有效 SHA-256 文件。
- [x] 发布包中的 Compose 只含三张 `:v0.1.0` GHCR 镜像、没有 `build`/`latest`,并可在解包目录用 `.env` 成功渲染。
- [x] `v0.1.1` 替代部署包也通过 checksum、解包和 Compose 渲染合同。
- [x] 合同测试拒绝非法 tag、额外/私有文件、错误权限、非确定性输出、未固定镜像或缺失许可证。
- [ ] PR/手动 Actions 构建三个 target 但不推送;只有严格版本 tag 进入来源资产 → Pre-release → GHCR → image manifest 流程。
- [x] PR Actions 成功构建三个 target 但不推送;只有严格版本 tag 进入来源资产 → Pre-release → GHCR → image manifest 流程。
- [x] `make check-container-files`、`go test ./internal/containercontract`、`make check-docs`、`make lint`、`make test`、`make security` 和 `git diff --check` 通过。
- [ ] 功能分支通过 PR 合并后,合并提交的 `v0.1.0` workflow 成功生成 Pre-release、部署资产、源码资产和三个 GHCR 镜像。
- [ ] 三个 package 经所有者改为 public 后,无 GHCR 登录也能 inspect/pull `v0.1.0`,且 digest/OCI 元数据与 `simplus-images-v0.1.0.json` 一致。
- [x] 功能分支通过 PR #5 合并;`v0.1.0` workflow 成功生成 Pre-release/部署/源码资产,并在任何镜像推送前失败关闭,旧 tag 保持不可移动且不完整。
- [ ] 修复分支通过 PR/CI 合并后,新合并提交的 `v0.1.1` workflow 成功生成完整 Pre-release、部署资产、源码资产、三个 GHCR 镜像和 digest manifest。
- [ ] 三个 package 经所有者改为 public 后,无 GHCR 登录也能 inspect/pull `v0.1.1`,且 digest/OCI 元数据与 `simplus-images-v0.1.1.json` 一致。
- [ ] 发布后仅执行解包、Compose config 和 pull 验证;不启动服务或接触真实硬件。

## Out of Scope
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Bug Analysis: GHCR HEAD probe exited before image publication

## 1. Root Cause Category

- **Category**: D/E — Test coverage gap plus implicit curl assumption.
- **Specific Cause**: the workflow used `curl --request HEAD` for two registry
manifest-existence probes. That changes the HTTP method but does not enable
curl's HEAD response handling, so curl still expected the `Content-Length`
body that GHCR correctly omitted and exited 18 before returning the HTTP
status to the shell.

The GitHub Actions log is decisive evidence: all three image jobs stopped in
the first probe with `curl: (18) transfer closed with 70 bytes remaining to
read`; no build, digest staging, tag promotion, or manifest publication ran.

## 2. Why Earlier Checks Missed It

1. Static workflow and action lint validated YAML/shell syntax, not curl's
protocol-mode semantics.
2. PR jobs intentionally never authenticate to or probe GHCR, so the correct
no-push policy also meant this tag-only branch first ran in production.
3. Contract tests checked immutable-tag ordering and failure closure but did
not distinguish curl's `--head` behavior from a manually selected method.

## 3. Prevention Mechanisms

| Priority | Mechanism | Specific action | Status |
| --- | --- | --- | --- |
| P0 | Runtime correctness | Replace both probes with `curl --head` while retaining explicit 200/404 classification | Done |
| P0 | Test coverage | Require exactly two `--head` probes and reject `--request HEAD` in the container workflow contract | Done |
| P1 | Documentation | Record the curl HEAD/body-transfer rule in the versioned release spec and error matrix | Done |
| P1 | Rollout discipline | Keep incomplete `v0.1.0` immutable and publish the code fix only from a new `v0.1.1` tag | In progress |

## 4. Systematic Expansion

- **Similar issues**: both pre-build reuse and pre-promotion race probes used
the same incorrect flag, so the fix and assertion cover both call sites.
- **Design improvement**: the registry response remains classified solely by
HTTP status; no response body is parsed or required.
- **Process improvement**: tag-only network operations need semantic contract
assertions even when PR policy correctly forbids external publication.

## 5. Knowledge Capture

- [x] Updated `.trellis/spec/core/infra/build-and-generated-files.md`.
- [x] Added a focused regression assertion to `internal/containercontract`.
- [x] Recorded the immutable failed-release evidence in the active task.
- [ ] Complete the replacement `v0.1.1` Pre-release and anonymous pull checks.
9 changes: 6 additions & 3 deletions .trellis/tasks/08-21-ghcr-release-install/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"assignee": "Leon",
"createdAt": "2026-08-21",
"completedAt": null,
"branch": "feat/ghcr-release-install",
"branch": "fix/ghcr-head-probe",
"base_branch": "main",
"worktree_path": null,
"commit": null,
Expand All @@ -23,6 +23,9 @@
"relatedFiles": [],
"notes": "",
"meta": {
"release_tag": "v0.1.0"
"release_tag": "v0.1.1",
"incomplete_release_tag": "v0.1.0",
"initial_merge_commit": "d7aaa34aeb6936f3683f37bacbef71ca9a013f57",
"initial_pr_url": "https://github.com/leonfox28/simplus/pull/5"
}
}
}
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ AT/QMI、设备路径或通用硬件写入口;真实副作用必须经过独
remap 及其他发行版尚未验证。Web 与 controller 只应开放给受信任局域网,不能直接暴露
到公网。

生产安装不需要克隆源码或在宿主构建镜像。首个计划发布的部署候选为 GitHub
Pre-release `v0.1.0`;发布资产和三个公开 GHCR package 完整后,从该 Release 下载
版本化部署包及校验文件:
生产安装不需要克隆源码或在宿主构建镜像。不可移动的 `v0.1.0` 已保留为不完整
Pre-release:部署/来源资产发布成功,但镜像发布在推送前失败,因此不得安装。首个完整
部署候选改为 `v0.1.1`;只有发布资产和三个公开 GHCR package 全部完整后,才从该
Release 下载版本化部署包及校验文件:

```bash
version=v0.1.0
version=v0.1.1
base="https://github.com/leonfox28/simplus/releases/download/$version"
curl -fLO "$base/simplus-compose-$version-linux-amd64.tar.gz"
curl -fLO "$base/simplus-compose-$version-linux-amd64.tar.gz.sha256"
Expand Down Expand Up @@ -83,7 +84,7 @@ docker compose logs bootstrap
`/var/lib/simplus` 数据。

三个 GHCR package 首次发布后需由仓库所有者显式改为 public,之后匿名 `pull` 才是有效
安装证据。发布包、对应源码和 `simplus-images-v0.1.0.json` digest 清单均保留在同一
安装证据。发布包、对应源码和 `simplus-images-v0.1.1.json` digest 清单均保留在同一
Pre-release;任一资产缺失时不要回退到生产机本地构建。

完整的宿主要求、权限模型、生命周期、更新方式和故障处理见
Expand Down
7 changes: 4 additions & 3 deletions docs/decisions/0021-container-production-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
蜂窝短信含义均不受影响。

生产安装接口进一步收敛为 GitHub Pre-release 中的版本化 `linux/amd64` 部署包和 GHCR
三镜像:管理员不再为生产部署克隆源码或本地构建。首个 `v0.1.0` 仍是部署候选;三个
首次 GHCR package 必须由仓库所有者显式改为 public,并取得匿名 pull 证据后,才能
宣称该候选可匿名安装。该发布变化不替代仍待完成的 clean-VM 生命周期验收。
三镜像:管理员不再为生产部署克隆源码或本地构建。不可移动的 `v0.1.0` 在来源资产发布
后、任何镜像推送前失败并保持不完整 Pre-release;修复使用新 tag `v0.1.1`,不补写旧
版本。三个首次 GHCR package 必须由仓库所有者显式改为 public,并取得匿名 pull 证据
后,才能宣称新候选可匿名安装。该发布变化不替代仍待完成的 clean-VM 生命周期验收。

## 背景

Expand Down
Loading
Loading