fix(ql3): bind alpha artifacts to CI evidence

This commit is contained in:
whyour
2026-08-27 06:57:08 +08:00
parent 4239464af6
commit 238df17fdf
10 changed files with 509 additions and 41 deletions
+15
View File
@@ -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 \
+5 -3
View File
@@ -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 archiveSHA-256 `01afb30cbe0c21f980ca083ad98fd316e659941f940dd8930ffd9ccfa7153edf`image ID `sha256:dfce2cc9d70044d75f72f2cc3075e1f24569fb9fe279d9c25a45698c19c3bde9`)、CycloneDX SBOM、release-candidate contract、manifest、verification evidence 与 checksumimage 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 的单架构工程产物,不是公开可下载 releaseamd64 同级 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 IDworkflow 精确绑定 `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/40Kubernetes 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 archiveSHA-256 `01afb30cbe0c21f980ca083ad98fd316e659941f940dd8930ffd9ccfa7153edf`image ID `sha256:dfce2cc9d70044d75f72f2cc3075e1f24569fb9fe279d9c25a45698c19c3bde9`)、CycloneDX SBOM、release-candidate contract、manifest、verification evidence 与 checksumimage 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 bytesread-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 的 manifestmaterializer 并未收到这些 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 IDoffline audit 不调用 Docker或网络;
- 首个真实可下载 v2 双架构 Trial Kit 仍需维护者授权的 `produce_alpha_artifacts=true` workflow 生成。
+1
View File
@@ -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 |
## 规则
+10 -19
View File
@@ -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 为 0128 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 bytesSHA-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 cancellationKubernetes 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 integrityCI 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 bytesSHA-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`
+5 -3
View File
@@ -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 drainSQLite integrity 已在同一架构的原生 Linux CI 中验证。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drainSQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
Edge 的验证上限为 Application 128 MiB、0.5 CPU、64 PIDStandalone 为 256 MiB、0.5 CPU、256 PIDoperator 为 128 MiB、0.5 CPU、32 PID。这里的数值是试运行门,不是所有 workload 的容量承诺。
+250 -11
View File
@@ -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) {
+89 -5
View File
@@ -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 });
}
});