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 include-hidden-files: true
retention-days: 14 retention-days: 14
if-no-files-found: warn if-no-files-found: warn
browser-smoke: browser-consumers:
name: Browser playback (${{ matrix.os }}) name: Browser consumers (${{ matrix.os }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 60 timeout-minutes: 60
env: env:
@@ -135,19 +135,6 @@ jobs:
run: yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log run: yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log
- name: Verify Vue installed consumers - name: Verify Vue installed consumers
run: yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log 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 - name: Verify iframe cached history and interrupted navigation
run: yarn test:iframe-history 2>&1 | tee refactor/.cache/ci/iframe-history.log run: yarn test:iframe-history 2>&1 | tee refactor/.cache/ci/iframe-history.log
- name: Compare installed performance and verify resource cleanup - name: Compare installed performance and verify resource cleanup
@@ -156,12 +143,9 @@ jobs:
if: always() if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: browser-smoke-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }} name: browser-consumers-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
path: | path: |
refactor/.cache/ci/ refactor/.cache/ci/
refactor/.cache/browser/
refactor/.cache/browser-source/
refactor/.cache/browser-installed/
refactor/.cache/iframe-history/ refactor/.cache/iframe-history/
refactor/.cache/react-consumer-*/ refactor/.cache/react-consumer-*/
refactor/.cache/vue-consumer-*/ refactor/.cache/vue-consumer-*/
@@ -185,6 +169,81 @@ jobs:
- ubuntu-latest - ubuntu-latest
- windows-latest - windows-latest
- macos-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: checks:
name: Checks and build (${{ matrix.os }}) name: Checks and build (${{ matrix.os }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -279,6 +338,7 @@ jobs:
- checks - checks
- coverage - coverage
- browser-smoke - browser-smoke
- browser-consumers
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
steps: 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`。两步不 `scripts/browser-validation/scope.ts`。两步不
吞失败,普通失败后仍保存另一范围的证据,分别上传browser-source/browser-installed。 吞失败,普通失败后仍保存另一范围的证据,分别上传browser-source/browser-installed。
入口与报告规则见[维护说明](../scripts/browser-validation/README.md)。source全量 入口与报告规则见[维护说明](../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 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 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 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:unit` | 原播放/DASH 回归、同夹具的新旧公共契约与 JS/TS loader 验证 |
| `yarn test:node` | test:unit 加工具链/文档构建回归,保留原 test:playback/test:dash-control 入口 | | `yarn test:node` | test:unit 加工具链/文档构建回归,保留原 test:playback/test:dash-control 入口 |
| `yarn test` | 统一执行 Node 与基线测试,源码/夹具维护入口见仓库 test/README.md | | `yarn test` | 统一执行 Node 与基线测试,源码/夹具维护入口见仓库 test/README.md |
@@ -38,7 +39,7 @@
| `yarn check:contracts --report` | 校验12类契约/22包归属、版本及报告对应;--write更新静态表,详见 [维护说明](contract-coverage.md) | | `yarn check:contracts --report` | 校验12类契约/22包归属、版本及报告对应;--write更新静态表,详见 [维护说明](contract-coverage.md) |
| `yarn ci:check` | 严格 Node/Yarn/锁检查、计划、只读 lint、类型、Node 和基线测试;允许写忽略缓存,不修改源码 | | `yarn ci:check` | 严格 Node/Yarn/锁检查、计划、只读 lint、类型、Node 和基线测试;允许写忽略缓存,不修改源码 |
| `yarn check:ci` | 只读校验实际工作流的完整系统矩阵、安装、缓存、报告和最终检查;已接入 ci:check | | `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 test:package:runtime` | 标准 Node 重装同一已检查 tarball;须先运行 test:package,其他 Node 使用显式 --expected-node |
| `yarn ci:build` | 21 库包、i18n、编辑器声明、文档 readiness smoke 和文档站构建,以及构建后包导入 smoke;会生成 dist 和 docs 内容 | | `yarn ci:build` | 21 库包、i18n、编辑器声明、文档 readiness smoke 和文档站构建,以及构建后包导入 smoke;会生成 dist 和 docs 内容 |
| `yarn check:impact --report` | 读取实际依赖/验证关系和Git变更,核对workflow必需命令,写CI影响报告;已接入ci:check,见[影响映射](impact-analysis.md) | | `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 分钟 | | checks | Linux、Windows | ci:check、ci:build;Linux 额外执行 actionlint | 45 分钟 |
| coverage | Linux、Windows | 既有源码映射与生命周期覆盖检查 | 15 分钟 | | coverage | Linux、Windows | 既有源码映射与生命周期覆盖检查 | 15 分钟 |
| browser-smoke | Linux、Windows、macOS | 同一 core/chapter tarball 的 Node 20/22/24 消费,三浏览器、iframe history、性能 | 60 分钟 | | browser-smoke | Linux、Windows、macOS × Chromium、Firefox、WebKit | 每个引擎独立打包/冻结安装完整 browser 包清单,再执行该引擎全部 source 与已列出的 installed 用例;9 个组合,最多同时运行 6 个 | 每个组合 60 分钟 |
| CI result | Linux | 汇总以上三个作业组,所有结果必须为 success | 5 分钟 | | 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、 构建与浏览器工具使用 .node-version 的 24.21.0。安装运行时消费分别使用固定 20.19.0、
22.12.0 和标准 Node,不重新构建候选。它们是根工具 engines 的边界测试点,发布包没有 22.12.0 和标准 Node,不重新构建候选。它们是根工具 engines 的边界测试点,发布包没有
@@ -68,12 +70,20 @@ Actions 固定完整 SHA,Node 来自 .node-version,Yarn 固定 1.22.22;安
矩阵不设置 fail-fast,单个系统失败后仍尽量收集其他系统证据;显式 bash 保留 tee 矩阵不设置 fail-fast,单个系统失败后仍尽量收集其他系统证据;显式 bash 保留 tee
上游命令的失败退出码。影响报告继续扩大核心/共享变更到全生态,当前不缩减必需作业。 上游命令的失败退出码。影响报告继续扩大核心/共享变更到全生态,当前不缩减必需作业。
稳定名称为 **CI result**。它以 always() 依赖 checks、coverage、browser-smoke,结构化 稳定名称为 **CI result**。它以 always() 依赖 checks、coverage、browser-smoke、browser-consumers,结构化
读取 needs;失败、取消、跳过、缺失、错误 JSON 或未知额外作业都失败。增加独立 job 时 读取 needs;失败、取消、跳过、缺失、错误 JSON 或未知额外作业都失败。增加独立 job 时
同时修改 ci-summary.mjs 的 requiredJobs、workflow needs、矩阵策略和测试,防止遗漏汇总。 同时修改 ci-summary.mjs 的 requiredJobs、workflow needs、矩阵策略和测试,防止遗漏汇总。
全局取消/runner 故障下的真实调度仍待 CI-04;本地退出码测试不证明远端作业一定被调度。 全局取消/runner 故障下的真实调度仍待 CI-04;本地退出码测试不证明远端作业一定被调度。
required checks 的实际绑定尚未设置,不能宣称分支保护已经生效。 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 读取 scripts/ci-context.mjs 从真实 checkout、package.json、.node-version 和 yarn.lock 读取
@@ -172,7 +182,7 @@ build:ts 生成声明 URL 清单后重建站点浏览器产物,check:editor-ty
此选项不表示全生态类型、SDK 或发布准入。 此选项不表示全生态类型、SDK 或发布准入。
十八包安装清单的弹幕/Mask 子集实跑 399 项耗时 22.5 分钟,见 十八包安装清单的弹幕/Mask 子集实跑 399 项耗时 22.5 分钟,见
[CI-01 记录](changes/2026-09-15-CI-01-danmuku-installed.md)。当前单个 [CI-01 记录](changes/2026-09-15-CI-01-danmuku-installed.md)。随后完成上述引擎与
browser-smoke 仍串行运行全部 source/installed 和其他消费者/性能检查, 消费者拆分,见[拆分记录](changes/2026-09-15-CI-01-browser-matrix.md)。60 分钟预算
60 分钟预算尚未经过完整远端验证;后续必须分片并核定时长,保留完整用例 尚未经过完整远端验证;保留完整用例和失败报告,后续依据各组合实测数据决定是否
和失败报告。不能从本地子集通过推断整个工作流可在预算内完成。 还需按文件分片。不能从本地子集通过推断整个工作流可在预算内完成。
+10 -5
View File
@@ -40,12 +40,15 @@ GitHub PR读取事件里的base SHA,push读取before SHA;初次push的全零
## CI门槛与能力边界 ## CI门槛与能力边界
六项必需命令不能从策略中漏掉:`ci:check`、`ci:build`、`test:package`、 八项必需调用不能从策略中漏掉:`ci:check`、`ci:build`、消费者 `test:package`、
`test:browser`、`test:coverage`、`test:performance`。固定yaml2.8.2解析实际workflow, 播放作业 `test:package --browser`、`test:browser:source`、`test:browser:installed`、
检查对应job/步骤无条件执行、没有continue-on-error、没有路径过滤,且checkout有 `test:coverage`、`test:performance`。两个浏览器范围只传当前矩阵的 `--project`,
完整历史。命令被移走、改成echo或增加条件会失败。命令名还必须存在于根scripts。 并使用固定 `!cancelled()` 条件在普通失败后继续采证;其余门槛保持无条件步骤。
固定 yaml 2.8.2 解析实际 workflow,检查对应 job 必需执行、没有 continue-on-error、
没有路径过滤,且 checkout 有完整历史。命令被移走、改成 echo 或更换执行条件会
失败。命令名还必须存在于根 scripts;策略单独记录已审查的参数和条件。
必需命令必须是步骤首个非空、非注释行,只接受独立命令或当前显式bash的固定tee日志 必需命令必须是步骤首个非空、非注释行,只接受独立命令或当前显式bash的固定tee日志
格式;其他shell包装语法需审查后扩展。条件包装、`|| true`、非bash的tee和YAML布尔 格式;其他shell包装语法需审查后扩展。任意条件包装、`|| true`、非bash的tee和YAML布尔
`if: false`均有拒绝反例;checkout本身也不能带条件或允许失败。这是受限结构校验, `if: false`均有拒绝反例;checkout本身也不能带条件或允许失败。这是受限结构校验,
不是通用shell解释器,也不证明脚本内部的测试语义。 不是通用shell解释器,也不证明脚本内部的测试语义。
@@ -53,6 +56,8 @@ GitHub PR读取事件里的base SHA,push读取before SHA;初次push的全零
- `test:package`当前实际只有core/chapter;脚本读取其names数组核对策略,变更范围 - `test:package`当前实际只有core/chapter;脚本读取其names数组核对策略,变更范围
后必须更新声明。其余19个库包完整安装消费仍缺证据,不能因识别为affected而过关。 后必须更新声明。其余19个库包完整安装消费仍缺证据,不能因识别为affected而过关。
`--browser` 已单独覆盖十八包安装产物及明确列出的浏览器子集,不扩张这个通用
Node/类型消费者列表,也不等于十八包全部类型/设备/SDK 组合通过。
- 覆盖率范围从实际coverage-policy读取;浏览器/性能命令运行已有用例,不代表所有 - 覆盖率范围从实际coverage-policy读取;浏览器/性能命令运行已有用例,不代表所有
包、真实设备、远程SDK已验收。 包、真实设备、远程SDK已验收。
- ENG-COVERAGE-01继续把契约、支持版本、测试ID、候选/报告及任务逐项连接; - 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" "scope": "All library builds, i18n, editor declarations, docs site and existing import suites"
}, },
{ {
"job": "browser-smoke", "job": "browser-consumers",
"command": "yarn test:package", "command": "yarn test:package",
"scope": "Installed core/chapter consumers only; other package installation acceptance remains pending" "scope": "Installed core/chapter consumers only; other package installation acceptance remains pending"
}, },
{ {
"job": "browser-smoke", "job": "browser-smoke",
"command": "yarn test:browser", "command": "yarn test:package",
"scope": "All existing three-engine browser suites; does not imply every package/device has a suite" "arguments": [
"--browser"
],
"scope": "Prepare the complete reviewed installed browser roster independently for each OS/engine leg"
}, },
{ {
"job": "browser-smoke", "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", "command": "yarn test:performance",
"scope": "Existing installed performance and resource-cleanup scenarios" "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-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-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) - 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) - 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) - 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) - 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 安装与原生负载矩阵 ## CI-01 弹幕 / Mask 安装与原生负载矩阵
十八包打包/隔离安装通过;十三文件、三引擎 399/399 断言通过,22.5 分钟, 十八包打包/隔离安装通过;十三文件、三引擎 399/399 断言通过,22.5 分钟,
+35 -18
View File
@@ -11,7 +11,9 @@ const systems = {
'checks': ['ubuntu-latest', 'windows-latest'], 'checks': ['ubuntu-latest', 'windows-latest'],
'coverage': ['ubuntu-latest', 'windows-latest'], 'coverage': ['ubuntu-latest', 'windows-latest'],
'browser-smoke': ['ubuntu-latest', 'windows-latest', 'macos-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) { export function validateCIWorkflow(source) {
const workflow = YAML.parse(source, { uniqueKeys: true }) const workflow = YAML.parse(source, { uniqueKeys: true })
@@ -46,14 +48,14 @@ export function validateCIWorkflow(source) {
continue continue
assert(!Object.hasOwn(job, 'if'), 'Required matrix jobs must not be skipped') assert(!Object.hasOwn(job, 'if'), 'Required matrix jobs must not be skipped')
assert.equal(job['runs-on'], '${{ matrix.os }}') 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.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.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') 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')) 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') assert(contextIndex >= 0, 'Cache inputs must come from the checked source')
const caches = job.steps.filter(step => step.uses?.startsWith('actions/cache@')) 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) { for (const cache of caches) {
assert(job.steps.indexOf(cache) > contextIndex, 'Record context before restoring 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') 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') 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'] 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') const consumers = workflow.jobs['browser-consumers']
let consumerIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package')) 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 [ 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-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'], ['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'], ['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') 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(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') 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') assert(probe?.run === command && !Object.hasOwn(probe, 'if'), 'Run the installed consumer on the selected Node')
consumerIndex = index + 1 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 consumerUpload = consumers.steps.find(step => step.uses?.startsWith('actions/upload-artifact@'))
const reactIndex = browser.steps.findIndex(step => step.run === 'yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log') for (const [command, log, directory] of [
assert(reactIndex > consumerIndex && !Object.hasOwn(browser.steps[reactIndex], 'if'), 'Run React installed consumers after restoring canonical Node') ['test:react-consumer', 'react-consumer', 'react-consumer-*/'],
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') ['test:vue-consumer', 'vue-consumer', 'vue-consumer-*/'],
const vueIndex = browser.steps.findIndex(step => step.run === 'yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log') ['test:iframe-history', 'iframe-history', 'iframe-history/'],
assert(vueIndex > consumerIndex && !Object.hasOwn(browser.steps[vueIndex], 'if'), 'Run Vue installed consumers after restoring canonical Node') ['test:performance', 'performance', 'performance/'],
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 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 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 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 2>&1 | tee refactor/.cache/ci/browser-installed.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 > Math.max(consumerIndex, reactIndex, vueIndex) && extraIndex < engineIndex, 'Prepare the shared installed browser roster after consumer probes and before browser checks') 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(!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') assert(sourceEngineIndex > extraIndex && sourceEngineIndex < engineIndex, 'Retain complete source checks before the additional installed suite')
for (const index of [sourceEngineIndex, engineIndex]) { 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') 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@')) 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/']) 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') 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@')) 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') 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 previous = index
} }
return { jobs: requiredJobs, systems, summary: summary.name } return { jobs: requiredJobs, systems, engines, summary: summary.name }
} }
export function validatePagesWorkflow(source) { 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') const source = fs.readFileSync('.github/workflows/nodejs.yml', 'utf8')
test('CI workflow preserves complete matrix jobs, pinned download caches and an unconditional summary', () => { test('CI workflow preserves complete matrix jobs, pinned download caches and an unconditional summary', () => {
assert.equal(validateCIWorkflow(source).summary, 'CI result') 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 [ for (const [name, mutate] of [
@@ -19,6 +25,13 @@ for (const [name, mutate] of [
['untracked extra job', w => w.jobs.extra = w.jobs.coverage], ['untracked extra job', w => w.jobs.extra = w.jobs.coverage],
['matrix exclusion', w => w.jobs.checks.strategy.matrix.exclude = [{ os: 'windows-latest' }]], ['matrix exclusion', w => w.jobs.checks.strategy.matrix.exclude = [{ os: 'windows-latest' }]],
['removed Windows build', w => w.jobs.checks.strategy.matrix.os.pop()], ['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], ['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'], ['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'], ['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'], ['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'], ['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'], ['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'], ['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-smoke'].steps.find(s => s.run?.includes('--expected-node 22.12.0')).if = 'false'], ['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'], ['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) => { ['manual partial browser package roster', (w) => {
const step = w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:package --browser')) 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'], ['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'], ['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'], ['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()'], ['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], ['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) => { ['missing separate source evidence', (w) => {
const upload = w.jobs['browser-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')) 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', '') 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 }], ['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-smoke'].steps.find(s => s.run?.startsWith('yarn test:react-consumer')).if = '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-smoke'].steps = w.jobs['browser-smoke'].steps.filter(s => !s.run?.startsWith('yarn test:react-consumer'))], ['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-smoke'].steps.find(s => s.uses?.startsWith('actions/upload-artifact@')).with.path = 'refactor/.cache/ci/'], ['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-smoke'].steps.find(s => s.run?.startsWith('yarn test:vue-consumer')).if = 'false'], ['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-smoke'].steps = w.jobs['browser-smoke'].steps.filter(s => !s.run?.startsWith('yarn test:vue-consumer'))], ['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-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-*/', '')], ['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\''], ['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}`, () => { test(`CI rejects ${name}`, () => {
const workflow = YAML.parse(source) 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 assert from 'node:assert/strict'
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
@@ -69,7 +70,7 @@ export function analyzeImpact(model, changedFiles) {
requiredChecks: model.policy.gates, requiredChecks: model.policy.gates,
requiredCIJobs: [...new Set(model.policy.gates.map(gate => gate.job))].sort(), requiredCIJobs: [...new Set(model.policy.gates.map(gate => gate.job))].sort(),
installedConsumerGaps: affectedPackages.filter(name => name !== model.policy.site && !model.policy.installedConsumerPackages.includes(name)), 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 read = file => JSON.parse(fs.readFileSync(path.join(directory, file), 'utf8'))
const policy = read('refactor/impact-policy.json') const policy = read('refactor/impact-policy.json')
assert.equal(policy.schemaVersion, 1) 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') 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}`) assert(gateCommands.includes(command), `Missing mandatory ecosystem gate: ${command}`)
const names = policy.packages.map(pkg => pkg.name).sort() const names = policy.packages.map(pkg => pkg.name).sort()
assert.equal(new Set(names).size, names.length, 'Duplicate mapped package') 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) { for (const gate of policy.gates) {
assert(/^yarn [a-z][\w:-]*$/.test(gate.command), 'Impact gates require an explicit repository script') 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}`) 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 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') 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] 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}`) 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 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) => { 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 return false
const shell = step.shell || job.defaults?.run?.shell || workflow.defaults?.run?.shell const shell = step.shell || job.defaults?.run?.shell || workflow.defaults?.run?.shell
const defaultBash = !shell && /^(?:ubuntu|macos)-/.test(job['runs-on']) const defaultBash = !shell && /^(?:ubuntu|macos)-/.test(job['runs-on'])
if (shell !== 'bash' && !defaultBash) if (shell !== 'bash' && !defaultBash)
return false return false
const first = step.run.split(/\r?\n/).map(line => line.trim()).find(line => line && !line.startsWith('#')) 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)) return first === invocation || (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}`) }), `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}`) 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/) 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) => { 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']) for (const file of ['yarn.lock', 'scripts/new-build-stage.mjs', 'types/new-boundary.d.ts'])
assert.equal(analyzeImpact(actual, [file]).affectedPackages.length, 22) 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.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@')).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.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) const workflow = YAML.parse(source)
edit(workflow) edit(workflow)
+3 -1
View File
@@ -776,7 +776,9 @@
"changes/2026-09-15-CI-01-jassub-installed.md", "changes/2026-09-15-CI-01-jassub-installed.md",
"baselines/ci-jassub-installed-validation.json", "baselines/ci-jassub-installed-validation.json",
"changes/2026-09-15-CI-01-danmuku-installed.md", "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; Local `--grep`, file names, workers and project options are supported for diagnosis;
config/output/report overrides and passing with zero tests are rejected. 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 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, 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. 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 { fileURLToPath } from 'node:url'
import { ciContext } from './ci-context.mjs' 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) { export function evaluateJobs(needs) {
const object = needs !== null && typeof needs === 'object' && !Array.isArray(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' }])) const success = () => Object.fromEntries(requiredJobs.map(job => [job, { result: 'success' }]))
test('CI summary requires every configured group and does not mutate provider results', () => { 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 needs = success()
const copy = structuredClone(needs) const copy = structuredClone(needs)
const report = evaluateJobs(needs) const report = evaluateJobs(needs)