ci: [CI-01] split playback engines from browser consumers

This commit is contained in:
Harvey Zhao committed 2026-09-15 04:18:18 +08:00
1 parent 92b04d75c0
commit 4a9ef16b16
16 files changed
+1031 -74

No files matched your search

+79 -19
View File
@@ -69,8 +69,8 @@ jobs:
include-hidden-files: true
retention-days: 14
if-no-files-found: warn
browser-smoke:
name: Browser playback (${{ matrix.os }})
browser-consumers:
name: Browser consumers (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
env:
@@ -135,19 +135,6 @@ jobs:
run: yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log
- name: Verify Vue installed consumers
run: yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log
- name: Prepare installed browser plugin packages
# Node/framework consumers above retain their two-package fixture scope.
# These extra installed packages are required by the browser helpers.
# Other ecosystem installation/SDK gates remain tracked by CI-01.
run: |
yarn test:package --browser 2>&1 | tee refactor/.cache/ci/browser-packages.log
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
- name: Run all source browser checks
if: ${{ !cancelled() }}
run: yarn test:browser:source 2>&1 | tee refactor/.cache/ci/browser-source.log
- name: Run installed browser checks
if: ${{ !cancelled() }}
run: yarn test:browser:installed 2>&1 | tee refactor/.cache/ci/browser-installed.log
- name: Verify iframe cached history and interrupted navigation
run: yarn test:iframe-history 2>&1 | tee refactor/.cache/ci/iframe-history.log
- name: Compare installed performance and verify resource cleanup
@@ -156,12 +143,9 @@ jobs:
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: browser-smoke-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
name: browser-consumers-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
refactor/.cache/ci/
refactor/.cache/browser/
refactor/.cache/browser-source/
refactor/.cache/browser-installed/
refactor/.cache/iframe-history/
refactor/.cache/react-consumer-*/
refactor/.cache/vue-consumer-*/
@@ -185,6 +169,81 @@ jobs:
- ubuntu-latest
- windows-latest
- macos-latest
browser-smoke:
name: Browser playback (${{ matrix.os }}, ${{ matrix.browser }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
env:
CI: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
package-manager-cache: false
- name: Record source and cache inputs
id: context
run: node scripts/ci-context.mjs
- name: Cache locked Yarn downloads
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 ref verified 2026-09-13; node24
with:
path: ${{ steps.context.outputs.yarn_cache }}
key: yarn-${{ runner.os }}-${{ runner.arch }}-node-${{ steps.context.outputs.node }}-yarn-${{ steps.context.outputs.yarn }}-${{ github.ref }}-${{ hashFiles('yarn.lock') }}
- name: Install the pinned package manager and dependencies
run: |
npm install --global --force yarn@1.22.22 2>&1 | tee refactor/.cache/ci/toolchain-install.log
yarn install --frozen-lockfile --non-interactive 2>&1 | tee refactor/.cache/ci/install.log
- name: Cache versioned browser downloads
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 ref verified 2026-09-13; node24
with:
path: ${{ steps.context.outputs.browser_cache }}
key: playwright-${{ runner.os }}-${{ runner.arch }}-node-${{ steps.context.outputs.node }}-yarn-${{ steps.context.outputs.yarn }}-${{ github.ref }}-${{ hashFiles('yarn.lock') }}-${{ steps.context.outputs.playwright }}
- name: Install test browsers
run: yarn test:browser:install --with-deps 2>&1 | tee refactor/.cache/ci/browser-install.log
- name: Prepare installed browser plugin packages
# Each engine builds and freezes its own complete installed browser roster.
# Other ecosystem installation/SDK gates remain tracked by CI-01.
run: |
yarn test:package --browser 2>&1 | tee refactor/.cache/ci/browser-packages.log
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
- name: Run all source browser checks
if: ${{ !cancelled() }}
run: yarn test:browser:source --project=${{ matrix.browser }} 2>&1 | tee refactor/.cache/ci/browser-source.log
- name: Run installed browser checks
if: ${{ !cancelled() }}
run: yarn test:browser:installed --project=${{ matrix.browser }} 2>&1 | tee refactor/.cache/ci/browser-installed.log
- name: Upload browser evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: browser-smoke-${{ matrix.os }}-${{ matrix.browser }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
refactor/.cache/ci/
refactor/.cache/browser-source/
refactor/.cache/browser-installed/
refactor/.cache/packages/latest.json
refactor/.cache/packages/run-*/*.json
refactor/.cache/packages/run-*/*.log
refactor/.cache/packages/run-*/*.tgz
refactor/.cache/packages/run-*/artifacts/
include-hidden-files: true
retention-days: 14
if-no-files-found: warn
defaults:
run:
shell: bash
strategy:
fail-fast: false
max-parallel: 6
matrix:
browser: [chromium, firefox, webkit]
os:
- ubuntu-latest
- windows-latest
- macos-latest
checks:
name: Checks and build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
@@ -279,6 +338,7 @@ jobs:
- checks
- coverage
- browser-smoke
- browser-consumers
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
@@ -0,0 +1,685 @@
{
"schemaVersion": 1,
"task": "CI-01",
"date": "2026-09-15",
"sourceHead": "92b04d75c0291d3639b581288d846352c7bbd9f4",
"candidate": "Working tree included in the accompanying CI-01 browser matrix checkpoint; code fingerprints normalize LF",
"environment": {
"node": "24.21.0",
"yarn": "1.22.22",
"platform": "win32",
"actionlint": "1.7.12"
},
"inputs": [
{
"file": ".github/workflows/nodejs.yml",
"sha256LF": "726b7f1f9126b2453123f8537ca4ac466333795b6673b4e7d93aac5e448fa9d0"
},
{
"file": "scripts/ci-summary.mjs",
"sha256LF": "993c99d9799bae72f1d97097bea4521c3964db365930a8f166719410f0578f05"
},
{
"file": "test/ci-summary.test.js",
"sha256LF": "c7fecee2e4f514bf3cd67ace03485856daa177ab51bee86a11e777577bb47eb9"
},
{
"file": "refactor/scripts/ci-workflow.mjs",
"sha256LF": "66353eb9531ae3bd9555b42668ba318d8540c353370cb2ac3f25c62373bf99a4"
},
{
"file": "refactor/scripts/ci-workflow.test.mjs",
"sha256LF": "154a65f97ae736667e107dff685fc055ce3e2da5e11f4ac31debd81435e1875f"
},
{
"file": "refactor/scripts/impact-model.mjs",
"sha256LF": "d243062448803b245a83a17518a6ef1024bf1b24be63c66f047cc87464e437e9"
},
{
"file": "refactor/scripts/impact.test.mjs",
"sha256LF": "5e113a7c733f9642633b082c80175d951e9278859ae966f8a300ef626581edfb"
},
{
"file": "refactor/impact-policy.json",
"sha256LF": "e4ab08484dff4bfdc2fcbd89f990a8eb7607e70d58982e121858cb4a1b001f86"
},
{
"file": "scripts/browser-check.mjs",
"sha256LF": "4c1dbfb6dc05cf4842ce6568181dcc76e076003ee268e5fe00aff0a771010811"
},
{
"file": "scripts/browser-validation/scope.ts",
"sha256LF": "3b0526de3f571bb95c429ff3785277771576bb077924699b04dafaaa886f047d"
},
{
"file": "playwright.config.js",
"sha256LF": "df2384f5626252ec933725867e400ec22eb92bc5e6f04873059b32decf2f2d56"
},
{
"file": "playwright.source.config.js",
"sha256LF": "00bebb9253de62b827c2e8734b6b54a6a23c56802eafc5d3996b241b5fe82092"
},
{
"file": "playwright.installed.config.js",
"sha256LF": "e158a28a91dea09dc5af17cc6f4b204020a6c1f7042c3ba0ecd90677ded0167f"
}
],
"collection": [
{
"scope": "source",
"exactUnion": true,
"noOverlap": true,
"cases": 4641,
"files": 154,
"records": [
{
"engine": "all",
"directory": "refactor/.cache/ci01-browser-matrix-source-all-collection",
"reportSha256": "b2e710d6e934c4b580595c09b6ff4eb54358099c85cee486d618385644dd69ea",
"invocationSha256": "9b9ebc452ba4886f9398b5f5c1916eae5294b62bc6afc8fdfbae5e252ceb3fab",
"args": [
"test",
"--config=playwright.source.config.js",
"--list"
],
"exitCode": 0,
"cases": 4641,
"files": 154,
"caseSetSha256": "fb2ab31fc294ffd38f53486c854465adf6848f7157f4f69301640c4c1e10cf8c",
"collectionOnly": true
},
{
"engine": "chromium",
"directory": "refactor/.cache/ci01-browser-matrix-source-chromium-collection",
"reportSha256": "d46893ffcec98aad841c2b8afd3a4aab806aa3c62a973230559fe7a4d551499d",
"invocationSha256": "dca7b9af6fc6f8a9a8b1a098fe927845efa6c01f3ba8c544eab9d76011bfc187",
"args": [
"test",
"--config=playwright.source.config.js",
"--list",
"--project=chromium"
],
"exitCode": 0,
"cases": 1547,
"files": 154,
"caseSetSha256": "65f157f3d7bdd27ee89ba5db8a4a4f0caa35d3fac8f8396349e42d16a7fa8cb8",
"collectionOnly": true
},
{
"engine": "firefox",
"directory": "refactor/.cache/ci01-browser-matrix-source-firefox-collection",
"reportSha256": "7e4b49e6203307c177d5d6258d9820e165c8707022a9cb83c6af8a47e892ebbf",
"invocationSha256": "94b2d3473db4d254b84710dfde10af95235df208f2cbebcefe2e79c530b9d994",
"args": [
"test",
"--config=playwright.source.config.js",
"--list",
"--project=firefox"
],
"exitCode": 0,
"cases": 1547,
"files": 154,
"caseSetSha256": "f25c545aefe6e3e617844fab7a80ec16457dc98ea8ed651b45b7cef1e9085c67",
"collectionOnly": true
},
{
"engine": "webkit",
"directory": "refactor/.cache/ci01-browser-matrix-source-webkit-collection",
"reportSha256": "7ee2e4f9e87b0c2d34e7173f71dcb2087b19cda206eaaea2e4b343d619f8b0fc",
"invocationSha256": "3e1d45a1428b44bd0b7b5b05d5d54124337c771061def338dbf82b44b40dcf45",
"args": [
"test",
"--config=playwright.source.config.js",
"--list",
"--project=webkit"
],
"exitCode": 0,
"cases": 1547,
"files": 154,
"caseSetSha256": "08bfe59e38abf8938a3896f75e90c34606e50d7e42f79608a6a730b267a45bba",
"collectionOnly": true
}
]
},
{
"scope": "installed",
"exactUnion": true,
"noOverlap": true,
"cases": 1980,
"files": 67,
"records": [
{
"engine": "all",
"directory": "refactor/.cache/ci01-browser-matrix-installed-all-collection",
"reportSha256": "5903ceb4231d6c21706b951e6c2e1faf6bcacb0828021ce72b63118192d05837",
"invocationSha256": "31bc2af2f6f93c817c4e66dad0b2401abda2375f0552c93ec018399fe874880b",
"args": [
"test",
"--config=playwright.installed.config.js",
"--list"
],
"exitCode": 0,
"cases": 1980,
"files": 67,
"caseSetSha256": "05c2abcdc0f42ce5c48e0cb49e9d4c6b9c0f85cd09df4f7cb59e7a27b53cb378",
"collectionOnly": true
},
{
"engine": "chromium",
"directory": "refactor/.cache/ci01-browser-matrix-installed-chromium-collection",
"reportSha256": "585b78a64df866293283349e046f22392755f3173519c2442c42fbb7894ace67",
"invocationSha256": "8601b7797b302fd6789d35597a7ba045bcc75815f23990c9652509d50846c1df",
"args": [
"test",
"--config=playwright.installed.config.js",
"--list",
"--project=chromium"
],
"exitCode": 0,
"cases": 660,
"files": 67,
"caseSetSha256": "67efda9f29268152b7f43ee699a54c3c2c157262d961e28a7ea6d6a58a42376f",
"collectionOnly": true
},
{
"engine": "firefox",
"directory": "refactor/.cache/ci01-browser-matrix-installed-firefox-collection",
"reportSha256": "81c6f61b51ca244d96b5d459f920e6fdd156a30ae793f9ce8ba101a3fe7b2279",
"invocationSha256": "2a88902ffaf661aeab1bdfc7f7756132974532aecbd962611b9d8c77f4706329",
"args": [
"test",
"--config=playwright.installed.config.js",
"--list",
"--project=firefox"
],
"exitCode": 0,
"cases": 660,
"files": 67,
"caseSetSha256": "6242f6445283fc3907d8bf8bacee09c5ae4a1f87aa5c50c1e6113f0db7250e60",
"collectionOnly": true
},
{
"engine": "webkit",
"directory": "refactor/.cache/ci01-browser-matrix-installed-webkit-collection",
"reportSha256": "e4673bf53807fbc48a721be1218dbeead39831b9a4c02c5038861a3d983ceae3",
"invocationSha256": "ee3cfedb2bd72e057ee25c60ae8f31bcc6aeaeaf9f575a55992f4023f8791918",
"args": [
"test",
"--config=playwright.installed.config.js",
"--list",
"--project=webkit"
],
"exitCode": 0,
"cases": 660,
"files": 67,
"caseSetSha256": "faa53758af148e8dde777494d56386c1f0cc8a55bc7821b14f057daba2f3f731",
"collectionOnly": true
}
]
}
],
"playback": [
{
"scope": "source",
"engine": "chromium",
"browser": "153.0.8010.12",
"directory": "refactor/.cache/ci01-browser-matrix-source-chromium-playback",
"reportSha256": "2b3903b30fe62fa5c7f7774225195acf6f2ea5fbb322e053cf28e75047b447c4",
"invocationSha256": "aab300aed50e5eb2ee5ae5ab90b615c1cc76a631798058354b37473e8b36876a",
"args": [
"test",
"--config=playwright.source.config.js",
"--project=chromium",
"playback.spec.js"
],
"exitCode": 0,
"stats": {
"startTime": "2026-09-14T20:12:36.490Z",
"duration": 20470.017,
"expected": 25,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"files": {
"asr-playback.spec.js": 6,
"auto-playback.spec.js": 15,
"playback.spec.js": 4
},
"core": {
"source": {
"kind": "workspace-build",
"entry": "packages\\artplayer\\src\\index.ts"
},
"sha256": "5139cabd5abf9b92684ee2dcdaa620c4db7f67c0cb1aa3f654435d739468bf9d",
"bytes": 378931
},
"installedPackages": 0,
"skips": []
},
{
"scope": "source",
"engine": "firefox",
"browser": "155.0",
"directory": "refactor/.cache/ci01-browser-matrix-source-firefox-playback",
"reportSha256": "fb1d902605692c57094bf5fb0890b1fd61a65dbf1492553bd813719f140d4635",
"invocationSha256": "639660214b4e91468a82b0c36f32f7e0d99efde027f87df3826d74531246555e",
"args": [
"test",
"--config=playwright.source.config.js",
"--project=firefox",
"playback.spec.js"
],
"exitCode": 0,
"stats": {
"startTime": "2026-09-14T20:12:57.801Z",
"duration": 26536.808,
"expected": 25,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"files": {
"asr-playback.spec.js": 6,
"auto-playback.spec.js": 15,
"playback.spec.js": 4
},
"core": {
"source": {
"kind": "workspace-build",
"entry": "packages\\artplayer\\src\\index.ts"
},
"sha256": "5139cabd5abf9b92684ee2dcdaa620c4db7f67c0cb1aa3f654435d739468bf9d",
"bytes": 378931
},
"installedPackages": 0,
"skips": []
},
{
"scope": "source",
"engine": "webkit",
"browser": "26.6",
"directory": "refactor/.cache/ci01-browser-matrix-source-webkit-playback",
"reportSha256": "a86da3a1b8f4754bed534c59e29aca993e0e9d1aebc4f661b2a8cbac26c86fd4",
"invocationSha256": "70e2732396ca23678490008b4b3a60c7f17121cc1e3acc841cca578b7b92b5f3",
"args": [
"test",
"--config=playwright.source.config.js",
"--project=webkit",
"playback.spec.js"
],
"exitCode": 0,
"stats": {
"startTime": "2026-09-14T20:13:25.154Z",
"duration": 20634.668999999998,
"expected": 19,
"skipped": 6,
"unexpected": 0,
"flaky": 0
},
"files": {
"asr-playback.spec.js": 6,
"auto-playback.spec.js": 15,
"playback.spec.js": 4
},
"core": {
"source": {
"kind": "workspace-build",
"entry": "packages\\artplayer\\src\\index.ts"
},
"sha256": "5139cabd5abf9b92684ee2dcdaa620c4db7f67c0cb1aa3f654435d739468bf9d",
"bytes": 378931
},
"installedPackages": 0,
"skips": [
{
"file": "asr-playback.spec.js",
"title": "ASR 2.1.0 + published-5.3.0: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR 2.1.0 + published: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR 2.1.0 + candidate: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR candidate + published-5.3.0: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR candidate + published: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR candidate + candidate: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
}
]
},
{
"scope": "installed",
"engine": "chromium",
"browser": "153.0.8010.12",
"directory": "refactor/.cache/ci01-browser-matrix-installed-chromium-playback",
"reportSha256": "8b97bdd6c4975d31ffe0e1ea713da23b2259de68738a1459ecba0f244c05da97",
"invocationSha256": "c7ba08cb8c264cfbb8c3bb69d05cf2851c8e5ee75430d80cea4c2f669170fb90",
"args": [
"test",
"--config=playwright.installed.config.js",
"--project=chromium",
"playback.spec.js"
],
"exitCode": 0,
"stats": {
"startTime": "2026-09-14T20:13:46.791Z",
"duration": 16926.979,
"expected": 10,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"files": {
"asr-playback.spec.js": 6,
"playback.spec.js": 4
},
"core": {
"source": {
"kind": "artifact",
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\packages\\run-ifGhEv\\artifacts\\artplayer\\dist\\artplayer.js"
},
"sha256": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6",
"bytes": 208943
},
"installedPackages": 18,
"skips": []
},
{
"scope": "installed",
"engine": "firefox",
"browser": "155.0",
"directory": "refactor/.cache/ci01-browser-matrix-installed-firefox-playback",
"reportSha256": "b3b6bd5cde862b6c492e001419dc174203530c170790fa7b33b59c2a32acae15",
"invocationSha256": "bd0d1808df00277943b6f9e88ede1014173ddba9156696f013a605e0176b2ce0",
"args": [
"test",
"--config=playwright.installed.config.js",
"--project=firefox",
"playback.spec.js"
],
"exitCode": 0,
"stats": {
"startTime": "2026-09-14T20:14:04.746Z",
"duration": 20473.375,
"expected": 10,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"files": {
"asr-playback.spec.js": 6,
"playback.spec.js": 4
},
"core": {
"source": {
"kind": "artifact",
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\packages\\run-ifGhEv\\artifacts\\artplayer\\dist\\artplayer.js"
},
"sha256": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6",
"bytes": 208943
},
"installedPackages": 18,
"skips": []
},
{
"scope": "installed",
"engine": "webkit",
"browser": "26.6",
"directory": "refactor/.cache/ci01-browser-matrix-installed-webkit-playback",
"reportSha256": "f1a25f0d38d5d0f83517e6bcab76fc2e26595570cc08d6280f8bb396772b280e",
"invocationSha256": "041150cf613bb3ef5ddf17f88158f5f8b6e231b4b285cc20f214febdf475faac",
"args": [
"test",
"--config=playwright.installed.config.js",
"--project=webkit",
"playback.spec.js"
],
"exitCode": 0,
"stats": {
"startTime": "2026-09-14T20:14:26.236Z",
"duration": 9805.599,
"expected": 4,
"skipped": 6,
"unexpected": 0,
"flaky": 0
},
"files": {
"asr-playback.spec.js": 6,
"playback.spec.js": 4
},
"core": {
"source": {
"kind": "artifact",
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\packages\\run-ifGhEv\\artifacts\\artplayer\\dist\\artplayer.js"
},
"sha256": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6",
"bytes": 208943
},
"installedPackages": 18,
"skips": [
{
"file": "asr-playback.spec.js",
"title": "ASR 2.1.0 + published-5.3.0: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR 2.1.0 + published: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR 2.1.0 + candidate: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR candidate + published-5.3.0: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR candidate + published: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
},
{
"file": "asr-playback.spec.js",
"title": "ASR candidate + candidate: native playback volume and mute",
"annotations": [
{
"type": "skip",
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
"location": {
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
"line": 27,
"column": 14
}
}
]
}
]
}
],
"verification": {
"ciTests": {
"passed": 77,
"failed": 0,
"skipped": 0,
"durationMs": 5997.7666,
"log": "refactor/.cache/ci01-browser-matrix-ci-tests-complete.log",
"logSha256": "5ffaa56850d47323810ed061542034e6822dee48ac4cb9fe180b0a4de6c4c5e2"
},
"actionlint": {
"exitCode": 0,
"log": "refactor/.cache/ci01-browser-matrix-actionlint.log",
"logSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"lint": {
"exitCode": 0,
"log": "refactor/.cache/ci01-browser-matrix-lint.log",
"logSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"workflow": {
"exitCode": 0,
"log": "refactor/.cache/ci01-browser-matrix-check-ci.log",
"logSha256": "aa3b88b3eef95df7b31cf71b72c2f7ac7848f61446c05b7323f938cd3aa0a09e"
},
"impact": {
"exitCode": 0,
"log": "refactor/.cache/ci01-browser-matrix-impact.log",
"logSha256": "06a3447b7906d7a99cf48c14582cefb16e71a941995bc366e65c16dd10a7ca54"
},
"toolchain": {
"exitCode": 0,
"log": "refactor/.cache/ci01-browser-matrix-toolchain.log",
"logSha256": "8b974d72fa8aad724a7c501b148cb1c5506c0aa1c7c41cd4a0a1a2ba5b9a307a"
}
},
"preservedFailures": [
{
"log": "refactor/.cache/ci01-browser-matrix-ci-tests.log",
"cause": "Impact policy still required the prior unsplit test:package and test:browser steps; updated explicit jobs, invocations and scope conditions.",
"logSha256": "92460d476817b3b32fa3fcff408b266f34e177fb568596383637a995ddfff15e"
},
{
"log": "refactor/.cache/ci01-browser-matrix-ci-tests-fixed.log",
"cause": "Impact gate identity used command alone and rejected test:package in two independent jobs; identity now includes job and reviewed arguments with mandatory gate regressions.",
"logSha256": "dc37d63f4f98c3d71e9530c72ae1811be2c6ec93d826e91cfc02ed2ef3533714"
}
],
"limitations": [
"Collection-only reports mark cases skipped without executing them; exact set equivalence is not playback acceptance.",
"Targeted playback uses the filename regex playback.spec.js: source selects asr-playback, auto-playback and playback; installed selects asr-playback and playback. This is not a full rerun.",
"WebKit has twelve existing ASR Web Audio capability skips across the two scopes, not successful ASR playback.",
"Local Windows engine runs do not verify remote Linux/macOS execution, cache behavior, runner queueing, cancellation scheduling, billing time or the 60-minute budget.",
"No prior Chapter/Audio/DASH/JASSUB failure or physical-device, SDK, type, license or release gate is closed by this CI split.",
"CI-01 stays doing; no push, deployment, branch-protection change or publication."
]
}
@@ -0,0 +1,72 @@
# CI-01 按浏览器引擎拆分播放作业
## 原因与实现
上一个检查点仅弹幕/Mask 安装子集就运行了 22.5 分钟。原 browser-smoke 每个
系统串行执行 Node 消费者、React/Vue、三个引擎的 source/installed、iframe
history 和性能检查。没有完整远端超时证据,但这批实测足以支持减少单个作业
的串行工作量;不能承诺此次拆分后每个远端组合一定在 60 分钟内完成。
- browser-smoke 使用三个 OS × 三个引擎,共 9 个组合,最多同时运行 6 个。
每个组合独立冻结安装、正常打包完整 browser 包清单,再执行所选引擎的
source 和 installed 两个范围。唯一新增参数是 `--project=${{ matrix.browser }}`。
- browser-consumers 每个 OS 一次,共 3 个组合,最多同时运行 3 个。保留同一
core/chapter tarball 的 Node 20.19.0、22.12.0、标准 24.21.0 消费,以及原有
React/Vue、iframe history 和性能三引擎检查。恢复 Node 的次序保持不变。
- 两组互不依赖;source/installed 保留 `!cancelled()`,普通失败不会阻止另一
范围采证,失败仍传入最终结果。每个作业有独立 runner,报告目录不混用。
- 稳定的 CI result 同时依赖 checks、coverage、browser-smoke、browser-consumers。
任一组失败、取消、跳过、缺失都不能通过。artifact 名增加引擎维度;消费者
使用独立前缀。既有固定 action SHA、只读权限、精确缓存键和失败上传保留。
- 影响策略将每个必需调用绑定到其实际 job、参数和条件。同名 test:package
在两个作业有不同用途,不能再只以命令名去重。原先残留的 test:browser
门槛改为两个真实的 scope 入口;没有通过删除门槛绕过校验。
代价是更多独立 runner 和重复打包。当前没有新增依赖、版本变化或生产 API
变更;没有修改用例断言、重试、超时、既有能力跳过或 installed 包清单。
远端队列、总计费分钟和实际加速幅度仍待测量。
## 验证
环境为 Windows、Node 24.21.0、Yarn 1.22.22;真实浏览器为 Chromium
153.0.8010.12、Firefox 155.0、WebKit 26.6。证据及输入指纹见
[机器记录](../baselines/ci-browser-matrix-validation.json)。
| 验证 | 结果及边界 |
| --- | --- |
| source 完整 collection | 154 文件、4,641 项;三个独立引擎各 1,547 项,按测试 ID 精确比较,并集完全相同且不重叠 |
| installed 完整 collection | 67 文件、1,980 项;三个独立引擎各 660 项,同样精确比较,无遗漏/重复 |
| source 定向实际运行 | Chromium 25、Firefox 25、WebKit 19 通过;WebKit 6 项 ASR 能力跳过 |
| installed 定向实际运行 | Chromium 10、Firefox 10、WebKit 4 通过;WebKit 6 项 ASR 能力跳过 |
| CI 工程测试 | 最终 77/77 通过,5,997.7666 ms;覆盖矩阵/引擎删减、artifact 冲突、跨组依赖、遗漏消费者、结果失败/取消/跳过/缺失和影响策略 |
| 静态检查 | actionlint 1.7.12、改动 JS/MJS 只读 ESLint、check:ci、check:impact --report、严格工具链通过 |
八次 collection 只列出用例,report 的 skipped 不代表实际能力跳过或播放通过。
六次真实运行分别使用 scope 入口加 `--project=<engine> playback.spec.js`;这个
文件正则匹配 source 的 asr-playback、auto-playback、playback 三文件,以及
installed 的 asr-playback、playback 两文件。它们合计 93 通过、12 能力跳过,
零失败、零重试;这不是全量 6,621 项重跑。WebKit 跳过原因是 Windows 下缺少
原生 AudioContext/AudioWorkletNode,不能记录为 ASR 播放成功。
核心旧/新播放器实际解码画面、播放/暂停、seek、切源和销毁仍由既有用例断言。
源码入口清除继承的安装 map;安装入口校验上轮 run-ifGhEv 十八包的构建新鲜度
和安装字节,真实服务的核心摘要与该 map 一致。本次没有重建生产包,因为改变的
是 CI 编排和校验脚本;安装检查拒绝陈旧输入的机制保持执行。
本地报告归档在 `refactor/.cache/ci01-browser-matrix-<scope>-<engine>-collection`
和 `...-playback`。相邻同前缀日志保留实际退出码对应输出。首轮 CI 测试因为旧
影响策略仍要求原作业调用而失败;第二轮暴露同名脚本去重问题,两次日志保留,
修正后全组 77 项通过。没有把最初失败覆盖为通过。
复核 collection 时分别运行两个 scope 的 `--list`,再分别加三个 `--project`;
比较 report 中每个 spec ID 与 projectName 的组合。再次运行前先归档本地同一
scope 的旧报告。完整 CI 使用上述不带文件过滤的工作流命令。
## 尚未完成
CI-01 保持 doing,任务总数 263、已完成 197 不变。远端 Linux/macOS/Windows
全量运行、实际缓存/取消/报告上传/60 分钟预算、分支保护、其余生态分发与设备
门槛尚未验收。既有 Chapter/Audio/DASH/JASSUB 失败未因本轮关闭。
没有推送、触发远端流程、部署或发布。当前改动随独立 CI-01 检查点本地提交;
提交后按 check:commits --report 审计。
+20 -10
View File
@@ -6,7 +6,8 @@
`scripts/browser-validation/scope.ts`。两步不
吞失败,普通失败后仍保存另一范围的证据,分别上传browser-source/browser-installed。
入口与报告规则见[维护说明](../scripts/browser-validation/README.md)。source全量
耗时/60分钟预算、其余插件安装矩阵与远端运行仍待CI-01/CI-04,不计为已通过。
目前按 OS × 引擎拆分播放作业,消费者另行执行;各作业的实际远端耗时/60分钟预算、
其余插件安装矩阵与远端运行仍待CI-01/CI-04,不计为已通过。
## 本地命令
@@ -27,7 +28,7 @@
| `yarn check:editor-types` | 只读核对全部编辑器声明、SDK notices 和实际 libUris;主 TS 5.9.3/历史 4.3.5 整组语义检查,ci:check 执行 |
| `yarn build:site-assets` / `yarn check:site-assets` | 从站点 browser/ TS 生成三个经典脚本;check 只读,root build:docs 先生成;完整 VitePress 构建另行验收 |
| `yarn typecheck:site-assets` | 严格检查站点 browser/ 模块;生成脚本由 typecheck:docs-tools 的 checkJs 覆盖;均进入 ci:check |
| `yarn test:react-consumer` / `yarn test:vue-consumer` | 仓库外 tarball 安装、原框架示例、开发/生产三引擎;browser-smoke 执行并上传独立目录,本地证据不代替远端矩阵 |
| `yarn test:react-consumer` / `yarn test:vue-consumer` | 仓库外 tarball 安装、原框架示例、开发/生产三引擎;browser-consumers 每个系统执行一次并上传独立目录,本地证据不代替远端矩阵 |
| `yarn test:unit` | 原播放/DASH 回归、同夹具的新旧公共契约与 JS/TS loader 验证 |
| `yarn test:node` | test:unit 加工具链/文档构建回归,保留原 test:playback/test:dash-control 入口 |
| `yarn test` | 统一执行 Node 与基线测试,源码/夹具维护入口见仓库 test/README.md |
@@ -38,7 +39,7 @@
| `yarn check:contracts --report` | 校验12类契约/22包归属、版本及报告对应;--write更新静态表,详见 [维护说明](contract-coverage.md) |
| `yarn ci:check` | 严格 Node/Yarn/锁检查、计划、只读 lint、类型、Node 和基线测试;允许写忽略缓存,不修改源码 |
| `yarn check:ci` | 只读校验实际工作流的完整系统矩阵、安装、缓存、报告和最终检查;已接入 ci:check |
| `yarn test:ci` | CI 汇总退出码、工作流/影响分析反例与隔离运行时校验;SITE-SMOKE-01 实测 50 项 |
| `yarn test:ci` | CI 汇总退出码、工作流/影响分析反例与隔离运行时校验;CI-01 引擎拆分后实测 77 项 |
| `yarn test:package:runtime` | 标准 Node 重装同一已检查 tarball;须先运行 test:package,其他 Node 使用显式 --expected-node |
| `yarn ci:build` | 21 库包、i18n、编辑器声明、文档 readiness smoke 和文档站构建,以及构建后包导入 smoke;会生成 dist 和 docs 内容 |
| `yarn check:impact --report` | 读取实际依赖/验证关系和Git变更,核对workflow必需命令,写CI影响报告;已接入ci:check,见[影响映射](impact-analysis.md) |
@@ -58,8 +59,9 @@ Actions 固定完整 SHA,Node 来自 .node-version,Yarn 固定 1.22.22;安
| --- | --- | --- | --- |
| checks | Linux、Windows | ci:check、ci:build;Linux 额外执行 actionlint | 45 分钟 |
| coverage | Linux、Windows | 既有源码映射与生命周期覆盖检查 | 15 分钟 |
| browser-smoke | Linux、Windows、macOS | 同一 core/chapter tarball 的 Node 20/22/24 消费,三浏览器、iframe history、性能 | 60 分钟 |
| CI result | Linux | 汇总以上三个作业组,所有结果必须为 success | 5 分钟 |
| browser-smoke | Linux、Windows、macOS × Chromium、Firefox、WebKit | 每个引擎独立打包/冻结安装完整 browser 包清单,再执行该引擎全部 source 与已列出的 installed 用例;9 个组合,最多同时运行 6 个 | 每个组合 60 分钟 |
| browser-consumers | Linux、Windows、macOS | 同一 core/chapter tarball 的 Node 20/22/24 消费,React/Vue 三引擎、iframe history、性能;每个系统一次,最多同时运行 3 个 | 每个系统 60 分钟 |
| CI result | Linux | 汇总以上四个作业组,所有结果必须为 success | 5 分钟 |
构建与浏览器工具使用 .node-version 的 24.21.0。安装运行时消费分别使用固定 20.19.0、
22.12.0 和标准 Node,不重新构建候选。它们是根工具 engines 的边界测试点,发布包没有
@@ -68,12 +70,20 @@ Actions 固定完整 SHA,Node 来自 .node-version,Yarn 固定 1.22.22;安
矩阵不设置 fail-fast,单个系统失败后仍尽量收集其他系统证据;显式 bash 保留 tee
上游命令的失败退出码。影响报告继续扩大核心/共享变更到全生态,当前不缩减必需作业。
稳定名称为 **CI result**。它以 always() 依赖 checks、coverage、browser-smoke,结构化
稳定名称为 **CI result**。它以 always() 依赖 checks、coverage、browser-smoke、browser-consumers,结构化
读取 needs;失败、取消、跳过、缺失、错误 JSON 或未知额外作业都失败。增加独立 job 时
同时修改 ci-summary.mjs 的 requiredJobs、workflow needs、矩阵策略和测试,防止遗漏汇总。
全局取消/runner 故障下的真实调度仍待 CI-04;本地退出码测试不证明远端作业一定被调度。
required checks 的实际绑定尚未设置,不能宣称分支保护已经生效。
播放与消费者作业互不依赖,避免一组失败阻止另一组采集证据。播放步骤仅传入
`--project=${{ matrix.browser }}`;不添加 grep、文件过滤、重试或容许失败。
每个组合内部仍顺序执行 source/installed,使用独立报告目录和端口;不同组合由
独立 runner 隔离。浏览器 artifact 名包含 OS、引擎、run ID 和 attempt,消费者
artifact 使用独立前缀。每个 runner 冻结安装和构建自己的产物,代价是增加 runner
数量和重复打包时间;下载缓存仍使用精确版本键。这是减少单任务串行工作量的调整,
实际排队时间、总计费分钟和远端速度仍待验证。
## 下载缓存、日志与维护
scripts/ci-context.mjs 从真实 checkout、package.json、.node-version 和 yarn.lock 读取
@@ -172,7 +182,7 @@ build:ts 生成声明 URL 清单后重建站点浏览器产物,check:editor-ty
此选项不表示全生态类型、SDK 或发布准入。
十八包安装清单的弹幕/Mask 子集实跑 399 项耗时 22.5 分钟,见
[CI-01 记录](changes/2026-09-15-CI-01-danmuku-installed.md)。当前单个
browser-smoke 仍串行运行全部 source/installed 和其他消费者/性能检查,
60 分钟预算尚未经过完整远端验证;后续必须分片并核定时长,保留完整用例
和失败报告。不能从本地子集通过推断整个工作流可在预算内完成。
[CI-01 记录](changes/2026-09-15-CI-01-danmuku-installed.md)。随后完成上述引擎与
消费者拆分,见[拆分记录](changes/2026-09-15-CI-01-browser-matrix.md)。60 分钟预算
尚未经过完整远端验证;保留完整用例和失败报告,后续依据各组合实测数据决定是否
还需按文件分片。不能从本地子集通过推断整个工作流可在预算内完成。
+10 -5
View File
@@ -40,12 +40,15 @@ GitHub PR读取事件里的base SHA,push读取before SHA;初次push的全零
## CI门槛与能力边界
六项必需命令不能从策略中漏掉:`ci:check`、`ci:build`、`test:package`、
`test:browser`、`test:coverage`、`test:performance`。固定yaml2.8.2解析实际workflow,
检查对应job/步骤无条件执行、没有continue-on-error、没有路径过滤,且checkout有
完整历史。命令被移走、改成echo或增加条件会失败。命令名还必须存在于根scripts。
八项必需调用不能从策略中漏掉:`ci:check`、`ci:build`、消费者 `test:package`、
播放作业 `test:package --browser`、`test:browser:source`、`test:browser:installed`、
`test:coverage`、`test:performance`。两个浏览器范围只传当前矩阵的 `--project`,
并使用固定 `!cancelled()` 条件在普通失败后继续采证;其余门槛保持无条件步骤。
固定 yaml 2.8.2 解析实际 workflow,检查对应 job 必需执行、没有 continue-on-error、
没有路径过滤,且 checkout 有完整历史。命令被移走、改成 echo 或更换执行条件会
失败。命令名还必须存在于根 scripts;策略单独记录已审查的参数和条件。
必需命令必须是步骤首个非空、非注释行,只接受独立命令或当前显式bash的固定tee日志
格式;其他shell包装语法需审查后扩展。条件包装、`|| true`、非bash的tee和YAML布尔
格式;其他shell包装语法需审查后扩展。任意条件包装、`|| true`、非bash的tee和YAML布尔
`if: false`均有拒绝反例;checkout本身也不能带条件或允许失败。这是受限结构校验,
不是通用shell解释器,也不证明脚本内部的测试语义。
@@ -53,6 +56,8 @@ GitHub PR读取事件里的base SHA,push读取before SHA;初次push的全零
- `test:package`当前实际只有core/chapter;脚本读取其names数组核对策略,变更范围
后必须更新声明。其余19个库包完整安装消费仍缺证据,不能因识别为affected而过关。
`--browser` 已单独覆盖十八包安装产物及明确列出的浏览器子集,不扩张这个通用
Node/类型消费者列表,也不等于十八包全部类型/设备/SDK 组合通过。
- 覆盖率范围从实际coverage-policy读取;浏览器/性能命令运行已有用例,不代表所有
包、真实设备、远程SDK已验收。
- ENG-COVERAGE-01继续把契约、支持版本、测试ID、候选/报告及任务逐项连接;
+24 -3
View File
@@ -282,17 +282,38 @@
"scope": "All library builds, i18n, editor declarations, docs site and existing import suites"
},
{
"job": "browser-smoke",
"job": "browser-consumers",
"command": "yarn test:package",
"scope": "Installed core/chapter consumers only; other package installation acceptance remains pending"
},
{
"job": "browser-smoke",
"command": "yarn test:browser",
"scope": "All existing three-engine browser suites; does not imply every package/device has a suite"
"command": "yarn test:package",
"arguments": [
"--browser"
],
"scope": "Prepare the complete reviewed installed browser roster independently for each OS/engine leg"
},
{
"job": "browser-smoke",
"command": "yarn test:browser:source",
"arguments": [
"--project=${{ matrix.browser }}"
],
"condition": "${{ !cancelled() }}",
"scope": "Complete source test collection across all three engines; source and historical/explicit controls retain their identities"
},
{
"job": "browser-smoke",
"command": "yarn test:browser:installed",
"arguments": [
"--project=${{ matrix.browser }}"
],
"condition": "${{ !cancelled() }}",
"scope": "Complete declared installed test subset across all three engines; not all-package/device acceptance"
},
{
"job": "browser-consumers",
"command": "yarn test:performance",
"scope": "Existing installed performance and resource-cleanup scenarios"
},
+1 -1
View File
@@ -509,7 +509,7 @@
- ENG-09: [记录](changes/2026-09-12-ENG-09-integration.md) [记录](baselines/engineering-integration.json)
- ENG-10: [记录](changes/2026-09-10-ENG-10-test-reliability.md) [记录](test-reliability.md)
- ENG-11: [记录](build-analysis.md) [记录](baselines/bundle-attribution.json) [记录](changes/2026-09-11-ENG-11-build-analysis.md)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json) [记录](changes/2026-09-14-CI-01-installed-full.md) [记录](baselines/ci-installed-full-validation.json) [记录](changes/2026-09-14-CI-01-source-chromium.md) [记录](baselines/ci-source-chromium-validation.json) [记录](changes/2026-09-15-CI-01-ads-installed.md) [记录](baselines/ci-ads-installed-validation.json) [记录](changes/2026-09-15-CI-01-subtitles-installed.md) [记录](baselines/ci-subtitles-installed-validation.json) [记录](changes/2026-09-15-CI-01-adaptive-installed.md) [记录](baselines/ci-adaptive-installed-validation.json) [记录](changes/2026-09-15-CI-01-asr-cast-installed.md) [记录](baselines/ci-asr-cast-installed-validation.json) [记录](changes/2026-09-15-CI-01-mediabunny-installed.md) [记录](baselines/ci-mediabunny-installed-validation.json) [记录](changes/2026-09-15-CI-01-jassub-installed.md) [记录](baselines/ci-jassub-installed-validation.json) [记录](changes/2026-09-15-CI-01-danmuku-installed.md) [记录](baselines/ci-danmuku-installed-validation.json)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json) [记录](changes/2026-09-14-CI-01-installed-full.md) [记录](baselines/ci-installed-full-validation.json) [记录](changes/2026-09-14-CI-01-source-chromium.md) [记录](baselines/ci-source-chromium-validation.json) [记录](changes/2026-09-15-CI-01-ads-installed.md) [记录](baselines/ci-ads-installed-validation.json) [记录](changes/2026-09-15-CI-01-subtitles-installed.md) [记录](baselines/ci-subtitles-installed-validation.json) [记录](changes/2026-09-15-CI-01-adaptive-installed.md) [记录](baselines/ci-adaptive-installed-validation.json) [记录](changes/2026-09-15-CI-01-asr-cast-installed.md) [记录](baselines/ci-asr-cast-installed-validation.json) [记录](changes/2026-09-15-CI-01-mediabunny-installed.md) [记录](baselines/ci-mediabunny-installed-validation.json) [记录](changes/2026-09-15-CI-01-jassub-installed.md) [记录](baselines/ci-jassub-installed-validation.json) [记录](changes/2026-09-15-CI-01-danmuku-installed.md) [记录](baselines/ci-danmuku-installed-validation.json) [记录](changes/2026-09-15-CI-01-browser-matrix.md) [记录](baselines/ci-browser-matrix-validation.json)
- CI-02: [记录](changes/2026-09-14-CI-02-pages-artifact.md) [记录](baselines/pages-artifact-validation.json) [记录](pages-deployment.md)
- PILOT-01: [记录](changes/2026-09-10-PILOT-01-chapter.md) [记录](baselines/pilot-validation.json)
- CORE-01: [记录](changes/2026-09-10-CORE-01-typed-utils.md) [记录](baselines/core-utils-validation.json)
+9
View File
@@ -1,5 +1,14 @@
# 进度与证据
## CI-01 浏览器引擎与消费者作业拆分
播放检查改为 3 个 OS × 3 个引擎;Node/React/Vue/iframe history/性能每个 OS
独立运行一次。CI result 要求四组作业全部成功,失败上传与 source/installed
双范围保留。全量清单按测试 ID 核对:源码 4,641、安装 1,980 项,拆分无遗漏
或重复;定向真实运行 93 通过、12 个 WebKit ASR 能力跳过,工程测试 77/77。
见[记录](changes/2026-09-15-CI-01-browser-matrix.md)。197/263 完成,CI-01
仍 doing;远端耗时、缓存、调度与完整矩阵尚未验证,不关闭既有失败或设备缺口。
## CI-01 弹幕 / Mask 安装与原生负载矩阵
十八包打包/隔离安装通过;十三文件、三引擎 399/399 断言通过,22.5 分钟,
+35 -18
View File
@@ -11,7 +11,9 @@ const systems = {
'checks': ['ubuntu-latest', 'windows-latest'],
'coverage': ['ubuntu-latest', 'windows-latest'],
'browser-smoke': ['ubuntu-latest', 'windows-latest', 'macos-latest'],
'browser-consumers': ['ubuntu-latest', 'windows-latest', 'macos-latest'],
}
const engines = ['chromium', 'firefox', 'webkit']
export function validateCIWorkflow(source) {
const workflow = YAML.parse(source, { uniqueKeys: true })
@@ -46,14 +48,14 @@ export function validateCIWorkflow(source) {
continue
assert(!Object.hasOwn(job, 'if'), 'Required matrix jobs must not be skipped')
assert.equal(job['runs-on'], '${{ matrix.os }}')
assert.deepEqual(job.strategy.matrix, { os: systems[id] }, 'Do not silently narrow the OS matrix or exclude combinations')
assert.deepEqual(job.strategy.matrix, id === 'browser-smoke' ? { os: systems[id], browser: engines } : { os: systems[id] }, 'Do not silently narrow the OS/engine matrix or exclude combinations')
assert.equal(job.strategy['fail-fast'], false, 'Do not cancel other matrix evidence after a failure')
assert.equal(job.defaults?.run?.shell, 'bash', 'Tee pipelines require the explicit Actions bash pipefail shell')
assert(upload.with.name.includes('matrix.os'), 'Matrix artifacts must have distinct names')
const contextIndex = job.steps.findIndex(step => step.id === 'context' && step.run === 'node scripts/ci-context.mjs' && !Object.hasOwn(step, 'if'))
assert(contextIndex >= 0, 'Cache inputs must come from the checked source')
const caches = job.steps.filter(step => step.uses?.startsWith('actions/cache@'))
assert.equal(caches.length, id === 'browser-smoke' ? 2 : 1, 'Only explicit Yarn and browser download caches are expected')
assert.equal(caches.length, id.startsWith('browser-') ? 2 : 1, 'Only explicit Yarn and browser download caches are expected')
for (const cache of caches) {
assert(job.steps.indexOf(cache) > contextIndex, 'Record context before restoring caches')
assert(!cache.with['restore-keys'] && !cache.with.enableCrossOsArchive, 'Do not restore loosely matched or cross-OS downloads')
@@ -67,33 +69,47 @@ export function validateCIWorkflow(source) {
assert(installIndex > contextIndex && caches.filter(cache => cache.with.path.includes('yarn_cache')).every(cache => job.steps.indexOf(cache) < installIndex), 'Always perform a frozen install after Yarn cache restore')
}
const browser = workflow.jobs['browser-smoke']
assert(browser.steps.some(step => step.run?.startsWith('yarn test:browser:install --with-deps') && !Object.hasOwn(step, 'if')), 'Browser dependencies must install even on cache hits')
let consumerIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package'))
const consumers = workflow.jobs['browser-consumers']
for (const job of [browser, consumers]) {
assert(!Object.hasOwn(job, 'needs'), 'Playback and consumers must collect independent evidence after sibling failures')
assert(job.steps.some(step => step.run === 'yarn test:browser:install --with-deps 2>&1 | tee refactor/.cache/ci/browser-install.log' && !Object.hasOwn(step, 'if')), 'Browser dependencies must install even on cache hits')
}
assert.equal(browser.strategy['max-parallel'], 6, 'Bound the nine playback legs to six concurrent runners')
assert.equal(consumers.strategy['max-parallel'], 3, 'Run consumers once per OS without engine duplication')
assert.equal(browser.steps.filter(step => step.uses?.startsWith('actions/setup-node@')).length, 1, 'Playback jobs retain the canonical runtime throughout')
let consumerIndex = consumers.steps.findIndex(step => step.run?.startsWith('yarn test:package 2>&1 | tee refactor/.cache/ci/package.log\n'))
assert(consumerIndex >= 0 && !Object.hasOwn(consumers.steps[consumerIndex], 'if') && consumers.steps[consumerIndex].run.includes('ARTPLAYER_BROWSER_ARTIFACTS=') && consumers.steps[consumerIndex].run.includes('GITHUB_ENV'), 'Prepare and select the core/chapter consumer artifacts')
for (const [id, version, command] of [
['consumer-node-20', '20.19.0', 'node scripts/package-runtime.mjs --expected-node 20.19.0 2>&1 | tee refactor/.cache/ci/consumer-node-20.log'],
['consumer-node-22', '22.12.0', 'node scripts/package-runtime.mjs --expected-node 22.12.0 2>&1 | tee refactor/.cache/ci/consumer-node-22.log'],
['restore-canonical-node', null, 'node scripts/package-runtime.mjs --canonical 2>&1 | tee refactor/.cache/ci/consumer-node-canonical.log'],
]) {
const index = browser.steps.findIndex(step => step.id === id)
const index = consumers.steps.findIndex(step => step.id === id)
assert(index > consumerIndex, 'Build once before each ordered consumer runtime switch')
const step = browser.steps[index]
const step = consumers.steps[index]
assert(step.uses?.startsWith('actions/setup-node@') && !Object.hasOwn(step, 'if'), 'Consumer Node setup cannot be skipped')
assert.deepEqual(step.with, version ? { 'node-version': version, 'package-manager-cache': false } : { 'node-version-file': '.node-version', 'package-manager-cache': false }, 'Use exact consumer versions and restore the canonical browser runtime')
const probe = browser.steps[index + 1]
const probe = consumers.steps[index + 1]
assert(probe?.run === command && !Object.hasOwn(probe, 'if'), 'Run the installed consumer on the selected Node')
consumerIndex = index + 1
}
assert(browser.steps.findIndex(step => step.run?.startsWith('yarn test:browser:source ')) > consumerIndex, 'Browser tools must run after canonical Node restoration')
const reactIndex = browser.steps.findIndex(step => step.run === 'yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log')
assert(reactIndex > consumerIndex && !Object.hasOwn(browser.steps[reactIndex], 'if'), 'Run React installed consumers after restoring canonical Node')
assert(browser.steps.some(step => step.uses?.startsWith('actions/upload-artifact@') && step.if === 'always()' && step.with.path.split('\n').includes('refactor/.cache/react-consumer-*/')), 'Retain React consumer failure evidence')
const vueIndex = browser.steps.findIndex(step => step.run === 'yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log')
assert(vueIndex > consumerIndex && !Object.hasOwn(browser.steps[vueIndex], 'if'), 'Run Vue installed consumers after restoring canonical Node')
assert(browser.steps.some(step => step.uses?.startsWith('actions/upload-artifact@') && step.if === 'always()' && step.with.path.split('\n').includes('refactor/.cache/vue-consumer-*/')), 'Retain Vue consumer failure evidence')
const consumerUpload = consumers.steps.find(step => step.uses?.startsWith('actions/upload-artifact@'))
for (const [command, log, directory] of [
['test:react-consumer', 'react-consumer', 'react-consumer-*/'],
['test:vue-consumer', 'vue-consumer', 'vue-consumer-*/'],
['test:iframe-history', 'iframe-history', 'iframe-history/'],
['test:performance', 'performance', 'performance/'],
]) {
const index = consumers.steps.findIndex(step => step.run === `yarn ${command} 2>&1 | tee refactor/.cache/ci/${log}.log`)
assert(index > consumerIndex && !Object.hasOwn(consumers.steps[index], 'if'), 'Run complete consumers after restoring canonical Node')
assert(consumerUpload.with.path.split('\n').includes(`refactor/.cache/${directory}`), 'Retain consumer failure evidence')
assert(!browser.steps.some(step => step.run?.includes(`yarn ${command}`)), 'Do not repeat all-engine consumers in each playback leg')
consumerIndex = index
}
const extraIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package --browser '))
const sourceEngineIndex = browser.steps.findIndex(step => step.run === 'yarn test:browser:source 2>&1 | tee refactor/.cache/ci/browser-source.log')
const engineIndex = browser.steps.findIndex(step => step.run === 'yarn test:browser:installed 2>&1 | tee refactor/.cache/ci/browser-installed.log')
assert(extraIndex > Math.max(consumerIndex, reactIndex, vueIndex) && extraIndex < engineIndex, 'Prepare the shared installed browser roster after consumer probes and before browser checks')
const sourceEngineIndex = browser.steps.findIndex(step => step.run === 'yarn test:browser:source --project=${{ matrix.browser }} 2>&1 | tee refactor/.cache/ci/browser-source.log')
const engineIndex = browser.steps.findIndex(step => step.run === 'yarn test:browser:installed --project=${{ matrix.browser }} 2>&1 | tee refactor/.cache/ci/browser-installed.log')
assert(extraIndex > browser.steps.findIndex(step => step.run?.startsWith('yarn test:browser:install ')) && extraIndex < engineIndex, 'Prepare the shared installed browser roster in every engine job before browser checks')
assert(!Object.hasOwn(browser.steps[extraIndex], 'if') && browser.steps[extraIndex].run.includes('GITHUB_ENV') && browser.steps[extraIndex].run.includes('ARTPLAYER_BROWSER_ARTIFACTS='), 'Always select the additional installed browser artifact map')
assert(sourceEngineIndex > extraIndex && sourceEngineIndex < engineIndex, 'Retain complete source checks before the additional installed suite')
for (const index of [sourceEngineIndex, engineIndex]) {
@@ -101,6 +117,7 @@ export function validateCIWorkflow(source) {
assert(!browser.steps[index]['continue-on-error'], 'Browser scope failures must fail CI')
}
const browserUpload = browser.steps.find(step => step.uses?.startsWith('actions/upload-artifact@'))
assert(browserUpload.with.name.includes('matrix.browser'), 'Browser artifacts must be distinct across engines')
for (const directory of ['refactor/.cache/browser-source/', 'refactor/.cache/browser-installed/'])
assert(browserUpload.if === 'always()' && browserUpload.with.path.split('\n').includes(directory), 'Retain independent source and installed reports even on failure')
const pages = workflow.jobs.checks.steps.find(step => step.uses?.startsWith('actions/upload-pages-artifact@'))
@@ -116,7 +133,7 @@ export function validateCIWorkflow(source) {
assert(index > previous && index < workflow.jobs.checks.steps.indexOf(pages) && workflow.jobs.checks.steps[index].if === pages.if, 'Staged entrypoints require ordered browser installation and verification before upload')
previous = index
}
return { jobs: requiredJobs, systems, summary: summary.name }
return { jobs: requiredJobs, systems, engines, summary: summary.name }
}
export function validatePagesWorkflow(source) {
+37 -9
View File
@@ -9,6 +9,12 @@ import { validateCIWorkflow } from './ci-workflow.mjs'
const source = fs.readFileSync('.github/workflows/nodejs.yml', 'utf8')
test('CI workflow preserves complete matrix jobs, pinned download caches and an unconditional summary', () => {
assert.equal(validateCIWorkflow(source).summary, 'CI result')
const { jobs } = YAML.parse(source)
const matrix = jobs['browser-smoke'].strategy.matrix
const legs = matrix.os.flatMap(os => matrix.browser.map(browser => `${os}/${browser}`))
assert.deepEqual(legs, ['ubuntu-latest', 'windows-latest', 'macos-latest'].flatMap(os => ['chromium', 'firefox', 'webkit'].map(browser => `${os}/${browser}`)))
assert.equal(new Set(legs).size, 9)
assert.deepEqual(jobs['browser-consumers'].strategy.matrix, { os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] })
})
for (const [name, mutate] of [
@@ -19,6 +25,13 @@ for (const [name, mutate] of [
['untracked extra job', w => w.jobs.extra = w.jobs.coverage],
['matrix exclusion', w => w.jobs.checks.strategy.matrix.exclude = [{ os: 'windows-latest' }]],
['removed Windows build', w => w.jobs.checks.strategy.matrix.os.pop()],
['removed playback engine', w => w.jobs['browser-smoke'].strategy.matrix.browser.pop()],
['engine matrix exclusion', w => w.jobs['browser-smoke'].strategy.matrix.exclude = [{ os: 'macos-latest', browser: 'webkit' }]],
['consumer engine duplication', w => w.jobs['browser-consumers'].strategy.matrix.browser = ['chromium', 'firefox', 'webkit']],
['browser waits on consumer success', w => w.jobs['browser-smoke'].needs = 'browser-consumers'],
['consumers wait on browser success', w => w.jobs['browser-consumers'].needs = 'browser-smoke'],
['unbounded playback concurrency', w => delete w.jobs['browser-smoke'].strategy['max-parallel']],
['browser artifact collision', w => w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.name = 'browser-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}'],
['fail-fast cancellation', w => w.jobs.coverage.strategy['fail-fast'] = true],
['artifact name collision', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.name = 'checks'],
['write permissions', w => w.permissions.contents = 'write'],
@@ -31,8 +44,8 @@ for (const [name, mutate] of [
['conditional frozen install', w => w.jobs.checks.steps.find(s => s.run?.includes('yarn install --frozen-lockfile')).if = 'false'],
['commented frozen install', w => w.jobs.checks.steps.find(s => s.run?.includes('yarn install --frozen-lockfile')).run = '# yarn install --frozen-lockfile --non-interactive'],
['conditional Node setup', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/setup-node@')).if = 'false'],
['wrong consumer Node version', w => w.jobs['browser-smoke'].steps.find(s => s.id === 'consumer-node-20').with['node-version'] = '24'],
['skipped installed consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.includes('--expected-node 22.12.0')).if = 'false'],
['wrong consumer Node version', w => w.jobs['browser-consumers'].steps.find(s => s.id === 'consumer-node-20').with['node-version'] = '24'],
['skipped installed consumer', w => w.jobs['browser-consumers'].steps.find(s => s.run?.includes('--expected-node 22.12.0')).if = 'false'],
['missing browser plugin installation', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --browser')).run = 'echo omitted'],
['manual partial browser package roster', (w) => {
const step = w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --browser'))
@@ -41,20 +54,35 @@ for (const [name, mutate] of [
['skipped browser plugin installation', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --browser')).if = 'false'],
['missing source browser checks', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:source ')).run = 'echo omitted'],
['filtered source browser checks', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:source ')).run += ' --grep subset'],
['fixed engine silently repeated', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:source ')).run = 'yarn test:browser:source --project=chromium 2>&1 | tee refactor/.cache/ci/browser-source.log'],
['installed engine selector removed', (w) => {
const step = w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:installed '))
step.run = step.run.replace(' --project=${{ matrix.browser }}', '')
}],
['source failure prevents installed evidence', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:installed ')).if = 'success()'],
['ignored installed failure', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:installed '))['continue-on-error'] = true],
['missing separate source evidence', (w) => {
const upload = w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@'))
upload.with.path = upload.with.path.replace('refactor/.cache/browser-source/\n', '')
}],
['browser runtime not restored', w => w.jobs['browser-smoke'].steps.find(s => s.id === 'restore-canonical-node').with = { 'node-version': '22.12.0', 'package-manager-cache': false }],
['skipped React consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:react-consumer')).if = 'false'],
['removed React consumer', w => w.jobs['browser-smoke'].steps = w.jobs['browser-smoke'].steps.filter(s => !s.run?.startsWith('yarn test:react-consumer'))],
['missing React evidence', w => w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path = 'refactor/.cache/ci/'],
['skipped Vue consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:vue-consumer')).if = 'false'],
['removed Vue consumer', w => w.jobs['browser-smoke'].steps = w.jobs['browser-smoke'].steps.filter(s => !s.run?.startsWith('yarn test:vue-consumer'))],
['missing Vue evidence', w => w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path = w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path.replace('refactor/.cache/vue-consumer-*/', '')],
['browser runtime not restored', w => w.jobs['browser-consumers'].steps.find(s => s.id === 'restore-canonical-node').with = { 'node-version': '22.12.0', 'package-manager-cache': false }],
['skipped React consumer', w => w.jobs['browser-consumers'].steps.find(s => s.run?.startsWith('yarn test:react-consumer')).if = 'false'],
['removed React consumer', w => w.jobs['browser-consumers'].steps = w.jobs['browser-consumers'].steps.filter(s => !s.run?.startsWith('yarn test:react-consumer'))],
['missing React evidence', w => w.jobs['browser-consumers'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path = 'refactor/.cache/ci/'],
['skipped Vue consumer', w => w.jobs['browser-consumers'].steps.find(s => s.run?.startsWith('yarn test:vue-consumer')).if = 'false'],
['removed Vue consumer', w => w.jobs['browser-consumers'].steps = w.jobs['browser-consumers'].steps.filter(s => !s.run?.startsWith('yarn test:vue-consumer'))],
['missing Vue evidence', w => w.jobs['browser-consumers'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path = w.jobs['browser-consumers'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path.replace('refactor/.cache/vue-consumer-*/', '')],
['Pages uploaded by both matrix legs', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/upload-pages-artifact@')).if = 'inputs.pages-artifact && github.ref == \'refs/heads/master\''],
['missing iframe history gate', w => w.jobs['browser-consumers'].steps = w.jobs['browser-consumers'].steps.filter(s => !s.run?.startsWith('yarn test:iframe-history'))],
['performance narrowed to one engine', (w) => {
const step = w.jobs['browser-consumers'].steps.find(s => s.run?.startsWith('yarn test:performance'))
step.run = step.run.replace('test:performance', 'test:performance --project=chromium')
}],
['browser consumer cache-hit skips dependencies', w => w.jobs['browser-consumers'].steps.find(s => s.run?.startsWith('yarn test:browser:install')).if = 'false'],
['missing independent consumer artifact selection', (w) => {
const step = w.jobs['browser-consumers'].steps.find(s => s.run?.startsWith('yarn test:package '))
step.run = step.run.split('\n')[0]
}],
]) {
test(`CI rejects ${name}`, () => {
const workflow = YAML.parse(source)
+22 -7
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-template-curly-in-string -- GitHub matrix expressions are literal workflow policy. */
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
@@ -69,7 +70,7 @@ export function analyzeImpact(model, changedFiles) {
requiredChecks: model.policy.gates,
requiredCIJobs: [...new Set(model.policy.gates.map(gate => gate.job))].sort(),
installedConsumerGaps: affectedPackages.filter(name => name !== model.policy.site && !model.policy.installedConsumerPackages.includes(name)),
limitation: 'CI gates remain unconditional. This dependency report selects no tests for omission and certifies neither missing package suites nor device/vendor acceptance.',
limitation: 'CI jobs remain required; both browser scopes also run after ordinary step failures. This dependency report selects no tests for omission and certifies neither missing package suites nor device/vendor acceptance.',
}
}
@@ -105,9 +106,18 @@ export function readImpactModel(directory) {
const read = file => JSON.parse(fs.readFileSync(path.join(directory, file), 'utf8'))
const policy = read('refactor/impact-policy.json')
assert.equal(policy.schemaVersion, 1)
const gateCommands = policy.gates.map(gate => gate.command)
const gateCommands = policy.gates.map(gate => [gate.job, gate.command, ...(gate.arguments || [])].join(' '))
assert.equal(new Set(gateCommands).size, gateCommands.length, 'Duplicate impact gate')
for (const command of ['yarn ci:check', 'yarn ci:build', 'yarn test:package', 'yarn test:browser', 'yarn test:coverage', 'yarn test:performance'])
for (const command of [
'checks yarn ci:check',
'checks yarn ci:build',
'browser-consumers yarn test:package',
'browser-smoke yarn test:package --browser',
'browser-smoke yarn test:browser:source --project=${{ matrix.browser }}',
'browser-smoke yarn test:browser:installed --project=${{ matrix.browser }}',
'coverage yarn test:coverage',
'browser-consumers yarn test:performance',
])
assert(gateCommands.includes(command), `Missing mandatory ecosystem gate: ${command}`)
const names = policy.packages.map(pkg => pkg.name).sort()
assert.equal(new Set(names).size, names.length, 'Duplicate mapped package')
@@ -188,6 +198,10 @@ export function readImpactModel(directory) {
for (const gate of policy.gates) {
assert(/^yarn [a-z][\w:-]*$/.test(gate.command), 'Impact gates require an explicit repository script')
assert(Object.hasOwn(scripts, gate.command.slice(5)), `Missing required command: ${gate.command}`)
if (gate.arguments !== undefined)
assert(Array.isArray(gate.arguments) && gate.arguments.length === 1 && ['--browser', '--project=${{ matrix.browser }}'].includes(gate.arguments[0]), 'Impact gate arguments must use a reviewed complete browser scope')
if (gate.condition !== undefined)
assert(gate.condition === '${{ !cancelled() }}' && ['yarn test:browser:source', 'yarn test:browser:installed'].includes(gate.command), 'Only browser scopes may collect evidence after ordinary failures')
}
const consumerSource = ts.createSourceFile('package-consumer.mjs', fs.readFileSync(path.join(directory, 'scripts/package-consumer.mjs'), 'utf8'), ts.ScriptTarget.Latest, true)
const consumerNames = consumerSource.statements.filter(ts.isVariableStatement).flatMap(statement => [...statement.declarationList.declarations]).filter(node => ts.isIdentifier(node.name) && node.name.text === 'names')
@@ -213,17 +227,18 @@ export function validateImpactWorkflow(text, model) {
const job = workflow.jobs?.[gate.job]
assert(job && !Object.hasOwn(job, 'if') && !job['continue-on-error'], `Required impact job must not be conditional or allowed to fail: ${gate.job}`)
const steps = job.steps || []
const command = gate.command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const invocation = [gate.command, ...(gate.arguments || [])].join(' ')
const command = invocation.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
assert(steps.some((step) => {
if (Object.hasOwn(step, 'if') || step['continue-on-error'] || typeof step.run !== 'string')
if ((gate.condition ? step.if !== gate.condition : Object.hasOwn(step, 'if')) || step['continue-on-error'] || typeof step.run !== 'string')
return false
const shell = step.shell || job.defaults?.run?.shell || workflow.defaults?.run?.shell
const defaultBash = !shell && /^(?:ubuntu|macos)-/.test(job['runs-on'])
if (shell !== 'bash' && !defaultBash)
return false
const first = step.run.split(/\r?\n/).map(line => line.trim()).find(line => line && !line.startsWith('#'))
return first === gate.command || (shell === 'bash' && new RegExp(`^${command} 2>&1 \\| tee refactor/\\.cache/ci/[a-z-]+\\.log$`).test(first))
}), `Required unconditional command missing from ${gate.job}: ${gate.command}`)
return first === invocation || (shell === 'bash' && new RegExp(`^${command} 2>&1 \\| tee refactor/\\.cache/ci/[a-z-]+\\.log$`).test(first))
}), `Required command or execution condition missing from ${gate.job}: ${invocation}`)
assert(steps.some(step => step.uses?.startsWith('actions/checkout@') && !Object.hasOwn(step, 'if') && !step['continue-on-error'] && step.with?.['fetch-depth'] === 0), `Impact and compatibility checks require full history: ${gate.job}`)
}
}
+21
View File
@@ -94,6 +94,23 @@ test('Policy cannot omit core consumers or claim stale installed-package coverag
assert.throws(repo.model, /scope extraction/)
})
test('Each split CI invocation remains mandatory even when its sibling uses the same script', (t) => {
const repo = fixture(t)
const gates = structuredClone(repo.policy.gates)
for (let index = 0; index < gates.length; index++) {
repo.policy.gates = gates.filter((gate, candidate) => candidate !== index)
repo.write('refactor/impact-policy.json', repo.policy)
assert.throws(repo.model, /Missing mandatory ecosystem gate/)
}
repo.policy.gates = [...gates, gates[0]]
repo.write('refactor/impact-policy.json', repo.policy)
assert.throws(repo.model, /Duplicate impact gate/)
repo.policy.gates = structuredClone(gates)
repo.policy.gates.find(gate => gate.command === 'yarn test:browser:installed').condition = 'success()'
repo.write('refactor/impact-policy.json', repo.policy)
assert.throws(repo.model, /Only browser scopes/)
})
test('Shared/unknown files and computed imports conservatively expand; unsafe or unmapped package paths are rejected', (t) => {
for (const file of ['yarn.lock', 'scripts/new-build-stage.mjs', 'types/new-boundary.d.ts'])
assert.equal(analyzeImpact(actual, [file]).affectedPackages.length, 22)
@@ -127,6 +144,10 @@ test('CI workflow cannot silently skip or soften required impact gates', () => {
(workflow) => { workflow.jobs.checks.steps.find(step => step.run?.startsWith('yarn ci:check')).if = false },
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.uses?.startsWith('actions/checkout@')).if = false },
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.uses?.startsWith('actions/checkout@')).with['fetch-depth'] = 1 },
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.run?.startsWith('yarn test:browser:source ')).if = 'success()' },
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.run?.startsWith('yarn test:browser:installed ')).run += ' --grep subset' },
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.run?.startsWith('yarn test:package --browser')).run = 'yarn test:package' },
(workflow) => { workflow.jobs['browser-consumers'].steps.find(step => step.run?.startsWith('yarn test:performance ')).run += ' || true' },
]) {
const workflow = YAML.parse(source)
edit(workflow)
+3 -1
View File
@@ -776,7 +776,9 @@
"changes/2026-09-15-CI-01-jassub-installed.md",
"baselines/ci-jassub-installed-validation.json",
"changes/2026-09-15-CI-01-danmuku-installed.md",
"baselines/ci-danmuku-installed-validation.json"
"baselines/ci-danmuku-installed-validation.json",
"changes/2026-09-15-CI-01-browser-matrix.md",
"baselines/ci-browser-matrix-validation.json"
]
},
{
+11
View File
@@ -49,6 +49,17 @@ per-engine/file counts and limitations; this is not full ecosystem or remote CI.
Local `--grep`, file names, workers and project options are supported for diagnosis;
config/output/report overrides and passing with zero tests are rejected.
CI runs a complete OS × engine matrix (Linux, Windows, macOS; Chromium, Firefox,
WebKit). Each playback job prepares `test:package --browser`, then invokes both
scope launchers with only `--project=${{ matrix.browser }}`. Matrix jobs have
independent runners, artifact names include the engine, and the final `CI result`
requires every combination. Node/framework consumers, iframe history and
performance run once per OS in the separate `browser-consumers` job. This avoids
repeating their existing three-engine suites for every playback engine. No test
filter or retry policy changed. Remote timings remain unverified; collection
equivalence proves coverage selection, not playback success. See
`../../refactor/changes/2026-09-15-CI-01-browser-matrix.md`.
The six-package Ads expansion ran 426 cases across 13 files: 425 passed and one
WebKit candidate Chapter quality-readiness check failed. All 171 Ads cases passed,
including 108 with verified candidate tarballs; this is not a green full suite.
+1 -1
View File
@@ -4,7 +4,7 @@ import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { ciContext } from './ci-context.mjs'
export const requiredJobs = ['checks', 'coverage', 'browser-smoke']
export const requiredJobs = ['checks', 'coverage', 'browser-smoke', 'browser-consumers']
export function evaluateJobs(needs) {
const object = needs !== null && typeof needs === 'object' && !Array.isArray(needs)
+1
View File
@@ -10,6 +10,7 @@ import { evaluateJobs, renderSummary, requiredJobs } from '../scripts/ci-summary
const success = () => Object.fromEntries(requiredJobs.map(job => [job, { result: 'success' }]))
test('CI summary requires every configured group and does not mutate provider results', () => {
assert.deepEqual(requiredJobs, ['checks', 'coverage', 'browser-smoke', 'browser-consumers'])
const needs = success()
const copy = structuredClone(needs)
const report = evaluateJobs(needs)