mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
fix(ql3): bind alpha artifacts to CI evidence
This commit is contained in:
@@ -591,6 +591,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
umask 077
|
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 \
|
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||||
--mode=create \
|
--mode=create \
|
||||||
--application-image="${IMAGE}" \
|
--application-image="${IMAGE}" \
|
||||||
@@ -599,6 +613,7 @@ jobs:
|
|||||||
--source-revision=${{ github.sha }} \
|
--source-revision=${{ github.sha }} \
|
||||||
--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json" \
|
--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json" \
|
||||||
--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.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 \
|
--readme=docs/operations/ql3-local-alpha-trial-kit.md \
|
||||||
--output="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
--output="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
||||||
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
- 目标版本:QingLong 3.x
|
- 目标版本:QingLong 3.x
|
||||||
- 作者:QingLong Maintainers
|
- 作者:QingLong Maintainers
|
||||||
- 创建日期:2026-07-17
|
- 创建日期:2026-07-17
|
||||||
- 最后更新:2026-08-26
|
- 最后更新:2026-08-27
|
||||||
- 讨论范围:架构与演进路线,不包含最终 UI 视觉方案
|
- 讨论范围:架构与演进路线,不包含最终 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 另行决策。
|
- 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-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-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-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。
|
只有 `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`;
|
- Application image ID `sha256:0d1d4b80ee46e9bb671d846f93d9a6d832c9856a91eed03f299055904da88a50`,operator image ID `sha256:b9122f481b1ba60d7eee9a3ed5ca57c9c141cbc389e7c7dbe19c6f6b1c98b49e`;
|
||||||
- image:`qinglong3-local-application:alpha2-e3c05862-arm64`,image ID `sha256:dfce2cc9d70044d75f72f2cc3075e1f24569fb9fe279d9c25a45698c19c3bde9`;
|
- 单一双镜像 archive 为 184,648,192 bytes,SHA-256 `145544c4a753192821bfbbb92000bb64af5978db57181595c9ffa9f404c1fd72`;
|
||||||
- archive SHA-256:`01afb30cbe0c21f980ca083ad98fd316e659941f940dd8930ffd9ccfa7153edf`;
|
- checksum、旧 v1 离线内容审计、archive reload、实际 package inventory/SBOM 对账和 128 MiB 无网络只读入口 smoke 均通过;
|
||||||
- 工作区目录 `.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 已复验;
|
- 同提交远端主 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` 同源通过。
|
||||||
- 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 收敛。
|
|
||||||
|
|
||||||
该实物保存在工作区忽略目录,不进入 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。
|
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。
|
||||||
|
|
||||||
提交 `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。
|
|
||||||
|
|
||||||
## 生成
|
## 生成
|
||||||
|
|
||||||
@@ -48,7 +38,8 @@ ADR-0503 已增加独立的 `qinglong3-local-operator`:它复用现有统一 `
|
|||||||
Local artifact 含:
|
Local artifact 含:
|
||||||
|
|
||||||
- 一个包含 Application 与短生命周期 operator 的 `qinglong3-local-trial-kit-<arch>.docker.tar`;共享 Node 基础层在 archive 中去重;
|
- 一个包含 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;
|
- 与实际只读镜像 inventory 对账过的 CycloneDX SBOM;
|
||||||
- 面向 Local 用户的 README 与覆盖全部内容文件的 `SHA256SUMS`。
|
- 面向 Local 用户的 README 与覆盖全部内容文件的 `SHA256SUMS`。
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# QingLong 3.0 Local Alpha Trial Kit
|
# 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` 必须满足:
|
`manifest.json` 必须满足:
|
||||||
|
|
||||||
- `schema` 为 `qinglong/alpha-local-trial-kit@v1`;
|
- `schema` 为 `qinglong/alpha-local-trial-kit@v2`;
|
||||||
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
||||||
- `architecture` 与主机相同;
|
- `architecture` 与主机相同;
|
||||||
- `maturity` 为 `alpha_candidate_not_public_release`。
|
- `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 审计:
|
如果同时持有 QingLong 源码和 Node.js 24,可执行严格的闭合文件集、manifest、SBOM 和 checksum 审计:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -62,7 +64,7 @@ docker run --rm --read-only --network none --cap-drop ALL \
|
|||||||
|
|
||||||
## Fresh 试运行边界
|
## 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 的容量承诺。
|
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 { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||||
|
|
||||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
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 ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||||
const ARCHIVE_MIN_BYTES = 1024;
|
const ARCHIVE_MIN_BYTES = 1024;
|
||||||
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
||||||
@@ -20,6 +21,7 @@ const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
|
|||||||
const FILES = Object.freeze({
|
const FILES = Object.freeze({
|
||||||
applicationSbom: 'qinglong3-local-application.cdx.json',
|
applicationSbom: 'qinglong3-local-application.cdx.json',
|
||||||
operatorSbom: 'qinglong3-local-operator.cdx.json',
|
operatorSbom: 'qinglong3-local-operator.cdx.json',
|
||||||
|
verificationEvidence: 'verification-evidence.json',
|
||||||
readme: 'README.md',
|
readme: 'README.md',
|
||||||
manifest: 'manifest.json',
|
manifest: 'manifest.json',
|
||||||
checksums: 'SHA256SUMS',
|
checksums: 'SHA256SUMS',
|
||||||
@@ -35,6 +37,14 @@ const VERIFICATION = Object.freeze({
|
|||||||
standaloneFreshLifecycle: 'passed',
|
standaloneFreshLifecycle: 'passed',
|
||||||
localApiCancellation: '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) {
|
function fail(message) {
|
||||||
throw new Error(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) {
|
function archiveName(architecture) {
|
||||||
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
||||||
}
|
}
|
||||||
@@ -288,6 +437,11 @@ function validateCreateOptions(options) {
|
|||||||
MAX_JSON_BYTES,
|
MAX_JSON_BYTES,
|
||||||
'operator SBOM',
|
'operator SBOM',
|
||||||
),
|
),
|
||||||
|
verificationEvidence: assertCanonicalFile(
|
||||||
|
options.verificationEvidence,
|
||||||
|
MAX_JSON_BYTES,
|
||||||
|
'trial kit verification evidence',
|
||||||
|
),
|
||||||
readme: assertCanonicalFile(
|
readme: assertCanonicalFile(
|
||||||
options.readme,
|
options.readme,
|
||||||
MAX_README_BYTES,
|
MAX_README_BYTES,
|
||||||
@@ -309,6 +463,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
normalized.operatorSbom,
|
normalized.operatorSbom,
|
||||||
'operator SBOM',
|
'operator SBOM',
|
||||||
);
|
);
|
||||||
|
const verificationEvidence = readBoundedJson(
|
||||||
|
normalized.verificationEvidence,
|
||||||
|
'trial kit verification evidence',
|
||||||
|
);
|
||||||
validateSbom(applicationSbom, {
|
validateSbom(applicationSbom, {
|
||||||
root: normalized.root,
|
root: normalized.root,
|
||||||
profile: 'local',
|
profile: 'local',
|
||||||
@@ -340,6 +498,13 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (application.id === operator.id) fail('trial kit images must be distinct');
|
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;
|
let created = false;
|
||||||
try {
|
try {
|
||||||
@@ -368,12 +533,16 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
normalized.operatorSbom,
|
normalized.operatorSbom,
|
||||||
path.join(normalized.outputRoot, FILES.operatorSbom),
|
path.join(normalized.outputRoot, FILES.operatorSbom),
|
||||||
);
|
);
|
||||||
|
copyExclusive(
|
||||||
|
normalized.verificationEvidence,
|
||||||
|
path.join(normalized.outputRoot, FILES.verificationEvidence),
|
||||||
|
);
|
||||||
copyExclusive(
|
copyExclusive(
|
||||||
normalized.readme,
|
normalized.readme,
|
||||||
path.join(normalized.outputRoot, FILES.readme),
|
path.join(normalized.outputRoot, FILES.readme),
|
||||||
);
|
);
|
||||||
const manifest = {
|
const manifest = {
|
||||||
schemaVersion: 2,
|
schemaVersion: 3,
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
maturity: 'alpha_candidate_not_public_release',
|
maturity: 'alpha_candidate_not_public_release',
|
||||||
product: 'local',
|
product: 'local',
|
||||||
@@ -387,7 +556,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
|
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
|
||||||
},
|
},
|
||||||
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
||||||
verification: { ...VERIFICATION },
|
verification: fileRecord(
|
||||||
|
normalized.outputRoot,
|
||||||
|
FILES.verificationEvidence,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
writeExclusive(
|
writeExclusive(
|
||||||
path.join(normalized.outputRoot, FILES.manifest),
|
path.join(normalized.outputRoot, FILES.manifest),
|
||||||
@@ -397,6 +569,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
archive,
|
archive,
|
||||||
FILES.applicationSbom,
|
FILES.applicationSbom,
|
||||||
FILES.operatorSbom,
|
FILES.operatorSbom,
|
||||||
|
FILES.verificationEvidence,
|
||||||
FILES.readme,
|
FILES.readme,
|
||||||
FILES.manifest,
|
FILES.manifest,
|
||||||
];
|
];
|
||||||
@@ -467,7 +640,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
'readme',
|
'readme',
|
||||||
'verification',
|
'verification',
|
||||||
]) ||
|
]) ||
|
||||||
manifest.schemaVersion !== 2 ||
|
manifest.schemaVersion !== 3 ||
|
||||||
manifest.schema !== SCHEMA ||
|
manifest.schema !== SCHEMA ||
|
||||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||||
manifest.product !== 'local' ||
|
manifest.product !== 'local' ||
|
||||||
@@ -475,9 +648,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
|
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
|
||||||
!ARCHITECTURES.includes(manifest.architecture) ||
|
!ARCHITECTURES.includes(manifest.architecture) ||
|
||||||
!exactKeys(manifest.images, ['application', 'operator']) ||
|
!exactKeys(manifest.images, ['application', 'operator']) ||
|
||||||
!exactKeys(manifest.sboms, ['application', 'operator']) ||
|
!exactKeys(manifest.sboms, ['application', 'operator'])
|
||||||
!exactKeys(manifest.verification, Object.keys(VERIFICATION)) ||
|
|
||||||
JSON.stringify(manifest.verification) !== JSON.stringify(VERIFICATION)
|
|
||||||
) {
|
) {
|
||||||
fail('trial kit manifest identity or shape is incompatible');
|
fail('trial kit manifest identity or shape is incompatible');
|
||||||
}
|
}
|
||||||
@@ -497,6 +668,11 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
bundleRoot,
|
bundleRoot,
|
||||||
);
|
);
|
||||||
validateFileRecord(manifest.sboms.operator, FILES.operatorSbom, bundleRoot);
|
validateFileRecord(manifest.sboms.operator, FILES.operatorSbom, bundleRoot);
|
||||||
|
validateFileRecord(
|
||||||
|
manifest.verification,
|
||||||
|
FILES.verificationEvidence,
|
||||||
|
bundleRoot,
|
||||||
|
);
|
||||||
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
||||||
validateOfflineSbom(
|
validateOfflineSbom(
|
||||||
readBoundedJson(
|
readBoundedJson(
|
||||||
@@ -511,12 +687,26 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
'local-operator',
|
'local-operator',
|
||||||
manifest.version,
|
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 = [
|
const expectedFiles = [
|
||||||
FILES.checksums,
|
FILES.checksums,
|
||||||
FILES.manifest,
|
FILES.manifest,
|
||||||
FILES.readme,
|
FILES.readme,
|
||||||
FILES.applicationSbom,
|
FILES.applicationSbom,
|
||||||
FILES.operatorSbom,
|
FILES.operatorSbom,
|
||||||
|
FILES.verificationEvidence,
|
||||||
expectedArchive,
|
expectedArchive,
|
||||||
].sort();
|
].sort();
|
||||||
const actualFiles = fs
|
const actualFiles = fs
|
||||||
@@ -535,6 +725,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
expectedArchive,
|
expectedArchive,
|
||||||
FILES.applicationSbom,
|
FILES.applicationSbom,
|
||||||
FILES.operatorSbom,
|
FILES.operatorSbom,
|
||||||
|
FILES.verificationEvidence,
|
||||||
FILES.readme,
|
FILES.readme,
|
||||||
FILES.manifest,
|
FILES.manifest,
|
||||||
];
|
];
|
||||||
@@ -555,6 +746,9 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
archiveSha256: manifest.archive.sha256,
|
archiveSha256: manifest.archive.sha256,
|
||||||
applicationImageId: manifest.images.application.id,
|
applicationImageId: manifest.images.application.id,
|
||||||
operatorImageId: manifest.images.operator.id,
|
operatorImageId: manifest.images.operator.id,
|
||||||
|
verificationSha256: manifest.verification.sha256,
|
||||||
|
workflowRunId: verificationEvidence.workflow.runId,
|
||||||
|
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
|
||||||
compatible: true,
|
compatible: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -576,6 +770,43 @@ function parseArguments(argv) {
|
|||||||
}
|
}
|
||||||
return { mode: 'audit', bundleRoot: path.resolve(values.bundle) };
|
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') {
|
if (values.mode === 'create') {
|
||||||
const expected = [
|
const expected = [
|
||||||
'application-image',
|
'application-image',
|
||||||
@@ -587,6 +818,7 @@ function parseArguments(argv) {
|
|||||||
'output',
|
'output',
|
||||||
'readme',
|
'readme',
|
||||||
'source-revision',
|
'source-revision',
|
||||||
|
'verification-evidence',
|
||||||
];
|
];
|
||||||
if (
|
if (
|
||||||
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
|
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
|
||||||
@@ -602,6 +834,7 @@ function parseArguments(argv) {
|
|||||||
operatorImage: values['operator-image'],
|
operatorImage: values['operator-image'],
|
||||||
applicationSbom: path.resolve(values['application-sbom']),
|
applicationSbom: path.resolve(values['application-sbom']),
|
||||||
operatorSbom: path.resolve(values['operator-sbom']),
|
operatorSbom: path.resolve(values['operator-sbom']),
|
||||||
|
verificationEvidence: path.resolve(values['verification-evidence']),
|
||||||
readme: path.resolve(values.readme),
|
readme: path.resolve(values.readme),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -610,10 +843,14 @@ function parseArguments(argv) {
|
|||||||
|
|
||||||
function runCli(argv) {
|
function runCli(argv) {
|
||||||
const options = parseArguments(argv);
|
const options = parseArguments(argv);
|
||||||
const report =
|
let report;
|
||||||
options.mode === 'create'
|
if (options.mode === 'record-verification') {
|
||||||
? createLocalAlphaTrialKit(options)
|
report = createLocalAlphaTrialKitVerificationEvidence(options);
|
||||||
: auditLocalAlphaTrialKit(options);
|
} else if (options.mode === 'create') {
|
||||||
|
report = createLocalAlphaTrialKit(options);
|
||||||
|
} else {
|
||||||
|
report = auditLocalAlphaTrialKit(options);
|
||||||
|
}
|
||||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
@@ -633,9 +870,11 @@ module.exports = Object.freeze({
|
|||||||
FILES,
|
FILES,
|
||||||
SCHEMA,
|
SCHEMA,
|
||||||
VERIFICATION,
|
VERIFICATION,
|
||||||
|
VERIFICATION_SCHEMA,
|
||||||
archiveName,
|
archiveName,
|
||||||
auditLocalAlphaTrialKit,
|
auditLocalAlphaTrialKit,
|
||||||
createLocalAlphaTrialKit,
|
createLocalAlphaTrialKit,
|
||||||
|
createLocalAlphaTrialKitVerificationEvidence,
|
||||||
parseArguments,
|
parseArguments,
|
||||||
runCli,
|
runCli,
|
||||||
sha256File,
|
sha256File,
|
||||||
|
|||||||
@@ -196,16 +196,41 @@ function auditWorkflow(contents, findings) {
|
|||||||
'"${OPERATOR_IMAGE}" --version',
|
'"${OPERATOR_IMAGE}" --version',
|
||||||
'scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
|
'scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
|
||||||
'scripts/ql3-local-alpha-trial-kit-bundle.cjs',
|
'scripts/ql3-local-alpha-trial-kit-bundle.cjs',
|
||||||
|
'--mode=record-verification',
|
||||||
'--mode=create',
|
'--mode=create',
|
||||||
'--mode=audit',
|
'--mode=audit',
|
||||||
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
|
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
|
||||||
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.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',
|
'--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) {
|
for (const value of required) {
|
||||||
if (!contents.includes(value))
|
if (!contents.includes(value))
|
||||||
finding(findings, 'LOCAL_OPERATOR_CI_CONTRACT_DRIFT', 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) {
|
function auditLocalOperatorImageContract(root) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const test = require('node:test');
|
|||||||
const {
|
const {
|
||||||
auditLocalAlphaTrialKit,
|
auditLocalAlphaTrialKit,
|
||||||
createLocalAlphaTrialKit,
|
createLocalAlphaTrialKit,
|
||||||
|
createLocalAlphaTrialKitVerificationEvidence,
|
||||||
parseArguments,
|
parseArguments,
|
||||||
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
|
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
|
||||||
const {
|
const {
|
||||||
@@ -61,6 +62,10 @@ function fixture(t) {
|
|||||||
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
|
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
|
||||||
const applicationSbom = path.join(fixtureRoot, 'application.json');
|
const applicationSbom = path.join(fixtureRoot, 'application.json');
|
||||||
const operatorSbom = path.join(fixtureRoot, 'operator.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');
|
const readme = path.join(fixtureRoot, 'README-source.md');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
applicationSbom,
|
applicationSbom,
|
||||||
@@ -73,13 +78,38 @@ function fixture(t) {
|
|||||||
)}\n`,
|
)}\n`,
|
||||||
);
|
);
|
||||||
fs.writeFileSync(readme, '# Local Alpha Trial Kit\n');
|
fs.writeFileSync(readme, '# Local Alpha Trial Kit\n');
|
||||||
return {
|
const paths = {
|
||||||
fixtureRoot,
|
fixtureRoot,
|
||||||
applicationSbom,
|
applicationSbom,
|
||||||
operatorSbom,
|
operatorSbom,
|
||||||
|
verificationEvidence,
|
||||||
readme,
|
readme,
|
||||||
outputRoot: path.join(fixtureRoot, 'bundle'),
|
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) {
|
function createOptions(paths) {
|
||||||
@@ -92,6 +122,7 @@ function createOptions(paths) {
|
|||||||
operatorImage: 'qinglong3-local-operator:test-arm64',
|
operatorImage: 'qinglong3-local-operator:test-arm64',
|
||||||
applicationSbom: paths.applicationSbom,
|
applicationSbom: paths.applicationSbom,
|
||||||
operatorSbom: paths.operatorSbom,
|
operatorSbom: paths.operatorSbom,
|
||||||
|
verificationEvidence: paths.verificationEvidence,
|
||||||
readme: paths.readme,
|
readme: paths.readme,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -117,15 +148,17 @@ function adapters(overrides = {}) {
|
|||||||
test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
|
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.sourceRevision, revision);
|
||||||
assert.equal(manifest.architecture, 'arm64');
|
assert.equal(manifest.architecture, 'arm64');
|
||||||
assert.equal(manifest.images.application.architecture, 'arm64');
|
assert.equal(manifest.images.application.architecture, 'arm64');
|
||||||
assert.equal(manifest.images.operator.architecture, 'arm64');
|
assert.equal(manifest.images.operator.architecture, 'arm64');
|
||||||
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
|
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
|
||||||
|
assert.equal(manifest.verification.file, 'verification-evidence.json');
|
||||||
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
||||||
assert.equal(report.compatible, true);
|
assert.equal(report.compatible, true);
|
||||||
assert.equal(report.sourceRevision, revision);
|
assert.equal(report.sourceRevision, revision);
|
||||||
|
assert.equal(report.workflowRunId, '32990652047');
|
||||||
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
|
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
|
||||||
'README.md',
|
'README.md',
|
||||||
'SHA256SUMS',
|
'SHA256SUMS',
|
||||||
@@ -133,6 +166,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
|||||||
'qinglong3-local-application.cdx.json',
|
'qinglong3-local-application.cdx.json',
|
||||||
'qinglong3-local-operator.cdx.json',
|
'qinglong3-local-operator.cdx.json',
|
||||||
'qinglong3-local-trial-kit-arm64.docker.tar',
|
'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);
|
assert.equal(fs.existsSync(paths.outputRoot), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('offline audit rejects archive mutation, extra files and SBOM substitution', (t) => {
|
test('offline audit rejects archive, file-set, SBOM and verification mutation', (t) => {
|
||||||
for (const mutation of ['archive', 'extra', 'sbom']) {
|
for (const mutation of ['archive', 'extra', 'sbom', 'verification']) {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
paths.outputRoot = path.join(paths.fixtureRoot, `bundle-${mutation}`);
|
paths.outputRoot = path.join(paths.fixtureRoot, `bundle-${mutation}`);
|
||||||
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||||
@@ -174,11 +208,16 @@ test('offline audit rejects archive mutation, extra files and SBOM substitution'
|
|||||||
);
|
);
|
||||||
} else if (mutation === 'extra') {
|
} else if (mutation === 'extra') {
|
||||||
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
|
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
|
||||||
} else {
|
} else if (mutation === 'sbom') {
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
|
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
|
||||||
path.join(paths.outputRoot, 'qinglong3-local-operator.cdx.json'),
|
path.join(paths.outputRoot, 'qinglong3-local-operator.cdx.json'),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
fs.appendFileSync(
|
||||||
|
path.join(paths.outputRoot, 'verification-evidence.json'),
|
||||||
|
'tamper',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot }),
|
() => 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', () => {
|
test('CLI grammar is exact and separates create from offline audit', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
parseArguments(['--mode=audit', '--bundle=/tmp/ql3-bundle']),
|
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']),
|
() => parseArguments(['--mode=create', '--output=/tmp/output']),
|
||||||
/create arguments are invalid/,
|
/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 });
|
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