From bc0525966e9ed35641f33a33a710b3bc3530fcc8 Mon Sep 17 00:00:00 2001 From: Leon <41661196+leonfox28@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:02:47 +0800 Subject: [PATCH] fix: use curl HEAD mode for GHCR probes --- .github/workflows/containers.yml | 4 +- .../core/infra/build-and-generated-files.md | 8 +++- .../08-21-ghcr-release-install/design.md | 15 ++++-- .../08-21-ghcr-release-install/implement.md | 14 ++++-- .../tasks/08-21-ghcr-release-install/prd.md | 28 ++++++++--- .../research/v0.1.0-release-failure.md | 48 +++++++++++++++++++ .../08-21-ghcr-release-install/task.json | 9 ++-- README.md | 11 +++-- .../0021-container-production-deployment.md | 7 +-- docs/installation.md | 10 ++-- docs/plans/active/mvp.md | 13 +++-- internal/containercontract/contract_test.go | 11 ++++- 12 files changed, 140 insertions(+), 38 deletions(-) create mode 100644 .trellis/tasks/08-21-ghcr-release-install/research/v0.1.0-release-failure.md diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index b8357a6..05d2aad 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -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}") @@ -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}") diff --git a/.trellis/spec/core/infra/build-and-generated-files.md b/.trellis/spec/core/infra/build-and-generated-files.md index ff4aa29..b5e9b74 100644 --- a/.trellis/spec/core/infra/build-and-generated-files.md +++ b/.trellis/spec/core/infra/build-and-generated-files.md @@ -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 @@ -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 | @@ -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 diff --git a/.trellis/tasks/08-21-ghcr-release-install/design.md b/.trellis/tasks/08-21-ghcr-release-install/design.md index 5c0210d..8802751 100644 --- a/.trellis/tasks/08-21-ghcr-release-install/design.md +++ b/.trellis/tasks/08-21-ghcr-release-install/design.md @@ -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 diff --git a/.trellis/tasks/08-21-ghcr-release-install/implement.md b/.trellis/tasks/08-21-ghcr-release-install/implement.md index 755a09b..9227b23 100644 --- a/.trellis/tasks/08-21-ghcr-release-install/implement.md +++ b/.trellis/tasks/08-21-ghcr-release-install/implement.md @@ -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 @@ -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` 和脱敏工作树/历史扫描均通过。 diff --git a/.trellis/tasks/08-21-ghcr-release-install/prd.md b/.trellis/tasks/08-21-ghcr-release-install/prd.md index 3af9e83..6df37f5 100644 --- a/.trellis/tasks/08-21-ghcr-release-install/prd.md +++ b/.trellis/tasks/08-21-ghcr-release-install/prd.md @@ -11,12 +11,20 @@ - 当前安装文档仍要求在源码 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--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`;生产用户不通过环境变量切换镜像版本。 @@ -24,13 +32,16 @@ - 只允许严格 `vX.Y.Z` Git tag 发布 `linux/amd64` 镜像;PR 和 `workflow_dispatch` 只构建验证,不推送。 - 不发布或引用 `latest`、`main` 或分支滚动标签。 -- 正式引用为 `ghcr.io/leonfox28/simplus-{control,agent,netd}:`;首发为 `v0.1.0`。 +- 正式引用为 `ghcr.io/leonfox28/simplus-{control,agent,netd}:`;`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-.json`,记录版本、Git commit、`linux/amd64` + 平台、完整 tag reference 和实际 `sha256` digest;本任务的完整清单应为 + `simplus-images-v0.1.1.json`。 - 任一来源、资产或镜像步骤失败时不得把 GitHub Release 标记为稳定可用。 - GitHub 首次生成的三个 private package 由仓库所有者显式改为 public;完成前不得宣告匿名安装可用。 @@ -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 @@ -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 diff --git a/.trellis/tasks/08-21-ghcr-release-install/research/v0.1.0-release-failure.md b/.trellis/tasks/08-21-ghcr-release-install/research/v0.1.0-release-failure.md new file mode 100644 index 0000000..60cf0db --- /dev/null +++ b/.trellis/tasks/08-21-ghcr-release-install/research/v0.1.0-release-failure.md @@ -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. diff --git a/.trellis/tasks/08-21-ghcr-release-install/task.json b/.trellis/tasks/08-21-ghcr-release-install/task.json index 36c8b9d..c42f07e 100644 --- a/.trellis/tasks/08-21-ghcr-release-install/task.json +++ b/.trellis/tasks/08-21-ghcr-release-install/task.json @@ -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, @@ -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" } -} \ No newline at end of file +} diff --git a/README.md b/README.md index b1d4198..401a25b 100644 --- a/README.md +++ b/README.md @@ -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" @@ -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;任一资产缺失时不要回退到生产机本地构建。 完整的宿主要求、权限模型、生命周期、更新方式和故障处理见 diff --git a/docs/decisions/0021-container-production-deployment.md b/docs/decisions/0021-container-production-deployment.md index 8cf1e7c..c2948c0 100644 --- a/docs/decisions/0021-container-production-deployment.md +++ b/docs/decisions/0021-container-production-deployment.md @@ -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 生命周期验收。 ## 背景 diff --git a/docs/installation.md b/docs/installation.md index 9b5a0d7..1c13a15 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -9,6 +9,10 @@ GHCR 镜像,不要求源码 checkout、Git、Go、Node、pnpm、Make 或本地 仓库;开发 VM 已完成真实硬件容器业务 HIL,但 clean Debian VM 生命周期尚未验收, 因此当前仍属于可审查的部署候选,而不是稳定发布版本。 +不可移动的 `v0.1.0` 已保留为不完整 Pre-release:部署包及对应源码资产发布成功,GHCR +manifest 探测随后在任何镜像构建/推送前失败,且没有生成 digest manifest。不要安装或 +补齐该版本;修复后的首个完整候选使用新 tag `v0.1.1`。 + 三个 production target 和隔离无硬件 Compose 已在 Debian 13/amd64 开发 VM 完成 smoke;该证据验证镜像、权限、netd preflight、bootstrap、Web 登录和数据保留重建, 不等于 clean-VM 或真实模组验收,也没有执行 RF、VoWiFi、短信或电话动作。 @@ -35,11 +39,11 @@ Docker Engine 的安装按 [Docker 官方 Debian 指南](https://docs.docker.com ## 下载部署包 -每个严格 `vX.Y.Z` tag 对应一个 `linux/amd64` 部署包。下面以首个部署候选 `v0.1.0` +每个严格 `vX.Y.Z` tag 对应一个 `linux/amd64` 部署包。下面以替代部署候选 `v0.1.1` 为例;版本变量必须同时决定下载目录和文件名,不要改为滚动的 `latest`: ```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" @@ -58,7 +62,7 @@ tag workflow 会先把部署包、strongSwan 包/对应源码和 Mihomo 对应 GitHub Pre-release,随后才推送三张镜像。三个 package 首次生成时默认 private,必须由 仓库所有者逐个改为 public 并完成匿名 pull 验证;该 visibility 变更不能再恢复为 private,在这一步完成前,部署候选不能宣称可匿名安装。Release 中的 -`simplus-images-v0.1.0.json` 记录实际 digest,供发布后审计,Compose 仍引用不可移动 +`simplus-images-v0.1.1.json` 记录实际 digest,供发布后审计,Compose 仍引用不可移动 的版本 tag。 ## 一次性宿主准备 diff --git a/docs/plans/active/mvp.md b/docs/plans/active/mvp.md index ab98bf4..c72d769 100644 --- a/docs/plans/active/mvp.md +++ b/docs/plans/active/mvp.md @@ -197,9 +197,12 @@ - [x] 将正式安装接口收敛为确定性 `linux/amd64` Release 部署包:包内 Compose 写死严格 版本 tag,`.env` 只保留两个端口和设备 GID;PR/手动 workflow 只验证构建,tag workflow 按部署包/对应源码 → 三张 GHCR 镜像 → digest manifest 的顺序保持 Pre-release; -- [ ] 发布首个不可移动的 `v0.1.0` Pre-release,由仓库所有者把三张首次 GHCR package - 显式改为 public,并在未登录 registry 的环境核对匿名 pull、OCI metadata、digest - manifest 和解包后的 Compose config/pull;该项不得执行 `up`、宿主准备或 HIL; +- [x] 保留不可移动但不完整的 `v0.1.0` Pre-release:部署/来源资产已发布,GHCR manifest + 探测在任何镜像构建或推送前失败,没有生成 digest manifest;不重跑、不补写该 tag; +- [ ] 用修复后的新 tag `v0.1.1` 发布首个完整 Pre-release,由仓库所有者把三张首次 + GHCR package 显式改为 public,并在未登录 registry 的环境核对匿名 pull、OCI + metadata、digest manifest 和解包后的 Compose config/pull;该项不得执行 `up`、 + 宿主准备或 HIL; - [x] 保持日常开发、Simulator、Go/Web 测试和 CI 使用本机/runner 工具,不提供 dev image;保留受限 `simplus-agent-dev` 供本机 HIL; - [x] 在当前 Debian 13/amd64 开发 VM 构建三个 production target,并以隔离空 @@ -259,8 +262,8 @@ 8. 将 ePDG FQDN、IKE responder identity 和远端选择从当前已验证运营商接入 profile 中解耦,并分别完成其他运营商 HIL;动态 IMS Home Domain 本身不能作为多运营商兼容证据。 9. 完成 Milestone 26 的 clean-VM Compose 生命周期验收;ARM64、rootless/userns、 Podman、Docker Desktop 与 SELinux 发行版仍需独立设计和证据。 -10. 在 `v0.1.0` 合并提交上完成 Pre-release/GHCR 首发和所有者 public visibility 门禁; - 若代码修复才可完成发布,保留原 tag 并使用新的 patch 版本。 +10. 保留失败关闭的 `v0.1.0`,在修复合并提交上以 `v0.1.1` 完成 Pre-release/GHCR + 首发和所有者 public visibility 门禁;后续仍不得移动或补写旧 tag。 每项真实副作用都需要独立设计、最小实现、明确授权和与风险相称的 HIL;Web/API 不得暴露任意 AT/QMI、设备路径、网络命令或配置路径。 diff --git a/internal/containercontract/contract_test.go b/internal/containercontract/contract_test.go index bdb0e20..d797025 100644 --- a/internal/containercontract/contract_test.go +++ b/internal/containercontract/contract_test.go @@ -264,6 +264,7 @@ func TestContainerWorkflowPublishesOnlyStrictVersionTagsAfterReleaseAssets(t *te "packages: write", "push-by-digest=true", "scripts/release/inspect-published-container-image.sh", + "--head --output /dev/null --write-out '%{http_code}'", "refusing to move an existing immutable image tag", "published version tag digest differs from the staged immutable digest", "Validate the exact public Release asset allowlist", @@ -278,7 +279,15 @@ func TestContainerWorkflowPublishesOnlyStrictVersionTagsAfterReleaseAssets(t *te t.Fatalf("container workflow is missing release contract %q", required) } } - for _, forbidden := range []string{"gh release upload \"$RELEASE_TAG\" release-assets/* --clobber", "tags: latest", "tags: main"} { + if count := strings.Count(text, "--head --output /dev/null --write-out '%{http_code}'"); count != 2 { + t.Fatalf("container workflow has %d registry HEAD probes, want 2", count) + } + for _, forbidden := range []string{ + "gh release upload \"$RELEASE_TAG\" release-assets/* --clobber", + "tags: latest", + "tags: main", + "--request HEAD", + } { if strings.Contains(text, forbidden) { t.Fatalf("container workflow contains mutable publication contract %q", forbidden) }