diff --git a/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md b/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md index af0948b7..d0656689 100644 --- a/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md +++ b/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md @@ -11,6 +11,20 @@ 最新增量证据(2026-08-20): +- D-371/ADR-0464(已接受并机器化):接受 ADR-0006 的 Node 24 与多架构分层,并把根 `ql3-release.json` 升级为 + `qinglong/release-identity@v2` 唯一事实源。3.0 Tier 1 精确为 `amd64`、`arm64`;`ppc64le`、`s390x` 是通过固定 + Node 24 与同等级原生门禁前不可发布的候选;`arm/v7` 在缺少 owner、可重复 toolchain 与设备报告时为 experimental blocked; + `arm/v6`、`386` 明确留在 `2.x` legacy line。release candidate 的 OCI platforms、原生 OS runner matrix、release set digest 与 + version audit 均从该身份派生,新增 Tier 1 却无原生 runner 会失败关闭;发布必需门新增 `architecture-support-tier`。这不新增 + workspace package、dependency、服务、timer、数据库对象或 Edge 常驻成本,也不把旧设备排除出后续受限 Worker 兼容路径。 + 聚焦 release/version/downstream closure 门为 `55/55`;完整 backend 工作区为 + `1,503 total / 1,501 pass / 2 conditional skip / 0 fail`(含一条不会提交的既有用户测试;D-371 提交范围为 + `1,502 total / 1,500 pass / 2 skip`),18-package clean build/逐包测试退出 0。release version、package boundary、 + cluster dependency、Edge import、cluster deployment 与 image release 六项审计全部 compatible,workspace 仍为 18 packages、 + 无 single/shallow package。14 档 Local artifact audit 串行复核全部 compatible;基础 Edge/Standalone 仍为 + `2,589,998 / 2,590,076` bytes,Application+AI 为 `4,493,151 / 4,493,283` bytes,MCP 为 + `7,315,930 / 7,316,038` bytes,证明发布政策未进入低资源部署闭包。 + - D-370/ADR-0463(实现与本地协议门已接受;首份原生 CI 双架构报告待实际 workflow 产生):为显式按需的 Cluster Admin Copilot Console 增加固定 Linux x64/arm64 容量与 assertion 生命周期发布证据。两个原生 Admin image matrix job 分别在 `192 MiB / swap 0 / 0.25 CPU / 32 PIDs`、只读 root、非 root、cap-drop ALL、no-new-privileges、默认 seccomp、8 MiB tmpfs 与 @@ -4959,9 +4973,9 @@ | D-11 | 新领域 API 使用 `/api/v3`,旧 API 通过兼容层继续服务 | Proposed | 允许渐进迁移而非一次性切换 | | D-12 | Workflow 首版只支持有界、可恢复的步骤模型 | Proposed | 避免无界 DAG 和 Agent 循环扩大可靠性风险 | | D-13 | Prompt、模型结果和 Tool 内容采集默认关闭 | Proposed | 降低 Secret 和业务数据泄漏风险 | -| D-14 | ql-core 按 Tier 发布明确的架构支持矩阵,不把“现有镜像可构建”等同于“Node 24 可支持” | Proposed | 路由、NAS 和异构 Worker 是核心用户场景,但 Node 24 官方产物当前不能覆盖 ARMv6、ARMv7 和 386 | +| D-14 | ql-core 按 Tier 发布明确的架构支持矩阵,不把“现有镜像可构建”等同于“Node 24 可支持” | Accepted(ADR-0006/0464:release identity v2 已机器化当前支持矩阵) | 路由、NAS 和异构 Worker 是核心用户场景,但 Node 24 官方产物当前不能覆盖 ARMv6、ARMv7 和 386 | | D-15 | UI Extension 不进入 3.0 首个稳定版本 | Deferred | 先稳定 Package、Tool 和 Runtime Extension 契约 | -| D-16 | 3.0 ql-core 以固定 Node.js 24 LTS 为运行时基线 | Proposed | 使用稳定 Web/SQLite 能力并消除不同发行版浮动 Node 版本造成的行为漂移 | +| D-16 | 3.0 ql-core 以固定 Node.js 24 LTS 为运行时基线 | Accepted(ADR-0006/0464:当前精确固定 24.18.0,变更必须升级发布身份并重跑完整矩阵) | 使用稳定 Web/SQLite 能力并消除不同发行版浮动 Node 版本造成的行为漂移 | | D-17 | 3.0 新领域持久化通过 typed schema 与 Repository 端口访问;SQLite 目标为 Drizzle + node:sqlite | Proposed | 避免新 Runtime 继续耦合 Sequelize,同时保留 PostgreSQL adapter 边界和可审查 migration | | D-18 | 取消先持久化为 Run 意图事件,再调用 Executor;实际终态由完成事实或 Reconciler 收敛 | Proposed | 使取消可跨重启恢复,并消除“已发信号但无审计事实”和迟到成功覆盖取消的竞态 | | D-19 | 跨进程取消派发使用独立的 durable lease/fencing 状态,不以 RunEvent 或内存锁代替并发控制 | Proposed | 在 edge SQLite 与 cluster PostgreSQL 上共享一致语义,限制多 Worker 重复 signal 和崩溃重试风暴 | @@ -6392,7 +6406,8 @@ ql-observability ### 7.9 多架构发布 -QingLong 当前容器发布覆盖 `amd64`、`arm/v6`、`arm/v7`、`arm64`、`ppc64le` 和 `386`。3.0 应维护明确的架构支持矩阵: +QingLong 2.x 容器发布曾覆盖 `amd64`、`arm/v6`、`arm/v7`、`arm64`、`ppc64le`、`s390x` 和 `386`。 +3.0 由 release identity v2 维护明确的架构支持矩阵: - `ql-core` 和 edge 镜像优先保持现有多架构覆盖,但每个架构必须标记 `supported`、`experimental` 或 `legacy-only`,不能只凭构建成功宣称支持。 - Alpine 与 Debian 发布物必须固定同一 Node.js 24 小版本,不能依赖发行版仓库的浮动 `nodejs`;升级 Node 小版本需要通过完整多架构门禁。 @@ -6405,15 +6420,19 @@ QingLong 当前容器发布覆盖 `amd64`、`arm/v6`、`arm/v7`、`arm64`、`ppc 截至 2026-07-18 的上游产物审计显示,Node.js 24 官方 release binary 提供 x64、arm64、ppc64le 和 s390x,没有 ARMv6、ARMv7 或 386;官方 `node:24` Docker manifest 的 Debian/Alpine 覆盖也小于 QingLong 当前矩阵。Node.js BUILDING 文档把 ARMv7 降为实验支持,不能把自定义构建当作官方维护基线。参考:[Node.js 24 release](https://nodejs.org/en/blog/release/v24.18.0)、[Node.js official image manifest](https://github.com/docker-library/official-images/blob/master/library/node)、[Node.js BUILDING](https://github.com/nodejs/node/blob/main/BUILDING.md)。 -因此 D-14 与 D-16 之间存在必须在首个 Beta 前解决的发布决策,当前建议的候选分层为: +D-371/ADR-0464 已接受 D-14 与 D-16 的一致解,当前分层为: | Tier | 3.0 候选架构 | 约束 | | --- | --- | --- | -| Tier 1 supported | amd64、arm64;ppc64le、s390x 在对应 libc 镜像通过后纳入 | 固定官方 Node 24 patch,完整 migration、任务与恢复门禁 | -| Tier 2 experimental | ARMv7 自定义 Node 24 构建 | 必须有可重复 toolchain、设备测试和明确无官方 binary 的提示,不进入默认 `latest` | -| legacy-only | ARMv6、386;以及未通过 Node 24 门禁的架构 | 继续使用受维护的 2.x 镜像;不能把 Node 20/22 镜像标为满足 D-16 的完整 3.0 | +| Tier 1 supported | amd64、arm64 | 固定 Node 24.18.0;完整原生 OS、OCI、migration、任务、恢复与资源门禁;只进入默认 3.0 manifest | +| candidate | ppc64le、s390x | 在固定 Node 24 镜像、原生 runner/设备和同等级门禁全部具备前不得进入 3.0 manifest | +| experimental blocked | ARMv7 | 当前没有已接受的 owner、可重复 toolchain 与设备证据;未来若补齐只能进入独立 experimental tag | +| legacy-only | ARMv6、386 | 继续使用显式 2.x legacy line;不能把 Node 20/22 镜像标为满足 D-16 的完整 3.0 | -这个分层是待评审候选,不等于已经决定放弃小设备。若 Maintainers 要求 ARMv6、ARMv7、386 在 3.0 与 Node 24 同时成为正式支持项,则必须先拥有并长期维护对应 Node 24 构建链;否则应明确修改 D-16 或维持 2.x legacy support line,不能发布互相矛盾的承诺。 +这个分层不等于放弃小设备。2.x legacy line 继续承担约定兼容窗口,旧设备参与 3.0 集群的受限 Worker 协议仍是 +独立后续决策;但不能因此把没有受支持 Node 24、数据库恢复与原生资源证据的设备标成完整 3.0 ql-core。若 +Maintainers 要求 ARMv6、ARMv7 或 386 成为 3.0 正式支持项,必须先修改 D-16 或长期维护对应 Node 24 构建链, +升级 release identity schema,并通过与现有 Tier 1 同等级的发布门禁。 ### 7.10 资源基准与发布门禁 diff --git a/docs/adr/ADR-0006-node24-and-multi-architecture-support.md b/docs/adr/ADR-0006-node24-and-multi-architecture-support.md index 02758404..1ac21f93 100644 --- a/docs/adr/ADR-0006-node24-and-multi-architecture-support.md +++ b/docs/adr/ADR-0006-node24-and-multi-architecture-support.md @@ -1,10 +1,11 @@ # ADR-0006:Node.js 24 与多架构支持分层 -- 状态:Proposed +- 状态:Accepted(由 ADR-0464 机器化;新增架构必须先满足对应原生门禁) - 日期:2026-07-18 - 决策者:QingLong Maintainers - 关联 RFC:[QL-RFC-0001](../QINGLONG_3_0_ARCHITECTURE_RFC.md) - 关联决策:D-05、D-14、D-16、D-17 +- Operationalized by:ADR-0464 官方参考: @@ -166,3 +167,15 @@ Node 22 能覆盖更多 32-bit ARM 镜像,但把同一个 3.0 tag 在不同架 - 接受 ARMv6、386 默认进入有 EOL 的 2.x legacy line,而不是伪装成 Node 24 3.0。 - 接受旧设备可通过受限 Worker 兼容路径参与 3.0,但不拥有完整 ql-core 权限。 - 接受任何 Node 22 的 3.0 compatibility runtime 都必须先显式修订 RFC。 + +## 9. 2026-08-20 实施状态 + +ADR-0464 已把本 ADR 从候选分层转为唯一发布身份中的机器契约。当前 3.0 Tier 1 精确为 `amd64`、`arm64`; +`ppc64le`、`s390x` 保持候选,必须在进入默认 manifest 前取得固定 Node 24 镜像与同等级原生门禁;`arm/v7` +因没有受维护 toolchain、owner 与设备证据而保持 experimental blocked;`arm/v6`、`386` 留在显式 `2.x` +legacy line。该状态不是删除小设备支持:2.x 兼容窗口和未来受限 Worker 路径仍需单独关闭,但不能冒充完整 3.0 +ql-core 支持。 + +根 `ql3-release.json` 的 `qinglong/release-identity@v2` 是唯一事实源;release candidate、原生 OS matrix、OCI +platform 列表、release set 和 version audit 都从它派生。任何新增 Tier 1 架构若没有对应原生 runner mapping, +发布候选会失败关闭。实现未新增 workspace package、运行时依赖、服务、timer、数据库对象或 Edge 常驻成本。 diff --git a/docs/adr/ADR-0464-machine-enforced-node24-architecture-support-tiers.md b/docs/adr/ADR-0464-machine-enforced-node24-architecture-support-tiers.md new file mode 100644 index 00000000..c567592b --- /dev/null +++ b/docs/adr/ADR-0464-machine-enforced-node24-architecture-support-tiers.md @@ -0,0 +1,89 @@ +# ADR-0464:机器化 Node 24 架构支持分层 + +- 状态:Accepted +- 日期:2026-08-20 +- 关联 RFC:QL-RFC-0001 D-14、D-16、D-371 +- 关联 ADR:ADR-0006、ADR-0088、ADR-0281、ADR-0329、ADR-0463 +- Accepts and operationalizes:ADR-0006 + +## 上下文 + +QingLong 部署跨度从低性能路由设备、NAS 到 Cluster control plane 与 Worker 节点。2.x 历史镜像覆盖的架构数量, +不能直接等价为完整 3.0 ql-core 的支持承诺:3.0 固定使用 Node 24 和 `node:sqlite`,而上游官方产物、镜像 +variant 与原生测试资源没有覆盖 ARMv6、ARMv7 和 386。此前 ADR-0006、D-14 与 D-16 仍是 Proposed,发布脚本 +虽然实际只生成 amd64/arm64,却没有一个机器可读、可审计的支持分层,因此文档、OCI manifest 与未来 matrix +可能独立漂移。 + +继续把所有历史架构放进一个默认 manifest 会让低配设备用户收到虚假升级承诺;反过来静默删除这些架构也会 +损害现有部署。3.0 必须同时诚实声明完整核心的边界,以及 2.x legacy 和未来受限 Worker 兼容路径的边界。 + +## 决策 + +1. 接受 ADR-0006。3.0 ql-core 的唯一生产运行时基线固定为 Node `24.18.0`,engine 为 + `>=24.18.0 <25`;不同架构不能在同一个 3.0 tag 下静默回退 Node major。 +2. 根 `ql3-release.json` 升级为 `qinglong/release-identity@v2`,增加 exact-shape + `architectureSupport`,作为版本、Node、workspace 边界和架构政策的单一发布事实源。 +3. 当前 Tier 1 精确为 `amd64`、`arm64`。只有它们进入默认 3.0 OCI platform 列表与原生 OS vulnerability + matrix;每个 image 都必须在对应原生 runner 构建、扫描并绑定 immutable OCI evidence。 +4. `ppc64le`、`s390x` 为 candidates,不是已支持平台。只有获得固定 Node 24 image variant、原生 + runner/设备、数据库 migration/backup/restore、任务、资源和发布证据后,才能通过新的 release identity schema + 变更进入 Tier 1。 +5. `arm/v7` 当前为 experimental blocked:没有已接受的 maintainer owner、可重复 Node 24 toolchain 与设备门。 + 补齐后也必须使用独立 experimental tag,不自动进入默认 manifest。 +6. `arm/v6`、`386` 为 legacy-only,显式绑定 `2.x` line。它们不能被标成满足 D-16 的完整 3.0 ql-core; + EOL、迁移、备份和受限 Worker 兼容协议分别治理,不能由本 ADR 虚构完成状态。 +7. `scripts/lib/ql3-release-identity.cjs` 对 v2 字段、顺序和值失败关闭并深冻结架构列表;非 canonical JSON、 + 添加未知字段或把 candidate 擅自加入 Tier 1 都会被拒绝。 +8. release candidate 从 Tier 1 派生 `linux/` platforms 和每镜像原生 runner matrix,并加入 + `architecture-support-tier` 必需门。Tier 1 没有受审 runner mapping 时发布失败,而不是降级为 QEMU 或跳过扫描。 +9. release identity digest 继续传入 candidate、image record、release set、catalog、publication closure 与 + deployment lock,支持矩阵变化会自然改变整条发布证据链;version audit 同时输出架构分层。 +10. 本实现不新增 workspace package、生产 dependency、runtime binary、服务、timer、端口、数据库对象或 + 部署 workload。它只改变离线发布权威和 CI matrix,因此 Edge/Standalone 常驻资源闭包保持不变。 + +## 被拒绝的替代方案 + +### 保留文档表格,发布脚本继续硬编码 amd64/arm64 + +拒绝。两份事实源会在新增架构、Node patch 或 workflow 重构时漂移,且 release set 无法证明自己遵循哪份政策。 + +### 为架构政策新增一个 workspace package + +拒绝。该政策只有一个根发布事实源和离线 validator,没有独立生产生命周期或依赖边界;新增 package 会重现 +单文件微包问题,并让低资源制品审计承担无收益的结构成本。 + +### 立即把 ppc64le、s390x 加入 Tier 1 + +拒绝。上游可能存在 Node binary 不等于当前 Debian image、数据库、恢复、资源和发布门已经完成;支持承诺必须 +由同等级证据产生。 + +### 把 ARMv7 直接归入 legacy-only 或 Tier 1 + +拒绝。它仍保留 experimental 的技术路径,但当前资产不足以发布;既不能掩盖未来可能性,也不能伪造现有支持。 + +### 让旧设备在 3.0 tag 下运行 Node 20/22 + +拒绝。同一版本跨架构拥有不同 SQLite、Web API、插件 engine 与安全周期,会使 ql-core capability 不可判断。 + +## 验证与证据 + +- release candidate、version transition、release set、catalog、publication closure 和 deployment lock 聚焦回归为 + `55/55`,覆盖 v2 schema、精确支持分层、来源派生 platforms/matrix、必需门与 post-create mutation 拒绝。 +- 架构漂移负向用例把 `ppc64le` 未经 schema 变更加入 Tier 1,release identity audit 必须失败关闭。 +- 完整 backend 工作区为 `1,503 total / 1,501 pass / 2 conditional skip / 0 fail`;其中包含一条既有、未跟踪且 + 不会提交的用户测试,因此 D-371 提交范围对应 `1,502 total / 1,500 pass / 2 skip`。沙箱首次运行唯一失败为 + loopback `listen EPERM`,允许 `127.0.0.1` 的宿主环境原样重跑后零失败,未把权限限制误判成代码回归。 +- 18-package clean build 与逐包测试退出 0。release version、package boundary、cluster dependency、Edge import、 + cluster deployment 与 image release 六项审计全部 compatible;workspace 保持 18 packages、无 single/shallow + package,证明本决策没有重新扩大 package 或依赖树。 +- 14 档 Local artifact audit 必须串行执行,因为并行 build/pack 会争用相同 package `dist`、产生不可信的短暂 + 字节差;串行复核全部 compatible。基础 Edge/Standalone 为 `2,589,998 / 2,590,076` bytes,Application+AI 为 + `4,493,151 / 4,493,283` bytes,MCP 为 `7,315,930 / 7,316,038` bytes,与 D-370 稳定基线一致。 + +## 后续边界 + +- 制定 2.x legacy line 的可见 EOL、备份/迁移说明,不能只在内部 RFC 中声明。 +- 为旧设备参与 3.0 设计最小 capability、身份、调度限制和 EOL 明确的受限 Worker 协议;legacy Worker 不获得 + Plugin Host、任意 Tool 或控制面数据库访问权。 +- ppc64le/s390x 进入 Tier 1 或 ARMv7 进入 experimental 前,必须先提交独立 ADR、runner/toolchain owner 与原生 + 证据;不能只修改数组让 CI 看似扩容。 diff --git a/ql3-release.json b/ql3-release.json index 986f1359..e1a90630 100644 --- a/ql3-release.json +++ b/ql3-release.json @@ -1,12 +1,30 @@ { - "schemaVersion": 1, - "schema": "qinglong/release-identity@v1", + "schemaVersion": 2, + "schema": "qinglong/release-identity@v2", "product": "qinglong3", "version": "3.0.0-alpha.0", "node": { "version": "24.18.0", "engine": ">=24.18.0 <25" }, + "architectureSupport": { + "tier1": [ + "amd64", + "arm64" + ], + "candidates": [ + "ppc64le", + "s390x" + ], + "experimentalBlocked": [ + "arm/v7" + ], + "legacyOnly": [ + "arm/v6", + "386" + ], + "legacyLine": "2.x" + }, "workspacePackageCount": 18, "legacyRootPackageExcluded": true } diff --git a/scripts/lib/ql3-release-identity.cjs b/scripts/lib/ql3-release-identity.cjs index 009f4c64..c236e274 100644 --- a/scripts/lib/ql3-release-identity.cjs +++ b/scripts/lib/ql3-release-identity.cjs @@ -5,7 +5,14 @@ const path = require('node:path'); const semver = require('semver'); const RELEASE_IDENTITY_PATH = 'ql3-release.json'; -const RELEASE_IDENTITY_SCHEMA = 'qinglong/release-identity@v1'; +const RELEASE_IDENTITY_SCHEMA = 'qinglong/release-identity@v2'; +const ARCHITECTURE_SUPPORT = Object.freeze({ + tier1: Object.freeze(['amd64', 'arm64']), + candidates: Object.freeze(['ppc64le', 's390x']), + experimentalBlocked: Object.freeze(['arm/v7']), + legacyOnly: Object.freeze(['arm/v6', '386']), + legacyLine: '2.x', +}); const MAX_RELEASE_IDENTITY_BYTES = 4096; const VERSION_PATTERN = /^3\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$/u; @@ -38,10 +45,11 @@ function normalizeReleaseIdentity(value) { 'product', 'version', 'node', + 'architectureSupport', 'workspacePackageCount', 'legacyRootPackageExcluded', ]) || - value.schemaVersion !== 1 || + value.schemaVersion !== 2 || value.schema !== RELEASE_IDENTITY_SCHEMA || value.product !== 'qinglong3' || typeof value.version !== 'string' || @@ -50,6 +58,15 @@ function normalizeReleaseIdentity(value) { !exactKeys(value.node, ['version', 'engine']) || value.node.version !== '24.18.0' || value.node.engine !== '>=24.18.0 <25' || + !exactKeys(value.architectureSupport, [ + 'tier1', + 'candidates', + 'experimentalBlocked', + 'legacyOnly', + 'legacyLine', + ]) || + JSON.stringify(value.architectureSupport) !== + JSON.stringify(ARCHITECTURE_SUPPORT) || value.workspacePackageCount !== 18 || value.legacyRootPackageExcluded !== true ) { @@ -58,6 +75,15 @@ function normalizeReleaseIdentity(value) { return Object.freeze({ ...value, node: Object.freeze({ ...value.node }), + architectureSupport: Object.freeze({ + ...value.architectureSupport, + tier1: Object.freeze([...value.architectureSupport.tier1]), + candidates: Object.freeze([...value.architectureSupport.candidates]), + experimentalBlocked: Object.freeze([ + ...value.architectureSupport.experimentalBlocked, + ]), + legacyOnly: Object.freeze([...value.architectureSupport.legacyOnly]), + }), }); } @@ -89,6 +115,7 @@ function readReleaseIdentity(root) { } module.exports = Object.freeze({ + ARCHITECTURE_SUPPORT, MAX_RELEASE_IDENTITY_BYTES, RELEASE_IDENTITY_PATH, RELEASE_IDENTITY_SCHEMA, diff --git a/scripts/ql3-release-candidate-contract.cjs b/scripts/ql3-release-candidate-contract.cjs index 8d49e1b4..9789d3d1 100644 --- a/scripts/ql3-release-candidate-contract.cjs +++ b/scripts/ql3-release-candidate-contract.cjs @@ -17,6 +17,18 @@ const PREDICATE_TYPE = 'https://qinglong.dev/attestations/release-candidate-contract/v1'; const MAX_REPORT_BYTES = 1024 * 1024; const RELEASE_SCOPES = Object.freeze(['all', 'cluster', 'local']); +const NATIVE_ARCHITECTURES = Object.freeze({ + amd64: Object.freeze({ + runner: 'ubuntu-24.04', + node_arch: 'x64', + image_arch: 'amd64', + }), + arm64: Object.freeze({ + runner: 'ubuntu-24.04-arm', + node_arch: 'arm64', + image_arch: 'arm64', + }), +}); const LOCAL_IMAGES = Object.freeze([ Object.freeze({ image: 'local', @@ -92,6 +104,18 @@ function selectedImages(scope) { return [...CLUSTER_IMAGES, ...LOCAL_IMAGES]; } +function nativeArchitectureMatrix(architectureSupport) { + return architectureSupport.tier1.map((architecture) => { + const native = NATIVE_ARCHITECTURES[architecture]; + if (!native) { + fail( + `Tier 1 architecture lacks a native release runner: ${architecture}`, + ); + } + return native; + }); +} + function validateIdentity(options) { if ( typeof options.version !== 'string' || @@ -147,6 +171,9 @@ function createReleaseCandidateContract(options) { }) .sort((left, right) => left.name.localeCompare(right.name, 'en')); const images = selectedImages(options.releaseScope); + const nativeArchitectures = nativeArchitectureMatrix( + releaseIdentity.architectureSupport, + ); const imageManifests = images.map((image) => { const manifest = readJson( path.join(root, image.runtime_root, 'package.json'), @@ -182,24 +209,14 @@ function createReleaseCandidateContract(options) { }); }); const publishMatrix = images.map(({ dockerfile, target, ...image }) => image); - const osMatrix = images.flatMap((image) => [ - { + const osMatrix = images.flatMap((image) => + nativeArchitectures.map((architecture) => ({ image: image.image, - runner: 'ubuntu-24.04', - node_arch: 'x64', - image_arch: 'amd64', + ...architecture, dockerfile: image.dockerfile, target: image.target, - }, - { - image: image.image, - runner: 'ubuntu-24.04-arm', - node_arch: 'arm64', - image_arch: 'arm64', - dockerfile: image.dockerfile, - target: image.target, - }, - ]); + })), + ); const unsigned = { schemaVersion: 1, schema: SCHEMA, @@ -220,7 +237,18 @@ function createReleaseCandidateContract(options) { ), nodeVersion: releaseIdentity.node.version, nodeEngine: releaseIdentity.node.engine, - platforms: ['linux/amd64', 'linux/arm64'], + architectureSupport: { + tier1: [...releaseIdentity.architectureSupport.tier1], + candidates: [...releaseIdentity.architectureSupport.candidates], + experimentalBlocked: [ + ...releaseIdentity.architectureSupport.experimentalBlocked, + ], + legacyOnly: [...releaseIdentity.architectureSupport.legacyOnly], + legacyLine: releaseIdentity.architectureSupport.legacyLine, + }, + platforms: releaseIdentity.architectureSupport.tier1.map( + (architecture) => `linux/${architecture}`, + ), }, workspace: { packageCount: workspacePackages.length, @@ -248,6 +276,7 @@ function createReleaseCandidateContract(options) { requiredGates: [ 'package-boundary', 'source-tag-version-identity', + 'architecture-support-tier', 'native-os-vulnerability', 'multiarch-oci-layout', 'production-dependency-audit', diff --git a/scripts/ql3-version-transition.cjs b/scripts/ql3-version-transition.cjs index 72fc4e1c..4a92fafb 100644 --- a/scripts/ql3-version-transition.cjs +++ b/scripts/ql3-version-transition.cjs @@ -284,11 +284,20 @@ function auditReleaseVersionContract(rootInput = DEFAULT_ROOT) { } return Object.freeze({ - schemaVersion: 1, + schemaVersion: identity.schemaVersion, schema: RELEASE_IDENTITY_SCHEMA, version: identity.version, nodeVersion: identity.node.version, nodeEngine: identity.node.engine, + architectureSupport: Object.freeze({ + tier1: Object.freeze([...identity.architectureSupport.tier1]), + candidates: Object.freeze([...identity.architectureSupport.candidates]), + experimentalBlocked: Object.freeze([ + ...identity.architectureSupport.experimentalBlocked, + ]), + legacyOnly: Object.freeze([...identity.architectureSupport.legacyOnly]), + legacyLine: identity.architectureSupport.legacyLine, + }), legacyRootPackageVersion: legacyRoot.version, legacyRootExcluded: true, workspacePackageCount: workspacePaths.length, diff --git a/test/back/ql3ReleaseCandidateContract.test.cjs b/test/back/ql3ReleaseCandidateContract.test.cjs index f1403c2f..57ff268a 100644 --- a/test/back/ql3ReleaseCandidateContract.test.cjs +++ b/test/back/ql3ReleaseCandidateContract.test.cjs @@ -41,13 +41,28 @@ test('freezes an independent low-resource local release family', () => { assert.equal(contract.workspace.packageCount, 18); assert.equal( contract.compatibility.releaseIdentitySchema, - 'qinglong/release-identity@v1', + 'qinglong/release-identity@v2', ); + assert.deepEqual(contract.compatibility.architectureSupport, { + tier1: ['amd64', 'arm64'], + candidates: ['ppc64le', 's390x'], + experimentalBlocked: ['arm/v7'], + legacyOnly: ['arm/v6', '386'], + legacyLine: '2.x', + }); + assert.deepEqual(contract.compatibility.platforms, [ + 'linux/amd64', + 'linux/arm64', + ]); assert.match( contract.compatibility.releaseIdentityDigest, /^sha256:[a-f0-9]{64}$/u, ); assert.match(contract.contractDigest, /^sha256:[a-f0-9]{64}$/u); + assert.equal( + contract.requiredGates.includes('architecture-support-tier'), + true, + ); assert.equal( contract.requiredGates.includes('offline-deployment-lock-materialization'), true, @@ -81,6 +96,33 @@ test('closes the cluster release family with the Worker image', () => { ); assert.equal(contract.releasePlan.clusterEvidenceRequired, true); assert.equal(contract.releasePlan.osMatrix.length, 8); + assert.deepEqual( + [ + ...new Set( + contract.releasePlan.osMatrix.map((entry) => entry.image_arch), + ), + ], + ['amd64', 'arm64'], + ); + assert.deepEqual( + contract.releasePlan.osMatrix.slice(0, 2).map((entry) => ({ + runner: entry.runner, + node_arch: entry.node_arch, + image_arch: entry.image_arch, + })), + [ + { + runner: 'ubuntu-24.04', + node_arch: 'x64', + image_arch: 'amd64', + }, + { + runner: 'ubuntu-24.04-arm', + node_arch: 'arm64', + image_arch: 'arm64', + }, + ], + ); assert.equal( contract.requiredGates.includes('edge-and-standalone-rollout'), false, diff --git a/test/back/ql3VersionTransition.test.cjs b/test/back/ql3VersionTransition.test.cjs index 4655be4d..eb9794ef 100644 --- a/test/back/ql3VersionTransition.test.cjs +++ b/test/back/ql3VersionTransition.test.cjs @@ -86,11 +86,18 @@ function replaceVersion( test('audits one source-derived QingLong 3 release identity', () => { assert.deepEqual(auditReleaseVersionContract(root), { - schemaVersion: 1, - schema: 'qinglong/release-identity@v1', + schemaVersion: 2, + schema: 'qinglong/release-identity@v2', version: SOURCE_VERSION, nodeVersion: '24.18.0', nodeEngine: '>=24.18.0 <25', + architectureSupport: { + tier1: ['amd64', 'arm64'], + candidates: ['ppc64le', 's390x'], + experimentalBlocked: ['arm/v7'], + legacyOnly: ['arm/v6', '386'], + legacyLine: '2.x', + }, legacyRootPackageVersion: LEGACY_VERSION, legacyRootExcluded: true, workspacePackageCount: 18, @@ -226,6 +233,19 @@ test('rejects invalid SemVer, downgrade, plan mutation and a symbolic-link ident /identity shape or value is incompatible/, ); copyFile(root, fixture, 'ql3-release.json'); + const architectureDrift = JSON.parse( + fs.readFileSync(invalidIdentityPath, 'utf8'), + ); + architectureDrift.architectureSupport.tier1.push('ppc64le'); + fs.writeFileSync( + invalidIdentityPath, + `${JSON.stringify(architectureDrift, null, 2)}\n`, + ); + assert.throws( + () => auditReleaseVersionContract(fixture), + /identity shape or value is incompatible/, + ); + copyFile(root, fixture, 'ql3-release.json'); fs.renameSync( path.join(fixture, 'ql3-release.json'), path.join(fixture, 'identity-target.json'),