mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-15 17:16:16 -08:00
ci: [CI-01] split playback engines from browser consumers
This commit is contained in:
1 parent
92b04d75c0
commit
4a9ef16b16
16 files changed
+1031
-74
No files matched your search
@@ -69,8 +69,8 @@ jobs:
|
||||
include-hidden-files: true
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
browser-smoke:
|
||||
name: Browser playback (${{ matrix.os }})
|
||||
browser-consumers:
|
||||
name: Browser consumers (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -135,19 +135,6 @@ jobs:
|
||||
run: yarn test:react-consumer 2>&1 | tee refactor/.cache/ci/react-consumer.log
|
||||
- name: Verify Vue installed consumers
|
||||
run: yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log
|
||||
- name: Prepare installed browser plugin packages
|
||||
# Node/framework consumers above retain their two-package fixture scope.
|
||||
# These extra installed packages are required by the browser helpers.
|
||||
# Other ecosystem installation/SDK gates remain tracked by CI-01.
|
||||
run: |
|
||||
yarn test:package --browser 2>&1 | tee refactor/.cache/ci/browser-packages.log
|
||||
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
|
||||
- name: Run all source browser checks
|
||||
if: ${{ !cancelled() }}
|
||||
run: yarn test:browser:source 2>&1 | tee refactor/.cache/ci/browser-source.log
|
||||
- name: Run installed browser checks
|
||||
if: ${{ !cancelled() }}
|
||||
run: yarn test:browser:installed 2>&1 | tee refactor/.cache/ci/browser-installed.log
|
||||
- name: Verify iframe cached history and interrupted navigation
|
||||
run: yarn test:iframe-history 2>&1 | tee refactor/.cache/ci/iframe-history.log
|
||||
- name: Compare installed performance and verify resource cleanup
|
||||
@@ -156,12 +143,9 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: browser-smoke-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
name: browser-consumers-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
refactor/.cache/ci/
|
||||
refactor/.cache/browser/
|
||||
refactor/.cache/browser-source/
|
||||
refactor/.cache/browser-installed/
|
||||
refactor/.cache/iframe-history/
|
||||
refactor/.cache/react-consumer-*/
|
||||
refactor/.cache/vue-consumer-*/
|
||||
@@ -185,6 +169,81 @@ jobs:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
browser-smoke:
|
||||
name: Browser playback (${{ matrix.os }}, ${{ matrix.browser }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: true
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
package-manager-cache: false
|
||||
- name: Record source and cache inputs
|
||||
id: context
|
||||
run: node scripts/ci-context.mjs
|
||||
- name: Cache locked Yarn downloads
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 ref verified 2026-09-13; node24
|
||||
with:
|
||||
path: ${{ steps.context.outputs.yarn_cache }}
|
||||
key: yarn-${{ runner.os }}-${{ runner.arch }}-node-${{ steps.context.outputs.node }}-yarn-${{ steps.context.outputs.yarn }}-${{ github.ref }}-${{ hashFiles('yarn.lock') }}
|
||||
- name: Install the pinned package manager and dependencies
|
||||
run: |
|
||||
npm install --global --force yarn@1.22.22 2>&1 | tee refactor/.cache/ci/toolchain-install.log
|
||||
yarn install --frozen-lockfile --non-interactive 2>&1 | tee refactor/.cache/ci/install.log
|
||||
- name: Cache versioned browser downloads
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 ref verified 2026-09-13; node24
|
||||
with:
|
||||
path: ${{ steps.context.outputs.browser_cache }}
|
||||
key: playwright-${{ runner.os }}-${{ runner.arch }}-node-${{ steps.context.outputs.node }}-yarn-${{ steps.context.outputs.yarn }}-${{ github.ref }}-${{ hashFiles('yarn.lock') }}-${{ steps.context.outputs.playwright }}
|
||||
- name: Install test browsers
|
||||
run: yarn test:browser:install --with-deps 2>&1 | tee refactor/.cache/ci/browser-install.log
|
||||
- name: Prepare installed browser plugin packages
|
||||
# Each engine builds and freezes its own complete installed browser roster.
|
||||
# Other ecosystem installation/SDK gates remain tracked by CI-01.
|
||||
run: |
|
||||
yarn test:package --browser 2>&1 | tee refactor/.cache/ci/browser-packages.log
|
||||
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
|
||||
- name: Run all source browser checks
|
||||
if: ${{ !cancelled() }}
|
||||
run: yarn test:browser:source --project=${{ matrix.browser }} 2>&1 | tee refactor/.cache/ci/browser-source.log
|
||||
- name: Run installed browser checks
|
||||
if: ${{ !cancelled() }}
|
||||
run: yarn test:browser:installed --project=${{ matrix.browser }} 2>&1 | tee refactor/.cache/ci/browser-installed.log
|
||||
- name: Upload browser evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: browser-smoke-${{ matrix.os }}-${{ matrix.browser }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
refactor/.cache/ci/
|
||||
refactor/.cache/browser-source/
|
||||
refactor/.cache/browser-installed/
|
||||
refactor/.cache/packages/latest.json
|
||||
refactor/.cache/packages/run-*/*.json
|
||||
refactor/.cache/packages/run-*/*.log
|
||||
refactor/.cache/packages/run-*/*.tgz
|
||||
refactor/.cache/packages/run-*/artifacts/
|
||||
include-hidden-files: true
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 6
|
||||
matrix:
|
||||
browser: [chromium, firefox, webkit]
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
checks:
|
||||
name: Checks and build (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -279,6 +338,7 @@ jobs:
|
||||
- checks
|
||||
- coverage
|
||||
- browser-smoke
|
||||
- browser-consumers
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,685 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "CI-01",
|
||||
"date": "2026-09-15",
|
||||
"sourceHead": "92b04d75c0291d3639b581288d846352c7bbd9f4",
|
||||
"candidate": "Working tree included in the accompanying CI-01 browser matrix checkpoint; code fingerprints normalize LF",
|
||||
"environment": {
|
||||
"node": "24.21.0",
|
||||
"yarn": "1.22.22",
|
||||
"platform": "win32",
|
||||
"actionlint": "1.7.12"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"file": ".github/workflows/nodejs.yml",
|
||||
"sha256LF": "726b7f1f9126b2453123f8537ca4ac466333795b6673b4e7d93aac5e448fa9d0"
|
||||
},
|
||||
{
|
||||
"file": "scripts/ci-summary.mjs",
|
||||
"sha256LF": "993c99d9799bae72f1d97097bea4521c3964db365930a8f166719410f0578f05"
|
||||
},
|
||||
{
|
||||
"file": "test/ci-summary.test.js",
|
||||
"sha256LF": "c7fecee2e4f514bf3cd67ace03485856daa177ab51bee86a11e777577bb47eb9"
|
||||
},
|
||||
{
|
||||
"file": "refactor/scripts/ci-workflow.mjs",
|
||||
"sha256LF": "66353eb9531ae3bd9555b42668ba318d8540c353370cb2ac3f25c62373bf99a4"
|
||||
},
|
||||
{
|
||||
"file": "refactor/scripts/ci-workflow.test.mjs",
|
||||
"sha256LF": "154a65f97ae736667e107dff685fc055ce3e2da5e11f4ac31debd81435e1875f"
|
||||
},
|
||||
{
|
||||
"file": "refactor/scripts/impact-model.mjs",
|
||||
"sha256LF": "d243062448803b245a83a17518a6ef1024bf1b24be63c66f047cc87464e437e9"
|
||||
},
|
||||
{
|
||||
"file": "refactor/scripts/impact.test.mjs",
|
||||
"sha256LF": "5e113a7c733f9642633b082c80175d951e9278859ae966f8a300ef626581edfb"
|
||||
},
|
||||
{
|
||||
"file": "refactor/impact-policy.json",
|
||||
"sha256LF": "e4ab08484dff4bfdc2fcbd89f990a8eb7607e70d58982e121858cb4a1b001f86"
|
||||
},
|
||||
{
|
||||
"file": "scripts/browser-check.mjs",
|
||||
"sha256LF": "4c1dbfb6dc05cf4842ce6568181dcc76e076003ee268e5fe00aff0a771010811"
|
||||
},
|
||||
{
|
||||
"file": "scripts/browser-validation/scope.ts",
|
||||
"sha256LF": "3b0526de3f571bb95c429ff3785277771576bb077924699b04dafaaa886f047d"
|
||||
},
|
||||
{
|
||||
"file": "playwright.config.js",
|
||||
"sha256LF": "df2384f5626252ec933725867e400ec22eb92bc5e6f04873059b32decf2f2d56"
|
||||
},
|
||||
{
|
||||
"file": "playwright.source.config.js",
|
||||
"sha256LF": "00bebb9253de62b827c2e8734b6b54a6a23c56802eafc5d3996b241b5fe82092"
|
||||
},
|
||||
{
|
||||
"file": "playwright.installed.config.js",
|
||||
"sha256LF": "e158a28a91dea09dc5af17cc6f4b204020a6c1f7042c3ba0ecd90677ded0167f"
|
||||
}
|
||||
],
|
||||
"collection": [
|
||||
{
|
||||
"scope": "source",
|
||||
"exactUnion": true,
|
||||
"noOverlap": true,
|
||||
"cases": 4641,
|
||||
"files": 154,
|
||||
"records": [
|
||||
{
|
||||
"engine": "all",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-source-all-collection",
|
||||
"reportSha256": "b2e710d6e934c4b580595c09b6ff4eb54358099c85cee486d618385644dd69ea",
|
||||
"invocationSha256": "9b9ebc452ba4886f9398b5f5c1916eae5294b62bc6afc8fdfbae5e252ceb3fab",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.source.config.js",
|
||||
"--list"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"cases": 4641,
|
||||
"files": 154,
|
||||
"caseSetSha256": "fb2ab31fc294ffd38f53486c854465adf6848f7157f4f69301640c4c1e10cf8c",
|
||||
"collectionOnly": true
|
||||
},
|
||||
{
|
||||
"engine": "chromium",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-source-chromium-collection",
|
||||
"reportSha256": "d46893ffcec98aad841c2b8afd3a4aab806aa3c62a973230559fe7a4d551499d",
|
||||
"invocationSha256": "dca7b9af6fc6f8a9a8b1a098fe927845efa6c01f3ba8c544eab9d76011bfc187",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.source.config.js",
|
||||
"--list",
|
||||
"--project=chromium"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"cases": 1547,
|
||||
"files": 154,
|
||||
"caseSetSha256": "65f157f3d7bdd27ee89ba5db8a4a4f0caa35d3fac8f8396349e42d16a7fa8cb8",
|
||||
"collectionOnly": true
|
||||
},
|
||||
{
|
||||
"engine": "firefox",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-source-firefox-collection",
|
||||
"reportSha256": "7e4b49e6203307c177d5d6258d9820e165c8707022a9cb83c6af8a47e892ebbf",
|
||||
"invocationSha256": "94b2d3473db4d254b84710dfde10af95235df208f2cbebcefe2e79c530b9d994",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.source.config.js",
|
||||
"--list",
|
||||
"--project=firefox"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"cases": 1547,
|
||||
"files": 154,
|
||||
"caseSetSha256": "f25c545aefe6e3e617844fab7a80ec16457dc98ea8ed651b45b7cef1e9085c67",
|
||||
"collectionOnly": true
|
||||
},
|
||||
{
|
||||
"engine": "webkit",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-source-webkit-collection",
|
||||
"reportSha256": "7ee2e4f9e87b0c2d34e7173f71dcb2087b19cda206eaaea2e4b343d619f8b0fc",
|
||||
"invocationSha256": "3e1d45a1428b44bd0b7b5b05d5d54124337c771061def338dbf82b44b40dcf45",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.source.config.js",
|
||||
"--list",
|
||||
"--project=webkit"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"cases": 1547,
|
||||
"files": 154,
|
||||
"caseSetSha256": "08bfe59e38abf8938a3896f75e90c34606e50d7e42f79608a6a730b267a45bba",
|
||||
"collectionOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "installed",
|
||||
"exactUnion": true,
|
||||
"noOverlap": true,
|
||||
"cases": 1980,
|
||||
"files": 67,
|
||||
"records": [
|
||||
{
|
||||
"engine": "all",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-installed-all-collection",
|
||||
"reportSha256": "5903ceb4231d6c21706b951e6c2e1faf6bcacb0828021ce72b63118192d05837",
|
||||
"invocationSha256": "31bc2af2f6f93c817c4e66dad0b2401abda2375f0552c93ec018399fe874880b",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.installed.config.js",
|
||||
"--list"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"cases": 1980,
|
||||
"files": 67,
|
||||
"caseSetSha256": "05c2abcdc0f42ce5c48e0cb49e9d4c6b9c0f85cd09df4f7cb59e7a27b53cb378",
|
||||
"collectionOnly": true
|
||||
},
|
||||
{
|
||||
"engine": "chromium",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-installed-chromium-collection",
|
||||
"reportSha256": "585b78a64df866293283349e046f22392755f3173519c2442c42fbb7894ace67",
|
||||
"invocationSha256": "8601b7797b302fd6789d35597a7ba045bcc75815f23990c9652509d50846c1df",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.installed.config.js",
|
||||
"--list",
|
||||
"--project=chromium"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"cases": 660,
|
||||
"files": 67,
|
||||
"caseSetSha256": "67efda9f29268152b7f43ee699a54c3c2c157262d961e28a7ea6d6a58a42376f",
|
||||
"collectionOnly": true
|
||||
},
|
||||
{
|
||||
"engine": "firefox",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-installed-firefox-collection",
|
||||
"reportSha256": "81c6f61b51ca244d96b5d459f920e6fdd156a30ae793f9ce8ba101a3fe7b2279",
|
||||
"invocationSha256": "2a88902ffaf661aeab1bdfc7f7756132974532aecbd962611b9d8c77f4706329",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.installed.config.js",
|
||||
"--list",
|
||||
"--project=firefox"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"cases": 660,
|
||||
"files": 67,
|
||||
"caseSetSha256": "6242f6445283fc3907d8bf8bacee09c5ae4a1f87aa5c50c1e6113f0db7250e60",
|
||||
"collectionOnly": true
|
||||
},
|
||||
{
|
||||
"engine": "webkit",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-installed-webkit-collection",
|
||||
"reportSha256": "e4673bf53807fbc48a721be1218dbeead39831b9a4c02c5038861a3d983ceae3",
|
||||
"invocationSha256": "ee3cfedb2bd72e057ee25c60ae8f31bcc6aeaeaf9f575a55992f4023f8791918",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.installed.config.js",
|
||||
"--list",
|
||||
"--project=webkit"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"cases": 660,
|
||||
"files": 67,
|
||||
"caseSetSha256": "faa53758af148e8dde777494d56386c1f0cc8a55bc7821b14f057daba2f3f731",
|
||||
"collectionOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"playback": [
|
||||
{
|
||||
"scope": "source",
|
||||
"engine": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-source-chromium-playback",
|
||||
"reportSha256": "2b3903b30fe62fa5c7f7774225195acf6f2ea5fbb322e053cf28e75047b447c4",
|
||||
"invocationSha256": "aab300aed50e5eb2ee5ae5ab90b615c1cc76a631798058354b37473e8b36876a",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.source.config.js",
|
||||
"--project=chromium",
|
||||
"playback.spec.js"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"stats": {
|
||||
"startTime": "2026-09-14T20:12:36.490Z",
|
||||
"duration": 20470.017,
|
||||
"expected": 25,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"files": {
|
||||
"asr-playback.spec.js": 6,
|
||||
"auto-playback.spec.js": 15,
|
||||
"playback.spec.js": 4
|
||||
},
|
||||
"core": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer\\src\\index.ts"
|
||||
},
|
||||
"sha256": "5139cabd5abf9b92684ee2dcdaa620c4db7f67c0cb1aa3f654435d739468bf9d",
|
||||
"bytes": 378931
|
||||
},
|
||||
"installedPackages": 0,
|
||||
"skips": []
|
||||
},
|
||||
{
|
||||
"scope": "source",
|
||||
"engine": "firefox",
|
||||
"browser": "155.0",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-source-firefox-playback",
|
||||
"reportSha256": "fb1d902605692c57094bf5fb0890b1fd61a65dbf1492553bd813719f140d4635",
|
||||
"invocationSha256": "639660214b4e91468a82b0c36f32f7e0d99efde027f87df3826d74531246555e",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.source.config.js",
|
||||
"--project=firefox",
|
||||
"playback.spec.js"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"stats": {
|
||||
"startTime": "2026-09-14T20:12:57.801Z",
|
||||
"duration": 26536.808,
|
||||
"expected": 25,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"files": {
|
||||
"asr-playback.spec.js": 6,
|
||||
"auto-playback.spec.js": 15,
|
||||
"playback.spec.js": 4
|
||||
},
|
||||
"core": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer\\src\\index.ts"
|
||||
},
|
||||
"sha256": "5139cabd5abf9b92684ee2dcdaa620c4db7f67c0cb1aa3f654435d739468bf9d",
|
||||
"bytes": 378931
|
||||
},
|
||||
"installedPackages": 0,
|
||||
"skips": []
|
||||
},
|
||||
{
|
||||
"scope": "source",
|
||||
"engine": "webkit",
|
||||
"browser": "26.6",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-source-webkit-playback",
|
||||
"reportSha256": "a86da3a1b8f4754bed534c59e29aca993e0e9d1aebc4f661b2a8cbac26c86fd4",
|
||||
"invocationSha256": "70e2732396ca23678490008b4b3a60c7f17121cc1e3acc841cca578b7b92b5f3",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.source.config.js",
|
||||
"--project=webkit",
|
||||
"playback.spec.js"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"stats": {
|
||||
"startTime": "2026-09-14T20:13:25.154Z",
|
||||
"duration": 20634.668999999998,
|
||||
"expected": 19,
|
||||
"skipped": 6,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"files": {
|
||||
"asr-playback.spec.js": 6,
|
||||
"auto-playback.spec.js": 15,
|
||||
"playback.spec.js": 4
|
||||
},
|
||||
"core": {
|
||||
"source": {
|
||||
"kind": "workspace-build",
|
||||
"entry": "packages\\artplayer\\src\\index.ts"
|
||||
},
|
||||
"sha256": "5139cabd5abf9b92684ee2dcdaa620c4db7f67c0cb1aa3f654435d739468bf9d",
|
||||
"bytes": 378931
|
||||
},
|
||||
"installedPackages": 0,
|
||||
"skips": [
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR 2.1.0 + published-5.3.0: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR 2.1.0 + published: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR 2.1.0 + candidate: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR candidate + published-5.3.0: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR candidate + published: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR candidate + candidate: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"scope": "installed",
|
||||
"engine": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-installed-chromium-playback",
|
||||
"reportSha256": "8b97bdd6c4975d31ffe0e1ea713da23b2259de68738a1459ecba0f244c05da97",
|
||||
"invocationSha256": "c7ba08cb8c264cfbb8c3bb69d05cf2851c8e5ee75430d80cea4c2f669170fb90",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.installed.config.js",
|
||||
"--project=chromium",
|
||||
"playback.spec.js"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"stats": {
|
||||
"startTime": "2026-09-14T20:13:46.791Z",
|
||||
"duration": 16926.979,
|
||||
"expected": 10,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"files": {
|
||||
"asr-playback.spec.js": 6,
|
||||
"playback.spec.js": 4
|
||||
},
|
||||
"core": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\packages\\run-ifGhEv\\artifacts\\artplayer\\dist\\artplayer.js"
|
||||
},
|
||||
"sha256": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6",
|
||||
"bytes": 208943
|
||||
},
|
||||
"installedPackages": 18,
|
||||
"skips": []
|
||||
},
|
||||
{
|
||||
"scope": "installed",
|
||||
"engine": "firefox",
|
||||
"browser": "155.0",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-installed-firefox-playback",
|
||||
"reportSha256": "b3b6bd5cde862b6c492e001419dc174203530c170790fa7b33b59c2a32acae15",
|
||||
"invocationSha256": "bd0d1808df00277943b6f9e88ede1014173ddba9156696f013a605e0176b2ce0",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.installed.config.js",
|
||||
"--project=firefox",
|
||||
"playback.spec.js"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"stats": {
|
||||
"startTime": "2026-09-14T20:14:04.746Z",
|
||||
"duration": 20473.375,
|
||||
"expected": 10,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"files": {
|
||||
"asr-playback.spec.js": 6,
|
||||
"playback.spec.js": 4
|
||||
},
|
||||
"core": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\packages\\run-ifGhEv\\artifacts\\artplayer\\dist\\artplayer.js"
|
||||
},
|
||||
"sha256": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6",
|
||||
"bytes": 208943
|
||||
},
|
||||
"installedPackages": 18,
|
||||
"skips": []
|
||||
},
|
||||
{
|
||||
"scope": "installed",
|
||||
"engine": "webkit",
|
||||
"browser": "26.6",
|
||||
"directory": "refactor/.cache/ci01-browser-matrix-installed-webkit-playback",
|
||||
"reportSha256": "f1a25f0d38d5d0f83517e6bcab76fc2e26595570cc08d6280f8bb396772b280e",
|
||||
"invocationSha256": "041150cf613bb3ef5ddf17f88158f5f8b6e231b4b285cc20f214febdf475faac",
|
||||
"args": [
|
||||
"test",
|
||||
"--config=playwright.installed.config.js",
|
||||
"--project=webkit",
|
||||
"playback.spec.js"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"stats": {
|
||||
"startTime": "2026-09-14T20:14:26.236Z",
|
||||
"duration": 9805.599,
|
||||
"expected": 4,
|
||||
"skipped": 6,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"files": {
|
||||
"asr-playback.spec.js": 6,
|
||||
"playback.spec.js": 4
|
||||
},
|
||||
"core": {
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"file": "D:\\github\\ArtPlayer\\refactor\\.cache\\packages\\run-ifGhEv\\artifacts\\artplayer\\dist\\artplayer.js"
|
||||
},
|
||||
"sha256": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6",
|
||||
"bytes": 208943
|
||||
},
|
||||
"installedPackages": 18,
|
||||
"skips": [
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR 2.1.0 + published-5.3.0: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR 2.1.0 + published: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR 2.1.0 + candidate: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR candidate + published-5.3.0: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR candidate + published: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "asr-playback.spec.js",
|
||||
"title": "ASR candidate + candidate: native playback volume and mute",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "skip",
|
||||
"description": "Windows WebKit lacks native AudioContext/AudioWorkletNode",
|
||||
"location": {
|
||||
"file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js",
|
||||
"line": 27,
|
||||
"column": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"ciTests": {
|
||||
"passed": 77,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"durationMs": 5997.7666,
|
||||
"log": "refactor/.cache/ci01-browser-matrix-ci-tests-complete.log",
|
||||
"logSha256": "5ffaa56850d47323810ed061542034e6822dee48ac4cb9fe180b0a4de6c4c5e2"
|
||||
},
|
||||
"actionlint": {
|
||||
"exitCode": 0,
|
||||
"log": "refactor/.cache/ci01-browser-matrix-actionlint.log",
|
||||
"logSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"lint": {
|
||||
"exitCode": 0,
|
||||
"log": "refactor/.cache/ci01-browser-matrix-lint.log",
|
||||
"logSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"workflow": {
|
||||
"exitCode": 0,
|
||||
"log": "refactor/.cache/ci01-browser-matrix-check-ci.log",
|
||||
"logSha256": "aa3b88b3eef95df7b31cf71b72c2f7ac7848f61446c05b7323f938cd3aa0a09e"
|
||||
},
|
||||
"impact": {
|
||||
"exitCode": 0,
|
||||
"log": "refactor/.cache/ci01-browser-matrix-impact.log",
|
||||
"logSha256": "06a3447b7906d7a99cf48c14582cefb16e71a941995bc366e65c16dd10a7ca54"
|
||||
},
|
||||
"toolchain": {
|
||||
"exitCode": 0,
|
||||
"log": "refactor/.cache/ci01-browser-matrix-toolchain.log",
|
||||
"logSha256": "8b974d72fa8aad724a7c501b148cb1c5506c0aa1c7c41cd4a0a1a2ba5b9a307a"
|
||||
}
|
||||
},
|
||||
"preservedFailures": [
|
||||
{
|
||||
"log": "refactor/.cache/ci01-browser-matrix-ci-tests.log",
|
||||
"cause": "Impact policy still required the prior unsplit test:package and test:browser steps; updated explicit jobs, invocations and scope conditions.",
|
||||
"logSha256": "92460d476817b3b32fa3fcff408b266f34e177fb568596383637a995ddfff15e"
|
||||
},
|
||||
{
|
||||
"log": "refactor/.cache/ci01-browser-matrix-ci-tests-fixed.log",
|
||||
"cause": "Impact gate identity used command alone and rejected test:package in two independent jobs; identity now includes job and reviewed arguments with mandatory gate regressions.",
|
||||
"logSha256": "dc37d63f4f98c3d71e9530c72ae1811be2c6ec93d826e91cfc02ed2ef3533714"
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Collection-only reports mark cases skipped without executing them; exact set equivalence is not playback acceptance.",
|
||||
"Targeted playback uses the filename regex playback.spec.js: source selects asr-playback, auto-playback and playback; installed selects asr-playback and playback. This is not a full rerun.",
|
||||
"WebKit has twelve existing ASR Web Audio capability skips across the two scopes, not successful ASR playback.",
|
||||
"Local Windows engine runs do not verify remote Linux/macOS execution, cache behavior, runner queueing, cancellation scheduling, billing time or the 60-minute budget.",
|
||||
"No prior Chapter/Audio/DASH/JASSUB failure or physical-device, SDK, type, license or release gate is closed by this CI split.",
|
||||
"CI-01 stays doing; no push, deployment, branch-protection change or publication."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# CI-01 按浏览器引擎拆分播放作业
|
||||
|
||||
## 原因与实现
|
||||
|
||||
上一个检查点仅弹幕/Mask 安装子集就运行了 22.5 分钟。原 browser-smoke 每个
|
||||
系统串行执行 Node 消费者、React/Vue、三个引擎的 source/installed、iframe
|
||||
history 和性能检查。没有完整远端超时证据,但这批实测足以支持减少单个作业
|
||||
的串行工作量;不能承诺此次拆分后每个远端组合一定在 60 分钟内完成。
|
||||
|
||||
- browser-smoke 使用三个 OS × 三个引擎,共 9 个组合,最多同时运行 6 个。
|
||||
每个组合独立冻结安装、正常打包完整 browser 包清单,再执行所选引擎的
|
||||
source 和 installed 两个范围。唯一新增参数是 `--project=${{ matrix.browser }}`。
|
||||
- browser-consumers 每个 OS 一次,共 3 个组合,最多同时运行 3 个。保留同一
|
||||
core/chapter tarball 的 Node 20.19.0、22.12.0、标准 24.21.0 消费,以及原有
|
||||
React/Vue、iframe history 和性能三引擎检查。恢复 Node 的次序保持不变。
|
||||
- 两组互不依赖;source/installed 保留 `!cancelled()`,普通失败不会阻止另一
|
||||
范围采证,失败仍传入最终结果。每个作业有独立 runner,报告目录不混用。
|
||||
- 稳定的 CI result 同时依赖 checks、coverage、browser-smoke、browser-consumers。
|
||||
任一组失败、取消、跳过、缺失都不能通过。artifact 名增加引擎维度;消费者
|
||||
使用独立前缀。既有固定 action SHA、只读权限、精确缓存键和失败上传保留。
|
||||
- 影响策略将每个必需调用绑定到其实际 job、参数和条件。同名 test:package
|
||||
在两个作业有不同用途,不能再只以命令名去重。原先残留的 test:browser
|
||||
门槛改为两个真实的 scope 入口;没有通过删除门槛绕过校验。
|
||||
|
||||
代价是更多独立 runner 和重复打包。当前没有新增依赖、版本变化或生产 API
|
||||
变更;没有修改用例断言、重试、超时、既有能力跳过或 installed 包清单。
|
||||
远端队列、总计费分钟和实际加速幅度仍待测量。
|
||||
|
||||
## 验证
|
||||
|
||||
环境为 Windows、Node 24.21.0、Yarn 1.22.22;真实浏览器为 Chromium
|
||||
153.0.8010.12、Firefox 155.0、WebKit 26.6。证据及输入指纹见
|
||||
[机器记录](../baselines/ci-browser-matrix-validation.json)。
|
||||
|
||||
| 验证 | 结果及边界 |
|
||||
| --- | --- |
|
||||
| source 完整 collection | 154 文件、4,641 项;三个独立引擎各 1,547 项,按测试 ID 精确比较,并集完全相同且不重叠 |
|
||||
| installed 完整 collection | 67 文件、1,980 项;三个独立引擎各 660 项,同样精确比较,无遗漏/重复 |
|
||||
| source 定向实际运行 | Chromium 25、Firefox 25、WebKit 19 通过;WebKit 6 项 ASR 能力跳过 |
|
||||
| installed 定向实际运行 | Chromium 10、Firefox 10、WebKit 4 通过;WebKit 6 项 ASR 能力跳过 |
|
||||
| CI 工程测试 | 最终 77/77 通过,5,997.7666 ms;覆盖矩阵/引擎删减、artifact 冲突、跨组依赖、遗漏消费者、结果失败/取消/跳过/缺失和影响策略 |
|
||||
| 静态检查 | actionlint 1.7.12、改动 JS/MJS 只读 ESLint、check:ci、check:impact --report、严格工具链通过 |
|
||||
|
||||
八次 collection 只列出用例,report 的 skipped 不代表实际能力跳过或播放通过。
|
||||
六次真实运行分别使用 scope 入口加 `--project=<engine> playback.spec.js`;这个
|
||||
文件正则匹配 source 的 asr-playback、auto-playback、playback 三文件,以及
|
||||
installed 的 asr-playback、playback 两文件。它们合计 93 通过、12 能力跳过,
|
||||
零失败、零重试;这不是全量 6,621 项重跑。WebKit 跳过原因是 Windows 下缺少
|
||||
原生 AudioContext/AudioWorkletNode,不能记录为 ASR 播放成功。
|
||||
|
||||
核心旧/新播放器实际解码画面、播放/暂停、seek、切源和销毁仍由既有用例断言。
|
||||
源码入口清除继承的安装 map;安装入口校验上轮 run-ifGhEv 十八包的构建新鲜度
|
||||
和安装字节,真实服务的核心摘要与该 map 一致。本次没有重建生产包,因为改变的
|
||||
是 CI 编排和校验脚本;安装检查拒绝陈旧输入的机制保持执行。
|
||||
|
||||
本地报告归档在 `refactor/.cache/ci01-browser-matrix-<scope>-<engine>-collection`
|
||||
和 `...-playback`。相邻同前缀日志保留实际退出码对应输出。首轮 CI 测试因为旧
|
||||
影响策略仍要求原作业调用而失败;第二轮暴露同名脚本去重问题,两次日志保留,
|
||||
修正后全组 77 项通过。没有把最初失败覆盖为通过。
|
||||
|
||||
复核 collection 时分别运行两个 scope 的 `--list`,再分别加三个 `--project`;
|
||||
比较 report 中每个 spec ID 与 projectName 的组合。再次运行前先归档本地同一
|
||||
scope 的旧报告。完整 CI 使用上述不带文件过滤的工作流命令。
|
||||
|
||||
## 尚未完成
|
||||
|
||||
CI-01 保持 doing,任务总数 263、已完成 197 不变。远端 Linux/macOS/Windows
|
||||
全量运行、实际缓存/取消/报告上传/60 分钟预算、分支保护、其余生态分发与设备
|
||||
门槛尚未验收。既有 Chapter/Audio/DASH/JASSUB 失败未因本轮关闭。
|
||||
|
||||
没有推送、触发远端流程、部署或发布。当前改动随独立 CI-01 检查点本地提交;
|
||||
提交后按 check:commits --report 审计。
|
||||
+20
-10
@@ -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 分钟预算
|
||||
尚未经过完整远端验证;保留完整用例和失败报告,后续依据各组合实测数据决定是否
|
||||
还需按文件分片。不能从本地子集通过推断整个工作流可在预算内完成。
|
||||
@@ -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、候选/报告及任务逐项连接;
|
||||
|
||||
@@ -282,17 +282,38 @@
|
||||
"scope": "All library builds, i18n, editor declarations, docs site and existing import suites"
|
||||
},
|
||||
{
|
||||
"job": "browser-smoke",
|
||||
"job": "browser-consumers",
|
||||
"command": "yarn test:package",
|
||||
"scope": "Installed core/chapter consumers only; other package installation acceptance remains pending"
|
||||
},
|
||||
{
|
||||
"job": "browser-smoke",
|
||||
"command": "yarn test:browser",
|
||||
"scope": "All existing three-engine browser suites; does not imply every package/device has a suite"
|
||||
"command": "yarn test:package",
|
||||
"arguments": [
|
||||
"--browser"
|
||||
],
|
||||
"scope": "Prepare the complete reviewed installed browser roster independently for each OS/engine leg"
|
||||
},
|
||||
{
|
||||
"job": "browser-smoke",
|
||||
"command": "yarn test:browser:source",
|
||||
"arguments": [
|
||||
"--project=${{ matrix.browser }}"
|
||||
],
|
||||
"condition": "${{ !cancelled() }}",
|
||||
"scope": "Complete source test collection across all three engines; source and historical/explicit controls retain their identities"
|
||||
},
|
||||
{
|
||||
"job": "browser-smoke",
|
||||
"command": "yarn test:browser:installed",
|
||||
"arguments": [
|
||||
"--project=${{ matrix.browser }}"
|
||||
],
|
||||
"condition": "${{ !cancelled() }}",
|
||||
"scope": "Complete declared installed test subset across all three engines; not all-package/device acceptance"
|
||||
},
|
||||
{
|
||||
"job": "browser-consumers",
|
||||
"command": "yarn test:performance",
|
||||
"scope": "Existing installed performance and resource-cleanup scenarios"
|
||||
},
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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 分钟,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,23 @@ test('Policy cannot omit core consumers or claim stale installed-package coverag
|
||||
assert.throws(repo.model, /scope extraction/)
|
||||
})
|
||||
|
||||
test('Each split CI invocation remains mandatory even when its sibling uses the same script', (t) => {
|
||||
const repo = fixture(t)
|
||||
const gates = structuredClone(repo.policy.gates)
|
||||
for (let index = 0; index < gates.length; index++) {
|
||||
repo.policy.gates = gates.filter((gate, candidate) => candidate !== index)
|
||||
repo.write('refactor/impact-policy.json', repo.policy)
|
||||
assert.throws(repo.model, /Missing mandatory ecosystem gate/)
|
||||
}
|
||||
repo.policy.gates = [...gates, gates[0]]
|
||||
repo.write('refactor/impact-policy.json', repo.policy)
|
||||
assert.throws(repo.model, /Duplicate impact gate/)
|
||||
repo.policy.gates = structuredClone(gates)
|
||||
repo.policy.gates.find(gate => gate.command === 'yarn test:browser:installed').condition = 'success()'
|
||||
repo.write('refactor/impact-policy.json', repo.policy)
|
||||
assert.throws(repo.model, /Only browser scopes/)
|
||||
})
|
||||
|
||||
test('Shared/unknown files and computed imports conservatively expand; unsafe or unmapped package paths are rejected', (t) => {
|
||||
for (const file of ['yarn.lock', 'scripts/new-build-stage.mjs', 'types/new-boundary.d.ts'])
|
||||
assert.equal(analyzeImpact(actual, [file]).affectedPackages.length, 22)
|
||||
@@ -127,6 +144,10 @@ test('CI workflow cannot silently skip or soften required impact gates', () => {
|
||||
(workflow) => { workflow.jobs.checks.steps.find(step => step.run?.startsWith('yarn ci:check')).if = false },
|
||||
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.uses?.startsWith('actions/checkout@')).if = false },
|
||||
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.uses?.startsWith('actions/checkout@')).with['fetch-depth'] = 1 },
|
||||
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.run?.startsWith('yarn test:browser:source ')).if = 'success()' },
|
||||
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.run?.startsWith('yarn test:browser:installed ')).run += ' --grep subset' },
|
||||
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.run?.startsWith('yarn test:package --browser')).run = 'yarn test:package' },
|
||||
(workflow) => { workflow.jobs['browser-consumers'].steps.find(step => step.run?.startsWith('yarn test:performance ')).run += ' || true' },
|
||||
]) {
|
||||
const workflow = YAML.parse(source)
|
||||
edit(workflow)
|
||||
|
||||
+3
-1
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user