mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
feat(ql3): add downloadable upgrade readiness
This commit is contained in:
@@ -763,6 +763,18 @@ jobs:
|
||||
docker rm "${QUICKSTART_CONTAINER}"
|
||||
test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"
|
||||
test -s "${QUICKSTART_ROOT}/owner-delivery/credential-019f8680-143d-4000-8000-000000000021.ready.json"
|
||||
LEGACY_READINESS_ROOT="${RUNNER_TEMP}/ql3-alpha-legacy-readiness-${{ matrix.image_arch }}"
|
||||
UPGRADE_EVIDENCE_ROOT="${RUNNER_TEMP}/ql3-alpha-upgrade-evidence-${{ matrix.image_arch }}"
|
||||
node scripts/ql3-local-alpha-upgrade-readiness-fixture.cjs \
|
||||
--output="${LEGACY_READINESS_ROOT}"
|
||||
sh "${BUNDLE_ROOT}/upgrade-readiness.sh" \
|
||||
edge "${LEGACY_READINESS_ROOT}" "${UPGRADE_EVIDENCE_ROOT}"
|
||||
grep -q '"status":"inspected"' \
|
||||
"${UPGRADE_EVIDENCE_ROOT}/results/sqlite-inspect.result.json"
|
||||
grep -q '"status":"inspected"' \
|
||||
"${UPGRADE_EVIDENCE_ROOT}/results/data-directory-inspect.result.json"
|
||||
test ! -e "${LEGACY_READINESS_ROOT}/db/database.sqlite-wal"
|
||||
test ! -e "${LEGACY_READINESS_ROOT}/db/database.sqlite-journal"
|
||||
trap - EXIT
|
||||
- name: Upload the tested native Local Alpha trial kit
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QingLong 3.0 Architecture RFC
|
||||
|
||||
- D-425/ADR-0520(已实现源码候选,双架构 Alpha 实物待生成):二十天研发的 Local 阶段产物开始服务现有 2.x 部署,而不再只支持 fresh 数据根。Trial Kit 新增 manifest/checksum/auditor 共同绑定的 canonical `upgrade-readiness.sh`;它只接受 `edge|standalone`、canonical 2.x data root 和尚不存在的 evidence root,先核对 exact Operator image/source/architecture,再以当前 UID:GID、read-only legacy bind mount、只读 rootfs、`network=none`、drop-all capabilities、128 MiB memory/swap、0.5 CPU、32 PID 运行正式 `local-sqlite.adoption.inspect` 与 `local-data-directory.adoption.inspect`。两个完整 plan 写入独立 `0700/0600` evidence,不解析或代替人工审核,不执行 stage、activation、transform/apply、cutover 或 rollback。Trial Kit/verification/auditor 升级为 `@v6/@v4/@v3`,Local milestone 升级为 `@v3` 并直接绑定双架构 readiness script digest;显式 artifact job 必须在原生 amd64/arm64 上创建生产形态 2.x fixture,实跑将要上传的 exact 脚本并要求两个结果均为 `inspected`。无新 package、依赖、migration、listener、daemon、timer、watcher 或 Cluster authority;聚焦 bundle/milestone/stage/fixture 回归为 `30/30`。该切片只是完整 side-by-side 升级与 clean/write-after 回退 rehearsal 的第一阶段,在普通 CI、显式 artifact run 与下载审计完成前不能宣传为已交付实物。
|
||||
|
||||
- 全范围 Alpha 阶段交付已形成真实实物:提交 `97333da34cce48cdfcfa1bbd5e8d48340802d2ef` 的显式 `all + headless` run `33265538836` 为 44/44 success,首次同时生成 Local headless 双架构 Trial Kit、Cluster control/control-ai/admin/worker 双架构 bundle、Local/Cluster milestone 和 `qinglong/alpha-stage-index@v2`。下载后的三个小索引均通过 `SHA256SUMS` 与仓库离线 auditor,绑定同一 source、workflow SHA/ref、run/attempt;stage index 为 Edge、Standalone、Cluster 精确选择 10 个 artifact。低配设备只需目标架构 Local 包且稳态仅运行 Application;集群节点只需目标架构 control/admin/worker,control-ai 显式可选。该交付保留至 2026-09-28,成熟度仍为 Alpha Integration Candidate,不等同于长期下载、受保护 tag、不可变 GHCR digest、签名、生产 deployment lock、2.x 升级/回退或 LTS。
|
||||
|
||||
- 低配 Local headless v5 阶段交付已补齐:绑定提交 `d459c3b45c36e856f4a1cb3ce5147905977d939d` 的显式 Local headless milestone run `33258604609` 为 42 success/2 scope skip/0 fail,生成 187,535,348-byte amd64、184,766,964-byte arm64 Trial Kit 与 5,626-byte milestone,保留至 2026-09-28。三个 artifact 的 GitHub ZIP SHA-256 分别为 `4d45e3f0e90159cc683a319a53ffbffc0c3ad95343e37a6f50aa8dca571d8204`、`21b6c9379531e45ec029ef64ee5ba9d51cc04ffffb50c5e29b90b175209a8c54`、`ece393032ab97776e9fe961f8e6a36fec2c4d1b6f20b9bf765816a9e3d6659c3`;内部 Docker archive SHA-256 为 `071100133783638cdeb04807ca313a968ca254366745ff9f624de6b62a2da8cc`、`29b411e31ec31b5ffe38a62cd5248725a03b1c35e1b92c5cbb155745680ede46`。下载后三件套 checksum、两个 Trial Kit auditor 与 milestone auditor 均返回 `compatible=true`,确认 `headless`、source、run/attempt、双架构 manifest/image identity 闭合。该变体不含 listener、Console、示例 Task 或 Console 管理依赖,面向路由/NAS 的默认最小 fresh Alpha;它与 Console 是独立 archive,仍不是公开 release、2.x 生产升级、物理低配容量承诺或长期支持版本。
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# ADR-0520:可下载的 Local Legacy 升级就绪盘点
|
||||
|
||||
- 状态:Accepted(D-425 源码候选,双架构 Alpha 实物待生成)
|
||||
- 日期:2026-08-30
|
||||
- 对应 RFC 切片:D-425
|
||||
- 关联 ADR:ADR-0476~ADR-0483、ADR-0503、ADR-0506、ADR-0511、ADR-0514
|
||||
|
||||
## 背景
|
||||
|
||||
QingLong 3.0 已经形成双架构 Local Trial Kit、Cluster bundle 和跨 Profile stage index,但当前可下载 Local quickstart 只支持 fresh 数据根。
|
||||
ADR-0476~ADR-0483 已在产品 CLI 中完成真实形态 2.x SQLite、完整 data directory、私有暂存、转换、原子应用、部署 lineage、停止态
|
||||
reconciliation bundle 和有界领域计划;部署用户仍无法从 Trial Kit 直接判断自己的 2.x 数据是否进入支持边界。
|
||||
|
||||
不能用“一键升级”掩盖这个缺口。SQLite stage 需要人工审核 `planDigest`,完整 data directory stage 又需要独立目录计划和 activation 双围栏;
|
||||
在未展示计划前自动复制、迁移或 cutover 会越过现有安全协议。另一方面,仅把长篇运维文档放入源码仓库也不是阶段产物:路由器、NAS 用户需要
|
||||
从已下载并审计的 exact Operator image 发起只读盘点,且不要求宿主安装 Node.js、jq 或 3.0 workspace。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. Trial Kit 增加 canonical `upgrade-readiness.sh`
|
||||
|
||||
Local Trial Kit 在既有 Docker archive、quickstart、SBOM 和 verification evidence 之外增加一个受 manifest、SHA256SUMS 和离线 auditor
|
||||
共同绑定的 POSIX shell 入口:
|
||||
|
||||
```sh
|
||||
sh upgrade-readiness.sh \
|
||||
edge \
|
||||
/opt/qinglong/data \
|
||||
/opt/qinglong3-alpha-upgrade-readiness
|
||||
```
|
||||
|
||||
入口只接受 `edge|standalone`、一个现存 canonical 2.x data root 和一个尚不存在的私有 evidence root。路径字符集有界,legacy 与 evidence
|
||||
root 必须不同;固定生产布局要求主库位于 `db/database.sqlite`。输出目录以当前 UID、`0700` 创建,command/result 为 `0600`。
|
||||
|
||||
### 2. 只运行两个正式产品 inspect
|
||||
|
||||
脚本通过 Trial Kit 中 exact、source-bound 的 Local Operator image 执行:
|
||||
|
||||
1. `local-sqlite.adoption.inspect`;
|
||||
2. `local-data-directory.adoption.inspect`。
|
||||
|
||||
legacy root 以 Docker read-only bind mount 提供,Operator 使用当前宿主 UID:GID、只读 rootfs、`network=none`、drop-all capabilities、
|
||||
`no-new-privileges`、128 MiB memory/swap、0.5 CPU、32 PID 和 8 MiB noexec tmpfs。结果只写入独立 evidence root。脚本不解析、改写或代替
|
||||
operator 审核两个完整结果,也不运行 stage、activation、application、cutover、target stop 或 Legacy rollback。
|
||||
|
||||
成功只表示两个 plan 已生成;它不表示 `assessment=reviewable`、不授权复制或升级,也不能从 plan digest 反推内容。操作者必须停止活跃 writer、
|
||||
审核 SQLite catalog/task inventory 与 data-directory disposition/预算/sidecar,再决定是否进入后续 rehearsal。
|
||||
|
||||
### 3. Artifact gate 必须运行将要上传的 exact 脚本
|
||||
|
||||
`ql3-ci.yml` 的显式 Local artifact job 在每个原生 amd64/arm64 runner 上:
|
||||
|
||||
- 创建包含 2.x `Crontabs`、`Dependences`、`Apps`、`Auths`、`Envs`、`Subscriptions`、`CrontabViews`、`CrontabStats`、
|
||||
`RunningInstances`、`PluginOwnedState` 和常见目录的私有生产形态 fixture;
|
||||
- 运行 bundle 目录中将要上传的 `upgrade-readiness.sh`,而不是源码模板;
|
||||
- 要求两个结果均为 `status=inspected`,并确认 source 未出现 SQLite WAL/journal;
|
||||
- 只有整个 job 成功才上传 bundle。
|
||||
|
||||
verification evidence 升级为 `qinglong/alpha-local-trial-kit-verification@v4` 并增加 `legacyUpgradeReadiness=passed`。Trial Kit 升级为
|
||||
`qinglong/alpha-local-trial-kit@v6`(manifest schemaVersion 7),auditor 升级为 `qinglong/alpha-local-trial-kit-audit@v3`;旧 v5 bundle 不会被
|
||||
改名冒充 v6。
|
||||
|
||||
### 4. 不增加 package 或常驻能力
|
||||
|
||||
该切片只增加 artifact template、CI fixture 和文档,不增加 workspace package、第三方依赖、数据库 migration、listener、daemon、timer、
|
||||
watcher、queue 或 Cluster authority。Operator 继续复用既有 Local adoption product CLI;Cluster 节点继续使用 PostgreSQL 专用升级、备份和
|
||||
deployment-lock 协议。
|
||||
|
||||
## 不采用的方案
|
||||
|
||||
1. **在 readiness 脚本中自动 stage/activate**:绕过两个 plan digest 的人工审核和停写围栏,拒绝。
|
||||
2. **把宿主 Node.js/jq 作为前置**:低配部署未必安装,且会扩大不可审计执行闭包,拒绝。
|
||||
3. **把 2.x data root 以读写方式挂载以“顺便修复”权限或 sidecar**:readiness 只能报告,不能修改证据现场,拒绝。
|
||||
4. **新增独立 migration package/image**:既有短生命周期 Operator 已拥有精确 adoption authority,新增交付单元只会重复依赖和扩大碎片化,拒绝。
|
||||
5. **把 inspect 成功宣传为升级兼容**:unknown asset、预算、sidecar、manual task/config/SSH 仍需审核,拒绝。
|
||||
|
||||
## 验收与后续
|
||||
|
||||
源码候选必须通过 bundle/fixture/milestone 聚焦测试、完整 backend、18-package clean build/test、package/Cluster/Edge boundary、双架构普通 CI 与
|
||||
显式 Local artifact run。只有 v6 amd64/arm64 bundle 被同 run Local milestone 收录并通过下载后 checksum/auditor,D-425 才能从源码候选升级为
|
||||
阶段实物。
|
||||
|
||||
D-425 是完整升级 rehearsal 的第一阶段,不重新定义最终目标。下一切片继续以两个已审核 plan digest 为显式输入,建立 side-by-side
|
||||
SQLite/data-directory stage、verify、activation、adopted start、clean `rollback_candidate` 与写后 `reconciliation_required` 证据;在该门完成前,
|
||||
Trial Kit 仍不得用于生产 cutover。
|
||||
@@ -523,6 +523,7 @@
|
||||
| [ADR-0517](./ADR-0517-strong-local-console-task-authoring-lease.md) | 强认证 Local Console Task authoring lease | Accepted(D-422 双架构 milestone 已交付) |
|
||||
| [ADR-0518](./ADR-0518-local-console-cron-trigger-administration.md) | Local Console cron Trigger 管理 | Accepted(D-423 双架构 milestone 已交付) |
|
||||
| [ADR-0519](./ADR-0519-local-console-secret-backed-automation.md) | Local Console Secret-backed 自动化 | Accepted(D-424 双架构 milestone 已交付) |
|
||||
| [ADR-0520](./ADR-0520-downloadable-local-legacy-upgrade-readiness.md) | 可下载的 Local Legacy 升级就绪盘点 | Accepted(D-425 源码候选,双架构 Alpha 实物待生成) |
|
||||
|
||||
## 规则
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
| D-422 Console Task 安全编辑切片 | 强认证完整定义读取、10 分钟一次性 authoring lease、第二份 exact save proof 与 revision/content/credential fence 已完成;Console 可无损编辑内建 argv command Task;同源双架构 Console v5 Trial Kit 与 milestone 已生成并验真 | Cluster 不复用 Local proof;尚无通用 workflow 编辑器、2.x 升级或生产远程管理;仍不是正式发布 |
|
||||
| D-423 Console cron Trigger 管理切片 | 已复用既有 immutable Trigger、Task pin、durable schedule 与原子 audit authority;Console/API 可 list/read/create/update/enable/disable `qinglong/cron@v1`,真实 SQLite/loopback 与同源双架构 Console milestone 已通过 | Cluster 不复用 Local proof;不提供删除或通用 Trigger schema 编辑;仍不是正式发布或生产升级 |
|
||||
| D-424 Console Secret-backed 自动化切片 | current-only metadata、强认证 AES-256-GCM create/rotate 与 Task pinned `SecretRef` 绑定已完成;真实 SQLite/loopback、本地与远端门、同源双架构 Console milestone 及离线 auditor 均通过 | Cluster 不复用 Local proof/custody;不提供明文读取、删除或历史浏览;仍不是正式发布或生产升级 |
|
||||
| D-425 2.x 升级就绪盘点 | v6 Trial Kit 源码候选新增 canonical `upgrade-readiness.sh`;2.x root 只读挂载,正式 Operator 在 128 MiB/无网络边界内生成 SQLite 与完整目录两个计划;bundle/milestone/stage 聚焦测试通过 | 双架构 artifact 尚未生成;只完成 inspect,不授权 stage、activation、cutover 或 rollback |
|
||||
|
||||
D-421 已关闭 D-420 记录的“Web Task mutation 必须独立设计”缺口,而且没有改名复用 run `33173769047` 的旧 archive。修复提交 `dc1686bd6fb3505174dd9a14098ae5c2c92a1a7f` 的普通主 CI [run 33229592307](https://github.com/whyour/qinglong/actions/runs/33229592307) 为 41 success/3 expected artifact-finalizer skip/0 fail,同源 Kubernetes deployment [run 33229592293](https://github.com/whyour/qinglong/actions/runs/33229592293) 成功;随后显式 Local Console milestone [run 33230227006](https://github.com/whyour/qinglong/actions/runs/33230227006) 为 42 success/2 scope skip/0 fail。由此 Web 创建能力已进入新的阶段实物,而不再只是候选源码。
|
||||
|
||||
@@ -39,6 +40,8 @@ D-423 已从“源码候选”升级为阶段实物:18-package clean build/tes
|
||||
|
||||
D-424 已从“源码候选”升级为阶段实物:本地 18-package clean build/test 为 `3,052 total / 3,030 pass / 22 conditional skip / 0 fail`,Local SQLite `250/250`、Local API `76/76`,默认 Edge 与 opt-in Console 资源门、Edge benchmark、package/source boundary 和 Cluster dependency audit 均通过。它证明 Secret metadata 不泄漏 custody 字段、create/rotate 必须 exact local presence、SQLite 只保存密文、Task 只保存 pinned `SecretRef`。提交 `f46fb44ac9534315b6965865bb3e990715bb2417` 的普通主 CI [run 33250825989](https://github.com/whyour/qinglong/actions/runs/33250825989) 为 41 success/3 expected artifact-finalizer skip/0 fail,同源 Kubernetes deployment [run 33250826046](https://github.com/whyour/qinglong/actions/runs/33250826046) 与三节点 Security Administration [run 33250825974](https://github.com/whyour/qinglong/actions/runs/33250825974) 成功;显式 Local Console milestone [run 33252179178](https://github.com/whyour/qinglong/actions/runs/33252179178) 为 42 success/2 scope skip/0 fail。首次 artifact run [33251389615](https://github.com/whyour/qinglong/actions/runs/33251389615) 触发 x64 router 绝对 RSS 门;同源码 ordinary run 的 peak process RSS 为 98,852,864 bytes,距 96 MiB 上限仅约 1.73 MiB,表现与 runner 基线波动一致。没有提高预算、改代码或复用 failed attempt,第二次全新 run 对同一 source 通过该门并闭合产物。
|
||||
|
||||
D-425 当前是严格区分的源码候选:Trial Kit、Local milestone 与 stage index 聚焦回归 `30/30`,其中将要上传的 bundle 文件集新增由 manifest、checksum 和 auditor 共同绑定的 `upgrade-readiness.sh`。脚本只接受独立 evidence root,把实际 2.x data root 只读挂载给 exact Operator,运行 `local-sqlite.adoption.inspect` 和 `local-data-directory.adoption.inspect`,不运行任何写入或切换。`ql3-ci.yml` 已要求显式 artifact job 在每个原生架构上创建生产形态 fixture 并实跑 bundle 内 exact 脚本;在普通 CI、双架构 artifact run 和下载审计完成前,不能把 v6 源码候选宣传为已交付实物。
|
||||
|
||||
默认低配 headless v5 也已从“可生成”升级为独立阶段实物。绑定提交 `d459c3b45c36e856f4a1cb3ce5147905977d939d` 的显式 Local headless milestone [run 33258604609](https://github.com/whyour/qinglong/actions/runs/33258604609) 为 42 success/2 scope skip/0 fail,完整矩阵继续覆盖双架构资源、Local/Cluster image、PostgreSQL HA、CloudNativePG、Secret/provider rotation 与 Local Profiles。该 run 没有复用 Console archive;下载后的两个 `headless` Trial Kit 与 milestone 均通过 `SHA256SUMS` 和仓库 auditor,返回 `compatible=true`。
|
||||
|
||||
首份跨部署全范围阶段实物绑定提交 `97333da34cce48cdfcfa1bbd5e8d48340802d2ef` 与 [run 33265538836](https://github.com/whyour/qinglong/actions/runs/33265538836),为 `44 success / 0 skip / 0 fail`。它生成 headless Local 双架构、Cluster control/control-ai/admin/worker 双架构、两个 milestone 与 `ql3-alpha-97333da34cce48cdfcfa1bbd5e8d48340802d2ef-stage-index`;三个小索引的 GitHub ZIP digest 分别为 Local `2e3bb8baeeadb40f34c130db68db8b1a7d6cf7a7c92a73a805e84990bf9875dc`、Cluster `292380a72f8b45233f6591624f6073154c2b7d2d00f62908af687193078524e2`、stage `2fbc67d478593df8bbb2ba362beb9f676be1882ac2e5386106057789906adece`,保留至 2026-09-28。下载后三个 `SHA256SUMS` 与仓库 auditor 全部 `compatible=true`;stage auditor 确认 `3.0.0-alpha.2`、同一 source/run/attempt、三种 Profile 和 10 个可选择 artifact。
|
||||
@@ -86,7 +89,7 @@ D-418 防止把“20 天代码和测试”冒充“用户已经能下载并完
|
||||
|
||||
该本地 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-0506 的 `qinglong/alpha-local-trial-kit@v2` 首次增加 source-bound verification,ADR-0511 的 `@v3` 增加 canonical quickstart,ADR-0513 的 `@v4` 再把 `headless|console` 变体绑定到 image、SBOM、verification、milestone 和 stage index,ADR-0514 的 `@v5` 增加标准 Owner credential presentation 与首自动化旅程。旧 runtime-only、v1/v2/v3/v4 bundle 均为历史工程证据,不能通过 v5 auditor。当前 headless、Console v5、Cluster 双架构 milestone 与跨 Profile stage index 均已有真实实物;下一阶段不再补“有没有产物”,而是补长期分发、签名、不可变 registry digest、部署锁和升级/回退证据。Public Release Set 继续受更严格发布门禁约束。
|
||||
ADR-0506 的 `qinglong/alpha-local-trial-kit@v2` 首次增加 source-bound verification,ADR-0511 的 `@v3` 增加 canonical quickstart,ADR-0513 的 `@v4` 再把 `headless|console` 变体绑定到 image、SBOM、verification、milestone 和 stage index,ADR-0514 的 `@v5` 增加标准 Owner credential presentation 与首自动化旅程,ADR-0520 的 `@v6` 增加只读 2.x 升级就绪盘点。旧 runtime-only、v1/v2/v3/v4/v5 bundle 均为历史工程证据,不能通过 v6 auditor。当前真实可下载实物仍是 headless/Console v5、Cluster 双架构 milestone 与跨 Profile stage index;v6 必须经过新的普通 CI、显式 artifact run 和下载后审计才可替代它们。Public Release Set 继续受更严格发布门禁约束。
|
||||
|
||||
## 生成
|
||||
|
||||
@@ -103,15 +106,16 @@ ADR-0506 的 `qinglong/alpha-local-trial-kit@v2` 首次增加 source-bound verif
|
||||
Local artifact 含:
|
||||
|
||||
- 一个包含所选 Application 与短生命周期 operator 的 archive;headless 为 `qinglong3-local-trial-kit-<arch>.docker.tar`,Console 为 `qinglong3-local-console-trial-kit-<arch>.docker.tar`,共享 Node 基础层在 archive 中去重;
|
||||
- schema 为 `qinglong/alpha-local-trial-kit@v5` 的 `manifest.json`,通过 `variant/archive/images/sboms/quickstart/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256;
|
||||
- schema 为 `qinglong/alpha-local-trial-kit@v6` 的 `manifest.json`,通过 `variant/archive/images/sboms/quickstart/upgradeReadiness/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256;
|
||||
- canonical `quickstart.sh`,在目标 Linux 设备上只依赖 POSIX shell、`sha256sum` 和 Docker,完成 checksum、load、identity、fresh Owner 与 Profile-bound Application active;
|
||||
- canonical `upgrade-readiness.sh`,把 2.x data root 只读挂载给 128 MiB/无网络 Operator,生成 SQLite 与完整目录两个私有 inspect 计划,不获得 stage/cutover authority;
|
||||
- `verification-evidence.json` 绑定 `workflow_dispatch` 的 workflow ref/SHA、run ID/attempt、同架构两个 exact image ID 和完整 gate 集;下载者仍须到 GitHub 交叉检查 run,它不替代正式签名;
|
||||
- 与实际只读镜像 inventory 对账过的 CycloneDX SBOM;
|
||||
- 面向 Local 用户的 README 与覆盖全部内容文件的 `SHA256SUMS`。
|
||||
|
||||
Cluster artifact 是每角色/架构一个六文件闭包:native Docker archive、精确 CycloneDX SBOM、workflow-bound verification evidence、README、`qinglong/alpha-cluster-image@v1` manifest 和覆盖全部内容文件的 `SHA256SUMS`。完整 CI 成功后,八个 bundle 由 `qinglong/alpha-cluster-milestone@v1` 小型索引闭合;索引本身不重复存放大 archive。
|
||||
|
||||
Local milestone 是 `qinglong/alpha-local-milestone@v2` 三文件闭包,绑定一个 variant 的双架构 Trial Kit。Stage index 是 `qinglong/alpha-stage-index@v2` 三文件闭包;它重新审计两个 milestone,要求 version/source/workflow SHA/ref/run/attempt 一致,并把 Local variant/Profile 与 Cluster 的 control/admin/worker 最小集、可选 control-ai 写为机器可读选择;它不重复存放任何镜像 archive。
|
||||
Local milestone 是 `qinglong/alpha-local-milestone@v3` 三文件闭包,绑定一个 variant 的双架构 Trial Kit,并直接记录两个架构的 `upgradeReadinessSha256`。Stage index 是 `qinglong/alpha-stage-index@v2` 三文件闭包;它重新审计两个 milestone,要求 version/source/workflow SHA/ref/run/attempt 一致,并把 Local variant/Profile 与 Cluster 的 control/admin/worker 最小集、可选 control-ai 写为机器可读选择;它不重复存放任何镜像 archive。
|
||||
|
||||
任何 required job 失败时不上传对应产物。artifact 名和 archive 内的 `ci-*` tag 都表示 commit-bound candidate,不能改名后冒充 `v3.x` release。
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
```
|
||||
|
||||
2. 打开 `manifest.json`,确认:
|
||||
- `schema` 为 `qinglong/alpha-local-milestone@v2`;
|
||||
- `schema` 为 `qinglong/alpha-local-milestone@v3`;
|
||||
- `variant` 为 `headless` 或 `console`,且两个架构记录都使用同一变体;
|
||||
- `sourceRevision` 是准备试用的完整 40 位提交;
|
||||
- `workflow.event` 为 `workflow_dispatch`,`workflow.job` 为 `local-alpha-milestone`;
|
||||
@@ -28,7 +28,7 @@
|
||||
- `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。
|
||||
5. 确认 milestone 的 `upgradeReadinessSha256` 与 Trial Kit manifest 中同名入口摘要一致,再按 Trial Kit 自带 `README.md` 完成 fresh smoke 或只读 2.x 升级就绪盘点。
|
||||
|
||||
若持有同一版本源码与 Node.js 24,可额外审计 milestone 索引:
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ sha256sum --check SHA256SUMS
|
||||
|
||||
`manifest.json` 必须满足:
|
||||
|
||||
- `schema` 为 `qinglong/alpha-local-trial-kit@v5`;
|
||||
- `schema` 为 `qinglong/alpha-local-trial-kit@v6`;
|
||||
- `variant` 为 `headless` 或 `console`,并与 milestone、application SBOM 和 artifact 名一致;
|
||||
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
||||
- `architecture` 与主机相同;
|
||||
@@ -45,7 +45,7 @@ node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||
|
||||
## 一条命令完成 Fresh 试运行
|
||||
|
||||
v5 bundle 内的 `quickstart.sh` 不依赖宿主 Node.js、jq 或 Compose,只需要 POSIX
|
||||
v6 bundle 内的 `quickstart.sh` 不依赖宿主 Node.js、jq 或 Compose,只需要 POSIX
|
||||
shell、`sha256sum` 和已启动的 Docker。必须选择一个尚不存在、与 2.x/生产数据完全
|
||||
隔离的绝对路径:
|
||||
|
||||
@@ -84,6 +84,36 @@ SecretRef 或 Trigger。D-420 后选择该 Run 还会经既有 `artifact.read`
|
||||
Attempt 首个 32 KiB 日志;后续内容继续使用 API 分页,页面不会自动轮询或下载整份日志。
|
||||
headless 不创建示例 Task,因此低配默认档没有示例数据或稳态开销。
|
||||
|
||||
## 只读检查现有 2.x 升级就绪度
|
||||
|
||||
v6 bundle 还包含 canonical `upgrade-readiness.sh`。它让现有部署用户先回答“这份 2.x SQLite 和完整 data directory 是否能形成可审核
|
||||
计划”,不会把 inspect 成功冒充自动升级。建议停止 2.x、同步器和下载器,确认主库位于 `db/database.sqlite`,再选择一个尚不存在且不在
|
||||
2.x data root 内的 evidence 路径:
|
||||
|
||||
```sh
|
||||
sh upgrade-readiness.sh \
|
||||
edge \
|
||||
/opt/qinglong/data \
|
||||
/opt/qinglong3-alpha-upgrade-readiness
|
||||
```
|
||||
|
||||
NAS/较大单机使用 `standalone`。脚本继续只依赖 POSIX shell、`sha256sum` 和 Docker;它先验证全包 checksum、exact Operator image ID、源码和
|
||||
架构,再把 2.x root 以 read-only bind mount 提供给当前 UID:GID 的短生命周期 Operator。Operator 固定无网络、只读 rootfs、128 MiB、
|
||||
0.5 CPU 和 32 PID,分别执行正式的 `local-sqlite.adoption.inspect` 与 `local-data-directory.adoption.inspect`。
|
||||
|
||||
两个完整结果保存在 evidence root 的:
|
||||
|
||||
- `results/sqlite-inspect.result.json`;
|
||||
- `results/data-directory-inspect.result.json`。
|
||||
|
||||
不要只保存终端输出或手工抄写 digest。审核 SQLite catalog/task inventory、完整目录的 `assessment`、disposition、预算、unsafe/unknown 条目、主库
|
||||
计数和 active sidecar,并保存两个 exact `planDigest`。脚本不会运行 stage、activation、transform/apply、cutover、target stop 或 Legacy
|
||||
rollback,也不会修改 source、修权限或删除 sidecar。readiness 成功不授权下一阶段;需要继续演练时按仓库运维协议显式提交审核后的 digest。
|
||||
|
||||
artifact job 必须在原生 amd64/arm64 上使用生产形态 2.x fixture 运行将要上传的 exact `upgrade-readiness.sh`,两个正式 Operator 结果都为
|
||||
`inspected` 后才能记录 `verification-evidence.json.gates.legacyUpgradeReadiness=passed` 并上传。该证明仍不是用户实际磁盘、停机窗口、I/O 峰值或
|
||||
生产数据内容兼容性承诺。
|
||||
|
||||
## 手工加载与最小 smoke
|
||||
|
||||
从 `manifest.json.archive.file` 找到 archive 后加载:
|
||||
@@ -111,7 +141,7 @@ docker run --rm --read-only --network none --cap-drop ALL \
|
||||
|
||||
## Fresh 试运行边界
|
||||
|
||||
完整 fresh setup、首 Owner ceremony、Owner presentation 安装、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。Console 还必须证明首页返回 200、未认证 API 返回 401,并用真实 Owner credential 完成 Task read、fenced start、`succeeded` 终态与 bounded log marker。v5 artifact job 必须从将要上传的目录实际执行 `quickstart.sh` 并完成 graceful stop。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
||||
完整 fresh setup、首 Owner ceremony、Owner presentation 安装、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。Console 还必须证明首页返回 200、未认证 API 返回 401,并用真实 Owner credential 完成 Task read、fenced start、`succeeded` 终态与 bounded log marker。v6 artifact job 必须从将要上传的目录实际执行 `quickstart.sh` 和 read-only `upgrade-readiness.sh`,并完成 graceful stop。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
||||
|
||||
Edge 的验证上限为 Application 128 MiB、0.5 CPU、64 PID;Standalone 为 256 MiB、0.5 CPU、256 PID;operator 为 128 MiB、0.5 CPU、32 PID。这里的数值是试运行门,不是所有 workload 的容量承诺。
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ function validateMilestoneRecord(record, product, sourceRevision, variant) {
|
||||
: 'cluster_integration_candidate_not_public_release';
|
||||
const expectedSchema =
|
||||
product === 'local'
|
||||
? 'qinglong/alpha-local-milestone@v2'
|
||||
? 'qinglong/alpha-local-milestone@v3'
|
||||
: 'qinglong/alpha-cluster-milestone@v1';
|
||||
if (
|
||||
!exactKeys(record, ['artifactName', 'schema', 'maturity', 'manifest']) ||
|
||||
@@ -382,10 +382,7 @@ function auditAlphaStageIndex(options) {
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
workflowRunId: manifest.workflow.runId,
|
||||
workflowRunAttempt: manifest.workflow.runAttempt,
|
||||
profiles: [
|
||||
...manifest.deploymentSelections.local.profiles,
|
||||
'cluster',
|
||||
],
|
||||
profiles: [...manifest.deploymentSelections.local.profiles, 'cluster'],
|
||||
artifactCount: 10,
|
||||
compatible: true,
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ const {
|
||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||
|
||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||
const SCHEMA = 'qinglong/alpha-local-milestone@v2';
|
||||
const SCHEMA = 'qinglong/alpha-local-milestone@v3';
|
||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||
const FILES = Object.freeze({
|
||||
readme: 'README.md',
|
||||
@@ -219,6 +219,7 @@ function bundleRecord(options, architecture) {
|
||||
applicationImageId: report.applicationImageId,
|
||||
operatorImageId: report.operatorImageId,
|
||||
verificationSha256: report.verificationSha256,
|
||||
upgradeReadinessSha256: report.upgradeReadinessSha256,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -232,6 +233,7 @@ function validateArtifactRecord(record, architecture, manifest) {
|
||||
'applicationImageId',
|
||||
'operatorImageId',
|
||||
'verificationSha256',
|
||||
'upgradeReadinessSha256',
|
||||
]) ||
|
||||
record.artifactName !==
|
||||
artifactName(manifest.sourceRevision, architecture, manifest.variant) ||
|
||||
@@ -245,6 +247,7 @@ function validateArtifactRecord(record, architecture, manifest) {
|
||||
!SHA256_PATTERN.test(record.applicationImageId || '') ||
|
||||
!SHA256_PATTERN.test(record.operatorImageId || '') ||
|
||||
!SHA256_PATTERN.test(record.verificationSha256 || '') ||
|
||||
!SHA256_PATTERN.test(record.upgradeReadinessSha256 || '') ||
|
||||
record.applicationImageId === record.operatorImageId
|
||||
) {
|
||||
fail(`${architecture} milestone artifact record is incompatible`);
|
||||
@@ -313,7 +316,7 @@ function auditLocalAlphaMilestone(options) {
|
||||
'artifacts',
|
||||
'readme',
|
||||
]) ||
|
||||
manifest.schemaVersion !== 2 ||
|
||||
manifest.schemaVersion !== 3 ||
|
||||
manifest.schema !== SCHEMA ||
|
||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||
manifest.product !== 'local' ||
|
||||
@@ -356,6 +359,8 @@ function auditLocalAlphaMilestone(options) {
|
||||
new Set(records.map((record) => record.archiveSha256)).size !==
|
||||
ARCHITECTURES.length ||
|
||||
new Set(records.map((record) => record.verificationSha256)).size !==
|
||||
ARCHITECTURES.length ||
|
||||
new Set(records.map((record) => record.upgradeReadinessSha256)).size !==
|
||||
ARCHITECTURES.length
|
||||
) {
|
||||
fail('milestone architecture subjects are not distinct');
|
||||
@@ -383,7 +388,7 @@ function auditLocalAlphaMilestone(options) {
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/alpha-local-milestone-audit@v2',
|
||||
schema: 'qinglong/alpha-local-milestone-audit@v3',
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
version: manifest.version,
|
||||
variant: manifest.variant,
|
||||
@@ -420,7 +425,9 @@ function finalizeLocalAlphaMilestone(options) {
|
||||
if (
|
||||
new Set(allImageIds).size !== allImageIds.length ||
|
||||
artifacts.amd64.archiveSha256 === artifacts.arm64.archiveSha256 ||
|
||||
artifacts.amd64.verificationSha256 === artifacts.arm64.verificationSha256
|
||||
artifacts.amd64.verificationSha256 === artifacts.arm64.verificationSha256 ||
|
||||
artifacts.amd64.upgradeReadinessSha256 ===
|
||||
artifacts.arm64.upgradeReadinessSha256
|
||||
) {
|
||||
fail('milestone architecture subjects must be distinct');
|
||||
}
|
||||
@@ -433,7 +440,7 @@ function finalizeLocalAlphaMilestone(options) {
|
||||
path.join(normalized.outputRoot, FILES.readme),
|
||||
);
|
||||
const manifest = {
|
||||
schemaVersion: 2,
|
||||
schemaVersion: 3,
|
||||
schema: SCHEMA,
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
|
||||
@@ -10,18 +10,23 @@ 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@v5';
|
||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v3';
|
||||
const SCHEMA = 'qinglong/alpha-local-trial-kit@v6';
|
||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v4';
|
||||
const QUICKSTART_TEMPLATE = path.join(
|
||||
DEFAULT_ROOT,
|
||||
'scripts/templates/ql3-local-alpha-quickstart.sh',
|
||||
);
|
||||
const UPGRADE_READINESS_TEMPLATE = path.join(
|
||||
DEFAULT_ROOT,
|
||||
'scripts/templates/ql3-local-alpha-upgrade-readiness.sh',
|
||||
);
|
||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||
const VARIANTS = Object.freeze(['headless', 'console']);
|
||||
const ARCHIVE_MIN_BYTES = 1024;
|
||||
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
||||
const MAX_README_BYTES = 512 * 1024;
|
||||
const MAX_QUICKSTART_BYTES = 256 * 1024;
|
||||
const MAX_UPGRADE_READINESS_BYTES = 256 * 1024;
|
||||
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
||||
const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
|
||||
const FILES = Object.freeze({
|
||||
@@ -29,6 +34,7 @@ const FILES = Object.freeze({
|
||||
operatorSbom: 'qinglong3-local-operator.cdx.json',
|
||||
verificationEvidence: 'verification-evidence.json',
|
||||
quickstart: 'quickstart.sh',
|
||||
upgradeReadiness: 'upgrade-readiness.sh',
|
||||
readme: 'README.md',
|
||||
manifest: 'manifest.json',
|
||||
checksums: 'SHA256SUMS',
|
||||
@@ -44,6 +50,7 @@ const VERIFICATION = Object.freeze({
|
||||
edgeFreshLifecycle: 'passed',
|
||||
standaloneFreshLifecycle: 'passed',
|
||||
localApiCancellation: 'passed',
|
||||
legacyUpgradeReadiness: 'passed',
|
||||
});
|
||||
|
||||
function verificationGates(variant) {
|
||||
@@ -447,6 +454,32 @@ function renderQuickstart(identity) {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
function renderUpgradeReadiness(identity) {
|
||||
const template = fs.readFileSync(
|
||||
assertCanonicalFile(
|
||||
UPGRADE_READINESS_TEMPLATE,
|
||||
MAX_UPGRADE_READINESS_BYTES,
|
||||
'upgrade readiness template',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const replacements = Object.freeze({
|
||||
'@@OPERATOR_IMAGE@@': identity.images.operator.reference,
|
||||
'@@OPERATOR_ID@@': identity.images.operator.id,
|
||||
'@@ARCHITECTURE@@': identity.architecture,
|
||||
'@@SOURCE_REVISION@@': identity.sourceRevision,
|
||||
'@@ARCHIVE@@': identity.archive.file,
|
||||
});
|
||||
let rendered = template;
|
||||
for (const [token, value] of Object.entries(replacements)) {
|
||||
rendered = rendered.replaceAll(token, value);
|
||||
}
|
||||
if (/@@[A-Z_]+@@/u.test(rendered)) {
|
||||
fail('upgrade readiness template contains an unresolved token');
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
function fileRecord(bundleRoot, name) {
|
||||
const filePath = path.join(bundleRoot, name);
|
||||
const stat = fs.lstatSync(filePath);
|
||||
@@ -622,8 +655,13 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
renderQuickstart(manifestIdentity),
|
||||
0o700,
|
||||
);
|
||||
writeExclusive(
|
||||
path.join(normalized.outputRoot, FILES.upgradeReadiness),
|
||||
renderUpgradeReadiness(manifestIdentity),
|
||||
0o700,
|
||||
);
|
||||
const manifest = {
|
||||
schemaVersion: 6,
|
||||
schemaVersion: 7,
|
||||
schema: SCHEMA,
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
@@ -638,6 +676,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
|
||||
},
|
||||
quickstart: fileRecord(normalized.outputRoot, FILES.quickstart),
|
||||
upgradeReadiness: fileRecord(
|
||||
normalized.outputRoot,
|
||||
FILES.upgradeReadiness,
|
||||
),
|
||||
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
||||
verification: fileRecord(
|
||||
normalized.outputRoot,
|
||||
@@ -654,6 +696,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
FILES.quickstart,
|
||||
FILES.upgradeReadiness,
|
||||
FILES.readme,
|
||||
FILES.manifest,
|
||||
];
|
||||
@@ -723,10 +766,11 @@ function auditLocalAlphaTrialKit(options) {
|
||||
'images',
|
||||
'sboms',
|
||||
'quickstart',
|
||||
'upgradeReadiness',
|
||||
'readme',
|
||||
'verification',
|
||||
]) ||
|
||||
manifest.schemaVersion !== 6 ||
|
||||
manifest.schemaVersion !== 7 ||
|
||||
manifest.schema !== SCHEMA ||
|
||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||
manifest.product !== 'local' ||
|
||||
@@ -773,6 +817,23 @@ function auditLocalAlphaTrialKit(options) {
|
||||
if (actualQuickstart !== expectedQuickstart) {
|
||||
fail('quickstart differs from the canonical deployment journey');
|
||||
}
|
||||
validateFileRecord(
|
||||
manifest.upgradeReadiness,
|
||||
FILES.upgradeReadiness,
|
||||
bundleRoot,
|
||||
);
|
||||
const expectedUpgradeReadiness = renderUpgradeReadiness(manifest);
|
||||
const actualUpgradeReadiness = fs.readFileSync(
|
||||
assertCanonicalFile(
|
||||
path.join(bundleRoot, FILES.upgradeReadiness),
|
||||
MAX_UPGRADE_READINESS_BYTES,
|
||||
'upgrade readiness',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
if (actualUpgradeReadiness !== expectedUpgradeReadiness) {
|
||||
fail('upgrade readiness differs from the canonical inspection journey');
|
||||
}
|
||||
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
||||
validateOfflineSbom(
|
||||
readBoundedJson(
|
||||
@@ -809,6 +870,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
FILES.quickstart,
|
||||
FILES.upgradeReadiness,
|
||||
expectedArchive,
|
||||
].sort();
|
||||
const actualFiles = fs
|
||||
@@ -829,6 +891,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
FILES.operatorSbom,
|
||||
FILES.verificationEvidence,
|
||||
FILES.quickstart,
|
||||
FILES.upgradeReadiness,
|
||||
FILES.readme,
|
||||
FILES.manifest,
|
||||
];
|
||||
@@ -842,7 +905,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/alpha-local-trial-kit-audit@v2',
|
||||
schema: 'qinglong/alpha-local-trial-kit-audit@v3',
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
version: manifest.version,
|
||||
architecture: manifest.architecture,
|
||||
@@ -851,6 +914,7 @@ function auditLocalAlphaTrialKit(options) {
|
||||
applicationImageId: manifest.images.application.id,
|
||||
operatorImageId: manifest.images.operator.id,
|
||||
quickstartSha256: manifest.quickstart.sha256,
|
||||
upgradeReadinessSha256: manifest.upgradeReadiness.sha256,
|
||||
verificationSha256: manifest.verification.sha256,
|
||||
workflowRunId: verificationEvidence.workflow.runId,
|
||||
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(
|
||||
`QingLong Local Alpha upgrade readiness fixture failed: ${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
if (argv.length !== 1) fail('usage: --output=/absolute/new/data-root');
|
||||
const match = /^--output=(\/.+)$/u.exec(argv[0]);
|
||||
if (!match) fail('output must be an absolute path');
|
||||
const output = path.resolve(match[1]);
|
||||
if (output !== match[1] || path.parse(output).root === output) {
|
||||
fail('output must be a normalized absolute non-root path');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function writePrivate(filePath, contents) {
|
||||
fs.writeFileSync(filePath, contents, {
|
||||
encoding: 'utf8',
|
||||
mode: 0o600,
|
||||
flag: 'wx',
|
||||
});
|
||||
}
|
||||
|
||||
function createLegacyDatabase(databasePath) {
|
||||
const database = new DatabaseSync(databasePath);
|
||||
try {
|
||||
database.exec(`
|
||||
CREATE TABLE "Crontabs" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255), command VARCHAR(255),
|
||||
schedule VARCHAR(255), timestamp VARCHAR(255), saved TINYINT(1), status DECIMAL,
|
||||
isSystem DECIMAL, pid DECIMAL, isDisabled DECIMAL, isPinned DECIMAL,
|
||||
log_path VARCHAR(255), labels JSON, last_running_time DECIMAL,
|
||||
last_execution_time DECIMAL, sub_id DECIMAL, extra_schedules JSON,
|
||||
task_before VARCHAR(255), task_after VARCHAR(255), log_name VARCHAR(255),
|
||||
allow_multiple_instances DECIMAL, work_dir VARCHAR(255),
|
||||
createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "Dependences" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255), type DECIMAL,
|
||||
timestamp VARCHAR(255), status DECIMAL, log JSON, remark VARCHAR(255),
|
||||
createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "Apps" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255), scopes JSON,
|
||||
client_id VARCHAR(255), client_secret VARCHAR(255), tokens JSON,
|
||||
createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "Auths" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, ip VARCHAR(255), type VARCHAR(255),
|
||||
info JSON, createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "Envs" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, value VARCHAR(255), timestamp VARCHAR(255),
|
||||
status DECIMAL, position DECIMAL, name VARCHAR(255), remarks VARCHAR(255),
|
||||
isPinned DECIMAL, labels JSON, createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "Subscriptions" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255), url VARCHAR(255),
|
||||
schedule VARCHAR(255), interval_schedule JSON, type VARCHAR(255),
|
||||
whitelist VARCHAR(255), blacklist VARCHAR(255), status DECIMAL,
|
||||
dependences VARCHAR(255), extensions VARCHAR(255), sub_before VARCHAR(255),
|
||||
sub_after VARCHAR(255), branch VARCHAR(255), pull_type VARCHAR(255),
|
||||
pull_option JSON, pid DECIMAL, is_disabled DECIMAL, log_path VARCHAR(255),
|
||||
schedule_type VARCHAR(255), alias VARCHAR(255), proxy VARCHAR(255),
|
||||
autoAddCron DECIMAL, autoDelCron DECIMAL, createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "CrontabViews" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255), position DECIMAL,
|
||||
isDisabled DECIMAL, filters JSON, sorts JSON, filterRelation VARCHAR(255),
|
||||
type DECIMAL, createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "CrontabStats" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, ref_id DECIMAL NOT NULL,
|
||||
date VARCHAR(255) NOT NULL, run_count DECIMAL, success_count DECIMAL,
|
||||
fail_count DECIMAL, total_time DECIMAL, max_time DECIMAL,
|
||||
createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "RunningInstances" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, cron_id DECIMAL NOT NULL,
|
||||
run_id VARCHAR(36), attempt_id VARCHAR(36), pid DECIMAL,
|
||||
log_path VARCHAR(255), started_at DECIMAL NOT NULL, finished_at DECIMAL,
|
||||
status DECIMAL NOT NULL, exit_code DECIMAL,
|
||||
createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE "PluginOwnedState" (id INTEGER PRIMARY KEY, payload TEXT NOT NULL);
|
||||
INSERT INTO "Crontabs" (
|
||||
id, name, command, schedule, status, isDisabled, isPinned, createdAt, updatedAt
|
||||
) VALUES (
|
||||
1, 'Alpha upgrade readiness fixture', 'task /ql/scripts/alpha.sh',
|
||||
'0 0 * * *', 1, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO "Envs" (
|
||||
id, name, value, status, position, createdAt, updatedAt
|
||||
) VALUES (
|
||||
1, 'ALPHA_READINESS_VALUE', 'synthetic-only', 0, 100,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO "PluginOwnedState" (id, payload)
|
||||
VALUES (1, '{"synthetic":true}');
|
||||
`);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
fs.chmodSync(databasePath, 0o600);
|
||||
}
|
||||
|
||||
function createFixture(output) {
|
||||
if (fs.existsSync(output)) fail('output must not already exist');
|
||||
fs.mkdirSync(output, { mode: 0o700 });
|
||||
for (const directory of ['config', 'scripts', 'db', 'upload', 'ssh.d']) {
|
||||
fs.mkdirSync(path.join(output, directory), { mode: 0o700 });
|
||||
}
|
||||
createLegacyDatabase(path.join(output, 'db', 'database.sqlite'));
|
||||
writePrivate(
|
||||
path.join(output, 'config', 'config.sh'),
|
||||
"export ALPHA_READINESS_CONFIG='synthetic-only'\n",
|
||||
);
|
||||
writePrivate(
|
||||
path.join(output, 'scripts', 'alpha.sh'),
|
||||
"#!/bin/sh\nprintf '%s\\n' 'synthetic alpha readiness'\n",
|
||||
);
|
||||
writePrivate(
|
||||
path.join(output, 'upload', 'README.txt'),
|
||||
'synthetic fixture\n',
|
||||
);
|
||||
return Object.freeze({
|
||||
output,
|
||||
database: path.join(output, 'db', 'database.sqlite'),
|
||||
});
|
||||
}
|
||||
|
||||
function runCli(argv) {
|
||||
const result = createFixture(parseArguments(argv));
|
||||
process.stdout.write(`${JSON.stringify({ status: 'created', ...result })}\n`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
runCli(process.argv.slice(2));
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`${error instanceof Error ? error.message : 'unknown failure'}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Object.freeze({ createFixture, parseArguments });
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
OPERATOR_IMAGE='@@OPERATOR_IMAGE@@'
|
||||
OPERATOR_ID='@@OPERATOR_ID@@'
|
||||
ARCHITECTURE='@@ARCHITECTURE@@'
|
||||
SOURCE_REVISION='@@SOURCE_REVISION@@'
|
||||
ARCHIVE='@@ARCHIVE@@'
|
||||
|
||||
fail() {
|
||||
printf '%s\n' "QingLong Local Alpha upgrade readiness failed: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
printf '%s\n' \
|
||||
'usage: sh upgrade-readiness.sh edge|standalone /absolute/legacy-data-root /absolute/new/evidence-root' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
safe_absolute_path() {
|
||||
case "$1" in
|
||||
/|*[!A-Za-z0-9_./-]*|*'/../'*|*'/./'*|*'/..'|*'/.'|*'//'*|*/)
|
||||
return 1
|
||||
;;
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
[ "$#" -eq 3 ] || usage
|
||||
profile=$1
|
||||
legacy_root=$2
|
||||
evidence_root=$3
|
||||
|
||||
case "$profile" in
|
||||
edge|standalone) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
safe_absolute_path "$legacy_root" || fail 'legacy data root is not a safe canonical absolute path'
|
||||
safe_absolute_path "$evidence_root" || fail 'evidence root is not a safe canonical absolute path'
|
||||
[ "$legacy_root" != "$evidence_root" ] || fail 'legacy and evidence roots must be distinct'
|
||||
[ -d "$legacy_root" ] || fail 'legacy data root does not exist'
|
||||
[ -f "$legacy_root/db/database.sqlite" ] || fail 'legacy db/database.sqlite does not exist'
|
||||
legacy_real=$(CDPATH= cd -- "$legacy_root" && pwd -P)
|
||||
[ "$legacy_real" = "$legacy_root" ] || fail 'legacy data root is not canonical'
|
||||
[ ! -e "$evidence_root" ] || fail 'evidence root must not already exist'
|
||||
evidence_parent=${evidence_root%/*}
|
||||
[ -n "$evidence_parent" ] || evidence_parent=/
|
||||
[ -d "$evidence_parent" ] || fail 'evidence root parent does not exist'
|
||||
evidence_parent_real=$(CDPATH= cd -- "$evidence_parent" && pwd -P)
|
||||
[ "$evidence_parent_real/${evidence_root##*/}" = "$evidence_root" ] || fail 'evidence root parent is not canonical'
|
||||
|
||||
command -v docker >/dev/null 2>&1 || fail 'docker is required'
|
||||
command -v sha256sum >/dev/null 2>&1 || fail 'sha256sum is required'
|
||||
command -v grep >/dev/null 2>&1 || fail 'grep is required'
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
(CDPATH= cd -- "$script_dir" && sha256sum --check SHA256SUMS)
|
||||
docker info >/dev/null 2>&1 || fail 'docker daemon is unavailable'
|
||||
docker load --input "$script_dir/$ARCHIVE" >/dev/null
|
||||
|
||||
operator_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}|{{index .Config.Labels "io.qinglong.lifecycle"}}|{{index .Config.Labels "io.qinglong.network"}}' "$OPERATOR_IMAGE")
|
||||
[ "$operator_identity" = "$OPERATOR_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION|short-lived|none-by-default" ] || fail 'operator image identity is incompatible'
|
||||
|
||||
old_umask=$(umask)
|
||||
umask 077
|
||||
mkdir -m 0700 "$evidence_root"
|
||||
mkdir -m 0700 "$evidence_root/results"
|
||||
|
||||
cat >"$evidence_root/sqlite-inspect.json" <<EOF
|
||||
{"schemaVersion":1,"operation":"local-sqlite.adoption.inspect","options":{"deploymentRoot":"/var/lib/qinglong3","profile":"$profile","sourcePath":"/var/lib/qinglong2/db/database.sqlite"}}
|
||||
EOF
|
||||
cat >"$evidence_root/data-directory-inspect.json" <<EOF
|
||||
{"schemaVersion":1,"operation":"local-data-directory.adoption.inspect","options":{"dataRoot":"/var/lib/qinglong2","profile":"$profile"}}
|
||||
EOF
|
||||
chmod 0600 "$evidence_root/sqlite-inspect.json" "$evidence_root/data-directory-inspect.json"
|
||||
|
||||
uid=$(id -u)
|
||||
gid=$(id -g)
|
||||
run_inspect() {
|
||||
command_file=$1
|
||||
result_file=$2
|
||||
docker run --rm --read-only --user "$uid:$gid" --network none \
|
||||
--cap-drop ALL --security-opt no-new-privileges \
|
||||
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
|
||||
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
||||
--mount "type=bind,src=$legacy_root,dst=/var/lib/qinglong2,readonly" \
|
||||
--mount "type=bind,src=$evidence_root,dst=/var/lib/qinglong3" \
|
||||
"$OPERATOR_IMAGE" adoption run \
|
||||
--command-file "/var/lib/qinglong3/$command_file" \
|
||||
>"$evidence_root/results/$result_file"
|
||||
}
|
||||
|
||||
run_inspect sqlite-inspect.json sqlite-inspect.result.json
|
||||
grep -q '"status":"inspected"' "$evidence_root/results/sqlite-inspect.result.json" || fail 'SQLite inspect did not report inspected'
|
||||
run_inspect data-directory-inspect.json data-directory-inspect.result.json
|
||||
grep -q '"status":"inspected"' "$evidence_root/results/data-directory-inspect.result.json" || fail 'data-directory inspect did not report inspected'
|
||||
|
||||
umask "$old_umask"
|
||||
printf '%s\n' \
|
||||
"QingLong 2.x upgrade readiness inspection completed ($profile, $ARCHITECTURE)." \
|
||||
"SQLite plan: $evidence_root/results/sqlite-inspect.result.json" \
|
||||
"Data-directory plan: $evidence_root/results/data-directory-inspect.result.json" \
|
||||
'The legacy data root was mounted read-only; no stage, activation, cutover or rollback was authorized.' \
|
||||
'Review both complete results and preserve their exact plan digests before any later rehearsal phase.'
|
||||
@@ -80,8 +80,8 @@ function writeMilestone(directory, manifest) {
|
||||
|
||||
function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||
return {
|
||||
schemaVersion: 2,
|
||||
schema: 'qinglong/alpha-local-milestone@v2',
|
||||
schemaVersion: 3,
|
||||
schema: 'qinglong/alpha-local-milestone@v3',
|
||||
maturity: 'alpha_candidate_not_public_release',
|
||||
product: 'local',
|
||||
variant,
|
||||
@@ -101,6 +101,7 @@ function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||
applicationImageId: digest('3'),
|
||||
operatorImageId: digest('4'),
|
||||
verificationSha256: digest('5'),
|
||||
upgradeReadinessSha256: digest('b'),
|
||||
},
|
||||
arm64: {
|
||||
artifactName: `ql3-alpha-${revision}-local-${variant}-arm64`,
|
||||
@@ -114,6 +115,7 @@ function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||
applicationImageId: digest('8'),
|
||||
operatorImageId: digest('9'),
|
||||
verificationSha256: digest('a'),
|
||||
upgradeReadinessSha256: digest('c'),
|
||||
},
|
||||
},
|
||||
readme: null,
|
||||
|
||||
@@ -29,7 +29,12 @@ const revision = 'a'.repeat(40);
|
||||
const runId = '33047425710';
|
||||
const runAttempt = '2';
|
||||
|
||||
function imageInspection(role, architecture, idCharacter, variant = 'headless') {
|
||||
function imageInspection(
|
||||
role,
|
||||
architecture,
|
||||
idCharacter,
|
||||
variant = 'headless',
|
||||
) {
|
||||
return {
|
||||
Id: `sha256:${idCharacter.repeat(64)}`,
|
||||
Os: 'linux',
|
||||
@@ -201,7 +206,11 @@ function finalizeOptions(paths) {
|
||||
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@v2');
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v3');
|
||||
assert.match(
|
||||
manifest.artifacts.amd64.upgradeReadinessSha256,
|
||||
/^sha256:[0-9a-f]{64}$/,
|
||||
);
|
||||
assert.equal(manifest.variant, 'headless');
|
||||
assert.equal(manifest.sourceRevision, revision);
|
||||
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
|
||||
|
||||
@@ -166,7 +166,7 @@ function adapters(overrides = {}, variant = 'headless') {
|
||||
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@v5');
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v6');
|
||||
assert.equal(manifest.variant, 'headless');
|
||||
assert.equal(manifest.sourceRevision, revision);
|
||||
assert.equal(manifest.architecture, 'arm64');
|
||||
@@ -175,6 +175,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
|
||||
assert.equal(manifest.verification.file, 'verification-evidence.json');
|
||||
assert.equal(manifest.quickstart.file, 'quickstart.sh');
|
||||
assert.equal(manifest.upgradeReadiness.file, 'upgrade-readiness.sh');
|
||||
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
|
||||
const syntax = spawnSync('sh', ['-n', quickstart], { encoding: 'utf8' });
|
||||
assert.equal(syntax.status, 0, syntax.stderr);
|
||||
@@ -197,6 +198,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||
'qinglong3-local-operator.cdx.json',
|
||||
'qinglong3-local-trial-kit-arm64.docker.tar',
|
||||
'quickstart.sh',
|
||||
'upgrade-readiness.sh',
|
||||
'verification-evidence.json',
|
||||
]);
|
||||
});
|
||||
@@ -229,6 +231,7 @@ test('materializes a distinct loopback Console trial kit without widening the he
|
||||
assert.equal(verification.gates.consoleLiveJourney, 'passed');
|
||||
assert.equal(verification.gates.firstAutomationJourney, 'passed');
|
||||
assert.equal(verification.gates.ownerCredentialPresentation, 'passed');
|
||||
assert.equal(verification.gates.legacyUpgradeReadiness, 'passed');
|
||||
const quickstartContents = fs.readFileSync(
|
||||
path.join(paths.outputRoot, 'quickstart.sh'),
|
||||
'utf8',
|
||||
@@ -251,6 +254,7 @@ test('materializes a distinct loopback Console trial kit without widening the he
|
||||
'qinglong3-local-console-trial-kit-arm64.docker.tar',
|
||||
'qinglong3-local-operator.cdx.json',
|
||||
'quickstart.sh',
|
||||
'upgrade-readiness.sh',
|
||||
'verification-evidence.json',
|
||||
]);
|
||||
});
|
||||
@@ -283,6 +287,7 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
|
||||
'archive',
|
||||
'extra',
|
||||
'quickstart',
|
||||
'upgrade-readiness',
|
||||
'sbom',
|
||||
'verification',
|
||||
]) {
|
||||
@@ -304,6 +309,11 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
|
||||
path.join(paths.outputRoot, 'quickstart.sh'),
|
||||
'# drift\n',
|
||||
);
|
||||
} else if (mutation === 'upgrade-readiness') {
|
||||
fs.appendFileSync(
|
||||
path.join(paths.outputRoot, 'upgrade-readiness.sh'),
|
||||
'# drift\n',
|
||||
);
|
||||
} else if (mutation === 'sbom') {
|
||||
fs.copyFileSync(
|
||||
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
|
||||
@@ -342,6 +352,7 @@ test('offline audit rejects a rehashed non-canonical quickstart', (t) => {
|
||||
'qinglong3-local-operator.cdx.json',
|
||||
'verification-evidence.json',
|
||||
'quickstart.sh',
|
||||
'upgrade-readiness.sh',
|
||||
'README.md',
|
||||
'manifest.json',
|
||||
];
|
||||
@@ -450,6 +461,79 @@ exit 1
|
||||
assert.doesNotMatch(calls, /--network (?!none)/);
|
||||
});
|
||||
|
||||
test('generated upgrade readiness drives two read-only legacy inspections', (t) => {
|
||||
const paths = fixture(t);
|
||||
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||
const fakeBin = path.join(paths.fixtureRoot, 'readiness-fake-bin');
|
||||
fs.mkdirSync(fakeBin);
|
||||
const dockerLog = path.join(paths.fixtureRoot, 'readiness-docker.log');
|
||||
const fakeDocker = path.join(fakeBin, 'docker');
|
||||
fs.writeFileSync(
|
||||
fakeDocker,
|
||||
`#!/bin/sh
|
||||
printf '%s\\n' "$*" >>"$FAKE_DOCKER_LOG"
|
||||
case "$1:$2" in
|
||||
info:|load:*) exit 0 ;;
|
||||
image:inspect)
|
||||
printf '%s\\n' 'sha256:${'2'.repeat(
|
||||
64,
|
||||
)}|arm64|65532:65532|${revision}|short-lived|none-by-default'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
case " $* " in
|
||||
*'/sqlite-inspect.json'*) printf '%s\\n' '{"status":"inspected","evidence":{"planDigest":"${'a'.repeat(
|
||||
64,
|
||||
)}"}}'; exit 0 ;;
|
||||
*'/data-directory-inspect.json'*) printf '%s\\n' '{"status":"inspected","evidence":{"planDigest":"${'b'.repeat(
|
||||
64,
|
||||
)}"}}'; exit 0 ;;
|
||||
esac
|
||||
exit 1
|
||||
`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
const legacyRoot = path.join(paths.fixtureRoot, 'legacy-data');
|
||||
fs.mkdirSync(path.join(legacyRoot, 'db'), { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(path.join(legacyRoot, 'db', 'database.sqlite'), 'legacy', {
|
||||
mode: 0o600,
|
||||
});
|
||||
const evidenceRoot = path.join(paths.fixtureRoot, 'upgrade-readiness');
|
||||
const run = spawnSync(
|
||||
'sh',
|
||||
[
|
||||
path.join(paths.outputRoot, 'upgrade-readiness.sh'),
|
||||
'edge',
|
||||
legacyRoot,
|
||||
evidenceRoot,
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
FAKE_DOCKER_LOG: dockerLog,
|
||||
PATH: `${fakeBin}:${process.env.PATH}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(run.status, 0, `${run.stderr}\n${run.stdout}`);
|
||||
assert.match(run.stdout, /upgrade readiness inspection completed/);
|
||||
assert.equal(
|
||||
JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(evidenceRoot, 'results', 'sqlite-inspect.result.json'),
|
||||
'utf8',
|
||||
),
|
||||
).status,
|
||||
'inspected',
|
||||
);
|
||||
const calls = fs.readFileSync(dockerLog, 'utf8');
|
||||
assert.match(calls, /dst=\/var\/lib\/qinglong2,readonly/);
|
||||
assert.match(calls, /--network none/);
|
||||
assert.match(calls, /--memory 128m --memory-swap 128m/);
|
||||
assert.doesNotMatch(calls, /adoption\.stage|activation\.prepare|cutover/);
|
||||
});
|
||||
|
||||
test('create rejects an image reference that could alter the shell journey', (t) => {
|
||||
const paths = fixture(t);
|
||||
const options = createOptions(paths);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createFixture,
|
||||
parseArguments,
|
||||
} = require('../../scripts/ql3-local-alpha-upgrade-readiness-fixture.cjs');
|
||||
|
||||
test('creates one private production-shaped QingLong 2.x readiness fixture', (t) => {
|
||||
const root = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-upgrade-readiness-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const output = path.join(root, 'legacy-data');
|
||||
const result = createFixture(output);
|
||||
assert.equal(result.output, output);
|
||||
assert.equal(fs.statSync(output).mode & 0o777, 0o700);
|
||||
assert.equal(fs.statSync(result.database).mode & 0o777, 0o600);
|
||||
const database = new DatabaseSync(result.database, { readOnly: true });
|
||||
try {
|
||||
const tables = database
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_schema WHERE type = 'table' ORDER BY name",
|
||||
)
|
||||
.all()
|
||||
.map(({ name }) => name);
|
||||
assert.deepEqual(tables, [
|
||||
'Apps',
|
||||
'Auths',
|
||||
'CrontabStats',
|
||||
'CrontabViews',
|
||||
'Crontabs',
|
||||
'Dependences',
|
||||
'Envs',
|
||||
'PluginOwnedState',
|
||||
'RunningInstances',
|
||||
'Subscriptions',
|
||||
'sqlite_sequence',
|
||||
]);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
assert.throws(() => createFixture(output), /must not already exist/);
|
||||
});
|
||||
|
||||
test('requires one normalized absolute output path', () => {
|
||||
assert.equal(parseArguments(['--output=/tmp/ql3-legacy']), '/tmp/ql3-legacy');
|
||||
assert.throws(() => parseArguments([]), /usage/);
|
||||
assert.throws(() => parseArguments(['--output=relative']), /absolute path/);
|
||||
assert.throws(
|
||||
() => parseArguments(['--output=/tmp/../tmp/legacy']),
|
||||
/normalized absolute non-root path/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user