test(release): [REL-04] checkpoint independent installed package rollback

This commit is contained in:
Harvey Zhao committed 2026-09-15 00:16:43 +08:00
1 parent 09459955c6
commit aa29dc9212
14 files changed
+4717 -5

No files matched your search

+3 -1
View File
@@ -45,7 +45,7 @@
"check:toolchain": "node scripts/check-toolchain.mjs",
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build,library,pages,site-vendor,browser-validation}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
"check:plan": "node refactor/scripts/plan.mjs --check",
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js test/dev-server.test.js test/pages-artifact.test.js test/site-notices.test.js test/vconsole-lifecycle.test.js test/browser-validation.test.js",
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js test/dev-server.test.js test/pages-artifact.test.js test/site-notices.test.js test/vconsole-lifecycle.test.js test/browser-validation.test.js test/rollback-files.test.js",
"test:baseline": "node --test refactor/scripts/*.test.mjs",
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn check:release-ledger && yarn lint && yarn typecheck:library && yarn typecheck:scaffold && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:llm && yarn check:vconsole && yarn check:site-notices && yarn typecheck:site-assets && yarn check:site-assets && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test",
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:vconsole && yarn build:site-notices && yarn build:test && yarn build:docs && yarn test:imports",
@@ -60,6 +60,8 @@
"test:browser:install": "playwright install chromium firefox webkit",
"test:browser:report": "playwright show-report refactor/.cache/browser/html",
"test:package": "node scripts/package-check.mjs",
"test:rollback": "node scripts/rollback-consumer.mjs",
"test:rollback:browser": "playwright test --config playwright.rollback.config.js",
"test:package:release": "node scripts/package-check.mjs --release",
"build:types": "node scripts/build-types.mjs --write",
"check:types": "node scripts/build-types.mjs --check",
+18
View File
@@ -0,0 +1,18 @@
import process from 'node:process'
import { defineConfig } from '@playwright/test'
import base from './playwright.config.js'
import { verifyRollbackArtifacts } from './scripts/rollback-artifacts.mjs'
verifyRollbackArtifacts(process.cwd(), process.env.ARTPLAYER_BROWSER_ARTIFACTS)
export default defineConfig(base, {
testMatch: ['**/playback.spec.js'],
grep: /candidate: real playback/,
retries: 0,
workers: 2,
outputDir: 'refactor/.cache/rollback-browser/results',
reporter: [
['list'],
['json', { outputFile: 'refactor/.cache/rollback-browser/report.json' }],
['html', { outputFolder: 'refactor/.cache/rollback-browser/html', open: 'never' }],
],
})
+1
View File
@@ -29,6 +29,7 @@
| [docs 页面与编辑器测试](docs-browser-testing.md) | 复用已有 HTML、加载版本、隔离状态和补强文档 smoke |
| [多轮复盘与 npm 准入](release-reviews.md) | Chrome 验证分工、三轮全局复盘、问题闭环和候选发布门槛 |
| [逐包发布准入台账](release-ledger.md) | 22包候选/证据绑定、源码和产物指纹、任务风险/许可阻断及严格预检 |
| [回退演练与维护](rollback-rehearsal.md) | 独立核心/插件回退、冻结安装文件检查、真实播放及剩余发布门槛 |
| [工具链与发布](toolchain-release.md) | TypeScript、Bun、构建、版本管理和发布回退 |
| [已实现的开发环境](toolchain-setup.md) | Node/Yarn 固定版本、锁文件、安装命令和实际验证范围 |
| [全包大版本策略](version-policy.md) | 每包分别升级一个 major 的目标清单、兼容要求和版本落地步骤 |
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,29 @@
# REL-04 核心与 Chapter 实际安装回退检查点
任务保持 doing,未增加完成任务数。生产源码、声明、分发文件及根锁未修改。
新增 `test:rollback`、`test:rollback:browser`,复用现有包构建和播放器浏览器用例。
安装流程、文件完整性校验、浏览器产物来源验证分别拆分,维护说明见
[回退演练](../rollback-rehearsal.md)。没有引入新依赖,使用已固定 Node/Yarn/Playwright。
## 验证
- `yarn test:package`:隔离生产构建与安装通过,36 项候选运行时检查,5/5 旧类型模式、8/8 精确类型模式。
- `yarn test:rollback`:七次连续安装/升级/回退均通过,237 项运行时检查;所有文件与冻结锁恢复一致。
- 最终安装产物在五种状态各执行三引擎真实播放,共 15/15,通过解码像素、播放/暂停/拖动/切源/销毁与 Chapter DOM 检查。
- Node 文件验证负例、相关工程回归、scoped lint、plan 检查及严格工具链结果随机器证据保存。
- 普通 installed runner 最初拒绝 REL-04 报告(它要求 ENG-07);这次尝试未运行浏览器。不改变其约束,新增独立回退验证入口后测试通过。初次七步报告及拒绝日志仍保存在 cache。
具体 HEAD、tarball/lock/报告指纹、命令和报告位置见
[机器证据](../baselines/rollback-consumer-validation.json)。此检查点证明核心/Chapter
流程,不证明所有包或最终 6.0.0 等候选已验收。浏览器读取已核验安装副本,原临时
消费者已清理;新旧包必须按 archive 指纹区分,不能只看版本号。
## 主线与未完成项
只读 fetch 后,master/origin/master/FETCH_HEAD 均为
`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`,当前 HEAD 比其领先 256 提交,
没有主线新增提交。仍需执行可重复的冲突同步演练、iframe 更名回退和全包恢复表。
Thumbnail 缺失完整历史包及站点远端恢复不因此关闭,正式发布仍须三轮复盘。
回退本次工程改动可以撤销本检查点提交;不涉及用户安装版本或远端状态。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 262 项,范围 22 个包及工作区/示例。
状态:todo 46 / doing 21 / blocked 0 / done 195 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 45 / doing 22 / blocked 0 / done 195 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -431,7 +431,7 @@
| REL-09 | workspace<br>落实全包下一 major 版本及依赖元数据 | REL-01, DOC-11 | 22 包版本目标落实、适用锁文件/依赖范围/示例/变更日志同步,核实 registry 版本占用 | 版本与 policy 一致,旧核心支持保留;必要拆子任务各自提交,候选构建前完成,不执行 publish | H | todo |
| REL-02 | workspace<br>生成候选 tarball 并验证新旧组合 | REL-01, CORE-22, ENG-07, REL-09 | 各候选本地 tarball/integrity 与新旧核心/插件消费者验证报告,外部门槛单独标注 | 隔离安装和可自动化组合通过;设备结论不伪造,最终发布绑定同一候选内容;在目标 major 版本确定后构建 pack,不在测试后改版本 | H | todo |
| REL-03 | workspace<br>完成真机、外部 SDK 与压力验收 | REL-02, EX-03, SITE-06, MOD-05 | Safari/移动/PiP/Cast/IMA/模型/长播放完整报告 | 包集成与全项目真机/SDK/压力结论齐全才完成本汇总;独立批次先建立自己的完整门槛子任务 | H | todo |
| REL-04 | workspace<br>提前演练回退与主线修复同步 | REL-08, ENG-07 | 分包旧版本/tag/依赖回退方案与 master 差异 | 隔离包演练可回退,主线修复同步流程可执行;正式每批再核对其实际回退产物 | H | todo |
| REL-04 | workspace<br>提前演练回退与主线修复同步 | REL-08, ENG-07 | 分包旧版本/tag/依赖回退方案与 master 差异 | 隔离包演练可回退,主线修复同步流程可执行;正式每批再核对其实际回退产物 | H | doing |
| REL-05 | workspace<br>经授权发布候选并收集反馈 | REL-03, REL-04, REVIEW-03 | 候选 tag、完整产物 integrity、反馈与复现记录 | 实际发布授权/操作/版本可追溯;本计划不自动执行发布 | H | todo |
| REL-06 | workspace<br>经授权分批正式发布 | REL-05 | 正式分包版本/tag/站点文档及兼容公告 | 候选验收通过,安装/浏览器复核和回退入口就绪;候选反馈修复须独立建任务并复验三轮受影响结论,正式内容与最终验证产物一致 | H | todo |
| REL-07 | workspace<br>关闭重构里程碑并维护后续队列 | REL-06, ENG-LINT-01 | 最终任务/设计/证据归档和维护指南 | 全范围任务有结论、无未解释兼容缺口;遗留项有明确后续责任 | L | todo |
@@ -658,6 +658,7 @@
- MOD-DEV-01: [记录](changes/2026-09-14-MOD-DEV-01-dev-server.md) [记录](baselines/dev-server-validation.json)
- MOD-03: [记录](changes/2026-09-14-MOD-03-slider-updates.md) [记录](baselines/slider-updates-validation.json) [记录](changes/2026-09-14-MOD-03-performance-acceptance.md) [记录](baselines/core-performance-validation.json)
- REL-08: [记录](changes/2026-09-14-REL-08-release-ledger.md) [记录](baselines/release-ledger-validation.json) [记录](release-ledger.md) [记录](release-ledger.json)
- REL-04: [记录](changes/2026-09-15-REL-04-consumer-checkpoint.md) [记录](rollback-rehearsal.md) [记录](baselines/rollback-consumer-validation.json)
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md) [记录](type-compatibility-policy.md) [记录](baselines/factory-compatibility-validation.json) [记录](changes/2026-09-13-PKG-FACTORY-01-compatible-types.md)
- CORE-25: [记录](changes/2026-09-13-CORE-25-defaults-ssr.md) [记录](baselines/defaults-ssr-validation.json)
- ENG-12: [记录](changes/2026-09-13-ENG-12-library-public.md) [记录](baselines/library-public-validation.json)
+10
View File
@@ -1,5 +1,15 @@
# 进度与证据
## REL-04 核心/Chapter 回退演练检查点
新增实际隔离安装回退脚本,七步安装/升级/分别回退/整体恢复通过,237 项运行时
检查及完整文件/冻结锁验证通过。五组状态三引擎真实播放 15/15;隔离候选类型
5/5 旧模式、8/8 精确模式通过。新增脚本未安装依赖,未修改生产产物或根锁。
主线 fetch 后仍为重构起点,无新增待同步提交。全包恢复表、iframe 更名回退和
冲突同步演练尚待完成,REL-04 保持 doing;262 项为 195 done、22 doing、45 todo。
见[检查点](changes/2026-09-15-REL-04-consumer-checkpoint.md)及
[维护说明](rollback-rehearsal.md)。
## PKG-VAST-05 增补真实playUrl和SDK跳过按钮
真实HTTP标签、广告解码、跳过事件/按钮点击及主片恢复最终5/6通过;Chromium默认
+70
View File
@@ -0,0 +1,70 @@
# 回退演练与维护入口
REL-04 仍在进行中。本页记录已经可重跑的核心/Chapter 演练,不代表所有包或最终大版本候选已可回退。
## 已实现的安装演练
固定 Node `.node-version`、Yarn Classic 1.22.22,先运行 `yarn test:package`,再运行
`yarn test:rollback`。前者在隔离快照中执行正常生产构建、类型生成和打包;后者读取
同一 HEAD 的构建报告及已冻结 npm 历史 tarball。不会改写工作区生产产物或根锁文件。
`scripts/rollback-consumer.mjs` 建立四套消费者 manifest/lock:旧核心+旧插件、
新核心+旧插件、旧核心+新插件、新核心+新插件。准备锁文件时从根锁解析离线依赖,
随后在同一个仓库外消费者目录中连续执行以下步骤:
| 步骤 | 安装状态 | 验证意图 |
| --- | --- | --- |
| install-old | 旧核心+旧插件 | 原始安装 |
| upgrade-core | 新核心+旧插件 | 独立升级核心 |
| upgrade-plugin | 新核心+新插件 | 升级插件 |
| rollback-plugin | 新核心+旧插件 | 单独回退插件 |
| restore-candidate | 新核心+新插件 | 恢复升级状态 |
| rollback-core | 旧核心+新插件 | 单独回退核心 |
| rollback-all | 旧核心+旧插件 | 恢复完整旧组合 |
每一步恢复对应 manifest/lock 后执行 Yarn `install --offline --frozen-lockfile
--force --ignore-scripts --non-interactive`;不会先删除 node_modules,因此能够检测
替换过程中遗留的新版文件。冻结锁字节必须不变,所有包成员及文件清单必须与实际
tarball 一致。版本号相同也检查内容;例如本次 Chapter 新旧均为 1.1.0。
`scripts/rollback-files.mjs` 专门检查安装清单、成员哈希和 workspace/symlink 逃逸。
仅忽略包根目录的 Yarn `.yarn-metadata.json`、`.yarn-tarball.tgz` 安装元数据。
负例覆盖相同版本的错误内容、新版残留文件及缺失文件;纳入 `test:node`。
消费者临时目录在成功或失败后清理,日志、锁和已核验安装副本保存在
`refactor/.cache/rollback/run-*`,`latest.json` 指向最近一次结果并记录成功状态。
复用 `test/package/runtime.cjs` 检查实际 CJS/ESM/UMD/AMD/legacy/i18n、SSR 与
Emitter/公开形状。旧核心显式使用历史配置,保留历史 SSR 失败断言;不会因为
回退而要求旧包支持新 `/runtime` 入口。安装脚本禁用不代表验证了 lifecycle hooks。
## 真实浏览器验证
设置 `ARTPLAYER_BROWSER_ARTIFACTS` 为成功演练输出中的某个
`<action>-browser-artifacts.json`,运行 `yarn test:rollback:browser`。
`playwright.rollback.config.js` 复用已提交播放用例,三引擎、零自动重试;检查实际
解码像素、播放、暂停、seek、切换 docs 媒体、Chapter DOM、事件与 destroy。
`scripts/rollback-artifacts.mjs` 在浏览器启动前校验:演练已成功、HEAD/Node 一致、
候选构建输入仍匹配当前源码/工具链、包来自已验证候选或冻结历史 archive、安装
副本所有文件和对应锁哈希一致。直接使用普通 installed runner 会因报告不是
ENG-07 而被拒绝;保留该门槛,回退使用独立验证入口。
浏览器通过独立 fixture 服务读取已核验安装副本,不是从仍存活的临时消费者目录
加载。用例中的 `candidate` 是路由名,具体版本以演练 profile 为准。
报告写入 `refactor/.cache/rollback-browser`;连续测试不同状态前,在前一进程
结束后把报告移入唯一 cache 子目录,防止覆盖。端口沿用测试服务 8084。
## 当前结果与剩余工作
2026-09-15,七步安装/回退均通过,共 237 个运行时检查;五组状态在三个桌面引擎中
15/15 真实播放通过。隔离候选构建另有 5/5 旧类型模式、8/8 精确类型模式通过。
详见 [机器证据](baselines/rollback-consumer-validation.json) 和
[检查点](changes/2026-09-15-REL-04-consumer-checkpoint.md)。
尚需:22 包逐包旧版本/tag/依赖恢复表、iframe 更名前后消费者回退、主线修改与
JS→TS 冲突同步/中止演练。已执行 `git fetch origin master`,当时 master 没有
新增待同步提交;这不等于验证过冲突解决。
Thumbnail 的完整旧 npm tarball 缺失、文档站的真实部署恢复继续受原有发布门槛
约束。正式每批需要重新绑定实际 next-major 候选及旧包产物并重跑对应验证。
不执行 npm tag 修改、unpublish、推送或部署,不把本演练当作这些动作的授权。
+6 -2
View File
@@ -4798,11 +4798,15 @@
"REL-08",
"ENG-07"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "分包旧版本/tag/依赖回退方案与 master 差异",
"acceptance": "隔离包演练可回退,主线修复同步流程可执行;正式每批再核对其实际回退产物",
"evidence": []
"evidence": [
"changes/2026-09-15-REL-04-consumer-checkpoint.md",
"rollback-rehearsal.md",
"baselines/rollback-consumer-validation.json"
]
},
{
"id": "REVIEW-01",
+37
View File
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { hash } from '../refactor/scripts/releases.mjs'
import { verifyInstalledArtifacts } from './installed-artifacts.mjs'
import { verifyRollbackFiles } from './rollback-files.mjs'
export function verifyRollbackArtifacts(root, mapFile) {
assert(mapFile, 'Set ARTPLAYER_BROWSER_ARTIFACTS to a rollback step map')
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const directory = path.dirname(fs.realpathSync(mapFile))
assert.equal(path.dirname(directory), fs.realpathSync(path.join(root, 'refactor/.cache/rollback')))
const report = read(path.join(directory, 'report.json'))
assert.equal(report.task, 'REL-04')
assert.equal(report.passed, true, 'Rollback installation must pass first')
assert.equal(report.node, process.versions.node)
assert.equal(report.source, execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', windowsHide: true }).trim())
const step = report.steps.find(item => item.browserArtifacts === path.basename(mapFile))
assert(step, 'Unknown rollback step map')
const candidate = verifyInstalledArtifacts(root, path.join(root, report.build, 'browser-artifacts.json'))
const historical = read(path.join(root, 'refactor/baselines/releases.json')).releases
const map = read(mapFile)
const expectedNames = ['artplayer', 'artplayer-plugin-chapter']
assert.deepEqual(Object.keys(map), expectedNames)
const packages = report.profiles[step.profile].packages.map((pkg) => {
const recorded = [...candidate.packages, ...historical].find(item => item.name === pkg.name && item.sha256 === pkg.sha256)
assert(recorded, 'Rollback package is not a verified candidate or frozen historical archive')
assert.equal(hash(fs.readFileSync(path.resolve(root, pkg.archive))), recorded.sha256)
assert.equal(map[pkg.name], `./${step.action}/node_modules/${pkg.name}/dist/${pkg.name}.js`)
return verifyRollbackFiles(path.join(directory, step.action), recorded)
})
assert.deepEqual(packages, step.packages)
assert.equal(hash(fs.readFileSync(path.join(directory, `${step.profile}-yarn.lock`))), step.lockSha256)
return { directory, action: step.action, packages }
}
+97
View File
@@ -0,0 +1,97 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { ensureArchive, hash } from '../refactor/scripts/releases.mjs'
import { consumerDirectory, names, readJson, removeConsumer, run, runtimeConsumer, workspace, writeJson } from './package-consumer.mjs'
import { verifyRollbackFiles } from './rollback-files.mjs'
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use pinned Yarn')
assert.equal(process.versions.node, fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim(), 'Use pinned Node')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn), 'Missing Yarn executable')
assert.equal(run([yarn, '--version'], workspace).trim(), '1.22.22')
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8', windowsHide: true }).trim()
const builds = fs.realpathSync(path.join(workspace, 'refactor/.cache/packages'))
const build = fs.realpathSync(path.resolve(workspace, readJson(path.join(builds, 'latest.json')).output))
assert.equal(path.dirname(build), builds, 'Use a direct package-check output directory')
const candidate = readJson(path.join(build, 'report.json'))
assert.equal(candidate.source, source, 'Run yarn test:package on the current commit first')
assert.deepEqual(candidate.packages.map(pkg => pkg.name), names, 'This rehearsal covers core and Chapter')
const parent = path.join(workspace, 'refactor/.cache/rollback')
fs.mkdirSync(parent, { recursive: true })
const output = fs.mkdtempSync(path.join(parent, 'run-'))
const relative = file => path.relative(workspace, file).replaceAll('\\', '/')
const report = { task: 'REL-04', capturedAt: new Date().toISOString(), source, node: process.versions.node, yarn: '1.22.22', passed: false, build: relative(build), scope: names, profiles: {}, steps: [], limitations: ['Early rehearsal; not final next-major candidate acceptance.', 'Runtime imports and file restoration only; browser playback and other package rollbacks require separate evidence.'] }
const consumer = consumerDirectory()
try {
const old = []
for (const release of readJson(path.join(workspace, 'refactor/baselines/releases.json')).releases) {
const archive = await ensureArchive(release)
old.push({ ...release, archive })
}
assert.deepEqual(old.map(pkg => pkg.name), names, 'Historical package order must match the rehearsal profiles')
const current = candidate.packages.map((pkg) => {
assert.equal(path.basename(pkg.archive), pkg.archive)
const archive = path.join(build, pkg.archive)
assert.equal(hash(fs.readFileSync(archive)), pkg.sha256, 'Candidate archive changed')
return { ...pkg, archive }
})
const profiles = { baseline: old, core: [current[0], old[1]], plugin: [old[0], current[1]], both: current }
for (const [id, packages] of Object.entries(profiles)) {
const dir = consumerDirectory()
try {
const manifest = { name: 'artplayer-rollback-consumer', private: true, dependencies: Object.fromEntries(packages.map(pkg => [pkg.name, `file:${pkg.archive.replaceAll('\\', '/')}`])) }
writeJson(path.join(dir, 'package.json'), manifest)
fs.copyFileSync(path.join(workspace, 'yarn.lock'), path.join(dir, 'yarn.lock'))
fs.writeFileSync(path.join(output, `${id}-prepare.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], dir))
for (const pkg of packages) verifyRollbackFiles(dir, pkg)
fs.copyFileSync(path.join(dir, 'package.json'), path.join(output, `${id}-package.json`))
fs.copyFileSync(path.join(dir, 'yarn.lock'), path.join(output, `${id}-yarn.lock`))
report.profiles[id] = { packages: packages.map(pkg => ({ name: pkg.name, version: pkg.version, archive: relative(pkg.archive), sha256: pkg.sha256 })), lockSha256: hash(fs.readFileSync(path.join(dir, 'yarn.lock'))) }
}
finally { removeConsumer(dir) }
}
const sequence = [
['baseline', 'install-old'],
['core', 'upgrade-core'],
['both', 'upgrade-plugin'],
['core', 'rollback-plugin'],
['both', 'restore-candidate'],
['plugin', 'rollback-core'],
['baseline', 'rollback-all'],
]
for (const [id, action] of sequence) {
fs.copyFileSync(path.join(output, `${id}-package.json`), path.join(consumer, 'package.json'))
const lock = fs.readFileSync(path.join(output, `${id}-yarn.lock`))
fs.writeFileSync(path.join(consumer, 'yarn.lock'), lock)
fs.writeFileSync(path.join(output, `${action}-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--force', '--ignore-scripts', '--non-interactive'], consumer))
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock, 'Rollback changed the frozen lock')
const packages = profiles[id].map(pkg => verifyRollbackFiles(consumer, pkg))
const runtime = runtimeConsumer(consumer, { baseline: id === 'baseline' || id === 'plugin' })
if (report.steps.length)
assert.deepEqual(runtime.observations.api, report.steps[0].runtime.observations.api, 'Rollback public API shape changed')
const artifacts = {}
for (const name of names) {
fs.cpSync(path.join(consumer, 'node_modules', name), path.join(output, action, 'node_modules', name), { recursive: true })
artifacts[name] = `./${action}/node_modules/${name}/dist/${name}.js`
}
writeJson(path.join(output, `${action}-browser-artifacts.json`), artifacts)
report.steps.push({ action, profile: id, lockSha256: hash(lock), packages, runtime, browserArtifacts: `${action}-browser-artifacts.json` })
writeJson(path.join(output, 'report.json'), report)
console.log(`${action}: exact installed files and ${runtime.checks.length} runtime checks passed`)
}
report.passed = true
}
catch (error) {
report.error = { name: error.name, message: error.message }
fs.writeFileSync(path.join(output, 'failure.log'), `${error.stack}\n${error.stdout || ''}\n${error.stderr || ''}`)
throw error
}
finally {
writeJson(path.join(output, 'report.json'), report)
writeJson(path.join(parent, 'latest.json'), { output: relative(output), passed: report.passed })
removeConsumer(consumer)
console.log(`Rollback report: ${relative(output)}`)
}
+36
View File
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { hash } from '../refactor/scripts/releases.mjs'
export function verifyRollbackFiles(directory, pkg) {
const root = path.join(directory, 'node_modules', pkg.name)
assert.equal(fs.realpathSync(root), path.resolve(root), 'Rollback package must not be a workspace link')
const actual = []
function visit(folder) {
for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
const file = path.join(folder, entry.name)
const relative = path.relative(root, file).replaceAll('\\', '/')
assert(!entry.isSymbolicLink(), `Unexpected installed link: ${relative}`)
if (entry.isDirectory()) {
visit(file)
}
else {
assert(entry.isFile(), `Unexpected installed special file: ${relative}`)
// Yarn Classic may add these files at the package root.
if (!['.yarn-metadata.json', '.yarn-tarball.tgz'].includes(relative))
actual.push(`package/${relative}`)
}
}
}
visit(root)
assert.deepEqual(actual.sort(), Object.keys(pkg.files).sort(), `Rollback contains missing or leftover files: ${pkg.name}`)
for (const [member, digest] of Object.entries(pkg.files)) {
assert(member.startsWith('package/') && !member.includes('\\') && !member.split('/').includes('..'), 'Unsafe rollback member')
assert.equal(hash(fs.readFileSync(path.join(root, member.slice(8)))), digest, `Rollback bytes differ: ${pkg.name}/${member}`)
}
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
assert.equal(manifest.name, pkg.name)
assert.equal(manifest.version, pkg.version)
return { name: pkg.name, version: pkg.version, archiveSha256: pkg.sha256, verifiedMembers: actual.length }
}
+8
View File
@@ -1,5 +1,13 @@
# Installed package checks
For independent core/plugin rollback, run `yarn test:package` then
`yarn test:rollback`. The seven-step rehearsal restores frozen consumer locks,
checks complete installed file inventories and runtime entrypoints, and saves
verified installation copies. `yarn test:rollback:browser` validates a selected
step through `ARTPLAYER_BROWSER_ARTIFACTS`. See
[rollback maintenance](../../refactor/rollback-rehearsal.md) for report paths,
browser scope and the remaining package-specific release gates.
Run `yarn test:package` with the pinned Node and Yarn after a frozen install. The
default consumer scope is core and chapter. For Ambilight/Canvas/Document PiP browser work,
run `yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip`.
+30
View File
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
// eslint-disable-next-line test/no-import-node-test -- Exercise stale and incomplete rollback installations.
import { test } from 'node:test'
import { hash } from '../refactor/scripts/releases.mjs'
import { consumerDirectory, removeConsumer } from '../scripts/package-consumer.mjs'
import { verifyRollbackFiles } from '../scripts/rollback-files.mjs'
test('rollback verifies content even when candidate and old version match', () => {
const dir = consumerDirectory()
try {
const root = path.join(dir, 'node_modules/artplayer')
fs.mkdirSync(root, { recursive: true })
const manifest = JSON.stringify({ name: 'artplayer', version: '1.0.0' })
fs.writeFileSync(path.join(root, 'package.json'), manifest)
fs.writeFileSync(path.join(root, 'index.js'), 'old')
const pkg = { name: 'artplayer', version: '1.0.0', files: { 'package/package.json': hash(manifest), 'package/index.js': hash('old') } }
assert.equal(verifyRollbackFiles(dir, pkg).verifiedMembers, 2)
fs.writeFileSync(path.join(root, 'index.js'), 'new')
assert.throws(() => verifyRollbackFiles(dir, pkg), /Rollback bytes differ/)
fs.writeFileSync(path.join(root, 'index.js'), 'old')
fs.writeFileSync(path.join(root, 'runtime.js'), 'new-only')
assert.throws(() => verifyRollbackFiles(dir, pkg), /leftover files/)
fs.unlinkSync(path.join(root, 'runtime.js'))
fs.unlinkSync(path.join(root, 'index.js'))
assert.throws(() => verifyRollbackFiles(dir, pkg), /missing or leftover/)
}
finally { removeConsumer(dir) }
})