feat(ql3): close local alpha milestone

This commit is contained in:
whyour
2026-08-27 20:44:33 +08:00
parent 78c261b556
commit a7308c78f9
9 changed files with 1156 additions and 6 deletions
+86 -6
View File
@@ -14,13 +14,22 @@ on:
required: true
default: false
type: boolean
alpha_artifact_scope:
description: Select the product family packaged by an authorized Alpha milestone
required: true
default: local
type: choice
options:
- local
- cluster
- all
permissions:
contents: read
concurrency:
group: ql3-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ql3-ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && github.run_id || 'validation' }}
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts) }}
jobs:
backend:
@@ -366,6 +375,7 @@ jobs:
run: |
node --test test/back/ql3LocalImageAudit.test.cjs
node --test test/back/ql3LocalOperatorImageAudit.test.cjs
node --test test/back/ql3LocalAlphaMilestone.test.cjs
pnpm audit:local-image:ql3
pnpm audit:local-operator-image:ql3
- name: Build the native AI-excluded image
@@ -587,7 +597,7 @@ jobs:
node scripts/ql3-local-api-cancellation-live-audit.cjs \
--report="${{ runner.temp }}/ql3-local-api-cancellation/standalone.json"
- name: Materialize and offline-audit the native Local Alpha trial kit
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
env:
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }}
@@ -622,7 +632,7 @@ jobs:
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
--mode=audit --bundle="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
- name: Upload the tested native Local Alpha trial kit
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ql3-alpha-${{ github.sha }}-local-${{ matrix.image_arch }}
@@ -825,7 +835,7 @@ jobs:
--image=${{ matrix.image }}
--inventory-root=/opt/qinglong/node_modules
- name: Package the tested native Cluster Alpha image
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'cluster' || inputs.alpha_artifact_scope == 'all')
env:
IMAGE: ${{ matrix.repository }}:ci-${{ matrix.image_arch }}
IMAGE_ARCH: ${{ matrix.image_arch }}
@@ -874,7 +884,7 @@ jobs:
fs.writeFileSync(process.env.MANIFEST, `${JSON.stringify(manifest)}\n`, { mode: 0o600, flag: 'wx' });
NODE
- name: Upload the tested native Cluster Alpha image
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'cluster' || inputs.alpha_artifact_scope == 'all')
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ql3-alpha-${{ github.sha }}-${{ matrix.image }}-${{ matrix.image_arch }}
@@ -1632,3 +1642,73 @@ jobs:
path: ${{ runner.temp }}/ql3-plugin-package-recovery-e2e/report.json
if-no-files-found: warn
retention-days: 14
local-alpha-milestone:
name: Finalize the Local Alpha milestone
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
needs:
- backend
- service-manager-bridge
- linux-resource-envelopes
- linux-resource-release-evidence
- supply-chain
- local-image
- cluster-image
- cluster-console-capacity-release-evidence
- image-oci
- worker-runtime
- local-profiles
- cluster-postgres
- cluster-postgres-ha
- cluster-cloudnativepg-live
- cluster-provider-credential-test-kubernetes-live
- cluster-secret-binding-mounted-provider-kubernetes-live
- cluster-vault-kv-worker-secret-live
- cluster-plugin-package-kubernetes-live
- cluster-plugin-package-recovery-e2e
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24.18.0'
- name: Download the exact amd64 Local Alpha trial kit
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ql3-alpha-${{ github.sha }}-local-amd64
path: ${{ runner.temp }}/ql3-alpha-local-milestone-input/amd64
- name: Download the exact arm64 Local Alpha trial kit
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ql3-alpha-${{ github.sha }}-local-arm64
path: ${{ runner.temp }}/ql3-alpha-local-milestone-input/arm64
- name: Close both native trial kits into one milestone index
run: |
set -euo pipefail
umask 077
node scripts/ql3-local-alpha-milestone.cjs \
--mode=finalize \
--amd64-bundle="${RUNNER_TEMP}/ql3-alpha-local-milestone-input/amd64" \
--arm64-bundle="${RUNNER_TEMP}/ql3-alpha-local-milestone-input/arm64" \
--source-revision=${{ github.sha }} \
--repository=${{ github.repository }} \
--workflow-ref="${{ github.workflow_ref }}" \
--workflow-sha=${{ github.workflow_sha }} \
--event=${{ github.event_name }} \
--run-id=${{ github.run_id }} \
--run-attempt=${{ github.run_attempt }} \
--readme=docs/operations/ql3-local-alpha-milestone.md \
--output="${RUNNER_TEMP}/ql3-alpha-local-milestone"
node scripts/ql3-local-alpha-milestone.cjs \
--mode=audit \
--milestone="${RUNNER_TEMP}/ql3-alpha-local-milestone"
- name: Upload the closed Local Alpha milestone index
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ql3-alpha-${{ github.sha }}-local-milestone
path: ${{ runner.temp }}/ql3-alpha-local-milestone
if-no-files-found: error
retention-days: 30
compression-level: 0
overwrite: false
include-hidden-files: false
+2
View File
@@ -11,6 +11,8 @@
最新增量证据(2026-08-27):
- D-413/ADR-0508(已实现,首份真实 milestone artifact 待授权):Local Alpha 不再把两个矩阵 job 各自上传的 amd64/arm64 大归档直接视为阶段版本。新增独立发布期 `local-alpha-milestone` finalizer,只有完整 `QingLong 3.0 CI` 的 19 个 job dependency 全部成功后,才重新下载并离线审计两套 v2 Trial Kit,要求同一 version/source/workflow SHA/run/attempt、精确 `amd64|arm64`、四个不同 Application/operator image ID、两个不同 archive digest 与 verification digest,再上传 `qinglong/alpha-local-milestone@v1` 三文件小型索引。没有 index 的孤立 artifact 明确是失败运行中间文件,不能交付用户。手动门新增 `alpha_artifact_scope=local|cluster|all` 且默认 Local,避免路由/NAS 试用无谓生成八套 Cluster archive;完整测试不因 scope 缩小。显式 milestone 以 run ID 隔离并发域且不被后续普通 push 取消,source 仍绑定触发时 exact SHA。该 finalizer 不新增 workspace package、镜像 layer、设备依赖、daemon、listener、timer、连接池或稳态 RSS;真实可下载产物仍需维护者明确触发 `produce_alpha_artifacts=true + alpha_artifact_scope=local` 后记录 index 与双 bundle digest。
- D-412/ADR-0507(已实现,真实公开发布待受保护 tag):Public Local Release Set 从“只签 Application、用户旅程却依赖另一个未发布 operator”的断层收敛为一对分别构建、扫描、签名和 multi-arch attestation 的镜像:`local` 是唯一常驻 Application`local-operator` 只承担 setup/upgrade/recovery 等短生命周期 Owner authority。release candidate 的 Local scope 精确包含两者,并分别要求 Application Edge/Standalone rollout 与 operator `--version`/`setup --help` 门;CI OCI 证据、OS 漏洞策略、release-set、catalog consumption、final tag closure 和 Local selection 均按六镜像总闭包升级。`qinglong/release-set-image-record@v2``qinglong/release-set@v4``application/vnd.qinglong.release-set.v4+json``qinglong/local-compose-release-image@v3``qinglong/local-compose-image-selection@v3` 失败关闭旧孵化 schema。Compose revision 只保存 operator digest 作为管理 authority,不生成 operator service,所以路由设备稳态仍只有 Application,没有新增进程、listener、timer、端口或 RSS;管理动作才短暂下载/运行 operator。18-package clean build 退出 0package boundary 保持 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`Local Owner CLI 为 301 total / 294 pass / 7 conditional skip / 0 failbackend 为 1,610 total / 1,608 pass / 2 conditional skip / 0 fail,静态 release workflow 审计 100/100。该切片证明发布机制闭合,不冒充已经存在 GHCR tag 或真实用户可下载 Public Release Set;首份正式可交付物仍需受保护 release tag 的六镜像、签名、catalog-bound Local/Cluster 部署证据和终态 closure。
- 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。
@@ -0,0 +1,77 @@
# ADR-0508:跨架构 Local Alpha 里程碑闭合
- 状态:Accepted(首份实际 milestone artifact 待维护者授权)
- 日期:2026-08-27
- 决策:D-413
- 关联:ADR-0503、ADR-0504、ADR-0506、ADR-0507
## 背景
Local Alpha workflow 已能在原生 amd64、arm64 runner 上分别生成 Application/operator 双镜像 Trial Kit,但两个矩阵作业各自上传 artifact。只要其中一个架构上传成功,GitHub Actions 页面就会留下一个可下载大归档,即使另一架构、PostgreSQL HA、资源门或其余完整 CI 随后失败。
这类孤立文件证明一个矩阵作业曾走到上传步骤,不能证明 QingLong 3.0 已形成阶段版本。开发二十天后的阶段产物必须有一个用户可识别的成功终态,而不是让部署者从几十个 job 和零散 artifact 猜测“哪些可以用”。
原有 boolean 还会同时生成 Local 两套与 Cluster 八套原生归档;只想验证低配路由/NAS 的维护者必须无谓承担 Cluster artifact 的构建归档和存储成本。手动 milestone 与普通 `next` push 共享 `cancel-in-progress` 并发域,也可能在大归档生成过程中被后续提交取消。
## 决策
### 1. Local Alpha 必须以跨架构 milestone index 为完成信号
新增发布期脚本 `ql3-local-alpha-milestone.cjs``local-alpha-milestone` job 只有在完整 `QingLong 3.0 CI` 的所有现有 job 成功后才运行,并重新下载、离线审计同一 run/attempt 的 amd64 与 arm64 Trial Kit。
finalizer 精确要求:
- 两套 bundle 具有同一版本、完整 source revision、workflow SHA、run ID 与 attempt
- 架构集合恰好为 `amd64|arm64`
- 两个 archive digest、两份 verification digest 和四个 Application/operator image ID 相互分离;
- 每套 bundle 自身的七文件闭包、SBOM、manifest、verification evidence 与 checksum 继续由 v2 auditor 通过;
- artifact 名称从 source 与架构唯一推导,调用方不能自报。
成功后只上传三文件小型索引:`manifest.json``README.md``SHA256SUMS`schema 为 `qinglong/alpha-local-milestone@v1`。索引绑定两个 artifact 名、各自 bundle manifest digest、archive digest、镜像 ID、verification digest 和 workflow identity。没有该索引的零散 Trial Kit 明确是失败或未闭合运行的中间文件,不是阶段交付物。
### 2. 手动产物按产品域选择
保留显式 `produce_alpha_artifacts=true` 授权,并增加 `alpha_artifact_scope=local|cluster|all`,默认 `local`
- `local` 只归档两套 Local Trial Kit 并生成 milestone index
- `cluster` 只归档 Cluster Integration Candidate 原生镜像,不生成 Local index
- `all` 同时生成两类。
无论选择哪个归档 scope,完整 CI 仍执行;scope 只控制大体积 artifact 的物化和上传,不跳过测试门,也不改变 Edge/Standalone/Cluster runtime closure。
### 3. Milestone run 不由普通 push 取消
普通 push/PR 继续共享 validation 并发域并允许 newer run 取消旧 run。显式 artifact milestone 使用自身 run ID 作为并发域,`cancel-in-progress=false`;后续 push 不会中断已经授权的双架构产物。checkout、evidence 与 index 仍绑定触发时的 exact SHA,不读取移动后的 branch head。
## 被拒绝的替代方案
### 把两个独立 artifact 都称为 Alpha
拒绝。部分上传、另一架构失败或完整 CI 失败时没有唯一成功信号,部署者无法可靠裁决成熟度。
### 把两个架构 archive 再复制进一个总 artifact
拒绝。每位用户只需要自己的架构;总包会重复下载和存储数百 MiB,并对低容量设备无益。小型 index 足以闭合身份和 digest。
### Local 两个矩阵 job 成功后立即发布 index
拒绝。同一源码的 backend、资源、供应链、PostgreSQL HA 或 Kubernetes live gate 仍可能失败。阶段版本必须等待完整 CI,而不是只等待镜像局部路径。
### 赋予 finalizer 删除孤立 artifact 的权限
拒绝。索引缺失已经能失败关闭,增加 `actions: write` 和删除 authority 会扩大 workflow 权限与事故半径。孤立文件按 30 天 retention 自动过期。
## 影响
- 首个真实 Local Alpha milestone 多一次双 artifact 下载和离线复核,只发生在显式里程碑运行;
- 普通 push/PR 不生成大归档或 milestone indexCI 成本基本不变;
- 默认 Local scope 不再无谓生成八套 Cluster archive
- 新脚本和索引属于发布期仓库工具,不新增 workspace package、设备依赖、镜像 layer、常驻进程、端口、timer、连接池或 RSS;
- Public Release Set 仍由受保护 tag、不可变 registry digest、签名、attestation、catalog 和 deployment closure 独立裁决。
## 验证
- finalizer 正向测试生成 exact 三文件 index,并复核双架构、run/attempt、四镜像主体与 archive digest
- 负向测试覆盖跨 attempt、跨架构 image identity 复用、index mutation、额外文件和 CLI grammar
- 静态 workflow audit 固定 scope 条件、milestone 独立并发域、19 个完整 CI dependency、双 artifact 下载、`finalize → audit → upload` 顺序和 30 天 retention
- 首份真实 `produce_alpha_artifacts=true + alpha_artifact_scope=local` 产物仍需维护者显式授权,生成后再把 ADR-0503 从 Proposed 转为 Accepted 并记录 index/bundle digest。
+1
View File
@@ -511,6 +511,7 @@
| [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 |
| [ADR-0507](./ADR-0507-public-local-application-and-operator-release-pair.md) | Public Local Application 与 Operator 发布对 | Accepted(首份真实公开发布待受保护 tag) |
| [ADR-0508](./ADR-0508-cross-architecture-local-alpha-milestone-closure.md) | 跨架构 Local Alpha 里程碑闭合 | Accepted(首份实际 milestone artifact 待维护者授权) |
## 规则
@@ -0,0 +1,44 @@
# QingLong 3.0 Local Alpha Milestone
本目录是一次完整 Local Alpha milestone run 的跨架构闭合索引。它本身不包含大体积 Docker archive`manifest.json` 精确列出同一次 GitHub Actions run 生成并重新审计的 amd64、arm64 Trial Kit artifact。只有该索引 artifact 与对应架构 Trial Kit 同时存在时,才能把那次运行称为阶段性可下载 Local Alpha。
## 成熟度边界
- `maturity` 固定为 `alpha_candidate_not_public_release`
- 适用于 fresh、隔离、非生产数据上的 Edge/Standalone 试运行;
- 不提供公开 GHCR tag、Cosign 签名、GitHub attestation、catalog、生产升级或长期支持承诺;
- Cluster/Kubernetes 仍按独立 Integration Candidate 与 Public Release Set 门验收。
单个架构 artifact 提前上传并不代表 milestone 成功。没有 `ql3-alpha-<sourceRevision>-local-milestone` 索引、索引 workflow 不是成功终态、run/attempt 不一致或索引审计失败时,已有的大归档只能作为失败运行的中间文件,不得交付用户。
## 选择并验证下载物
1. 对本目录执行:
```sh
sha256sum --check SHA256SUMS
```
2. 打开 `manifest.json`,确认:
- `schema``qinglong/alpha-local-milestone@v1`
- `sourceRevision` 是准备试用的完整 40 位提交;
- `workflow.event``workflow_dispatch``workflow.job``local-alpha-milestone`
- GitHub Actions 中对应 `runId/runAttempt` 的整条 `QingLong 3.0 CI` 为成功终态;
- `artifacts` 恰好包含 `amd64``arm64`
3. 根据主机架构下载 `artifacts.<architecture>.artifactName` 指向的 Trial Kit。
4. 对 Trial Kit 先执行其 `SHA256SUMS`,再确认其中 `manifest.json` 的 SHA-256 与 milestone 的 `bundleManifest.sha256` 完全一致。
5. 按 Trial Kit 自带 `README.md` 完成 Docker archive 加载、镜像 ID 对账和受限资源 smoke。
若持有同一版本源码与 Node.js 24,可额外审计 milestone 索引:
```sh
node scripts/ql3-local-alpha-milestone.cjs \
--mode=audit \
--milestone=/absolute/path/to/ql3-alpha-local-milestone
```
该命令验证索引目录的闭合文件集、checksum、双架构记录、镜像主体分离和 workflow identity。对应架构 Trial Kit 仍必须使用它自己的离线 auditor;索引审计不会重新执行 Docker、漏洞扫描或真实用户旅程。
## 运行与回退
实际运行只需要匹配主机架构的一套 Trial Kit,不需要同时下载另一架构。低配路由器稳态只运行 Local ApplicationOperator 只在 setup、upgrade 或 recovery 动作期间短暂运行。停止并删除 Alpha 容器及 fresh 测试目录即可回退,不能把本套件直接指向 2.x 唯一数据目录。
@@ -2,6 +2,8 @@
本目录是绑定一个 QingLong 3.0 源码提交、一个 Linux 架构和一次显式 GitHub milestone run 的阶段试运行套件,不是公开 release 或生产升级承诺。它同时包含常驻 Application 镜像和短生命周期 operator 镜像;两者共享的 OCI layer 只在同一个 Docker archive 中保存一次。
一套 Trial Kit 只有被同一 run 的 `ql3-alpha-<sourceRevision>-local-milestone` 跨架构索引收录后才是可交付阶段产物。单个矩阵 job 提前上传、另一架构或完整 CI 随后失败时留下的 artifact 只是中间文件。先按 milestone `manifest.json` 选择本机架构并核对本 bundle manifest digest,再执行下述离线验收。
## 适用范围
- `amd64``arm64` Linux Docker 主机;
+1
View File
@@ -40,6 +40,7 @@
"test:local-api-cancellation-live:ql3": "node scripts/ql3-local-api-cancellation-live-contract.cjs",
"test:local-alpha-trial-kit:ql3": "node scripts/ql3-local-alpha-trial-kit-live-contract.cjs",
"bundle:local-alpha-trial-kit:ql3": "node scripts/ql3-local-alpha-trial-kit-bundle.cjs",
"finalize:local-alpha-milestone:ql3": "node scripts/ql3-local-alpha-milestone.cjs",
"audit:local-api-cancellation-live:ql3": "node scripts/ql3-local-api-cancellation-live-audit.cjs",
"test:provider-credential-test-kubernetes-live:ql3": "pnpm --filter @qinglong/cluster-admin build && node scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs",
"audit:provider-credential-test-kubernetes-live:ql3": "node scripts/ql3-provider-credential-test-kubernetes-live-audit.cjs",
+644
View File
@@ -0,0 +1,644 @@
#!/usr/bin/env node
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const {
auditLocalAlphaTrialKit,
sha256File,
} = require('./ql3-local-alpha-trial-kit-bundle.cjs');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const SCHEMA = 'qinglong/alpha-local-milestone@v1';
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
const FILES = Object.freeze({
readme: 'README.md',
manifest: 'manifest.json',
checksums: 'SHA256SUMS',
});
const WORKFLOW_IDENTITY = Object.freeze({
repository: 'whyour/qinglong',
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
event: 'workflow_dispatch',
job: 'local-alpha-milestone',
});
const REQUIRED_WORKFLOW_NEEDS = Object.freeze([
'backend',
'service-manager-bridge',
'linux-resource-envelopes',
'linux-resource-release-evidence',
'supply-chain',
'local-image',
'cluster-image',
'cluster-console-capacity-release-evidence',
'image-oci',
'worker-runtime',
'local-profiles',
'cluster-postgres',
'cluster-postgres-ha',
'cluster-cloudnativepg-live',
'cluster-provider-credential-test-kubernetes-live',
'cluster-secret-binding-mounted-provider-kubernetes-live',
'cluster-vault-kv-worker-secret-live',
'cluster-plugin-package-kubernetes-live',
'cluster-plugin-package-recovery-e2e',
]);
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
const DECIMAL_ID_PATTERN = /^[1-9][0-9]{0,19}$/u;
const ATTEMPT_PATTERN = /^[1-9][0-9]{0,5}$/u;
const MAX_JSON_BYTES = 4 * 1024 * 1024;
const MAX_README_BYTES = 512 * 1024;
function fail(message) {
throw new Error(message);
}
function exactKeys(value, expected) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
JSON.stringify(Object.keys(value)) === JSON.stringify(expected)
);
}
function assertCanonicalFile(filePath, maximumBytes, label) {
const resolved = path.resolve(filePath);
const stat = fs.lstatSync(resolved);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.size < 2 ||
stat.size > maximumBytes ||
fs.realpathSync(resolved) !== resolved
) {
fail(`${label} must be one bounded canonical regular file`);
}
return resolved;
}
function readBoundedJson(filePath, label) {
const resolved = assertCanonicalFile(filePath, MAX_JSON_BYTES, label);
try {
return JSON.parse(fs.readFileSync(resolved, 'utf8'));
} catch {
fail(`${label} must contain valid JSON`);
}
}
function writeExclusive(filePath, contents, mode = 0o600) {
const descriptor = fs.openSync(
filePath,
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
mode,
);
try {
fs.writeFileSync(descriptor, contents);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function copyExclusive(source, destination) {
fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
fs.chmodSync(destination, 0o600);
}
function fileRecord(filePath, name) {
const stat = fs.lstatSync(filePath);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 2) {
fail(`milestone file is invalid: ${name}`);
}
return Object.freeze({
file: name,
sha256: sha256File(filePath),
bytes: stat.size,
});
}
function checksumContents(root, names) {
return `${names
.map((name) => `${sha256File(path.join(root, name)).slice(7)} ${name}`)
.join('\n')}\n`;
}
function artifactName(sourceRevision, architecture) {
return `ql3-alpha-${sourceRevision}-local-${architecture}`;
}
function validateIdentity(options) {
if (
!REVISION_PATTERN.test(options.sourceRevision || '') ||
options.repository !== WORKFLOW_IDENTITY.repository ||
options.workflowRef !== WORKFLOW_IDENTITY.workflowRef ||
options.workflowSha !== options.sourceRevision ||
options.eventName !== WORKFLOW_IDENTITY.event ||
!DECIMAL_ID_PATTERN.test(options.runId || '') ||
!ATTEMPT_PATTERN.test(options.runAttempt || '')
) {
fail('milestone workflow identity is invalid');
}
}
function validateFinalizeOptions(options) {
validateIdentity(options);
const root = fs.realpathSync(path.resolve(options.root || DEFAULT_ROOT));
const outputRoot = path.resolve(options.outputRoot || '');
const parent = path.dirname(outputRoot);
if (
!path.isAbsolute(outputRoot) ||
fs.existsSync(outputRoot) ||
fs.realpathSync(parent) !== parent
) {
fail('milestone output is invalid');
}
const bundles = {};
for (const architecture of ARCHITECTURES) {
const bundleRoot = fs.realpathSync(
path.resolve(options.bundles?.[architecture] || ''),
);
if (!fs.lstatSync(bundleRoot).isDirectory()) {
fail(`${architecture} bundle root is invalid`);
}
bundles[architecture] = bundleRoot;
}
if (bundles.amd64 === bundles.arm64) {
fail('milestone architectures must use distinct bundles');
}
return Object.freeze({
root,
outputRoot,
bundles: Object.freeze(bundles),
readme: assertCanonicalFile(
options.readme,
MAX_README_BYTES,
'milestone README',
),
sourceRevision: options.sourceRevision,
repository: options.repository,
workflowRef: options.workflowRef,
workflowSha: options.workflowSha,
eventName: options.eventName,
runId: options.runId,
runAttempt: options.runAttempt,
});
}
function bundleRecord(options, architecture) {
const bundleRoot = options.bundles[architecture];
const report = auditLocalAlphaTrialKit({ bundleRoot });
if (
report.compatible !== true ||
report.architecture !== architecture ||
report.sourceRevision !== options.sourceRevision ||
report.workflowRunId !== options.runId ||
report.workflowRunAttempt !== options.runAttempt
) {
fail(`${architecture} trial kit is detached from the milestone run`);
}
return Object.freeze({
artifactName: artifactName(options.sourceRevision, architecture),
architecture,
bundleManifest: fileRecord(
path.join(bundleRoot, 'manifest.json'),
'manifest.json',
),
archiveSha256: report.archiveSha256,
applicationImageId: report.applicationImageId,
operatorImageId: report.operatorImageId,
verificationSha256: report.verificationSha256,
});
}
function validateArtifactRecord(record, architecture, manifest) {
if (
!exactKeys(record, [
'artifactName',
'architecture',
'bundleManifest',
'archiveSha256',
'applicationImageId',
'operatorImageId',
'verificationSha256',
]) ||
record.artifactName !==
artifactName(manifest.sourceRevision, architecture) ||
record.architecture !== architecture ||
!exactKeys(record.bundleManifest, ['file', 'sha256', 'bytes']) ||
record.bundleManifest.file !== 'manifest.json' ||
!SHA256_PATTERN.test(record.bundleManifest.sha256 || '') ||
!Number.isSafeInteger(record.bundleManifest.bytes) ||
record.bundleManifest.bytes < 2 ||
!SHA256_PATTERN.test(record.archiveSha256 || '') ||
!SHA256_PATTERN.test(record.applicationImageId || '') ||
!SHA256_PATTERN.test(record.operatorImageId || '') ||
!SHA256_PATTERN.test(record.verificationSha256 || '') ||
record.applicationImageId === record.operatorImageId
) {
fail(`${architecture} milestone artifact record is incompatible`);
}
}
function validateWorkflow(document, sourceRevision, runId, runAttempt) {
if (
!exactKeys(document, [
'repository',
'workflowRef',
'workflowSha',
'event',
'job',
'runId',
'runAttempt',
]) ||
document.repository !== WORKFLOW_IDENTITY.repository ||
document.workflowRef !== WORKFLOW_IDENTITY.workflowRef ||
document.workflowSha !== sourceRevision ||
document.event !== WORKFLOW_IDENTITY.event ||
document.job !== WORKFLOW_IDENTITY.job ||
document.runId !== runId ||
document.runAttempt !== runAttempt ||
!DECIMAL_ID_PATTERN.test(document.runId || '') ||
!ATTEMPT_PATTERN.test(document.runAttempt || '')
) {
fail('milestone manifest workflow identity is incompatible');
}
}
function auditLocalAlphaMilestone(options) {
const milestoneRoot = fs.realpathSync(
path.resolve(options.milestoneRoot || ''),
);
if (!fs.lstatSync(milestoneRoot).isDirectory()) {
fail('milestone root must be a canonical directory');
}
const expectedFiles = Object.values(FILES).sort();
const actualFiles = fs
.readdirSync(milestoneRoot, { withFileTypes: true })
.map((entry) => {
if (!entry.isFile() || entry.isSymbolicLink()) {
fail(`milestone contains a non-regular entry: ${entry.name}`);
}
return entry.name;
})
.sort();
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
fail('milestone file set is not closed');
}
const manifest = readBoundedJson(
path.join(milestoneRoot, FILES.manifest),
'milestone manifest',
);
if (
!exactKeys(manifest, [
'schemaVersion',
'schema',
'maturity',
'product',
'version',
'sourceRevision',
'workflow',
'artifacts',
'readme',
]) ||
manifest.schemaVersion !== 1 ||
manifest.schema !== SCHEMA ||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
manifest.product !== 'local' ||
typeof manifest.version !== 'string' ||
manifest.version.length < 3 ||
manifest.version.length > 64 ||
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
!exactKeys(manifest.artifacts, ARCHITECTURES) ||
!exactKeys(manifest.readme, ['file', 'sha256', 'bytes']) ||
manifest.readme.file !== FILES.readme ||
!SHA256_PATTERN.test(manifest.readme.sha256 || '') ||
!Number.isSafeInteger(manifest.readme.bytes) ||
manifest.readme.bytes < 2
) {
fail('milestone manifest identity or shape is incompatible');
}
validateWorkflow(
manifest.workflow,
manifest.sourceRevision,
manifest.workflow?.runId,
manifest.workflow?.runAttempt,
);
for (const architecture of ARCHITECTURES) {
validateArtifactRecord(
manifest.artifacts[architecture],
architecture,
manifest,
);
}
const records = ARCHITECTURES.map(
(architecture) => manifest.artifacts[architecture],
);
const imageIds = records.flatMap((record) => [
record.applicationImageId,
record.operatorImageId,
]);
if (
new Set(imageIds).size !== imageIds.length ||
new Set(records.map((record) => record.archiveSha256)).size !==
ARCHITECTURES.length ||
new Set(records.map((record) => record.verificationSha256)).size !==
ARCHITECTURES.length
) {
fail('milestone architecture subjects are not distinct');
}
const actualReadme = fileRecord(
path.join(milestoneRoot, FILES.readme),
FILES.readme,
);
if (
actualReadme.sha256 !== manifest.readme.sha256 ||
actualReadme.bytes !== manifest.readme.bytes
) {
fail('milestone README differs from manifest');
}
const expectedChecksums = checksumContents(milestoneRoot, [
FILES.readme,
FILES.manifest,
]);
const actualChecksums = fs.readFileSync(
path.join(milestoneRoot, FILES.checksums),
'utf8',
);
if (actualChecksums !== expectedChecksums) {
fail('milestone SHA256SUMS differs from the closed file set');
}
return Object.freeze({
schemaVersion: 1,
schema: 'qinglong/alpha-local-milestone-audit@v1',
sourceRevision: manifest.sourceRevision,
version: manifest.version,
workflowRunId: manifest.workflow.runId,
workflowRunAttempt: manifest.workflow.runAttempt,
architectures: [...ARCHITECTURES],
compatible: true,
});
}
function finalizeLocalAlphaMilestone(options) {
const normalized = validateFinalizeOptions(options);
const artifacts = {
amd64: bundleRecord(normalized, 'amd64'),
arm64: bundleRecord(normalized, 'arm64'),
};
const versions = new Set(
ARCHITECTURES.map((architecture) => {
const manifest = readBoundedJson(
path.join(normalized.bundles[architecture], 'manifest.json'),
`${architecture} trial kit manifest`,
);
return manifest.version;
}),
);
const release = readReleaseIdentity(normalized.root);
if (versions.size !== 1 || !versions.has(release.version)) {
fail('milestone trial kits must have one release version');
}
const allImageIds = ARCHITECTURES.flatMap((architecture) => [
artifacts[architecture].applicationImageId,
artifacts[architecture].operatorImageId,
]);
if (
new Set(allImageIds).size !== allImageIds.length ||
artifacts.amd64.archiveSha256 === artifacts.arm64.archiveSha256 ||
artifacts.amd64.verificationSha256 === artifacts.arm64.verificationSha256
) {
fail('milestone architecture subjects must be distinct');
}
let created = false;
try {
fs.mkdirSync(normalized.outputRoot, { mode: 0o700 });
created = true;
copyExclusive(
normalized.readme,
path.join(normalized.outputRoot, FILES.readme),
);
const manifest = {
schemaVersion: 1,
schema: SCHEMA,
maturity: 'alpha_candidate_not_public_release',
product: 'local',
version: [...versions][0],
sourceRevision: normalized.sourceRevision,
workflow: {
repository: normalized.repository,
workflowRef: normalized.workflowRef,
workflowSha: normalized.workflowSha,
event: normalized.eventName,
job: WORKFLOW_IDENTITY.job,
runId: normalized.runId,
runAttempt: normalized.runAttempt,
},
artifacts,
readme: fileRecord(
path.join(normalized.outputRoot, FILES.readme),
FILES.readme,
),
};
writeExclusive(
path.join(normalized.outputRoot, FILES.manifest),
`${JSON.stringify(manifest, null, 2)}\n`,
);
writeExclusive(
path.join(normalized.outputRoot, FILES.checksums),
checksumContents(normalized.outputRoot, [FILES.readme, FILES.manifest]),
);
auditLocalAlphaMilestone({ milestoneRoot: normalized.outputRoot });
return Object.freeze(manifest);
} catch (error) {
if (created) {
fs.rmSync(normalized.outputRoot, { recursive: true, force: true });
}
throw error;
}
}
function countOccurrences(contents, token) {
return contents.split(token).length - 1;
}
function auditLocalAlphaMilestoneWorkflow(root = DEFAULT_ROOT) {
const workflowPath = path.join(
fs.realpathSync(path.resolve(root)),
'.github/workflows/ql3-ci.yml',
);
const workflow = fs.readFileSync(workflowPath, 'utf8');
const findings = [];
const milestoneStart = workflow.indexOf('\n local-alpha-milestone:\n');
const milestone = milestoneStart >= 0 ? workflow.slice(milestoneStart) : '';
const requiredWorkflowTokens = [
'alpha_artifact_scope:',
'default: local',
'- local',
'- cluster',
'- all',
"github.run_id || 'validation'",
"cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts) }}",
];
if (requiredWorkflowTokens.some((token) => !workflow.includes(token))) {
findings.push('MILESTONE_DISPATCH_OR_CONCURRENCY_DRIFT');
}
const localScopeCondition =
"github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')";
const clusterScopeCondition =
"github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'cluster' || inputs.alpha_artifact_scope == 'all')";
if (
countOccurrences(workflow, localScopeCondition) !== 3 ||
countOccurrences(workflow, clusterScopeCondition) !== 2
) {
findings.push('MILESTONE_SCOPE_CONTRACT_DRIFT');
}
const milestoneTokens = [
' name: Finalize the Local Alpha milestone',
' needs:',
'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c',
`name: ql3-alpha-${'${{ github.sha }}'}-local-amd64`,
`name: ql3-alpha-${'${{ github.sha }}'}-local-arm64`,
'scripts/ql3-local-alpha-milestone.cjs',
'--mode=finalize',
'--mode=audit',
`name: ql3-alpha-${'${{ github.sha }}'}-local-milestone`,
'retention-days: 30',
'overwrite: false',
];
if (
milestoneStart < 0 ||
milestoneTokens.some((token) => !milestone.includes(token)) ||
REQUIRED_WORKFLOW_NEEDS.some(
(job) => !milestone.includes(` - ${job}\n`),
)
) {
findings.push('MILESTONE_FINALIZER_CONTRACT_DRIFT');
}
const finalizerIndex = milestone.indexOf('--mode=finalize');
const auditIndex = milestone.indexOf('--mode=audit');
const uploadIndex = milestone.lastIndexOf('actions/upload-artifact@');
if (
finalizerIndex < 0 ||
auditIndex <= finalizerIndex ||
uploadIndex <= auditIndex
) {
findings.push('MILESTONE_FINALIZER_GATE_ORDER_DRIFT');
}
return Object.freeze({
schemaVersion: 1,
schema: 'qinglong/alpha-local-milestone-workflow-audit@v1',
requiredNeeds: [...REQUIRED_WORKFLOW_NEEDS],
findings: Object.freeze(findings),
compatible: findings.length === 0,
});
}
function parseArguments(argv) {
const values = {};
for (const argument of argv) {
const match = /^--([a-z0-9-]+)=(.+)$/u.exec(argument);
if (!match || Object.hasOwn(values, match[1])) {
fail('arguments are invalid');
}
values[match[1]] = match[2];
}
if (values.mode === 'audit') {
if (
JSON.stringify(Object.keys(values).sort()) !==
JSON.stringify(['milestone', 'mode'])
) {
fail('audit arguments are invalid');
}
return { mode: 'audit', milestoneRoot: path.resolve(values.milestone) };
}
if (values.mode === 'audit-workflow') {
if (
JSON.stringify(Object.keys(values).sort()) !==
JSON.stringify(['mode', 'root'])
) {
fail('workflow audit arguments are invalid');
}
return { mode: 'audit-workflow', root: path.resolve(values.root) };
}
const expected = [
'amd64-bundle',
'arm64-bundle',
'event',
'mode',
'output',
'readme',
'repository',
'run-attempt',
'run-id',
'source-revision',
'workflow-ref',
'workflow-sha',
];
if (
values.mode !== 'finalize' ||
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
) {
fail('finalize arguments are invalid');
}
return {
mode: 'finalize',
outputRoot: path.resolve(values.output),
bundles: {
amd64: path.resolve(values['amd64-bundle']),
arm64: path.resolve(values['arm64-bundle']),
},
readme: path.resolve(values.readme),
sourceRevision: values['source-revision'],
repository: values.repository,
workflowRef: values['workflow-ref'],
workflowSha: values['workflow-sha'],
eventName: values.event,
runId: values['run-id'],
runAttempt: values['run-attempt'],
};
}
function runCli(argv) {
const options = parseArguments(argv);
let report;
if (options.mode === 'finalize') {
report = finalizeLocalAlphaMilestone(options);
} else if (options.mode === 'audit-workflow') {
report = auditLocalAlphaMilestoneWorkflow(options.root);
if (!report.compatible) fail(JSON.stringify(report));
} else {
report = auditLocalAlphaMilestone(options);
}
process.stdout.write(`${JSON.stringify(report)}\n`);
return report;
}
if (require.main === module) {
try {
runCli(process.argv.slice(2));
} catch (error) {
process.stderr.write(
`${
error instanceof Error ? error.message : 'Local Alpha milestone failed'
}\n`,
);
process.exitCode = 1;
}
}
module.exports = Object.freeze({
ARCHITECTURES,
FILES,
REQUIRED_WORKFLOW_NEEDS,
SCHEMA,
artifactName,
auditLocalAlphaMilestone,
auditLocalAlphaMilestoneWorkflow,
finalizeLocalAlphaMilestone,
parseArguments,
runCli,
});
+299
View File
@@ -0,0 +1,299 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
auditLocalAlphaTrialKit,
createLocalAlphaTrialKit,
createLocalAlphaTrialKitVerificationEvidence,
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
const {
auditLocalAlphaMilestone,
auditLocalAlphaMilestoneWorkflow,
finalizeLocalAlphaMilestone,
parseArguments,
} = require('../../scripts/ql3-local-alpha-milestone.cjs');
const {
createClusterImageSbom,
} = require('../../scripts/ql3-cluster-image-sbom.cjs');
const {
readReleaseIdentity,
} = require('../../scripts/lib/ql3-release-identity.cjs');
const root = path.resolve(__dirname, '../..');
const version = readReleaseIdentity(root).version;
const revision = 'a'.repeat(40);
const runId = '33047425710';
const runAttempt = '2';
function imageInspection(role, architecture, idCharacter) {
return {
Id: `sha256:${idCharacter.repeat(64)}`,
Os: 'linux',
Architecture: architecture,
Config: {
User: '65532:65532',
Labels: {
'org.opencontainers.image.title':
role === 'application'
? 'QingLong 3.0 Local Application'
: 'QingLong 3.0 Local Operator',
'org.opencontainers.image.source': 'https://github.com/whyour/qinglong',
'org.opencontainers.image.revision': revision,
'org.opencontainers.image.version': version,
...(role === 'application'
? {
'io.qinglong.profile': 'edge,standalone',
'io.qinglong.ai': 'excluded',
}
: {
'io.qinglong.lifecycle': 'short-lived',
'io.qinglong.authority': 'local-owner-management',
'io.qinglong.network': 'none-by-default',
}),
},
},
};
}
function createBundle(fixtureRoot, architecture, options = {}) {
const bundleFixture = path.join(fixtureRoot, architecture);
fs.mkdirSync(bundleFixture);
const applicationSbom = path.join(bundleFixture, 'application.json');
const operatorSbom = path.join(bundleFixture, 'operator.json');
const verificationEvidence = path.join(bundleFixture, 'verification.json');
const readme = path.join(bundleFixture, 'README-source.md');
const outputRoot = path.join(bundleFixture, 'bundle');
fs.writeFileSync(
applicationSbom,
`${JSON.stringify(createClusterImageSbom({ root, image: 'local' }))}\n`,
);
fs.writeFileSync(
operatorSbom,
`${JSON.stringify(
createClusterImageSbom({ root, image: 'local-operator' }),
)}\n`,
);
fs.writeFileSync(readme, '# Local Alpha Trial Kit\n');
const defaultCharacters =
architecture === 'amd64'
? { application: '1', operator: '2' }
: { application: '3', operator: '4' };
const characters = options.characters || defaultCharacters;
const applicationImage = `qinglong3-local-application:test-${architecture}`;
const operatorImage = `qinglong3-local-operator:test-${architecture}`;
const adapters = {
inspectImage(image) {
const role = image.includes('operator') ? 'operator' : 'application';
return imageInspection(role, architecture, characters[role]);
},
saveImages(images, archivePath) {
assert.deepEqual(images, [applicationImage, operatorImage]);
fs.writeFileSync(
archivePath,
Buffer.alloc(2048, architecture === 'amd64' ? 7 : 8),
{ flag: 'wx' },
);
},
};
createLocalAlphaTrialKitVerificationEvidence(
{
root,
output: verificationEvidence,
architecture,
sourceRevision: revision,
applicationImage,
operatorImage,
repository: 'whyour/qinglong',
workflowRef:
'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
workflowSha: revision,
eventName: 'workflow_dispatch',
job: 'local-image',
runId,
runAttempt: options.runAttempt || runAttempt,
},
adapters,
);
createLocalAlphaTrialKit(
{
root,
outputRoot,
architecture,
sourceRevision: revision,
applicationImage,
operatorImage,
applicationSbom,
operatorSbom,
verificationEvidence,
readme,
},
adapters,
);
assert.equal(
auditLocalAlphaTrialKit({ bundleRoot: outputRoot }).compatible,
true,
);
return outputRoot;
}
function fixture(t, options = {}) {
const fixtureRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-milestone-')),
);
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
const readme = path.join(fixtureRoot, 'README-source.md');
fs.writeFileSync(readme, '# QingLong 3.0 Local Alpha Milestone\n');
return {
fixtureRoot,
readme,
outputRoot: path.join(fixtureRoot, 'milestone'),
bundles: {
amd64: createBundle(fixtureRoot, 'amd64', options.amd64),
arm64: createBundle(fixtureRoot, 'arm64', options.arm64),
},
};
}
function finalizeOptions(paths) {
return {
root,
outputRoot: paths.outputRoot,
bundles: paths.bundles,
readme: paths.readme,
sourceRevision: revision,
repository: 'whyour/qinglong',
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
workflowSha: revision,
eventName: 'workflow_dispatch',
runId,
runAttempt,
};
}
test('finalizes two exact native trial kits into one closed milestone index', (t) => {
const paths = fixture(t);
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v1');
assert.equal(manifest.sourceRevision, revision);
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
assert.equal(
manifest.artifacts.amd64.artifactName,
`ql3-alpha-${revision}-local-amd64`,
);
assert.equal(
manifest.artifacts.arm64.artifactName,
`ql3-alpha-${revision}-local-arm64`,
);
assert.notEqual(
manifest.artifacts.amd64.archiveSha256,
manifest.artifacts.arm64.archiveSha256,
);
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
'README.md',
'SHA256SUMS',
'manifest.json',
]);
const report = auditLocalAlphaMilestone({
milestoneRoot: paths.outputRoot,
});
assert.equal(report.compatible, true);
assert.deepEqual(report.architectures, ['amd64', 'arm64']);
assert.equal(report.workflowRunId, runId);
assert.equal(report.workflowRunAttempt, runAttempt);
});
test('rejects a trial kit from another run attempt before publishing', (t) => {
const paths = fixture(t, { arm64: { runAttempt: '1' } });
assert.throws(
() => finalizeLocalAlphaMilestone(finalizeOptions(paths)),
/detached from the milestone run/,
);
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('rejects cross-architecture image or archive identity reuse', (t) => {
const paths = fixture(t, {
arm64: { characters: { application: '1', operator: '2' } },
});
assert.throws(
() => finalizeLocalAlphaMilestone(finalizeOptions(paths)),
/architecture subjects must be distinct/,
);
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('offline milestone audit rejects mutation and extra files', (t) => {
const paths = fixture(t);
finalizeLocalAlphaMilestone(finalizeOptions(paths));
fs.appendFileSync(path.join(paths.outputRoot, 'README.md'), 'tamper\n');
assert.throws(
() => auditLocalAlphaMilestone({ milestoneRoot: paths.outputRoot }),
/README differs/,
);
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
assert.throws(
() => auditLocalAlphaMilestone({ milestoneRoot: paths.outputRoot }),
/file set is not closed/,
);
});
test('workflow audit requires full-CI needs, scoped packaging and finalizer order', () => {
const report = auditLocalAlphaMilestoneWorkflow(root);
assert.equal(report.compatible, true);
assert.deepEqual(report.findings, []);
assert.equal(report.requiredNeeds.includes('cluster-postgres-ha'), true);
});
test('workflow audit rejects a partial milestone finalizer', (t) => {
const fixtureRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-workflow-')),
);
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
fs.mkdirSync(path.join(fixtureRoot, '.github/workflows'), {
recursive: true,
});
const workflow = fs
.readFileSync(path.join(root, '.github/workflows/ql3-ci.yml'), 'utf8')
.replace(' - cluster-postgres-ha\n', '')
.replace(
'scripts/ql3-local-alpha-milestone.cjs',
'scripts/unreviewed-finalizer.cjs',
);
fs.writeFileSync(
path.join(fixtureRoot, '.github/workflows/ql3-ci.yml'),
workflow,
);
const report = auditLocalAlphaMilestoneWorkflow(fixtureRoot);
assert.equal(report.compatible, false);
assert.equal(
report.findings.includes('MILESTONE_FINALIZER_CONTRACT_DRIFT'),
true,
);
});
test('CLI grammar separates finalization, index audit and workflow audit', () => {
assert.deepEqual(
parseArguments(['--mode=audit', '--milestone=/tmp/ql3-alpha-milestone']),
{
mode: 'audit',
milestoneRoot: '/tmp/ql3-alpha-milestone',
},
);
assert.deepEqual(
parseArguments(['--mode=audit-workflow', `--root=${root}`]),
{ mode: 'audit-workflow', root },
);
assert.throws(
() =>
parseArguments([
'--mode=audit',
'--milestone=/tmp/ql3-alpha-milestone',
'--allow-partial=true',
]),
/audit arguments are invalid/,
);
});