feat(ci): [CI-NPM-02] verify downloaded bundles against fresh release evidence

This commit is contained in:
Harvey Zhao committed 2026-09-16 00:59:42 +08:00
1 parent 2983cb7c51
commit 6f7fd7931d
12 files changed
+560 -19

No files matched your search

@@ -0,0 +1,79 @@
{
"schemaVersion": 1,
"task": "CI-NPM-02",
"verifiedAt": "2026-09-15T16:59:12.794Z",
"startedFrom": "2983cb7c51f65e6cf1bdd8c8c3387c75e0c3287c",
"node": "v24.21.0",
"yarn": "1.22.22",
"commands": [
{
"command": "yarn test:release-bundle",
"exitCode": 0,
"tests": 36,
"pass": 36,
"fail": 0,
"skipped": 0,
"durationMs": 2158.2169,
"log": {
"path": "refactor/.cache/ci-npm02-tests-final.log",
"sha256": "0f4f27201e3f03cf6d20add909e901024209e8b04ded143780a17b05bf533b6a"
}
},
{
"command": "node node_modules/typescript/bin/tsc -p scripts/tsconfig.release.json --noEmit",
"exitCode": 0
},
{
"command": "node node_modules/eslint/bin/eslint.js scripts/release/bundle.ts scripts/release/verify.ts scripts/verify-release.mjs refactor/scripts/release-verify.test.mjs",
"exitCode": 0
}
],
"currentRepositoryRejection": {
"directory": "D:\\github\\ArtPlayer\\refactor\\.cache\\npm02-blocked-fmr1O8",
"expected": {
"names": [
"artplayer"
],
"tag": "next",
"sourceCommit": "2983cb7c51f65e6cf1bdd8c8c3387c75e0c3287c",
"manifestSha256": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
},
"syntheticManifest": true,
"inspector": "default buildLedger, actual current repository",
"rejected": true,
"message": "Release preflight is blocked; candidate bundle was not prepared",
"record": {
"path": "refactor/.cache/ci-npm02-repository-rejection.json",
"sha256": "fd7a0058dfd08c3240c5b4ca757bf388a890033a7c049432c497944aeebe0b5e"
},
"cleanSourceCliUsed": false,
"explanation": "The component default inspector computed the actual current repository ledger; a synthetic empty manifest with its digest did not override blocked gates. This is a rejection check, not real bundle acceptance."
},
"inputs": [
{
"path": "scripts/release/bundle.ts",
"sha256": "30c1f2334ed88a60e2b340b15a1a74423b08fb278425088d32f809715903c70d"
},
{
"path": "scripts/release/verify.ts",
"sha256": "27ac68bde4fb8b364bc23247ba9af1a2234b0db6ec594d6a66dda44b84b94e9e"
},
{
"path": "scripts/verify-release.mjs",
"sha256": "215d684511e598da8703277471d871c756ddae5fe3df23670e2e8495f661df58"
},
{
"path": "refactor/scripts/release-verify.test.mjs",
"sha256": "ef83c92c9d768cf04b3d8da3726166a1477fcee4b5b20461f4fe2325e14ba724"
}
],
"limits": {
"successfulFixturesAreSynthetic": true,
"realCandidateAccepted": false,
"workflowProvenanceVerified": false,
"registryOccupancyVerified": false,
"remoteWorkflowRun": false,
"publicationAuthorized": false,
"formalReviewStarted": false
}
}
@@ -0,0 +1,60 @@
# CI-NPM-02: verify downloaded candidate contents
The previous bundle preparation step copied exact registered tarballs, but had
no consumer-side validator for a downloaded copy. CI-NPM-02 is an independent
local prerequisite of CI-03, alongside CI-NPM-01. The original workflow and
remote-validation requirements remain; no unfinished dependency was removed.
`scripts/release/verify.ts` now requires independently supplied source commit,
manifest digest, package batch and tag. It validates the manifest digest before
reading its claims, computes the repository release ledger, and rejects blocked
or stale candidate evidence. The downloaded preflight must exactly equal the
fresh report. Downloaded tarballs must equal registered candidate bytes and
match their SHA-512 integrity. Metadata is compared with the shared manifest
description generated from the fresh report, including versions, source/input
fingerprints, registry, tag and toolchain. All files are checked again after a
second ledger read to detect changes during verification.
The file roster must contain exactly the manifest, preflight and selected
tarballs. Nested paths, symlinks/junctions, redirected directories/parents,
non-files, missing files and additional files are rejected. Verification only
reads files and retains failed downloads for diagnosis. The CLI enforces the
canonical Node/Yarn and clean source guard on each ledger read. Its test seam
cannot be supplied through a CLI argument. No dependencies were added.
Preparation and verification share batch validation and manifest construction
in `bundle.ts`. This preserves the existing bundle format, including the
publication-authority limitation, while avoiding two separately maintained
definitions. `yarn release:verify-bundle` exposes the operation; the existing
release test command now includes its tests, and `test:baseline` automatically
includes the new file. The release TypeScript project already covers the new
module. Maintenance instructions are in `scripts/release/README.md`.
The tests use real local tarballs with explicitly synthetic ready reports. They
cover successful read-only validation and rejection of independent-digest
changes, 17 self-consistent manifest modifications, stale/current blockers,
changed preflight, changed registered/downloaded bytes, missing/extra files,
redirected paths, invalid independent inputs and mutation during the second
inspection. The original preparation and cleanup tests remain in the same run.
These tests do not establish that any real ArtPlayer package is publishable.
Actual command results and the current-repository rejection are recorded in
[the validation record](../baselines/npm-bundle-verification.json).
On canonical Node 24.21.0/Yarn 1.22.22 the final release tests passed 36/36
(zero failures/skips, 2,158.2169 ms); strict release TypeScript and targeted lint
passed. A direct call with the default inspector read the actual repository
ledger and correctly rejected blocked preflight. That call used a synthetic
empty manifest/digest and bypassed only the CLI's clean-worktree wrapper to test
the component during implementation; it did not inject a replacement ledger or
accept a real candidate. CLI tests separately verify required arguments, unknown
flags and the Yarn boundary, while the existing tests cover the clean Git guard.
This is local implementation only. CI-03 must still establish trusted remote
repository/workflow/run/attempt/artifact identity and pass independent expected
values into the verifier, check registry occupancy, and implement publication
and partial-failure recovery. CI-04 must exercise the real remote workflow.
Different environments or missing restored evidence may fail exact preflight
comparison; that failure requires diagnosis, never forged reports or replacement
builds. A verification result is not authorization or a durable guarantee for
files changed afterward. No push, deployment, npm publication or formal review
round was performed.
+6
View File
@@ -60,6 +60,12 @@ CI-NPM-01 从 CI-03 拆出本地精确候选交付准备,供后续 artifact
验收全部保留。当前包仍被准入门槛阻止,详见[记录](changes/2026-09-15-CI-NPM-01-bundle.md)
及[实现维护](../scripts/release/README.md)。
CI-NPM-02补齐下载后的内容校验:`yarn release:verify-bundle`要求独立的源码SHA、
manifest摘要和明确包批次/tag,重算当前仓库台账并比对已登记tarball,不能由
下载报告自证准入。它不读取GitHub/npm,不证明workflow/run/artifact来源或版本
占用,也不授权发布。CI-03仍须建立可信来源传递、下载与发布步骤;不能以此
本地子任务代替CI-01、CI-03或CI-04的完整验收。
| 任务 | 交付 |
| --- | --- |
| ENG-02 | 只读脚本、PR/主线检查及部署隔离基础 |
+5 -3
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 283 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 284 项,范围 22 个包及工作区/示例。
状态:todo 41 / doing 20 / blocked 0 / done 222 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 41 / doing 20 / blocked 0 / done 223 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -96,13 +96,14 @@
| --- | --- | --- | --- | --- | --- | --- |
| CI-01 | workspace<br>增强兼容矩阵、并发缓存与 CI 报告 | DOC-10, ENG-08, ENG-09, ENG-10 | OS/Node/TS/浏览器与影响范围矩阵、缓存、超时、汇总检查和 artifact 报告 | 固定安装、失败/取消不误报、核心影响全生态;检查只读,失败证据可追溯 | H | doing |
| CI-02 | workspace<br>分离并改进 GitHub Pages 部署 | DOC-10, ENG-02, SITE-03 | Pages artifact 部署配置、旧路径/域名核对、预检和迁移恢复指南 | 部署只取受信任已验证产物;本地实现可验收,远端 source/环境和实际部署状态单独登记 | H | done |
| CI-03 | workspace<br>建立 npm 分包候选与发布工作流 | DOC-10, CI-01, REL-08, REL-04, CI-NPM-01 | 候选准备、精确 artifact 发布配置、OIDC 评估、版本/tag/registry 预检和部分失败恢复 | 不自动发布;明确逐包信任前置和 dry run 限制,不能重建未验证内容或重发冲突版本;按版本清单校验各包下一 major 和预发布/正式 tag,保留旧核心支持范围 | H | todo |
| CI-03 | workspace<br>建立 npm 分包候选与发布工作流 | DOC-10, CI-01, REL-08, REL-04, CI-NPM-01, CI-NPM-02 | 候选准备、精确 artifact 发布配置、OIDC 评估、版本/tag/registry 预检和部分失败恢复 | 不自动发布;明确逐包信任前置和 dry run 限制,不能重建未验证内容或重发冲突版本;按版本清单校验各包下一 major 和预发布/正式 tag,保留旧核心支持范围 | H | todo |
| CI-04 | workspace<br>验收 GitHub 流水线与远端发布准入 | CI-01, CI-02, CI-03, SITE-06, CI-BROWSER-01, CI-TYPES-01, CI-TYPES-02, SITE-EDITOR-VAST-01, CI-JASSUB-SOURCE-01 | 静态/干净环境检查、真实 PR 正反例、候选 dry run、required checks/Pages/npm 必需配置状态及运维指南 | 必要 Actions 证据和远端配置核对齐全;缺失保持未完成,真实 publish/deploy 仍在授权发布步骤执行 | H | todo |
| CI-BROWSER-01 | workspace<br>分离源码与已安装产物浏览器验证范围 | ENG-05, ENG-07 | 完整源码入口、明确已安装包子集、分开的报告目录与失败传播 | 混用输入旧红新绿;源码默认保留所有spec,已安装入口严格校验四包来源;两类报告都保留,源码失败不能误报全绿;不代表完整远端或全包验收 | M | done |
| CI-NPM-01 | workspace<br>从已验收候选准备不可重建的npm交付包 | DOC-10, REL-08, REL-04 | 复用严格准入台账、复制精确tarball、绑定源码/工具链/证据/摘要的本地准备命令及反向测试;供CI-03后续受信任artifact工作流使用 | 缺候选或任一准入缺口即拒绝;不构建、不安装、不联网或发布;阻止路径越界、脏源码、复制期间漂移及半成品冒充完成,声明远端信任/OIDC/registry预检仍未实现 | H | done |
| CI-TYPES-01 | workspace, artplayer-plugin-audio-track, artplayer-plugin-hls-control<br>接入 Audio/HLS 安装后严格类型消费者 | ENG-07, PKG-AUDIO-04, PKG-HLS-04 | 共享隔离编译器模块、真实 tarball 的五模式正反例和声明路径证据 | 旧/新编译器所有正例通过,每条无效调用实际报错;仓库外声明解析、缺声明/any/逃逸负例有效;保留独立运行时和浏览器验收 | M | done |
| CI-TYPES-02 | workspace<br>将全部库包的独立安装类型验证接入CI | CI-TYPES-01, ENG-07 | 21库包完整检查清单、实际构建和隔离安装测试调度、失败证据汇总及必需CI门槛 | 不得漏包或用通用导入替代历史类型契约;准备失败不消费旧产物;记录每包实际结果并传播失败,保留运行时/设备/未决兼容门槛 | M | done |
| CI-JASSUB-SOURCE-01 | workspace, artplayer-plugin-jassub<br>将当前源码纳入 JASSUB 原生字幕默认验证 | CI-BROWSER-01, PKG-JASSUB-07 | 源码与发布包原生字幕配对、来源标识及独立销毁验证,真实 CLI 收集回归和三引擎证据 | 默认各三核心覆盖当前源码和发布包,明确显式诊断与安装映射边界;候选九项真实 WASM 绘制和清理通过;保留旧版失败,不冒充全量或真机验收 | M | done |
| CI-NPM-02 | workspace<br>校验下载候选与实时发布台账 | CI-NPM-01, REL-09 | 独立摘要/源码/包批次输入、完整下载文件核验、实时台账重算和篡改/失效测试;供CI-03复用 | 不能以下载报告自证准入,不重建或发布;拒绝摘要/候选/版本/路径/台账漂移,明确远端来源和registry仍待核实 | H | done |
## 2.1 早期试点
@@ -744,3 +745,4 @@
- CI-JASSUB-SOURCE-01: [记录](changes/2026-09-15-CI-JASSUB-SOURCE-01-native-inputs.md) [记录](baselines/jassub-source-selection-validation.json)
- DOC-REVIEW-01: [记录](changes/2026-09-15-DOC-REVIEW-01-user-guidance.md) [记录](release-reviews.md) [记录](ai-workflow.md)
- PKG-DANMUKU-START-01: [记录](baselines/ci-installed-webkit-validation.json) [记录](changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md) [记录](baselines/danmuku-start-validation.json)
- CI-NPM-02: [记录](changes/2026-09-16-CI-NPM-02-verify-bundle.md) [记录](baselines/npm-bundle-verification.json)
+11
View File
@@ -1,5 +1,16 @@
# 进度与证据
## CI-NPM-02 下载候选内容核验
新增`release:verify-bundle`,以独立提供的源码SHA、manifest摘要、包批次/tag
核验下载内容,并两次重算当前仓库发布台账。严格比对preflight、登记tarball字节、
SHA-512、版本/工具链及文件清单;拒绝额外文件、路径重定向和校验过程中变化。
没有新增依赖。36项准备/下载校验测试、严格TS及lint通过;真实仓库台账仍正确
拒绝放行。合成通过夹具不作为真实候选准入。见[记录](changes/2026-09-16-CI-NPM-02-verify-bundle.md)
及[证据](baselines/npm-bundle-verification.json)。
CI-NPM-02从CI-03拆出并成为其依赖;CI-03原依赖与远端来源、registry、发布流程
全部保留。223 done、20 doing、41 todo,共284项。没有启动复盘或执行远端写入。
## REL-09 全包下一 major 版本落实
22个workspace已设置各自下一major并新增unreleased CHANGELOG;核心/弹幕6.0.0、
+213
View File
@@ -0,0 +1,213 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Release boundary tests use the existing Node baseline runner.
import test from 'node:test'
import { prepareReleaseBundle } from '../../scripts/release/bundle.ts'
import { verifyReleaseBundle } from '../../scripts/release/verify.ts'
const digest = (bytes, algorithm = 'sha256', encoding = 'hex') => createHash(algorithm).update(bytes).digest(encoding)
// The successful reports below are synthetic. No real ArtPlayer release gate is waived.
function fixture(t) {
const repository = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-verify-test-'))
fs.mkdirSync(path.join(repository, 'refactor/.cache'), { recursive: true })
t.after(() => {
assert.equal(path.dirname(fs.realpathSync(repository)), fs.realpathSync(os.tmpdir()))
assert(path.basename(repository).startsWith('artplayer-verify-test-'))
fs.rmSync(repository, { recursive: true })
})
const names = ['artplayer', 'artplayer-plugin-chapter']
const packages = names.map((name) => {
const source = path.join(repository, name, 'package')
fs.mkdirSync(source, { recursive: true })
fs.writeFileSync(path.join(source, 'package.json'), JSON.stringify({ name, version: '6.0.0', main: 'index.js' }))
fs.writeFileSync(path.join(source, 'index.js'), 'module.exports = 42\n')
const file = `refactor/.cache/${name}.tgz`
execFileSync('tar', ['-czf', path.join(repository, file), '-C', path.dirname(source), 'package'], { stdio: 'pipe' })
const bytes = fs.readFileSync(path.join(repository, file))
return { name, version: '6.0.0', distribution: 'npm', fingerprint: `input-${name}`, status: 'evidence-complete', blockers: [], candidate: { path: file, version: '6.0.0', sourceCommit: 'a'.repeat(40), inputFingerprint: `input-${name}`, integrity: `sha512-${digest(bytes, 'sha512', 'base64')}`, errors: [] } }
})
const report = { schemaVersion: 1, sourceCommit: 'b'.repeat(40), evidenceComplete: true, publicationAuthorized: false, toolchain: { node: 'v24.21.0', canonicalNode: '24.21.0', packageManager: 'yarn@1.22.22', lock: { sha256: 'c'.repeat(64) } }, packages }
const inspect = () => structuredClone(report)
const prepared = prepareReleaseBundle(repository, names, 'next', inspect)
const directory = path.join(repository, 'downloaded')
fs.cpSync(prepared.directory, directory, { recursive: true })
const manifestFile = path.join(directory, 'manifest.json')
const expected = { sourceCommit: report.sourceCommit, manifestSha256: digest(fs.readFileSync(manifestFile)), names, tag: 'next' }
const verify = (inspector = inspect) => verifyReleaseBundle(repository, directory, expected, inspector)
const modifyManifest = (change) => {
const manifest = JSON.parse(fs.readFileSync(manifestFile))
change(manifest)
fs.writeFileSync(manifestFile, JSON.stringify(manifest))
expected.manifestSha256 = digest(fs.readFileSync(manifestFile))
}
return { repository, directory, report, expected, verify, inspect, modifyManifest, manifestFile }
}
test('Downloaded bundle is checked against independent inputs and two fresh ledger reads without gaining authority', (t) => {
const f = fixture(t)
const before = fs.readdirSync(f.directory).map(name => [name, digest(fs.readFileSync(path.join(f.directory, name)))])
let reads = 0
const result = f.verify(() => {
reads++
return f.inspect()
})
assert.equal(reads, 2)
assert.equal(result.packages.length, 2)
assert.equal(result.contentVerified, true)
assert.equal(result.publicationAuthorized, false)
assert.equal(result.workflowProvenanceVerified, false)
assert.equal(result.registryOccupancyVerified, false)
assert.deepEqual(fs.readdirSync(f.directory).map(name => [name, digest(fs.readFileSync(path.join(f.directory, name)))]), before)
})
test('A changed manifest is rejected before interpreting its report or inspecting the repository', (t) => {
const f = fixture(t)
fs.appendFileSync(f.manifestFile, ' ')
assert.throws(() => f.verify(() => {
throw new Error('must not inspect')
}), /independent digest/)
})
test('Self-consistent forged manifest fields cannot replace fresh repository evidence', (t) => {
const f = fixture(t)
const original = fs.readFileSync(f.manifestFile)
const cases = [
m => m.publicationAuthorized = true,
m => m.registry = 'https://example.invalid/',
m => m.tag = 'latest',
m => m.sourceCommit = 'd'.repeat(40),
m => m.toolchain.lock.sha256 = 'e'.repeat(64),
m => m.preflight.sha256 = 'e'.repeat(64),
m => m.preflight.file = '../preflight.json',
m => m.packages.reverse(),
m => m.packages.push(m.packages[0]),
m => m.packages[0].version = '7.0.0',
m => m.packages[0].file = '../escape.tgz',
m => m.packages[0].sha256 = 'e'.repeat(64),
m => m.packages[0].integrity = 'sha512-forged',
m => m.packages[0].sourceCommit = 'd'.repeat(40),
m => m.packages[0].inputFingerprint = 'new-inputs',
m => m.kind = 'other-bundle',
m => m.extraApproval = true,
]
for (const change of cases) {
fs.writeFileSync(f.manifestFile, original)
f.modifyManifest(change)
assert.throws(() => f.verify(), /metadata differs/)
}
})
test('A freshly blocked or changed ledger rejects even an intact formerly accepted bundle', (t) => {
const f = fixture(t)
const changes = [
r => r.evidenceComplete = false,
r => r.packages[0].status = 'blocked',
r => r.packages[0].blockers.push('new finding'),
r => r.packages[0].distribution = 'site',
r => r.packages[0].candidate.errors.push('changed candidate'),
r => r.packages[0].candidate.inputFingerprint = 'stale',
r => r.sourceCommit = 'd'.repeat(40),
r => r.toolchain.lock.sha256 = 'd'.repeat(64),
]
for (const change of changes) {
assert.throws(() => f.verify(() => {
const report = f.inspect()
change(report)
return report
}))
}
fs.writeFileSync(path.join(f.directory, 'preflight.json'), JSON.stringify({ ...f.report, extraApproval: true }))
f.modifyManifest(m => m.preflight.sha256 = digest(fs.readFileSync(path.join(f.directory, 'preflight.json'))))
assert.throws(() => f.verify(), /Downloaded preflight differs/)
})
test('Changed archive bytes, even if copied into the registered location, cannot reuse candidate integrity', (t) => {
const f = fixture(t)
const file = path.join(f.directory, 'artplayer-6.0.0.tgz')
fs.appendFileSync(file, 'tampered')
assert.throws(() => f.verify(), /downloaded tarball differs/)
fs.copyFileSync(file, path.join(f.repository, f.report.packages[0].candidate.path))
assert.throws(() => f.verify(), /candidate integrity changed/)
})
test('Extra files and missing archives fail without deleting downloaded evidence', (t) => {
const f = fixture(t)
fs.writeFileSync(path.join(f.directory, 'extra.sh'), 'exit 1')
assert.throws(() => f.verify(), /Unexpected or missing bundle files/)
assert(fs.existsSync(path.join(f.directory, 'extra.sh')))
fs.unlinkSync(path.join(f.directory, 'extra.sh'))
fs.unlinkSync(path.join(f.directory, 'artplayer-6.0.0.tgz'))
assert.throws(() => f.verify(), /ENOENT/)
assert(fs.existsSync(f.manifestFile))
})
test('Redirected directories, redirected parents and non-file members are rejected', (t) => {
const f = fixture(t)
const link = path.join(f.repository, 'redirect')
fs.symlinkSync(f.directory, link, 'junction')
assert.throws(() => verifyReleaseBundle(f.repository, link, f.expected, f.inspect), /regular directory|Redirected bundle/)
const parent = path.join(f.repository, 'redirect-parent')
fs.symlinkSync(f.repository, parent, 'junction')
assert.throws(() => verifyReleaseBundle(f.repository, path.join(parent, 'downloaded'), f.expected, f.inspect), /Redirected bundle/)
fs.unlinkSync(path.join(f.directory, 'artplayer-6.0.0.tgz'))
fs.symlinkSync(path.join(f.repository, 'artplayer'), path.join(f.directory, 'artplayer-6.0.0.tgz'), 'junction')
assert.throws(() => f.verify(), /not a regular file/)
})
test('Source or downloaded content changed during verification cannot produce a successful result', (t) => {
const mutations = [
f => fs.appendFileSync(f.manifestFile, ' '),
f => fs.appendFileSync(path.join(f.directory, 'preflight.json'), ' '),
f => fs.appendFileSync(path.join(f.directory, 'artplayer-6.0.0.tgz'), ' '),
f => fs.writeFileSync(path.join(f.directory, 'late-file'), ' '),
f => f.report.toolchain.lock.sha256 = 'd'.repeat(64),
]
for (const mutate of mutations) {
const f = fixture(t)
let reads = 0
assert.throws(() => f.verify(() => {
if (++reads === 2)
mutate(f)
return f.inspect()
}), /changed during verification/)
assert.equal(reads, 2)
}
})
test('Invalid independent selection, tag, source and digest fail before reading downloaded contents', (t) => {
const f = fixture(t)
for (const change of [{ names: [] }, { names: ['artplayer', 'artplayer'] }, { names: ['../artplayer'] }, { tag: 'arbitrary' }, { sourceCommit: 'master' }, { manifestSha256: '' }])
assert.throws(() => verifyReleaseBundle(f.repository, 'not-a-directory', { ...f.expected, ...change }, f.inspect), /Supply|Select/)
})
test('Verifier CLI rejects omitted inputs, unknown options and an unsupported package manager', (t) => {
const f = fixture(t)
const script = path.resolve('scripts/verify-release.mjs')
const full = ['--directory', f.directory, '--packages', f.expected.names.join(','), '--tag', 'next', '--source-commit', f.expected.sourceCommit, '--manifest-sha256', f.expected.manifestSha256]
for (const [args, message] of [[[], /are required/], [['--unknown'], /Unknown option/], [full, /Use yarn release:verify-bundle/]]) {
assert.throws(() => execFileSync(process.execPath, [script, ...args], { stdio: 'pipe', env: { ...process.env, npm_config_user_agent: 'npm/11.5.1' } }), (error) => {
assert.equal(error.status, 1)
assert.match(error.stderr.toString(), message)
return true
})
}
})
test('A second-inspection exception is preserved and downloaded evidence is retained', (t) => {
const f = fixture(t)
const failure = new Error('fresh evidence read failed')
let reads = 0
assert.throws(() => f.verify(() => {
if (++reads === 2)
throw failure
return f.inspect()
}), error => error === failure)
assert.equal(reads, 2)
assert(fs.existsSync(f.manifestFile))
})
+22 -1
View File
@@ -835,7 +835,8 @@
"CI-01",
"REL-08",
"REL-04",
"CI-NPM-01"
"CI-NPM-01",
"CI-NPM-02"
],
"status": "todo",
"risk": "H",
@@ -6138,6 +6139,26 @@
"changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md",
"baselines/danmuku-start-validation.json"
]
},
{
"id": "CI-NPM-02",
"phase": "2.2 GitHub CI/CD",
"title": "校验下载候选与实时发布台账",
"scope": [
"workspace"
],
"dependsOn": [
"CI-NPM-01",
"REL-09"
],
"status": "done",
"risk": "H",
"deliverable": "独立摘要/源码/包批次输入、完整下载文件核验、实时台账重算和篡改/失效测试;供CI-03复用",
"acceptance": "不能以下载报告自证准入,不重建或发布;拒绝摘要/候选/版本/路径/台账漂移,明确远端来源和registry仍待核实",
"evidence": [
"changes/2026-09-16-CI-NPM-02-verify-bundle.md",
"baselines/npm-bundle-verification.json"
]
}
]
}