From 4a9ef16b168eecc1f39b08290415d76a25852314 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Tue, 15 Sep 2026 04:18:18 +0800 Subject: [PATCH] ci: [CI-01] split playback engines from browser consumers --- .github/workflows/nodejs.yml | 98 ++- .../ci-browser-matrix-validation.json | 685 ++++++++++++++++++ .../2026-09-15-CI-01-browser-matrix.md | 72 ++ refactor/ci-setup.md | 30 +- refactor/impact-analysis.md | 15 +- refactor/impact-policy.json | 27 +- refactor/plan.md | 2 +- refactor/progress.md | 9 + refactor/scripts/ci-workflow.mjs | 53 +- refactor/scripts/ci-workflow.test.mjs | 46 +- refactor/scripts/impact-model.mjs | 29 +- refactor/scripts/impact.test.mjs | 21 + refactor/tasks.json | 4 +- scripts/browser-validation/README.md | 11 + scripts/ci-summary.mjs | 2 +- test/ci-summary.test.js | 1 + 16 files changed, 1031 insertions(+), 74 deletions(-) create mode 100644 refactor/baselines/ci-browser-matrix-validation.json create mode 100644 refactor/changes/2026-09-15-CI-01-browser-matrix.md diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index a4b822ceb..46397a4cc 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -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: diff --git a/refactor/baselines/ci-browser-matrix-validation.json b/refactor/baselines/ci-browser-matrix-validation.json new file mode 100644 index 000000000..6c8abb73b --- /dev/null +++ b/refactor/baselines/ci-browser-matrix-validation.json @@ -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." + ] +} diff --git a/refactor/changes/2026-09-15-CI-01-browser-matrix.md b/refactor/changes/2026-09-15-CI-01-browser-matrix.md new file mode 100644 index 000000000..97b0b3408 --- /dev/null +++ b/refactor/changes/2026-09-15-CI-01-browser-matrix.md @@ -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= 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---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 审计。 diff --git a/refactor/ci-setup.md b/refactor/ci-setup.md index ab3557060..cd4fb2a71 100644 --- a/refactor/ci-setup.md +++ b/refactor/ci-setup.md @@ -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 分钟预算 +尚未经过完整远端验证;保留完整用例和失败报告,后续依据各组合实测数据决定是否 +还需按文件分片。不能从本地子集通过推断整个工作流可在预算内完成。 diff --git a/refactor/impact-analysis.md b/refactor/impact-analysis.md index 7a456bb91..b59c02844 100644 --- a/refactor/impact-analysis.md +++ b/refactor/impact-analysis.md @@ -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、候选/报告及任务逐项连接; diff --git a/refactor/impact-policy.json b/refactor/impact-policy.json index 70a3623ea..552ebe196 100644 --- a/refactor/impact-policy.json +++ b/refactor/impact-policy.json @@ -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" }, diff --git a/refactor/plan.md b/refactor/plan.md index 69979984a..98f5104e5 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -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) diff --git a/refactor/progress.md b/refactor/progress.md index fb3e1fdd5..dddfd6a42 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -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 分钟, diff --git a/refactor/scripts/ci-workflow.mjs b/refactor/scripts/ci-workflow.mjs index 45df61d73..0c52141c2 100644 --- a/refactor/scripts/ci-workflow.mjs +++ b/refactor/scripts/ci-workflow.mjs @@ -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) { diff --git a/refactor/scripts/ci-workflow.test.mjs b/refactor/scripts/ci-workflow.test.mjs index 1a14069fd..d1fa62b74 100644 --- a/refactor/scripts/ci-workflow.test.mjs +++ b/refactor/scripts/ci-workflow.test.mjs @@ -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) diff --git a/refactor/scripts/impact-model.mjs b/refactor/scripts/impact-model.mjs index e1affe429..3b318fcb0 100644 --- a/refactor/scripts/impact-model.mjs +++ b/refactor/scripts/impact-model.mjs @@ -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}`) } } diff --git a/refactor/scripts/impact.test.mjs b/refactor/scripts/impact.test.mjs index 28b4aee3a..bd2250539 100644 --- a/refactor/scripts/impact.test.mjs +++ b/refactor/scripts/impact.test.mjs @@ -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) diff --git a/refactor/tasks.json b/refactor/tasks.json index 5a5761877..c2d97e8df 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -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" ] }, { diff --git a/scripts/browser-validation/README.md b/scripts/browser-validation/README.md index 7de6e2940..f6833e812 100644 --- a/scripts/browser-validation/README.md +++ b/scripts/browser-validation/README.md @@ -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. diff --git a/scripts/ci-summary.mjs b/scripts/ci-summary.mjs index ef082eaf5..524069d9b 100644 --- a/scripts/ci-summary.mjs +++ b/scripts/ci-summary.mjs @@ -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) diff --git a/test/ci-summary.test.js b/test/ci-summary.test.js index f6c8bf63a..5dd701b53 100644 --- a/test/ci-summary.test.js +++ b/test/ci-summary.test.js @@ -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)