From 1b8188834768605f312a5ed14dab52d75b9c880c Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Wed, 16 Sep 2026 01:52:36 +0800 Subject: [PATCH] fix(release): [REL-02] accept Yarn directory entries in candidate archives --- .../major-candidate-archive-validation.json | 278 ++++++++++++++++++ ...26-09-16-REL-02-yarn-candidate-archives.md | 52 ++++ refactor/plan.md | 2 +- refactor/progress.md | 10 + refactor/release-ledger.md | 4 + refactor/scripts/release-ledger.mjs | 5 +- refactor/scripts/release-ledger.test.mjs | 48 +++ refactor/tasks.json | 4 +- scripts/package-archive.mjs | 17 ++ scripts/package-check.mjs | 18 +- scripts/release/README.md | 6 + 11 files changed, 425 insertions(+), 19 deletions(-) create mode 100644 refactor/baselines/major-candidate-archive-validation.json create mode 100644 refactor/changes/2026-09-16-REL-02-yarn-candidate-archives.md create mode 100644 scripts/package-archive.mjs diff --git a/refactor/baselines/major-candidate-archive-validation.json b/refactor/baselines/major-candidate-archive-validation.json new file mode 100644 index 000000000..a83c70805 --- /dev/null +++ b/refactor/baselines/major-candidate-archive-validation.json @@ -0,0 +1,278 @@ +{ + "task": "REL-02", + "sourceCommit": "e54fc998ea711f686143be73b82945cf3c719442", + "capturedAt": "2026-09-15T17:51:35.519Z", + "node": "v24.21.0", + "yarn": "1.22.22", + "preparation": { + "command": "yarn test:package --browser", + "seconds": 106.36, + "report": { + "path": "refactor/.cache/packages/run-xQsBHN/report.json", + "sha256": "3b1f8ff09fb6f3d5c68783406c8c2922570f4592f8212d20ed1f7218a9c6d877" + }, + "log": { + "path": "refactor/.cache/rel02-candidates-package.log", + "sha256": "7456f57c2ebdc7782a3030a775a64b2baa1f43e733869e79dfe7ec82908f8a9d" + }, + "packages": 20, + "runtimeScope": [ + "artplayer", + "artplayer-plugin-chapter" + ], + "runtimeChecks": 36, + "legacyTypeModes": 5, + "accurateTypeModes": 8, + "additionalTypeScope": [ + "artplayer-plugin-audio-track", + "artplayer-plugin-hls-control" + ] + }, + "thumbnail": { + "commands": [ + "yarn build artplayer-tool-thumbnail", + "yarn test:thumbnail-types-package" + ], + "buildSeconds": 1.66, + "consumerSeconds": 11.96, + "compilerCases": 12, + "report": { + "path": "refactor/.cache/thumbnail-package-types-rCbvkz/report.json", + "sha256": "4b793ffa328d8630ff9d039b80bbb0f68f7e4d865bbbb5ecb1128a77c44cac12" + }, + "logs": [ + { + "path": "refactor/.cache/rel02-candidates-thumbnail-build.log", + "sha256": "4cab99627eb04a08b9a6342d61aae2934b2c3df63388843f0857444026d51133" + }, + { + "path": "refactor/.cache/rel02-candidates-thumbnail.log", + "sha256": "8e011afb610cb18e5118fc4e83f7167908d42e9c91c72f643219a2f4fb219717" + } + ] + }, + "browser": { + "command": "yarn test:browser:installed danmuku-timing-diagnostic.spec.js audio-buffering.spec.js --grep \"pending|sampling|sample|timing|held\"", + "artifactMap": { + "path": "refactor/.cache/packages/run-xQsBHN/browser-artifacts.json", + "sha256": "bf0ca0fdfd6f1aa336f7abe65e4bb2225b959ff1dfd15c8902438bbf2f3a967b" + }, + "stats": { + "startTime": "2026-09-15T17:44:46.263Z", + "duration": 52819.146, + "expected": 36, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "report": { + "path": "refactor/.cache/rel02-candidates-browser/report.json", + "sha256": "67aa867dec7eedaa006df3cc1b332d0685b1147a8160d1c167c8d1e076002641" + }, + "invocation": { + "path": "refactor/.cache/rel02-candidates-browser/invocation.json", + "sha256": "f70fc5e116621af3c021c31ff5b6ac3b419b0f15c3a0ec007c55a293e30bdeb3" + }, + "result": { + "path": "refactor/.cache/rel02-candidates-browser/result.json", + "sha256": "0132d84089bcff324bda8625ed72010ef60e2fbc3b6def23d5a75dcf512527d0" + }, + "scope": "24 Danmuku native sampling/eligibility cases plus 12 Audio held-switch cases in three desktop engines; no native starvation closure" + }, + "inputReports": [ + { + "path": "refactor/.cache/release-ledger-G9z39H/report.json", + "sha256": "2e6b4993cd79e0c97d62b51376fb4e891c03ea549030e1741eb0626620a2d58b" + }, + { + "path": "refactor/.cache/release-ledger-HnUiNG/report.json", + "sha256": "c9bc1cce17336957e5de7b94cea3ae80350b396a3197dcb4f80b08dee158b85f" + } + ], + "failure": { + "actualArchive": "refactor/.cache/packages/run-xQsBHN/artplayer.tgz", + "error": "Invalid archive path: package", + "collectionLog": { + "path": "refactor/.cache/rel02-candidates-provenance.log", + "sha256": "1795db66a068399de6798c3b536a7c51ceca8b76768b6542a5bbc18430e40355" + }, + "regressionLog": { + "path": "refactor/.cache/rel02-yarn-archive-before.log", + "sha256": "9ac61668623d0867e33cd8d82da544a3c51570e097916474cd91aaaee6bb07c1" + }, + "regressionExitCode": 1 + }, + "correctedChecker": { + "actualArchives": [ + { + "name": "artplayer", + "version": "6.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer.tgz", + "sha256": "a9913669915970ec5070a4ed80a60ab885624cde7641265e350c07bee1f7ae9c", + "integrity": "sha512-tcg5Z64NAiVg2NyzZ/UvxIe1Q9gxG5NCHPVbnBaXukGDt7FUzDo1fUekUhzJtMZUKv0227WMtD968sRfvAy8Sw==" + }, + { + "name": "artplayer-plugin-chapter", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-chapter.tgz", + "sha256": "bd5b9d0d467bbd06fb2e60058d27f392bdf62a4ad2f5978cf0ce39fc1bba4ac1", + "integrity": "sha512-kw31aC1S+rT+DSC5PGsbwjli26pRJy/5PL75GLYbq8P+9ORm4p3xO3JHiM33qyg0PArckP3NSfNWuxNXVzLGJA==" + }, + { + "name": "artplayer-plugin-ambilight", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-ambilight.tgz", + "sha256": "c70002b5ef9260db5aa378360f25bcb3f547b65156182314ecefece7dad8ec49", + "integrity": "sha512-u8Sid+prhTO9muhdj4L1A5sacrAcD7yHQbhccIioaZVLZg+uNi6CLn/9yooPsYkgjIVU/2jeqkGCLbIf22602A==" + }, + { + "name": "artplayer-proxy-canvas", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-proxy-canvas.tgz", + "sha256": "5ae7532d29ccdac3114ccb40dab7ea18e024c38d0ea6fa8425571954aee180f6", + "integrity": "sha512-HH57q3Qhr2Fq1B8KlU6BKCHtSRxDAyHIv1n4SqzhPYxsU9JTN14UlqnA9tSuMTmf5W0qUslkXVdzzSI9z/Svtg==" + }, + { + "name": "artplayer-plugin-document-pip", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-document-pip.tgz", + "sha256": "e06b96ebd9f500b2ba14fb20dcc3ff85643e7c17b93c1a092130c520a23226db", + "integrity": "sha512-ONGOJAoFxM/uSXoZzrV7UaqNbnTekS9CU58uR6GuUKsP/miaav4ZLQKTdnET2QUdOZDmt6H9aZzV7/UEMqnBNA==" + }, + { + "name": "artplayer-plugin-ads", + "version": "3.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-ads.tgz", + "sha256": "8227e59b9a33545964af42b1520763132ef0c6531d81564ea871d0fb6403191c", + "integrity": "sha512-duNwmozO5vwKaS0qnk2r9cmMTVabL1nsGZVV/eFq36tWShLVzq+ZGbCGPiGtXqlMORwf0A/PzKMg9DAaL95vKg==" + }, + { + "name": "artplayer-plugin-audio-track", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-audio-track.tgz", + "sha256": "0cd3c0b7e380880afcc8fc47af2e3274e7cf6d459e22d8e5d59a20b1f6b48c1d", + "integrity": "sha512-5DKXPYVMM8EbXGTCVShGOxyKTPhR4qv6YdIPprmQJ9ovnrCZOorMm6aATwH36GcZ5uxvftL0/19YwaarCEheag==" + }, + { + "name": "artplayer-plugin-vtt-thumbnail", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-vtt-thumbnail.tgz", + "sha256": "dab10ac4273ff19572e98218b43cb295b6b86ab6f3e58c5ed3a082b51951cd8a", + "integrity": "sha512-QBePktREdD7UEkv/7s8JsVpFelM/ZY6GP10WWL/2R5O97C/BiKGOqFreY0IAjzoXwgNy2WmTDcCwkPw5tMq+vQ==" + }, + { + "name": "artplayer-plugin-multiple-subtitles", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-multiple-subtitles.tgz", + "sha256": "4ef6783ebfa8b99ad770c2f922f3d7f4f5835148c3c560a43e90e2237cf6f0e7", + "integrity": "sha512-Jp8zkuKlZzgOZr859VEKy9rJgKF9agN4mc1YaOFOdXK/CTWPugvk1BwUkpTYdHvV4BPsXWT3LGF/JKDqsPyhAw==" + }, + { + "name": "artplayer-plugin-hls-control", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-hls-control.tgz", + "sha256": "5b87ad54604a6f084c2af7adf9edba004f0a4760efe2ca30f422fb02fd92f188", + "integrity": "sha512-VCJh4S9P+PnpWdees2a3j5Q29y8CMBiUwX92Zjcj1UrKjLfn6L9hFcvr+5Rc2bEMu+up+Qwcc7JbQqrBs+NXnA==" + }, + { + "name": "artplayer-plugin-dash-control", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-dash-control.tgz", + "sha256": "22885a9df3d00703ec4cc044ac55bb62738f24029b1ae4649139376ef2fbc6f1", + "integrity": "sha512-P9KwbaW4yOhHswJNkSgkxg9ZeXVGducq2dq9F/NjuxeqO/AipWDIUP924RBDbDjyc7XDQKQ6WALV8HUqyeWEGQ==" + }, + { + "name": "artplayer-plugin-auto-thumbnail", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-auto-thumbnail.tgz", + "sha256": "74ad1f31633da4614458c64ee47a8e43707ada7f79191356928f530fefd8a6d9", + "integrity": "sha512-OdpEqH39pAT94qBZ9f1wmPIxmmWvo5dLqIvztLU9hsOpX8luuuyxWa9ShCNq55PGggberAXMvGbfzk1p9dISag==" + }, + { + "name": "artplayer-plugin-asr", + "version": "3.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-asr.tgz", + "sha256": "9b224b8a5e3ab6bf8a020102311b6e4781bdbb8c996a7d1a641404672f94ad6a", + "integrity": "sha512-2jLnZ8JJJ0k0fE8mAxW3vXcQPVGz/yRUpFcVARELvBF0JEgEmzVix+G2zpKaloQrZru8nliw0gBmuTQczNBrXA==" + }, + { + "name": "artplayer-plugin-chromecast", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-chromecast.tgz", + "sha256": "025de33f82160a136dba4abcf9024a26c0a663c2745bbdb8b9cbb77dfdfc9eda", + "integrity": "sha512-eP+lmJ3hulu5sBVPCSZI+Ll7nxAFQ/UTcIMoPVA9hIxhzn1APAMkOXUBjBjyrkiwdb8oU664Umbys4BRCPBbyg==" + }, + { + "name": "artplayer-proxy-mediabunny", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-proxy-mediabunny.tgz", + "sha256": "42d28808395d04cbb1839525e068680fbba935a1fa0cc21796d46503c2b6d620", + "integrity": "sha512-0Feras8kJkaXME9Pq17d5HGFwjIfkSVyULFnFwWakfPBn234Vm0EfPT17GbbE/aPFXcDMCcorbs/TcAt+rwcgQ==" + }, + { + "name": "artplayer-plugin-jassub", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-jassub.tgz", + "sha256": "27c9252c0ea648c3e835d7f08d10ca7df04ecda9eec66dbfdb4c45a59b806986", + "integrity": "sha512-6y/IJzxtwX6vjxd2kpEkoSFGOwyB9CkGfexZjb5gmJS4A5nuWnswUYYAKK1NhoD6U+KNgQOHvQ1xlBa11Bv2MQ==" + }, + { + "name": "artplayer-plugin-danmuku", + "version": "6.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-danmuku.tgz", + "sha256": "cd679867754432d4d7129a72eb673cbaec40aac122dad033ff23239580990446", + "integrity": "sha512-zL/I+nCxg+BsuhNhcN4nbB/rQkeaxstsNRGJ5a6mr3HWfBwfs8tkMmAsYf+EvE3YECODnmBcVvlLyUDYhmejbg==" + }, + { + "name": "artplayer-plugin-danmuku-mask", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-danmuku-mask.tgz", + "sha256": "d82ccc30f91d44bc25c79bdb21975f48bd724c52bff01181755da630de020e4d", + "integrity": "sha512-LIcKv9Ny6Ts3g0/BEes4prGliqrr2XytTLh2u04hyZCdDAFPhUKPIGOtKPnojUrpd9DPX5i0bRv067DuXqyYVA==" + }, + { + "name": "artplayer-tool-iframe", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-tool-iframe.tgz", + "sha256": "66adbe3dd7d9a5a0f9c42e2ffbc98a97677e9d410353c671ce046bcad31ffb45", + "integrity": "sha512-8be0ck6LtxZQqjHMTeLVFOOo2eq1yL5rLASEVuZK7z9/CVaBl2hoatAuOnMRD8QloE58nL3a+J4mbrH10vX0mQ==" + }, + { + "name": "artplayer-plugin-vast", + "version": "2.0.0", + "path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-vast.tgz", + "sha256": "ab0b1d57e7c12a9f77679e5a57bf424a47b3498fa0cff20551036d387eb7043d", + "integrity": "sha512-rp/ZfsPIGCPr7dvR369tg72bQn9M9iVARcL2Axo+u+CuO976LhBhk+hteUeFs1NpQDwB745ZzyCtNS8Ap3ITTA==" + }, + { + "name": "artplayer-tool-thumbnail", + "version": "5.0.0", + "path": "refactor/.cache/thumbnail-package-types-rCbvkz/candidate.tgz", + "sha256": "6eea43186a3020f12a463fc4cdef3dc9972ed1cbc04f2cad5c158054e51cf459", + "integrity": "sha512-FUGZyHcM3ZyIGAKU0fRX8HJot+56fU+zntCp0fuBR08I5yCpuf943uHH35roLp7NYJbzC8qlGaq2eIdXlaEb2A==" + } + ], + "passed": 21, + "regressionTests": { + "passed": 80, + "failed": 0, + "skipped": 0, + "durationMs": 2774.1443, + "log": { + "path": "refactor/.cache/rel02-yarn-archive-after.log", + "sha256": "b4abe3606cb95409a64e7e1c33f87120e875c5e4bd75d3d1756746562ff7f663" + } + }, + "checks": [ + "targeted ESLint exit 0", + "tsc -p scripts/tsconfig.release.json --noEmit exit 0" + ] + }, + "limitations": [ + "These build/browser reports preceded the archive-checker correction and are not rebound to its new fingerprint.", + "All 21 archives are accepted by the corrected content checker, but no candidate or release gate has been registered.", + "The only before/after input change was the browser-generated docs/uncompiled/artplayer-plugin-danmuku/index.js, affecting site inputs only; library fingerprints were unchanged until the checker fix.", + "Complete combinations, physical devices, rollback, remote CI and user-guided formal reviews remain open." + ], + "publicationAuthorized": false +} diff --git a/refactor/changes/2026-09-16-REL-02-yarn-candidate-archives.md b/refactor/changes/2026-09-16-REL-02-yarn-candidate-archives.md new file mode 100644 index 000000000..eee5f27f4 --- /dev/null +++ b/refactor/changes/2026-09-16-REL-02-yarn-candidate-archives.md @@ -0,0 +1,52 @@ +# REL-02 checkpoint: accept actual Yarn candidate archives + +The 20-package browser preparation on source commit +`e54fc998ea711f686143be73b82945cf3c719442` passed in 106.36 seconds. +It built in an isolated snapshot, packed and installed the target majors, +verified frozen consumer locks and installed members, and ran the core/Chapter +36 runtime checks, 5 legacy and 8 accurate type modes, plus Audio/HLS fixtures. +Thumbnail 5.0.0 was built separately and passed its 12 historical/candidate +compiler cases. This is not all-plugin runtime or type acceptance. + +Using the actual 20-package installed map, the focused desktop Chromium, +Firefox and Windows WebKit run passed 36 tests with zero retries, failures or +skips: 24 Danmuku sampling/eligibility cases and 12 Audio held-switch cases. +The native starvation failures, other combinations and physical device gaps +remain open. Windows WebKit does not establish physical Safari behavior. + +Candidate registration then exposed a real checker defect: Yarn writes its +root and nested directory entries as `package` and `package/dist`, without +trailing slashes. `checkedCandidate` incorrectly used the historical archive +inventory helper, which rejected the root as `Invalid archive path: package`. +No candidate was registered and no release gate was bypassed. + +The type-aware tar listing logic already used by isolated installation is now +in `scripts/package-archive.mjs`. Both installation and candidate registration +use it; `scripts/package-check.mjs` retains its existing `packedFiles` export. +The helper accepts directory entries with or without trailing slashes and +rejects links, special files, duplicate members, wrong roots and traversal. +Historical archive inventory remains unchanged. No dependency, package API, +runtime implementation or npm entry changed. + +A self-contained tar fixture reproduces Yarn's directory layout. It failed +before the fix, and checks both accepted directory forms plus hard links, +symlinks, FIFO entries, a regular-file root, duplicates and invalid paths after +the fix. The combined ledger/bundle/verifier/package tests passed 80/80 in +2774.1443 ms; targeted lint and strict release TypeScript checks passed. +The corrected checker also accepted all 21 actual prepared archives, checking +their recorded SHA-256, SHA-512, manifest names and target versions. + +Detailed reports, logs, immutable archive bindings and the preserved browser +report are indexed in [the validation record](../baselines/major-candidate-archive-validation.json). +Before/after library input fingerprints were unchanged during preparation. +The browser server generated a Danmuku development bundle, changing only the +site input inventory. The archive-checker correction subsequently changed +shared validation inputs, so the earlier build/browser results are retained +as observed and are not rebound to the new fingerprint. + +REL-02 remains doing. Commit this checker correction before rebuilding and +registering candidates with its exact committed validation inputs. Candidate +registration, complete combinations and candidate-specific rollback remain +unfinished. Reverting this checkpoint restores the incorrect Yarn rejection; +existing historical reports remain available. No formal review, remote write, +deployment or publication was performed. diff --git a/refactor/plan.md b/refactor/plan.md index 654b371e0..5252acb61 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -698,7 +698,7 @@ - REL-08: [记录](changes/2026-09-14-REL-08-release-ledger.md) [记录](baselines/release-ledger-validation.json) [记录](release-ledger.md) [记录](release-ledger.json) - REL-01: [记录](version-plan.md) [记录](version-plan.json) [记录](baselines/version-registry-2026-09-16.json) [记录](changes/2026-09-16-REL-01-version-plan.md) - REL-09: [记录](changes/2026-09-16-REL-09-major-versions.md) [记录](baselines/major-version-preparation.json) [记录](baselines/version-registry-prepared-2026-09-16.json) [记录](version-plan.md) -- REL-02: [记录](changes/2026-09-16-REL-02-types-inputs.md) [记录](baselines/major-installed-types-validation.json) +- REL-02: [记录](changes/2026-09-16-REL-02-types-inputs.md) [记录](baselines/major-installed-types-validation.json) [记录](changes/2026-09-16-REL-02-yarn-candidate-archives.md) [记录](baselines/major-candidate-archive-validation.json) - REL-04: [记录](changes/2026-09-15-REL-04-consumer-checkpoint.md) [记录](rollback-rehearsal.md) [记录](baselines/rollback-consumer-validation.json) [记录](changes/2026-09-15-REL-04-workflows-checkpoint.md) [记录](rollback-inventory.md) [记录](baselines/rollback-workflows-validation.json) [记录](changes/2026-09-15-REL-04-pages-recovery.md) [记录](baselines/pages-recovery-validation.json) [记录](changes/2026-09-15-REL-04-acceptance.md) [记录](baselines/rollback-acceptance-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) diff --git a/refactor/progress.md b/refactor/progress.md index b977cc3d9..9cceba2ab 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,15 @@ # 进度与证据 +## REL-02 Yarn候选归档检查修复 + +统一20包构建/隔离安装通过,Thumbnail单独构建及12组类型消费通过;实际安装 +产物的弹幕采样24项和Audio等待顺序12项三浏览器回归全部通过,保留原断流缺口。 +登记时发现发布校验器误拒绝Yarn无尾斜杠目录,现与安装流程共用条目类型校验, +80项相关测试、lint及严格TS通过,并实际接受21个归档。见[记录](changes/2026-09-16-REL-02-yarn-candidate-archives.md) +和[证据](baselines/major-candidate-archive-validation.json)。此前产物报告不套用修复后 +的新指纹;下一步固定提交再准备登记候选。REL-02仍doing,数量224/21/40不变。 +未启动用户保留的复盘,也没有推送、远端派发、部署或发布。 + ## REL-02 全包安装类型验证与输入指纹修复 21个库的18组隔离消费命令全部通过,包含核心、所有插件/proxy/工具的目标major diff --git a/refactor/release-ledger.md b/refactor/release-ledger.md index e5e4056bd..5f1ea830d 100644 --- a/refactor/release-ledger.md +++ b/refactor/release-ledger.md @@ -65,6 +65,10 @@ site-manifest文件包含package、version、outputRoot和files,每个file为p outputRoot必须在仓库内;目录完整文件集须等于files,新增、缺失、替换或符号链接输出 均拒绝。清单SRI和每个实际部署文件字节分别验证;URL正确性仍必须有site-urls报告。 +候选tar成员检查与隔离安装共用scripts/package-archive.mjs,按实际条目类型识别 +目录,接受Yarn pack生成的无尾斜杠package和子目录;拒绝链接、特殊文件、重复 +成员和越界路径。冻结历史archive的原校验器仍用于历史内容核对,不用于新候选。 + `inputFingerprint`包含本包、其实际依赖闭包,以及共享构建/工具链/测试/CI输入。 依赖来自impact-model的显式关系和源码/声明/manifest扫描;未解释动态导入按全部包 保守处理。站点还包含docs及example。第三方清单、本校验器、兼容/环境/版本/复盘 diff --git a/refactor/scripts/release-ledger.mjs b/refactor/scripts/release-ledger.mjs index 21283a274..6591cf5f0 100644 --- a/refactor/scripts/release-ledger.mjs +++ b/refactor/scripts/release-ledger.mjs @@ -5,9 +5,10 @@ import path from 'node:path' import process from 'node:process' import { fileURLToPath, pathToFileURL } from 'node:url' import { parseArgs } from 'node:util' +import { packedFiles } from '../../scripts/package-archive.mjs' import { readImpactModel, repositoryPath } from './impact-model.mjs' import { dependencyClosure, evaluatePackage, findingPackages, validateLedger } from './release-ledger-model.mjs' -import { archiveFiles, hash, readMember } from './releases.mjs' +import { hash, readMember } from './releases.mjs' export const root = fileURLToPath(new URL('../../', import.meta.url)) @@ -74,7 +75,7 @@ export function checkedCandidate(directory, row) { } else { assert.equal(binding.kind, 'npm-tarball') - archiveFiles(artifact) + packedFiles(artifact) const manifest = JSON.parse(readMember(artifact, 'package/package.json')) assert.equal(manifest.name, row.name, 'Tarball package name mismatch') assert.equal(manifest.version, binding.version, 'Tarball version mismatch') diff --git a/refactor/scripts/release-ledger.test.mjs b/refactor/scripts/release-ledger.test.mjs index c3421a10d..63a76210a 100644 --- a/refactor/scripts/release-ledger.test.mjs +++ b/refactor/scripts/release-ledger.test.mjs @@ -1,10 +1,12 @@ import assert from 'node:assert/strict' +import { Buffer } from 'node:buffer' import { execFileSync } from 'node:child_process' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' // eslint-disable-next-line test/no-import-node-test -- Release evidence must fail closed under stale or mis-scoped input. import test from 'node:test' +import { gzipSync } from 'node:zlib' import { dependencyClosure, evaluatePackage, findingPackages, libraryGates, sharedTasks, siteGates, validateLedger } from './release-ledger-model.mjs' import { checkedCandidate, checkedReport, fingerprintInputs, fingerprintOf, localFile, root } from './release-ledger.mjs' import { hash } from './releases.mjs' @@ -218,3 +220,49 @@ test('Release candidate checks actual tarball manifest and detects digest/versio save('candidate.tgz', 'tampered') assert.match(checkedCandidate(directory, row).errors[0], /Candidate integrity mismatch/) }) + +function candidateArchive(entries) { + const blocks = [] + for (const { name, type = '0', content = '', link = '' } of entries) { + const bytes = Buffer.from(content) + const header = Buffer.alloc(512) + header.write(name) + header.write('0000755\0', 100) + header.write('0000000\0', 108) + header.write('0000000\0', 116) + header.write(`${bytes.length.toString(8).padStart(11, '0')}\0`, 124) + header.write('00000000000\0', 136) + header.fill(32, 148, 156) + header.write(type, 156) + header.write(link, 157) + header.write('ustar\0', 257) + header.write('00', 263) + header.write(`${header.reduce((sum, byte) => sum + byte, 0).toString(8).padStart(6, '0')}\0 `, 148) + blocks.push(header, bytes, Buffer.alloc((512 - bytes.length % 512) % 512)) + } + return gzipSync(Buffer.concat([...blocks, Buffer.alloc(1024)])) +} + +test('Release candidate accepts Yarn directory entries without trailing slashes and rejects unsafe entry types', (t) => { + const { directory, save } = temp(t) + execFileSync('git', ['init', '-q', directory]) + execFileSync('git', ['-c', 'user.name=Ledger test', '-c', 'user.email=ledger@example.invalid', 'commit', '--allow-empty', '-qm', 'fixture'], { cwd: directory }) + const sourceCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: directory, encoding: 'utf8' }).trim() + const manifest = { name: 'example', version: '2.0.0' } + const entries = [{ name: 'package', type: '5' }, { name: 'package/dist', type: '5' }, { name: 'package/package.json', content: JSON.stringify(manifest) }, { name: 'package/dist/index.js', content: 'export default 1' }] + const row = { name: manifest.name, distribution: 'npm', candidate: { kind: 'npm-tarball', path: 'candidate.tgz', version: manifest.version, sourceCommit } } + const verify = (members) => { + const bytes = candidateArchive(members) + save('candidate.tgz', bytes) + row.candidate.integrity = `sha512-${hash(bytes, 'sha512', 'base64')}` + return checkedCandidate(directory, row).errors + } + assert.deepEqual(verify(entries), []) + assert.deepEqual(verify(entries.map(entry => entry.type === '5' ? { ...entry, name: `${entry.name}/` } : entry)), []) + for (const type of ['1', '2', '6']) + assert.match(verify([...entries, { name: 'package/dist/link.js', type, link: 'package/dist/index.js' }])[0], /links and special files/) + assert.match(verify([{ name: 'package', content: 'not a directory' }, ...entries.slice(1)])[0], /Invalid package root/) + assert.match(verify([...entries, entries[2]])[0], /Duplicate archive members/) + assert.match(verify([...entries, { name: 'outside.json', content: '{}' }])[0], /Invalid package root/) + assert.match(verify([...entries, { name: 'package/../outside.json', content: '{}' }])[0], /Unsafe package member/) +}) diff --git a/refactor/tasks.json b/refactor/tasks.json index 523ffc1a8..d938261de 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -5015,7 +5015,9 @@ "acceptance": "隔离安装和可自动化组合通过;设备结论不伪造,最终发布绑定同一候选内容;在目标 major 版本确定后构建 pack,不在测试后改版本;逐包核验完整回退产物、实际候选版本/integrity、应用导入与冻结锁,重跑对应回退并保留报告;缺失历史分发不准入", "evidence": [ "changes/2026-09-16-REL-02-types-inputs.md", - "baselines/major-installed-types-validation.json" + "baselines/major-installed-types-validation.json", + "changes/2026-09-16-REL-02-yarn-candidate-archives.md", + "baselines/major-candidate-archive-validation.json" ] }, { diff --git a/scripts/package-archive.mjs b/scripts/package-archive.mjs new file mode 100644 index 000000000..696220133 --- /dev/null +++ b/scripts/package-archive.mjs @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' + +export function packedFiles(archive) { + const args = { encoding: 'utf8', windowsHide: true } + const names = execFileSync('tar', ['-tzf', archive], args).trim().split(/\r?\n/) + const entries = execFileSync('tar', ['-tvzf', archive], args).trim().split(/\r?\n/) + assert.equal(names.length, entries.length, 'Archive listing mismatch') + assert.equal(new Set(names).size, names.length, 'Duplicate archive members') + return names.filter((name, index) => { + const type = entries[index][0] + assert(['d', '-'].includes(type), 'Package links and special files are unsupported') + assert((type === 'd' && name === 'package') || name.startsWith('package/'), 'Invalid package root') + assert(!name.includes('\\') && !name.split('/').includes('..'), 'Unsafe package member') + return type === '-' + }).sort() +} diff --git a/scripts/package-check.mjs b/scripts/package-check.mjs index f4a49d91a..6ab86f842 100644 --- a/scripts/package-check.mjs +++ b/scripts/package-check.mjs @@ -7,8 +7,11 @@ import { fileURLToPath } from 'node:url' import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs' import { installedPackages } from './browser-validation/scope.ts' import { installedPluginTypes } from './consumers/packages.ts' +import { packedFiles } from './package-archive.mjs' import { consumerDirectory, names as defaultNames, readJson, removeConsumer, run, runtimeConsumer, typeConsumers, workspace, writeJson } from './package-consumer.mjs' +export { packedFiles } from './package-archive.mjs' + export function packageOptions(args) { assert(args.every(arg => ['--release', '--browser'].includes(arg) || arg.startsWith('--include=')), 'Unknown package-check option') const includes = args.filter(arg => arg.startsWith('--include=')) @@ -60,21 +63,6 @@ export function historicalDistributionFiles(name, { baseline, sources }) { return members } -export function packedFiles(archive) { - const args = { encoding: 'utf8', windowsHide: true } - const names = execFileSync('tar', ['-tzf', archive], args).trim().split(/\r?\n/) - const entries = execFileSync('tar', ['-tvzf', archive], args).trim().split(/\r?\n/) - assert.equal(names.length, entries.length, 'Archive listing mismatch') - assert.equal(new Set(names).size, names.length, 'Duplicate archive members') - return names.filter((name, index) => { - const type = entries[index][0] - assert(['d', '-'].includes(type), 'Package links and special files are unsupported') - assert((type === 'd' && name === 'package') || name.startsWith('package/'), 'Invalid package root') - assert(!name.includes('\\') && !name.split('/').includes('..'), 'Unsafe package member') - return type === '-' - }).sort() -} - export async function publishedConsumer() { const dir = consumerDirectory() const releases = readJson(path.join(workspace, 'refactor/baselines/releases.json')).releases diff --git a/scripts/release/README.md b/scripts/release/README.md index b42a0f2a0..65a9a40b6 100644 --- a/scripts/release/README.md +++ b/scripts/release/README.md @@ -28,6 +28,12 @@ reports are not evidence that an ArtPlayer package is ready to publish. ## Responsibilities +- `../package-archive.mjs` checks actual tar entry types and paths for both + package installation checks and candidate registration. It accepts Yarn's + directory entries without a trailing slash while rejecting links, special + files, duplicate members and paths outside `package/`. The legacy + `package-check.mjs` export forwards to this helper for existing scripts. + - `../prepare-release.mjs` is the Node entry and failure exit-code boundary. - `prepare.ts` parses the explicit package batch/tag, requires the canonical Node/Yarn, and checks clean Git state before both ledger reads. Ignored generated