mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
fix(ql3): bind alpha artifacts to CI evidence
This commit is contained in:
@@ -591,6 +591,20 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||
--mode=record-verification \
|
||||
--application-image="${IMAGE}" \
|
||||
--operator-image="${OPERATOR_IMAGE}" \
|
||||
--architecture=${{ matrix.image_arch }} \
|
||||
--source-revision=${{ github.sha }} \
|
||||
--repository=${{ github.repository }} \
|
||||
--workflow-ref="${{ github.workflow_ref }}" \
|
||||
--workflow-sha=${{ github.workflow_sha }} \
|
||||
--event=${{ github.event_name }} \
|
||||
--job=${{ github.job }} \
|
||||
--run-id=${{ github.run_id }} \
|
||||
--run-attempt=${{ github.run_attempt }} \
|
||||
--output="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"
|
||||
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||
--mode=create \
|
||||
--application-image="${IMAGE}" \
|
||||
@@ -599,6 +613,7 @@ jobs:
|
||||
--source-revision=${{ github.sha }} \
|
||||
--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json" \
|
||||
--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json" \
|
||||
--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json" \
|
||||
--readme=docs/operations/ql3-local-alpha-trial-kit.md \
|
||||
--output="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
||||
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
- 目标版本:QingLong 3.x
|
||||
- 作者:QingLong Maintainers
|
||||
- 创建日期:2026-07-17
|
||||
- 最后更新:2026-08-26
|
||||
- 最后更新:2026-08-27
|
||||
- 讨论范围:架构与演进路线,不包含最终 UI 视觉方案
|
||||
|
||||
最新增量证据(2026-08-26):
|
||||
最新增量证据(2026-08-27):
|
||||
|
||||
- Alpha 阶段产物门(已有源码一致的 `3.0.0-alpha.2` 本地 runtime engineering candidate,用户试运行套件正在闭环):`QingLong 3.0 CI` 的显式 `produce_alpha_artifacts` 门只在手动里程碑运行中归档已经通过原生测试的镜像,而不把普通 push/PR 的中间构建冒充发布。source `e3c05862b8c2690d69f58b098cdc128a09c83f97` 已产出 Local arm64 Application Docker archive(SHA-256 `01afb30cbe0c21f980ca083ad98fd316e659941f940dd8930ffd9ccfa7153edf`,image ID `sha256:dfce2cc9d70044d75f72f2cc3075e1f24569fb9fe279d9c25a45698c19c3bde9`)、CycloneDX SBOM、release-candidate contract、manifest、verification evidence 与 checksum;image identity/architecture/non-root user、read-only/no-network、128 MiB/0.5 CPU smoke、Edge/Standalone lifecycle/graceful stop/SQLite integrity、库存对账与 Trivy 0.70.0 HIGH/CRITICAL=0 已复验。但该 archive 只有 headless runtime,未携带完成 fresh setup/Owner 管理所需的独立 `ql3` 制品,因此按 D-408 重新准确分类为“运行时工程候选”,不再冒充完整用户 Alpha。macOS Docker Desktop 无法等价证明的 Local API cancellation 由原生 Linux arm64 job `97986754052` 通过;本机 Edge 首次 startup receipt 在 Docker Desktop 文件桥出现一次瞬态,精确重跑和原生 Linux门均通过,证据未隐藏首次失败。远端 CI run `32903679764` 首轮为 37/40(两项 GitHub action 内部 DNS 失败、一次 PostgreSQL 18 x64 scheduler 并发断言),failed-only attempt 2 收敛为 40/40;独立 Kubernetes deployment run `32903679644` 与三节点 Security Administration/CNPG/PVC run `32903679570` 同源成功。因尚未授权向 GitHub 上传源码派生镜像/SBOM,当前仍是 owner-private 的单架构工程产物,不是公开可下载 release;amd64 同级 archive、public GHCR digest、签名/attestation、catalog、deployment-lock 与生产 HA/DR/CSI/IdP 仍是升级为完整双架构里程碑或 Public Release Set 的硬门。
|
||||
- D-411/ADR-0506(已实现,真实 downloadable v2 artifact 待授权):Local Alpha materializer 不再凭调用 `create` 就把九个 gate 无条件写成 `passed`。bundle schema 升为 `qinglong/alpha-local-trial-kit@v2`,新增 `verification-evidence.json`,其 subject 精确绑定版本、source、Tier-1 架构与 Application/operator image ID,workflow 精确绑定 `whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next`、workflow SHA、`workflow_dispatch`、`local-image`、run ID/attempt。CI 静态门固定 `fresh journey → native cancellation → record-verification → create → audit → upload`,evidence 作为第七个闭合文件进入 manifest byte/SHA-256 与 `SHA256SUMS`;create/audit 均拒绝跨源码、跨架构、跨镜像或跨 workflow 复制。旧 v1 bundle 因没有来源证明只保留为工程候选。提交 `4239464a` 的主 CI run `32990652047` 已 40/40,Kubernetes run `32990652416` 与三节点 Security run `32990653482` 同源成功,证明源码的双架构门;但本地 `4239464a` v1 archive 不是 exact CI artifact,仍不能冒充 v2 用户 Alpha。该增强只增加一个小型发布期 JSON,不新增 workspace package、镜像 layer、设备依赖、常驻进程、RSS 或端口;首个真实双架构 v2 下载物仍需维护者显式授权 milestone workflow。
|
||||
|
||||
- Alpha 阶段产物历史基线(当前性已由 D-411 收紧):`QingLong 3.0 CI` 的显式 `produce_alpha_artifacts` 门只在手动里程碑运行中归档已经通过原生测试的镜像,而不把普通 push/PR 的中间构建冒充发布。source `e3c05862b8c2690d69f58b098cdc128a09c83f97` 已产出 Local arm64 Application Docker archive(SHA-256 `01afb30cbe0c21f980ca083ad98fd316e659941f940dd8930ffd9ccfa7153edf`,image ID `sha256:dfce2cc9d70044d75f72f2cc3075e1f24569fb9fe279d9c25a45698c19c3bde9`)、CycloneDX SBOM、release-candidate contract、manifest、verification evidence 与 checksum;image identity/architecture/non-root user、read-only/no-network、128 MiB/0.5 CPU smoke、Edge/Standalone lifecycle/graceful stop/SQLite integrity、库存对账与 Trivy 0.70.0 HIGH/CRITICAL=0 已复验。但该 archive 只有 headless runtime,未携带完成 fresh setup/Owner 管理所需的独立 `ql3` 制品,因此按 D-408 重新准确分类为“运行时工程候选”,不再冒充完整用户 Alpha。macOS Docker Desktop 无法等价证明的 Local API cancellation 由原生 Linux arm64 job `97986754052` 通过;本机 Edge 首次 startup receipt 在 Docker Desktop 文件桥出现一次瞬态,精确重跑和原生 Linux门均通过,证据未隐藏首次失败。远端 CI run `32903679764` 首轮为 37/40(两项 GitHub action 内部 DNS 失败、一次 PostgreSQL 18 x64 scheduler 并发断言),failed-only attempt 2 收敛为 40/40;独立 Kubernetes deployment run `32903679644` 与三节点 Security Administration/CNPG/PVC run `32903679570` 同源成功。该证据保留为演进记录,不再代表当前 v2 Alpha bundle 资格;public GHCR digest、签名/attestation、catalog、deployment-lock 与生产 HA/DR/CSI/IdP 仍是 Public Release Set 的硬门。
|
||||
|
||||
- D-408/ADR-0503(进行中):阶段产物成熟度现在按真实部署用户旅程而非“已有 Dockerfile/镜像”裁决。新增独立 `qinglong3-local-operator` 短生命周期镜像,复用既有 `@qinglong/local-owner-cli` 的统一 `ql3` 入口而不新增 workspace package;它默认 `65532:65532`、无端口、无 listener/daemon/timer、network none,和常驻 Local Application 保持物理制品分离,因此 Owner/bootstrap authority 不进入 runtime closure,Edge 稳态资源零变化。本机基于未提交工作树构建的 arm64 operator 原型 ID 为 `sha256:115e90a7442b3c92db0c566f8fc8a560e689878b67eace0236836681a14689ae`,运行库存为 9 package/904 files/9,479,647 bytes;read-only、drop ALL、no-new-privileges、128 MiB/0.5 CPU/32 PID 下的 `ql3 --version` 与 `ql3 setup --help` 已通过。这些数值只证明实现可构建,不冒充 commit-bound release evidence。Alpha workflow 将在同一原生 runner 上把 Application 与 operator 通过一次 `docker image save` 合并为去重的 `qinglong3-local-trial-kit-<arch>.docker.tar`,manifest 同时绑定两个 image ID、共同 archive SHA-256、source/version/architecture,并从镜像入口完成 fresh setup exact replay、Identity provision、challenge、首 Owner claim/ack、Application active/SIGTERM drain 和 SQLite integrity。Docker Desktop bind mount 根目录会把宿主 UID 501 映射为容器 root、子文件仍为 501,不能等价满足完整 POSIX lineage;本机失败被记录为平台不等价,未放宽门禁或伪装通过。D-408 转 Accepted 仍需同一提交的原生 Linux x64/arm64 journey 成功和完整回归;实际上传双架构 trial kit 仍需维护者显式授权,Public Release Set 是否正式增加 operator artifact 另行决策。
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# ADR-0506:源码绑定的 Local Alpha 验证证据
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-27
|
||||
- 决策:D-411
|
||||
- 关联:ADR-0503、ADR-0504、ADR-0505
|
||||
|
||||
## 背景
|
||||
|
||||
ADR-0504 把 Local Trial Kit 的写入与离线审计收敛为一个 materializer,但 v1 manifest 在 `create` 时会无条件写入九个 `passed` 字段。调用者只需提供可构建镜像和 SBOM,即可得到一份声称已经通过漏洞扫描、fresh Owner、Edge/Standalone lifecycle 与原生 cancellation 的 manifest;materializer 并未收到这些 gate 的任何证据。
|
||||
|
||||
提交 `4239464a` 的 macOS Docker Desktop 复验进一步暴露了该语义错误:bundle 内容、checksum、reload 和入口 smoke 均通过,但完整 Owner 旅程因 bind-mount UID 语义失败;同提交的原生 Linux CI 后续通过。内容完整性、源码实现通过和 exact artifact 已被验证是三种不同事实,不能由一个常量同时代替。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. Alpha bundle 必须消费独立的 verification evidence
|
||||
|
||||
Local Alpha schema 升为 `qinglong/alpha-local-trial-kit@v2`。`create` 新增必需的 `--verification-evidence`,并把 `verification` 从九个常量改为 `verification-evidence.json` 的文件记录。闭合目录从六个文件增加为七个,evidence 同时进入 manifest byte/SHA-256 绑定与 `SHA256SUMS`。
|
||||
|
||||
缺少 evidence、旧 v1 manifest、额外文件、evidence mutation 或 subject 漂移全部失败关闭。旧 schema 尚未公开发布,因此不保留会继续接受无来源 `passed` 声明的兼容分支。
|
||||
|
||||
### 2. evidence 绑定 exact workflow 与 artifact subject
|
||||
|
||||
新增同一 materializer 的 `record-verification` 模式,只接受:
|
||||
|
||||
- repository `whyour/qinglong`;
|
||||
- workflow ref `whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next`;
|
||||
- `workflow_dispatch` event 与 `local-image` job;
|
||||
- 完整 source/workflow SHA、十进制 run ID/attempt;
|
||||
- Tier-1 architecture,以及从 Docker inspection 获得的 Application/operator exact image ID。
|
||||
|
||||
evidence 的 subject 同时绑定版本、源码、架构和两个不同镜像 ID;九个 gate 保持 exact closed set。bundle create 与 offline audit 都重新匹配 evidence subject 和 manifest,不允许把另一架构、另一镜像或另一 run 的 evidence 复制进来。
|
||||
|
||||
GitHub workflow 只能在 Trivy、inventory、SBOM、128 MiB entrypoint、fresh lifecycle、完整 Trial Kit journey 与原生 cancellation 均成功之后记录 evidence;静态审计固定 `journey → cancellation → record-verification → create → audit → upload` 顺序。
|
||||
|
||||
### 3. workflow provenance 不是公开发布签名
|
||||
|
||||
evidence 提供可定位、可交叉检查的 GitHub run identity,不声称调用者无法伪造 JSON。下载者仍需到 GitHub 检查该 run 的 source、attempt 和结论。公开 Release Set 继续使用独立的 immutable digest、Cosign 与 GitHub attestation,不以 Alpha evidence 替代供应链签名。
|
||||
|
||||
普通 push/PR 不生成或上传 Alpha artifact;本地构建、push CI 通过或 source-equivalent native gate 只能支持工程候选判断。只有显式 milestone dispatch 产生的 exact-image evidence 才允许 materializer 声称 Local Alpha Trial Kit。
|
||||
|
||||
## 被拒绝的替代方案
|
||||
|
||||
### 保留 manifest 内的固定 `passed` 字段
|
||||
|
||||
拒绝。它把“脚本知道有哪些 gate”错误提升为“这些 gate 已对当前 artifact 执行”。
|
||||
|
||||
### 仅加入 GitHub run URL,不绑定镜像 ID
|
||||
|
||||
拒绝。同一源码可以因基础 package、构建平台或配置产生不同镜像;source-level run 不能自动证明另一个本地 archive 的 exact bytes。
|
||||
|
||||
### 在 Alpha 阶段实现第二套签名系统
|
||||
|
||||
拒绝。正式 release workflow 已承担签名和 attestation;Alpha 需要的是诚实、可定位的阶段证据,不应复制生产发布 authority。
|
||||
|
||||
## 影响
|
||||
|
||||
- 本地无 evidence 的 archive 不再冒充用户 Alpha,只能按工程候选处理;
|
||||
- 手动 milestone artifact 多一个小型 JSON 文件,不增加 Docker archive layer、workspace package、设备常驻 RSS、端口或依赖;
|
||||
- 下载者可从 bundle 确定 exact GitHub run/attempt,并验证 evidence 与 image ID/source/architecture 一致;
|
||||
- 旧的本地 `2620be05` 与 `4239464a` v1 bundle 保留为历史工程证据,但不能通过 v2 auditor。
|
||||
|
||||
## 验证
|
||||
|
||||
- 聚焦测试覆盖 v2 七文件正常物化、非 milestone provenance、detached workflow、archive/SBOM/evidence mutation、额外文件和闭合 CLI grammar;
|
||||
- Local operator workflow audit 要求 GitHub workflow identity contexts、evidence input和严格 gate 顺序;
|
||||
- `record-verification` 和 `create` 都从 image inspection 绑定两个不同 image ID;offline audit 不调用 Docker或网络;
|
||||
- 首个真实可下载 v2 双架构 Trial Kit 仍需维护者授权的 `produce_alpha_artifacts=true` workflow 生成。
|
||||
@@ -509,6 +509,7 @@
|
||||
| [ADR-0503](./ADR-0503-downloadable-local-alpha-trial-kit.md) | 可下载的 Local Alpha 试运行套件 | Proposed(原生 Linux 双架构已通过;实际可下载归档待维护者授权) |
|
||||
| [ADR-0504](./ADR-0504-canonical-local-alpha-trial-kit-materialization.md) | Local Alpha Trial Kit 单一物化与离线审计 | Accepted |
|
||||
| [ADR-0505](./ADR-0505-pinned-alpine-openssl-runtime-security-patch.md) | 固定 Alpine OpenSSL 运行时安全补丁 | Accepted |
|
||||
| [ADR-0506](./ADR-0506-source-bound-local-alpha-verification-evidence.md) | 源码绑定的 Local Alpha 验证证据 | Accepted |
|
||||
|
||||
## 规则
|
||||
|
||||
|
||||
@@ -13,28 +13,18 @@
|
||||
|
||||
只有 `Local Alpha Trial Kit` 可以称为本阶段“用户可试运行产物”。单个 headless runtime 和 Cluster archive 都只是工程候选;后者还不满足正式 Kubernetes deployment-lock 的 GHCR immutable digest 与 catalog provenance。
|
||||
|
||||
## 当前阶段实物(2026-08-26)
|
||||
## 当前阶段实物(2026-08-27)
|
||||
|
||||
当前已经存在一份与 `3.0.0-alpha.2` 源码身份一致、owner-private、可重新加载的 Local arm64 runtime engineering candidate,而不是只有源码或 Dockerfile:
|
||||
提交 `4239464af6937d56528a0a2c573d12329bc7ca55` 已形成最新 owner-private arm64 工程候选:
|
||||
|
||||
- source revision:`e3c05862b8c2690d69f58b098cdc128a09c83f97`;
|
||||
- image:`qinglong3-local-application:alpha2-e3c05862-arm64`,image ID `sha256:dfce2cc9d70044d75f72f2cc3075e1f24569fb9fe279d9c25a45698c19c3bde9`;
|
||||
- archive SHA-256:`01afb30cbe0c21f980ca083ad98fd316e659941f940dd8930ffd9ccfa7153edf`;
|
||||
- 工作区目录 `.tmp/ql3-alpha-e3c05862b8c2690d69f58b098cdc128a09c83f97-local-arm64/` 包含 `manifest.json`、`verification-evidence.json`、release-candidate contract、CycloneDX 1.5 SBOM、`README.md` 与 `SHA256SUMS`;全套 checksum、`docker load` 后身份和资源约束 smoke 已复验;
|
||||
- HIGH/CRITICAL OS vulnerability 为 0;128 MiB、0.5 CPU、read-only、no-network、drop-all 下的 Edge/Standalone fresh lifecycle、graceful stop 与 SQLite integrity 已通过;本机 Edge 首次运行曾在 Docker Desktop 文件桥上出现一次 startup receipt 发布瞬态,精确重跑通过,未将首次失败隐藏为成功;
|
||||
- 原生 Linux arm64 Local image job `97986754052` 已覆盖 Docker Desktop 无法等价证明的 Local API cancellation。完整 CI run `32903679764` attempt 2 为 40/40,独立 Kubernetes deployment run `32903679644` 与三节点 Security Administration run `32903679570` 同源通过。首轮 CI 的两项 `pnpm/action-setup` 内部 DNS 失败和一次 PostgreSQL 18 x64 scheduler 并发断言均在 failed-only rerun 收敛。
|
||||
- Application image ID `sha256:0d1d4b80ee46e9bb671d846f93d9a6d832c9856a91eed03f299055904da88a50`,operator image ID `sha256:b9122f481b1ba60d7eee9a3ed5ca57c9c141cbc389e7c7dbe19c6f6b1c98b49e`;
|
||||
- 单一双镜像 archive 为 184,648,192 bytes,SHA-256 `145544c4a753192821bfbbb92000bb64af5978db57181595c9ffa9f404c1fd72`;
|
||||
- checksum、旧 v1 离线内容审计、archive reload、实际 package inventory/SBOM 对账和 128 MiB 无网络只读入口 smoke 均通过;
|
||||
- 同提交远端主 CI run `32990652047` 为 40/40,原生 Linux amd64/arm64 均通过 Application/operator Trivy、fresh Edge/Standalone、完整 Trial Kit journey 和 Local API cancellation;Kubernetes deployment run `32990652416` 与三节点 Security Administration run `32990653482` 同源通过。
|
||||
|
||||
该实物保存在工作区忽略目录,不进入 Git,也尚未上传 GitHub。公开下载仍需维护者明确授权上传。它只含 headless Application,没有可下载的 `ql3 setup/owner/task/...` 管理制品;因此它足以证明 runtime 工程可用性,但不能独立完成部署用户旅程。此前“单架构内部试运行材料”的表述按 D-408 收紧为“运行时工程候选”。
|
||||
该本地 archive 不是新的 v2 Local Alpha Trial Kit。它在 ADR-0506 前生成,manifest v1 会无条件写入 `passed`,且 macOS Docker Desktop 因 bind-mount UID 映射无法对 exact 本地 archive 完成 Owner pepper 旅程;原生 CI 证明同源码实现,不自动证明另一个 archive 的 exact image bytes。它因此保留为工程候选,不冒充已获 workflow evidence 的用户 Alpha。
|
||||
|
||||
ADR-0503 已增加独立的 `qinglong3-local-operator`:它复用现有统一 `ql3` CLI,每次执行一个 command-file 命令后退出,不进入常驻 Application。提交 `2253b99066e0c221e11dc01384f496ec2a50e4bd` 的原生 Linux amd64/arm64 已同时通过 fresh setup、首 Owner ceremony、Application active/stop 和 SQLite integrity,CI run `32918632202` 为 40/40。ADR-0504 又把一次 `docker image save`、manifest、SBOM、README、`SHA256SUMS` 和离线审计收敛为同一个 materializer;下一项未完成的外部里程碑是维护者授权生成并保留两个可下载 archive。
|
||||
|
||||
提交 `2620be0587c29c2384e7f587c490dc11e357dfc8` 已通过该 materializer 生成新的本地私有 arm64 Trial Kit,位于 `.tmp/ql3-alpha-2620be0587c29c2384e7f587c490dc11e357dfc8-local-arm64/`:
|
||||
|
||||
- 单一双镜像 archive 为 178,765,312 bytes,SHA-256 `7456202efb252e665d658664d69693cfbc170e02ec19923302d5c354bcaa140f`;
|
||||
- Application image ID 为 `sha256:88c3027609c5f18a15111cb5820e34191d758ac6b4a41fc46d4a6bdf41fd71dd`,operator image ID 为 `sha256:529b86b85e18d6bd4ec8644d9da82d49ea45902534da27a99ee65ad4058e513b`;
|
||||
- 两份 CycloneDX SBOM 均与镜像内实际 package inventory 对账为 `inventoryVerified=true`;闭合目录离线审计、`SHA256SUMS`、archive reload 及 128 MiB 无网络只读 entrypoint smoke 全部通过。
|
||||
|
||||
这证明当前提交已经存在可重复生成和离线复核的单架构完整 Trial Kit 实物,但它仍只在维护者工作区,不等于 amd64/arm64 两份可下载 GitHub artifact。
|
||||
ADR-0506 现要求 `qinglong/alpha-local-trial-kit@v2` 额外包含 `verification-evidence.json`,绑定显式 `workflow_dispatch` 的 source、workflow SHA/ref、run/attempt、架构和两个 image ID。旧 `e3c05862` runtime-only archive、`2620be05` v1 Trial Kit 与 `4239464a` v1 archive 均为历史工程证据,不能通过 v2 auditor。下一项外部里程碑仍是维护者授权 `produce_alpha_artifacts=true`,由同一次原生 milestone job 生成 exact-image evidence 和双架构可下载 archive。
|
||||
|
||||
## 生成
|
||||
|
||||
@@ -48,7 +38,8 @@ ADR-0503 已增加独立的 `qinglong3-local-operator`:它复用现有统一 `
|
||||
Local artifact 含:
|
||||
|
||||
- 一个包含 Application 与短生命周期 operator 的 `qinglong3-local-trial-kit-<arch>.docker.tar`;共享 Node 基础层在 archive 中去重;
|
||||
- schema 为 `qinglong/alpha-local-trial-kit@v1` 的 `manifest.json`,通过 `archive/images/sboms/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID、文件长度/SHA-256 与已通过 gate;
|
||||
- schema 为 `qinglong/alpha-local-trial-kit@v2` 的 `manifest.json`,通过 `archive/images/sboms/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256;
|
||||
- `verification-evidence.json` 绑定 `workflow_dispatch` 的 workflow ref/SHA、run ID/attempt、同架构两个 exact image ID 和完整 gate 集;下载者仍须到 GitHub 交叉检查 run,它不替代正式签名;
|
||||
- 与实际只读镜像 inventory 对账过的 CycloneDX SBOM;
|
||||
- 面向 Local 用户的 README 与覆盖全部内容文件的 `SHA256SUMS`。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# QingLong 3.0 Local Alpha Trial Kit
|
||||
|
||||
本目录是绑定一个 QingLong 3.0 源码提交和一个 Linux 架构的阶段试运行套件,不是公开 release 或生产升级承诺。它同时包含常驻 Application 镜像和短生命周期 operator 镜像;两者共享的 OCI layer 只在同一个 Docker archive 中保存一次。
|
||||
本目录是绑定一个 QingLong 3.0 源码提交、一个 Linux 架构和一次显式 GitHub milestone run 的阶段试运行套件,不是公开 release 或生产升级承诺。它同时包含常驻 Application 镜像和短生命周期 operator 镜像;两者共享的 OCI layer 只在同一个 Docker archive 中保存一次。
|
||||
|
||||
## 适用范围
|
||||
|
||||
@@ -21,11 +21,13 @@ sha256sum --check SHA256SUMS
|
||||
|
||||
`manifest.json` 必须满足:
|
||||
|
||||
- `schema` 为 `qinglong/alpha-local-trial-kit@v1`;
|
||||
- `schema` 为 `qinglong/alpha-local-trial-kit@v2`;
|
||||
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
||||
- `architecture` 与主机相同;
|
||||
- `maturity` 为 `alpha_candidate_not_public_release`。
|
||||
|
||||
`manifest.json.verification` 必须指向同目录的 `verification-evidence.json`。该 evidence 的 subject 必须与 manifest 中的版本、源码、架构和两个 image ID 完全一致;workflow 必须是 `whyour/qinglong` 的 `ql3-ci.yml@refs/heads/next`、`workflow_dispatch`、`local-image`。使用 `workflow.runId` 和 `workflow.runAttempt` 打开对应 GitHub Actions run,确认 source 和结论;JSON provenance 是可交叉检查的阶段证据,不是 Cosign/GitHub attestation。
|
||||
|
||||
如果同时持有 QingLong 源码和 Node.js 24,可执行严格的闭合文件集、manifest、SBOM 和 checksum 审计:
|
||||
|
||||
```sh
|
||||
@@ -62,7 +64,7 @@ docker run --rm --read-only --network none --cap-drop ALL \
|
||||
|
||||
## Fresh 试运行边界
|
||||
|
||||
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drain 和 SQLite integrity 已在同一架构的原生 Linux CI 中验证。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
||||
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
||||
|
||||
Edge 的验证上限为 Application 128 MiB、0.5 CPU、64 PID;Standalone 为 256 MiB、0.5 CPU、256 PID;operator 为 128 MiB、0.5 CPU、32 PID。这里的数值是试运行门,不是所有 workload 的容量承诺。
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ const { auditClusterImageSbom } = require('./ql3-cluster-image-sbom.cjs');
|
||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||
|
||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||
const SCHEMA = 'qinglong/alpha-local-trial-kit@v1';
|
||||
const SCHEMA = 'qinglong/alpha-local-trial-kit@v2';
|
||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v1';
|
||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||
const ARCHIVE_MIN_BYTES = 1024;
|
||||
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
||||
@@ -20,6 +21,7 @@ const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
|
||||
const FILES = Object.freeze({
|
||||
applicationSbom: 'qinglong3-local-application.cdx.json',
|
||||
operatorSbom: 'qinglong3-local-operator.cdx.json',
|
||||
verificationEvidence: 'verification-evidence.json',
|
||||
readme: 'README.md',
|
||||
manifest: 'manifest.json',
|
||||
checksums: 'SHA256SUMS',
|
||||
@@ -35,6 +37,14 @@ const VERIFICATION = Object.freeze({
|
||||
standaloneFreshLifecycle: 'passed',
|
||||
localApiCancellation: 'passed',
|
||||
});
|
||||
const WORKFLOW_IDENTITY = Object.freeze({
|
||||
repository: 'whyour/qinglong',
|
||||
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||
event: 'workflow_dispatch',
|
||||
job: 'local-image',
|
||||
});
|
||||
const DECIMAL_ID_PATTERN = /^[1-9][0-9]{0,19}$/u;
|
||||
const ATTEMPT_PATTERN = /^[1-9][0-9]{0,5}$/u;
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(message);
|
||||
@@ -230,6 +240,145 @@ function validateOfflineSbom(document, profile, version) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateVerificationEvidence(document, expected) {
|
||||
if (
|
||||
!exactKeys(document, [
|
||||
'schemaVersion',
|
||||
'schema',
|
||||
'subject',
|
||||
'workflow',
|
||||
'gates',
|
||||
]) ||
|
||||
document.schemaVersion !== 1 ||
|
||||
document.schema !== VERIFICATION_SCHEMA ||
|
||||
!exactKeys(document.subject, [
|
||||
'version',
|
||||
'sourceRevision',
|
||||
'architecture',
|
||||
'applicationImageId',
|
||||
'operatorImageId',
|
||||
]) ||
|
||||
document.subject.version !== expected.version ||
|
||||
document.subject.sourceRevision !== expected.sourceRevision ||
|
||||
document.subject.architecture !== expected.architecture ||
|
||||
document.subject.applicationImageId !== expected.applicationImageId ||
|
||||
document.subject.operatorImageId !== expected.operatorImageId ||
|
||||
document.subject.applicationImageId === document.subject.operatorImageId ||
|
||||
!exactKeys(document.workflow, [
|
||||
'repository',
|
||||
'workflowRef',
|
||||
'workflowSha',
|
||||
'event',
|
||||
'job',
|
||||
'runId',
|
||||
'runAttempt',
|
||||
]) ||
|
||||
document.workflow.repository !== WORKFLOW_IDENTITY.repository ||
|
||||
document.workflow.workflowRef !== WORKFLOW_IDENTITY.workflowRef ||
|
||||
document.workflow.workflowSha !== expected.sourceRevision ||
|
||||
document.workflow.event !== WORKFLOW_IDENTITY.event ||
|
||||
document.workflow.job !== WORKFLOW_IDENTITY.job ||
|
||||
!DECIMAL_ID_PATTERN.test(document.workflow.runId || '') ||
|
||||
!ATTEMPT_PATTERN.test(document.workflow.runAttempt || '') ||
|
||||
!exactKeys(document.gates, Object.keys(VERIFICATION)) ||
|
||||
JSON.stringify(document.gates) !== JSON.stringify(VERIFICATION)
|
||||
) {
|
||||
fail('trial kit verification evidence is incompatible');
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
function validateVerificationOptions(options) {
|
||||
const root = fs.realpathSync(path.resolve(options.root || DEFAULT_ROOT));
|
||||
const output = path.resolve(options.output || '');
|
||||
const parent = path.dirname(output);
|
||||
if (
|
||||
!ARCHITECTURES.includes(options.architecture) ||
|
||||
!REVISION_PATTERN.test(options.sourceRevision || '') ||
|
||||
!path.isAbsolute(output) ||
|
||||
fs.existsSync(output) ||
|
||||
fs.realpathSync(parent) !== parent ||
|
||||
options.repository !== WORKFLOW_IDENTITY.repository ||
|
||||
options.workflowRef !== WORKFLOW_IDENTITY.workflowRef ||
|
||||
options.workflowSha !== options.sourceRevision ||
|
||||
options.eventName !== WORKFLOW_IDENTITY.event ||
|
||||
options.job !== WORKFLOW_IDENTITY.job ||
|
||||
!DECIMAL_ID_PATTERN.test(options.runId || '') ||
|
||||
!ATTEMPT_PATTERN.test(options.runAttempt || '')
|
||||
) {
|
||||
fail('verification evidence identity or output is invalid');
|
||||
}
|
||||
return {
|
||||
root,
|
||||
output,
|
||||
architecture: options.architecture,
|
||||
sourceRevision: options.sourceRevision,
|
||||
applicationImage: validateImageReference(
|
||||
options.applicationImage,
|
||||
'application',
|
||||
),
|
||||
operatorImage: validateImageReference(options.operatorImage, 'operator'),
|
||||
repository: options.repository,
|
||||
workflowRef: options.workflowRef,
|
||||
workflowSha: options.workflowSha,
|
||||
eventName: options.eventName,
|
||||
job: options.job,
|
||||
runId: options.runId,
|
||||
runAttempt: options.runAttempt,
|
||||
};
|
||||
}
|
||||
|
||||
function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
||||
const normalized = validateVerificationOptions(options);
|
||||
const release = readReleaseIdentity(normalized.root);
|
||||
const inspectImage = adapters.inspectImage || inspectDockerImage;
|
||||
const application = normalizeImageInspection(
|
||||
inspectImage(normalized.applicationImage),
|
||||
{
|
||||
architecture: normalized.architecture,
|
||||
reference: normalized.applicationImage,
|
||||
revision: normalized.sourceRevision,
|
||||
role: 'application',
|
||||
version: release.version,
|
||||
},
|
||||
);
|
||||
const operator = normalizeImageInspection(
|
||||
inspectImage(normalized.operatorImage),
|
||||
{
|
||||
architecture: normalized.architecture,
|
||||
reference: normalized.operatorImage,
|
||||
revision: normalized.sourceRevision,
|
||||
role: 'operator',
|
||||
version: release.version,
|
||||
},
|
||||
);
|
||||
if (application.id === operator.id) fail('trial kit images must be distinct');
|
||||
const evidence = {
|
||||
schemaVersion: 1,
|
||||
schema: VERIFICATION_SCHEMA,
|
||||
subject: {
|
||||
version: release.version,
|
||||
sourceRevision: normalized.sourceRevision,
|
||||
architecture: normalized.architecture,
|
||||
applicationImageId: application.id,
|
||||
operatorImageId: operator.id,
|
||||
},
|
||||
workflow: {
|
||||
repository: normalized.repository,
|
||||
workflowRef: normalized.workflowRef,
|
||||
workflowSha: normalized.workflowSha,
|
||||
event: normalized.eventName,
|
||||
job: normalized.job,
|
||||
runId: normalized.runId,
|
||||
runAttempt: normalized.runAttempt,
|
||||
},
|
||||
gates: { ...VERIFICATION },
|
||||
};
|
||||
validateVerificationEvidence(evidence, evidence.subject);
|
||||
writeExclusive(normalized.output, `${JSON.stringify(evidence, null, 2)}\n`);
|
||||
return evidence;
|
||||
}
|
||||
|
||||
function archiveName(architecture) {
|
||||
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
||||
}
|
||||
@@ -288,6 +437,11 @@ function validateCreateOptions(options) {
|
||||
MAX_JSON_BYTES,
|
||||
'operator SBOM',
|
||||
),
|
||||
verificationEvidence: assertCanonicalFile(
|
||||
options.verificationEvidence,
|
||||
MAX_JSON_BYTES,
|
||||
'trial kit verification evidence',
|
||||
),
|
||||
readme: assertCanonicalFile(
|
||||
options.readme,
|
||||
MAX_README_BYTES,
|
||||
@@ -309,6 +463,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
normalized.operatorSbom,
|
||||
'operator SBOM',
|
||||
);
|
||||
const verificationEvidence = readBoundedJson(
|
||||
normalized.verificationEvidence,
|
||||
'trial kit verification evidence',
|
||||
);
|
||||
validateSbom(applicationSbom, {
|
||||
root: normalized.root,
|
||||
profile: 'local',
|
||||
@@ -340,6 +498,13 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
},
|
||||
);
|
||||
if (application.id === operator.id) fail('trial kit images must be distinct');
|
||||
validateVerificationEvidence(verificationEvidence, {
|
||||
version: release.version,
|
||||
sourceRevision: normalized.sourceRevision,
|
||||
architecture: normalized.architecture,
|
||||
applicationImageId: application.id,
|
||||
operatorImageId: operator.id,
|
||||
});
|
||||
|
||||
let created = false;
|
||||
try {
|
||||
@@ -368,12 +533,16 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
normalized.operatorSbom,
|
||||
path.join(normalized.outputRoot, FILES.operatorSbom),
|
||||
);
|
||||
copyExclusive(
|
||||
normalized.verificationEvidence,
|
||||
path.join(normalized.outputRoot, FILES.verificationEvidence),
|
||||
);
|
||||
copyExclusive(
|
||||
normalized.readme,
|
||||
path.join(normalized.outputRoot, FILES.readme),
|
||||
);
|
||||
const manifest = {
|
||||
schemaVersion: 2,
|
||||
schemaVersion: 3,
|
||||
schema: SCHEMA,
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
@@ -387,7 +556,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
|
||||
},
|
||||
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
||||
verification: { ...VERIFICATION },
|
||||
verification: fileRecord(
|
||||
normalized.outputRoot,
|
||||
FILES.verificationEvidence,
|
||||
),
|
||||
};
|
||||
writeExclusive(
|
||||
path.join(normalized.outputRoot, FILES.manifest),
|
||||
@@ -397,6 +569,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
archive,
|
||||
FILES.applicationSbom,
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
FILES.readme,
|
||||
FILES.manifest,
|
||||
];
|
||||
@@ -467,7 +640,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
'readme',
|
||||
'verification',
|
||||
]) ||
|
||||
manifest.schemaVersion !== 2 ||
|
||||
manifest.schemaVersion !== 3 ||
|
||||
manifest.schema !== SCHEMA ||
|
||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||
manifest.product !== 'local' ||
|
||||
@@ -475,9 +648,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
|
||||
!ARCHITECTURES.includes(manifest.architecture) ||
|
||||
!exactKeys(manifest.images, ['application', 'operator']) ||
|
||||
!exactKeys(manifest.sboms, ['application', 'operator']) ||
|
||||
!exactKeys(manifest.verification, Object.keys(VERIFICATION)) ||
|
||||
JSON.stringify(manifest.verification) !== JSON.stringify(VERIFICATION)
|
||||
!exactKeys(manifest.sboms, ['application', 'operator'])
|
||||
) {
|
||||
fail('trial kit manifest identity or shape is incompatible');
|
||||
}
|
||||
@@ -497,6 +668,11 @@ function auditLocalAlphaTrialKit(options) {
|
||||
bundleRoot,
|
||||
);
|
||||
validateFileRecord(manifest.sboms.operator, FILES.operatorSbom, bundleRoot);
|
||||
validateFileRecord(
|
||||
manifest.verification,
|
||||
FILES.verificationEvidence,
|
||||
bundleRoot,
|
||||
);
|
||||
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
||||
validateOfflineSbom(
|
||||
readBoundedJson(
|
||||
@@ -511,12 +687,26 @@ function auditLocalAlphaTrialKit(options) {
|
||||
'local-operator',
|
||||
manifest.version,
|
||||
);
|
||||
const verificationEvidence = validateVerificationEvidence(
|
||||
readBoundedJson(
|
||||
path.join(bundleRoot, FILES.verificationEvidence),
|
||||
'trial kit verification evidence',
|
||||
),
|
||||
{
|
||||
version: manifest.version,
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
architecture: manifest.architecture,
|
||||
applicationImageId: manifest.images.application.id,
|
||||
operatorImageId: manifest.images.operator.id,
|
||||
},
|
||||
);
|
||||
const expectedFiles = [
|
||||
FILES.checksums,
|
||||
FILES.manifest,
|
||||
FILES.readme,
|
||||
FILES.applicationSbom,
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
expectedArchive,
|
||||
].sort();
|
||||
const actualFiles = fs
|
||||
@@ -535,6 +725,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
expectedArchive,
|
||||
FILES.applicationSbom,
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
FILES.readme,
|
||||
FILES.manifest,
|
||||
];
|
||||
@@ -555,6 +746,9 @@ function auditLocalAlphaTrialKit(options) {
|
||||
archiveSha256: manifest.archive.sha256,
|
||||
applicationImageId: manifest.images.application.id,
|
||||
operatorImageId: manifest.images.operator.id,
|
||||
verificationSha256: manifest.verification.sha256,
|
||||
workflowRunId: verificationEvidence.workflow.runId,
|
||||
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
|
||||
compatible: true,
|
||||
});
|
||||
}
|
||||
@@ -576,6 +770,43 @@ function parseArguments(argv) {
|
||||
}
|
||||
return { mode: 'audit', bundleRoot: path.resolve(values.bundle) };
|
||||
}
|
||||
if (values.mode === 'record-verification') {
|
||||
const expected = [
|
||||
'application-image',
|
||||
'architecture',
|
||||
'event',
|
||||
'job',
|
||||
'mode',
|
||||
'operator-image',
|
||||
'output',
|
||||
'repository',
|
||||
'run-attempt',
|
||||
'run-id',
|
||||
'source-revision',
|
||||
'workflow-ref',
|
||||
'workflow-sha',
|
||||
];
|
||||
if (
|
||||
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
|
||||
) {
|
||||
fail('record-verification arguments are invalid');
|
||||
}
|
||||
return {
|
||||
mode: 'record-verification',
|
||||
output: path.resolve(values.output),
|
||||
architecture: values.architecture,
|
||||
sourceRevision: values['source-revision'],
|
||||
applicationImage: values['application-image'],
|
||||
operatorImage: values['operator-image'],
|
||||
repository: values.repository,
|
||||
workflowRef: values['workflow-ref'],
|
||||
workflowSha: values['workflow-sha'],
|
||||
eventName: values.event,
|
||||
job: values.job,
|
||||
runId: values['run-id'],
|
||||
runAttempt: values['run-attempt'],
|
||||
};
|
||||
}
|
||||
if (values.mode === 'create') {
|
||||
const expected = [
|
||||
'application-image',
|
||||
@@ -587,6 +818,7 @@ function parseArguments(argv) {
|
||||
'output',
|
||||
'readme',
|
||||
'source-revision',
|
||||
'verification-evidence',
|
||||
];
|
||||
if (
|
||||
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
|
||||
@@ -602,6 +834,7 @@ function parseArguments(argv) {
|
||||
operatorImage: values['operator-image'],
|
||||
applicationSbom: path.resolve(values['application-sbom']),
|
||||
operatorSbom: path.resolve(values['operator-sbom']),
|
||||
verificationEvidence: path.resolve(values['verification-evidence']),
|
||||
readme: path.resolve(values.readme),
|
||||
};
|
||||
}
|
||||
@@ -610,10 +843,14 @@ function parseArguments(argv) {
|
||||
|
||||
function runCli(argv) {
|
||||
const options = parseArguments(argv);
|
||||
const report =
|
||||
options.mode === 'create'
|
||||
? createLocalAlphaTrialKit(options)
|
||||
: auditLocalAlphaTrialKit(options);
|
||||
let report;
|
||||
if (options.mode === 'record-verification') {
|
||||
report = createLocalAlphaTrialKitVerificationEvidence(options);
|
||||
} else if (options.mode === 'create') {
|
||||
report = createLocalAlphaTrialKit(options);
|
||||
} else {
|
||||
report = auditLocalAlphaTrialKit(options);
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
return report;
|
||||
}
|
||||
@@ -633,9 +870,11 @@ module.exports = Object.freeze({
|
||||
FILES,
|
||||
SCHEMA,
|
||||
VERIFICATION,
|
||||
VERIFICATION_SCHEMA,
|
||||
archiveName,
|
||||
auditLocalAlphaTrialKit,
|
||||
createLocalAlphaTrialKit,
|
||||
createLocalAlphaTrialKitVerificationEvidence,
|
||||
parseArguments,
|
||||
runCli,
|
||||
sha256File,
|
||||
|
||||
@@ -196,16 +196,41 @@ function auditWorkflow(contents, findings) {
|
||||
'"${OPERATOR_IMAGE}" --version',
|
||||
'scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
|
||||
'scripts/ql3-local-alpha-trial-kit-bundle.cjs',
|
||||
'--mode=record-verification',
|
||||
'--mode=create',
|
||||
'--mode=audit',
|
||||
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
|
||||
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
|
||||
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
|
||||
'--readme=docs/operations/ql3-local-alpha-trial-kit.md',
|
||||
'--repository=${{ github.repository }}',
|
||||
'--workflow-ref="${{ github.workflow_ref }}"',
|
||||
'--workflow-sha=${{ github.workflow_sha }}',
|
||||
'--event=${{ github.event_name }}',
|
||||
'--job=${{ github.job }}',
|
||||
'--run-id=${{ github.run_id }}',
|
||||
'--run-attempt=${{ github.run_attempt }}',
|
||||
];
|
||||
for (const value of required) {
|
||||
if (!contents.includes(value))
|
||||
finding(findings, 'LOCAL_OPERATOR_CI_CONTRACT_DRIFT', value);
|
||||
}
|
||||
let cursor = -1;
|
||||
for (const value of [
|
||||
'name: Run the downloadable Local Alpha trial kit journey',
|
||||
'name: Run authenticated Local API cancellation through real Linux processes',
|
||||
'--mode=record-verification',
|
||||
'--mode=create',
|
||||
'--mode=audit',
|
||||
'name: Upload the tested native Local Alpha trial kit',
|
||||
]) {
|
||||
const index = contents.indexOf(value, cursor + 1);
|
||||
if (index <= cursor) {
|
||||
finding(findings, 'LOCAL_OPERATOR_CI_GATE_ORDER_DRIFT', value);
|
||||
break;
|
||||
}
|
||||
cursor = index;
|
||||
}
|
||||
}
|
||||
|
||||
function auditLocalOperatorImageContract(root) {
|
||||
|
||||
@@ -8,6 +8,7 @@ const test = require('node:test');
|
||||
const {
|
||||
auditLocalAlphaTrialKit,
|
||||
createLocalAlphaTrialKit,
|
||||
createLocalAlphaTrialKitVerificationEvidence,
|
||||
parseArguments,
|
||||
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
|
||||
const {
|
||||
@@ -61,6 +62,10 @@ function fixture(t) {
|
||||
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
|
||||
const applicationSbom = path.join(fixtureRoot, 'application.json');
|
||||
const operatorSbom = path.join(fixtureRoot, 'operator.json');
|
||||
const verificationEvidence = path.join(
|
||||
fixtureRoot,
|
||||
'verification-evidence-source.json',
|
||||
);
|
||||
const readme = path.join(fixtureRoot, 'README-source.md');
|
||||
fs.writeFileSync(
|
||||
applicationSbom,
|
||||
@@ -73,13 +78,38 @@ function fixture(t) {
|
||||
)}\n`,
|
||||
);
|
||||
fs.writeFileSync(readme, '# Local Alpha Trial Kit\n');
|
||||
return {
|
||||
const paths = {
|
||||
fixtureRoot,
|
||||
applicationSbom,
|
||||
operatorSbom,
|
||||
verificationEvidence,
|
||||
readme,
|
||||
outputRoot: path.join(fixtureRoot, 'bundle'),
|
||||
};
|
||||
createLocalAlphaTrialKitVerificationEvidence(
|
||||
verificationOptions(paths),
|
||||
adapters(),
|
||||
);
|
||||
return paths;
|
||||
}
|
||||
|
||||
function verificationOptions(paths, overrides = {}) {
|
||||
return {
|
||||
root,
|
||||
output: paths.verificationEvidence,
|
||||
architecture: 'arm64',
|
||||
sourceRevision: revision,
|
||||
applicationImage: 'qinglong3-local-application:test-arm64',
|
||||
operatorImage: 'qinglong3-local-operator:test-arm64',
|
||||
repository: 'whyour/qinglong',
|
||||
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||
workflowSha: revision,
|
||||
eventName: 'workflow_dispatch',
|
||||
job: 'local-image',
|
||||
runId: '32990652047',
|
||||
runAttempt: '1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createOptions(paths) {
|
||||
@@ -92,6 +122,7 @@ function createOptions(paths) {
|
||||
operatorImage: 'qinglong3-local-operator:test-arm64',
|
||||
applicationSbom: paths.applicationSbom,
|
||||
operatorSbom: paths.operatorSbom,
|
||||
verificationEvidence: paths.verificationEvidence,
|
||||
readme: paths.readme,
|
||||
};
|
||||
}
|
||||
@@ -117,15 +148,17 @@ function adapters(overrides = {}) {
|
||||
test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
const paths = fixture(t);
|
||||
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v1');
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v2');
|
||||
assert.equal(manifest.sourceRevision, revision);
|
||||
assert.equal(manifest.architecture, 'arm64');
|
||||
assert.equal(manifest.images.application.architecture, 'arm64');
|
||||
assert.equal(manifest.images.operator.architecture, 'arm64');
|
||||
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
|
||||
assert.equal(manifest.verification.file, 'verification-evidence.json');
|
||||
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
||||
assert.equal(report.compatible, true);
|
||||
assert.equal(report.sourceRevision, revision);
|
||||
assert.equal(report.workflowRunId, '32990652047');
|
||||
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
|
||||
'README.md',
|
||||
'SHA256SUMS',
|
||||
@@ -133,6 +166,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
'qinglong3-local-application.cdx.json',
|
||||
'qinglong3-local-operator.cdx.json',
|
||||
'qinglong3-local-trial-kit-arm64.docker.tar',
|
||||
'verification-evidence.json',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -159,8 +193,8 @@ test('fails closed and removes a partial output on incompatible image identity',
|
||||
assert.equal(fs.existsSync(paths.outputRoot), false);
|
||||
});
|
||||
|
||||
test('offline audit rejects archive mutation, extra files and SBOM substitution', (t) => {
|
||||
for (const mutation of ['archive', 'extra', 'sbom']) {
|
||||
test('offline audit rejects archive, file-set, SBOM and verification mutation', (t) => {
|
||||
for (const mutation of ['archive', 'extra', 'sbom', 'verification']) {
|
||||
const paths = fixture(t);
|
||||
paths.outputRoot = path.join(paths.fixtureRoot, `bundle-${mutation}`);
|
||||
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||
@@ -174,11 +208,16 @@ test('offline audit rejects archive mutation, extra files and SBOM substitution'
|
||||
);
|
||||
} else if (mutation === 'extra') {
|
||||
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
|
||||
} else {
|
||||
} else if (mutation === 'sbom') {
|
||||
fs.copyFileSync(
|
||||
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
|
||||
path.join(paths.outputRoot, 'qinglong3-local-operator.cdx.json'),
|
||||
);
|
||||
} else {
|
||||
fs.appendFileSync(
|
||||
path.join(paths.outputRoot, 'verification-evidence.json'),
|
||||
'tamper',
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() => auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot }),
|
||||
@@ -188,6 +227,34 @@ test('offline audit rejects archive mutation, extra files and SBOM substitution'
|
||||
}
|
||||
});
|
||||
|
||||
test('create rejects verification detached from the reviewed workflow', (t) => {
|
||||
const paths = fixture(t);
|
||||
const evidence = JSON.parse(
|
||||
fs.readFileSync(paths.verificationEvidence, 'utf8'),
|
||||
);
|
||||
evidence.workflow.job = 'unreviewed-job';
|
||||
fs.writeFileSync(paths.verificationEvidence, `${JSON.stringify(evidence)}\n`);
|
||||
assert.throws(
|
||||
() => createLocalAlphaTrialKit(createOptions(paths), adapters()),
|
||||
/verification evidence is incompatible/,
|
||||
);
|
||||
assert.equal(fs.existsSync(paths.outputRoot), false);
|
||||
});
|
||||
|
||||
test('verification recorder rejects non-milestone workflow provenance', (t) => {
|
||||
const paths = fixture(t);
|
||||
const output = path.join(paths.fixtureRoot, 'unreviewed-verification.json');
|
||||
assert.throws(
|
||||
() =>
|
||||
createLocalAlphaTrialKitVerificationEvidence(
|
||||
verificationOptions(paths, { output, eventName: 'push' }),
|
||||
adapters(),
|
||||
),
|
||||
/verification evidence identity or output is invalid/,
|
||||
);
|
||||
assert.equal(fs.existsSync(output), false);
|
||||
});
|
||||
|
||||
test('CLI grammar is exact and separates create from offline audit', () => {
|
||||
assert.deepEqual(
|
||||
parseArguments(['--mode=audit', '--bundle=/tmp/ql3-bundle']),
|
||||
@@ -206,4 +273,21 @@ test('CLI grammar is exact and separates create from offline audit', () => {
|
||||
() => parseArguments(['--mode=create', '--output=/tmp/output']),
|
||||
/create arguments are invalid/,
|
||||
);
|
||||
const recorded = parseArguments([
|
||||
'--mode=record-verification',
|
||||
'--application-image=qinglong3-local-application:test-arm64',
|
||||
'--operator-image=qinglong3-local-operator:test-arm64',
|
||||
'--architecture=arm64',
|
||||
`--source-revision=${revision}`,
|
||||
'--repository=whyour/qinglong',
|
||||
'--workflow-ref=whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||
`--workflow-sha=${revision}`,
|
||||
'--event=workflow_dispatch',
|
||||
'--job=local-image',
|
||||
'--run-id=32990652047',
|
||||
'--run-attempt=1',
|
||||
'--output=/tmp/verification-evidence.json',
|
||||
]);
|
||||
assert.equal(recorded.mode, 'record-verification');
|
||||
assert.equal(recorded.runId, '32990652047');
|
||||
});
|
||||
|
||||
@@ -124,3 +124,44 @@ test('rejects removal of the fresh Owner journey or two-image manifest', () => {
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects Alpha evidence recorded before all native gates', () => {
|
||||
const temporaryRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ql3-local-alpha-evidence-order-'),
|
||||
);
|
||||
try {
|
||||
fs.mkdirSync(path.join(temporaryRoot, 'deploy/containers'), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.cpSync(
|
||||
path.join(root, 'deploy/containers/ql3-local-operator'),
|
||||
path.join(temporaryRoot, 'deploy/containers/ql3-local-operator'),
|
||||
{ recursive: true },
|
||||
);
|
||||
fs.mkdirSync(path.join(temporaryRoot, '.github/workflows'), {
|
||||
recursive: true,
|
||||
});
|
||||
const workflow = fs
|
||||
.readFileSync(path.join(root, '.github/workflows/ql3-ci.yml'), 'utf8')
|
||||
.replace('--mode=record-verification', '--mode=temporary-marker')
|
||||
.replace('--mode=audit', '--mode=record-verification')
|
||||
.replace('--mode=temporary-marker', '--mode=audit');
|
||||
fs.writeFileSync(
|
||||
path.join(temporaryRoot, '.github/workflows/ql3-ci.yml'),
|
||||
workflow,
|
||||
);
|
||||
fs.copyFileSync(
|
||||
path.join(root, 'ql3-release.json'),
|
||||
path.join(temporaryRoot, 'ql3-release.json'),
|
||||
);
|
||||
const report = auditLocalOperatorImageContract(temporaryRoot);
|
||||
assert.equal(report.compatible, false);
|
||||
assert.ok(
|
||||
report.findings.some(
|
||||
({ code }) => code === 'LOCAL_OPERATOR_CI_GATE_ORDER_DRIFT',
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user