mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
ci: [CI-01] add OS matrices, scoped caches and required result gate
This commit is contained in:
1 parent
2c73e2b105
commit
585e11c53c
15 files changed
+677
-32
No files matched your search
+103
-22
@@ -3,7 +3,7 @@ name: Node CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master, 'codex/**']
|
||||
branches: [ master, 'codex/**' ]
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -42,28 +42,37 @@ jobs:
|
||||
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 toolchain and dependencies
|
||||
run: |
|
||||
npm install --global --force yarn@1.22.22
|
||||
yarn install --frozen-lockfile --non-interactive
|
||||
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
|
||||
yarn check:toolchain --strict
|
||||
- name: Verify source maps and critical lifecycle coverage
|
||||
run: yarn test:coverage
|
||||
run: yarn test:coverage 2>&1 | tee refactor/.cache/ci/coverage.log
|
||||
- name: Upload coverage and source-map evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
refactor/.cache/ci/
|
||||
refactor/.cache/coverage/latest.json
|
||||
refactor/.cache/coverage/run-*/
|
||||
include-hidden-files: true
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
browser-smoke:
|
||||
name: Browser playback smoke
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
name: Browser playback (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: true
|
||||
steps:
|
||||
@@ -75,28 +84,42 @@ jobs:
|
||||
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
|
||||
yarn install --frozen-lockfile --non-interactive
|
||||
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
|
||||
run: yarn test:browser:install --with-deps 2>&1 | tee refactor/.cache/ci/browser-install.log
|
||||
- name: Build and check installed tarballs
|
||||
run: |
|
||||
yarn test:package
|
||||
yarn test:package 2>&1 | tee refactor/.cache/ci/package.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 three engines
|
||||
run: yarn test:browser
|
||||
run: yarn test:browser 2>&1 | tee refactor/.cache/ci/browser.log
|
||||
- name: Verify iframe cached history and interrupted navigation
|
||||
run: yarn test:iframe-history
|
||||
run: yarn test:iframe-history 2>&1 | tee refactor/.cache/ci/iframe-history.log
|
||||
- name: Compare installed performance and verify resource cleanup
|
||||
run: yarn test:performance
|
||||
run: yarn test:performance 2>&1 | tee refactor/.cache/ci/performance.log
|
||||
- name: Upload browser evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: browser-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
name: browser-smoke-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
refactor/.cache/ci/
|
||||
refactor/.cache/browser/
|
||||
refactor/.cache/iframe-history/
|
||||
refactor/.cache/performance/
|
||||
@@ -108,10 +131,21 @@ jobs:
|
||||
include-hidden-files: true
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 3
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
checks:
|
||||
name: Checks and build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
name: Checks and build (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
CI: true
|
||||
defaults:
|
||||
@@ -126,14 +160,23 @@ jobs:
|
||||
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
|
||||
run: npm install --global --force yarn@1.22.22
|
||||
run: npm install --global --force yarn@1.22.22 2>&1 | tee refactor/.cache/ci/toolchain-install.log
|
||||
- name: Validate workflow syntax
|
||||
run: |
|
||||
curl --fail --silent --show-error --location https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz --output "$RUNNER_TEMP/actionlint.tar.gz"
|
||||
printf '%s %s\n' '8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8' "$RUNNER_TEMP/actionlint.tar.gz" | sha256sum --check
|
||||
tar -xzf "$RUNNER_TEMP/actionlint.tar.gz" -C "$RUNNER_TEMP" actionlint
|
||||
"$RUNNER_TEMP/actionlint" -color
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- name: Frozen install
|
||||
run: |
|
||||
mkdir -p refactor/.cache/ci
|
||||
@@ -155,14 +198,52 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: checks-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
name: checks-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: refactor/.cache/ci/
|
||||
include-hidden-files: true
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
- name: Prepare Pages artifact
|
||||
if: inputs.pages-artifact && github.ref == 'refs/heads/master'
|
||||
if: inputs.pages-artifact && github.ref == 'refs/heads/master' && matrix.os == 'ubuntu-latest'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: docs
|
||||
include-hidden-files: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
ci-result:
|
||||
name: CI result
|
||||
if: always()
|
||||
needs:
|
||||
- checks
|
||||
- coverage
|
||||
- browser-smoke
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
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: Require every matrix job to succeed
|
||||
env:
|
||||
ARTPLAYER_CI_NEEDS: ${{ toJSON(needs) }}
|
||||
run: node scripts/ci-summary.mjs
|
||||
- name: Upload final status
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ci-result-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: refactor/.cache/ci/
|
||||
include-hidden-files: true
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
+5
-3
@@ -45,9 +45,9 @@
|
||||
"check:toolchain": "node scripts/check-toolchain.mjs",
|
||||
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
|
||||
"check:plan": "node refactor/scripts/plan.mjs --check",
|
||||
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js",
|
||||
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js",
|
||||
"test:baseline": "node --test refactor/scripts/*.test.mjs",
|
||||
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn lint && yarn check:types && yarn typecheck && yarn test",
|
||||
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn lint && yarn check:types && yarn typecheck && yarn test",
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
@@ -90,7 +90,9 @@
|
||||
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs",
|
||||
"test:multiple-subtitles": "node --test test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
|
||||
"test:multiple-subtitles-types-package": "node refactor/scripts/multiple-subtitles-package-types.mjs",
|
||||
"test:jassub": "node --test test/jassub.test.js refactor/scripts/jassub-contract.test.mjs"
|
||||
"test:jassub": "node --test test/jassub.test.js refactor/scripts/jassub-contract.test.mjs",
|
||||
"check:ci": "node refactor/scripts/ci-workflow.mjs",
|
||||
"test:ci": "node --test test/ci-summary.test.js refactor/scripts/ci-workflow.test.mjs refactor/scripts/impact.test.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "CI-01",
|
||||
"date": "2026-09-13",
|
||||
"baseCommit": "2c73e2b105ee55f95e3ffc906ea4ea1f25221622",
|
||||
"candidate": "working tree in the accompanying CI-01 checkpoint commit",
|
||||
"inputs": [
|
||||
{
|
||||
"file": ".github/workflows/nodejs.yml",
|
||||
"sha256": "823cc76c209f4d54fe66d818a5114333a4412fe710796be7d28bb4cb0bd6f5f6"
|
||||
},
|
||||
{
|
||||
"file": "package.json",
|
||||
"sha256": "c6059a5d3dfddbaf31a389b73db252d612cfb14ea64b19cfe0e9657e0340e0cf"
|
||||
},
|
||||
{
|
||||
"file": "scripts/ci-context.mjs",
|
||||
"sha256": "2a59a334ded9079fbcc7892b8203f59a79e252bb4246b66a61f683e347f1d14d"
|
||||
},
|
||||
{
|
||||
"file": "scripts/ci-summary.mjs",
|
||||
"sha256": "8878541a6ddcbcf23d4e6d86612093f8ce605b1a3c9952cb1d8f463343ed5602"
|
||||
},
|
||||
{
|
||||
"file": "refactor/scripts/ci-workflow.mjs",
|
||||
"sha256": "beb14540abe2784db14462d295883da4f04f9cc14b781851b4e64da5e0693671"
|
||||
},
|
||||
{
|
||||
"file": "refactor/scripts/ci-workflow.test.mjs",
|
||||
"sha256": "9323a85ef5cccb43c512ca86701a3984e3587821faf1ded3a82deeb2878fb8fc"
|
||||
},
|
||||
{
|
||||
"file": "test/ci-summary.test.js",
|
||||
"sha256": "fa28903bb90612c108c5cf89223e7de003137a0f0cc168021c97e46770fcd052"
|
||||
}
|
||||
],
|
||||
"environment": {
|
||||
"os": "Windows",
|
||||
"node": "24.21.0",
|
||||
"yarn": "1.22.22",
|
||||
"actionlint": "1.7.12"
|
||||
},
|
||||
"focused": {
|
||||
"command": "yarn test:ci",
|
||||
"passed": 37,
|
||||
"failed": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"ciCheck": {
|
||||
"command": "yarn ci:check",
|
||||
"passed": 2422,
|
||||
"unit": 2031,
|
||||
"engineering": 23,
|
||||
"baseline": 368,
|
||||
"seconds": 244.45
|
||||
},
|
||||
"actionlint": {
|
||||
"exitCode": 0,
|
||||
"logSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"explicitLint": {
|
||||
"exitCode": 0,
|
||||
"logSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"logs": [
|
||||
{
|
||||
"file": "refactor/.cache/ci01-focused.log",
|
||||
"sha256": "e76424c6290d7cddb2a50be95bfb32705c51b07f04bc02728c3ed521acf6e2a2",
|
||||
"retention": "local ignored cache; outcome retained in this report"
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ci01-ci.log",
|
||||
"sha256": "71c6d04073d6af4b6fd225693d785c6e157b6cb8216de1a91e3ffb833d2e8383",
|
||||
"retention": "local ignored cache; outcome retained in this report"
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ci01-actionlint-final.log",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"retention": "local ignored cache; outcome retained in this report"
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ci01-lint.log",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"retention": "local ignored cache; outcome retained in this report"
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ci01-package-final.log",
|
||||
"sha256": "b0705f902491cb71954cf8d3f537f9ca05fe027a1cefc2ed93960443443838fc",
|
||||
"retention": "local ignored cache; outcome retained in this report"
|
||||
}
|
||||
],
|
||||
"cacheAction": {
|
||||
"ref": "refs/tags/v5",
|
||||
"sha": "caa296126883cff596d87d8935842f9db880ef25",
|
||||
"source": "https://api.github.com/repos/actions/cache/git/refs/tags/v5",
|
||||
"runtime": "node24"
|
||||
},
|
||||
"remoteActionsRun": null,
|
||||
"taskComplete": false,
|
||||
"remaining": [
|
||||
"Lowest Node consumer and tooling runtime matrix",
|
||||
"Full ecosystem installed-package/browser scope",
|
||||
"Impact-based selection with equivalent coverage and complete reporting",
|
||||
"CI-04 hosted runner, cache hit/miss, failed PR and required-check evidence"
|
||||
],
|
||||
"inputByteScope": "Local working-tree bytes; checkout line endings may differ. Only action version comments were added after full ci:check; final actionlint and test:ci reran on the recorded workflow.",
|
||||
"metadata": {
|
||||
"plan": "223 tasks; 121 done, 16 doing, 86 todo",
|
||||
"risks": 200,
|
||||
"testsPassed": 9
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# CI-01 系统矩阵、缓存与结果汇总检查点
|
||||
|
||||
2026-09-13,基于 2c73e2b105ee55f95e3ffc906ea4ea1f25221622,分支 codex/compatible-modernization。
|
||||
本批修改工程流程,不修改播放器或插件的生产源码、公开声明、版本、运行时依赖或构建产物。
|
||||
CI-01 保持 doing;没有推送、部署、修改仓库保护设置或发布 npm。
|
||||
|
||||
## 旧行为与本批改动
|
||||
|
||||
原 checks 仅 Linux,浏览器仅 macOS,coverage 为 Linux/Windows;没有缓存或统一结果作业。
|
||||
checks 扩为 Linux/Windows(45 分钟),浏览器扩为 Linux/Windows/macOS(60 分钟),
|
||||
coverage 保留双系统(15 分钟)。每个系统运行完整已有命令,矩阵 fail-fast=false;
|
||||
浏览器仍是 Chromium/Firefox/WebKit,真实设备和全生态覆盖不能由这个配置推导。
|
||||
|
||||
新增固定名称 CI result,依赖 checks、coverage、browser-smoke,并在前序失败后运行。
|
||||
只有三个作业组全部 success 才返回 0;失败、取消、跳过、缺失、错误 JSON 或未知额外作业
|
||||
均返回 1。GitHub 的 needs 是矩阵作业组的汇总结果,本地不伪造或推断每个远端子作业。
|
||||
整个运行被强制取消或 runner 不可用时能否执行最终作业,仍需 CI-04 实际验证。
|
||||
|
||||
Yarn 只缓存下载内容,浏览器缓存与 Playwright 精确版本绑定;key 包含 OS、架构、Node、
|
||||
Yarn、ref 和锁摘要,不设宽松恢复前缀,不缓存 node_modules 或构建产物。每次冻结安装,
|
||||
浏览器即使命中缓存仍 install --with-deps。缓存目录由 Node 在 step 内从 RUNNER_TEMP
|
||||
解析,写入 GITHUB_ENV/OUTPUT;初始把 runner.temp 放 job env 的 actionlint 失败已修复。
|
||||
|
||||
三个矩阵使用显式 bash,保留 tee 上游失败码;安装和执行日志、源码/工具/锁 metadata、
|
||||
各类既有报告 always 上传,名称区分系统、run 和 attempt,保留 14 天。Pages 产物只有
|
||||
受信任 master 的 Linux checks 分支上传,部署仍由原独立 workflow 的准入门槛控制。
|
||||
|
||||
## 模块与维护入口
|
||||
|
||||
- scripts/ci-context.mjs:只依赖 Node 内置模块,读取真实 checkout 和固定版本/锁,写运行 metadata 与缓存路径。
|
||||
- scripts/ci-summary.mjs:独立评估结构化 needs,生成 summary.json/Markdown、Job Summary 和 CLI 退出码。
|
||||
- refactor/scripts/ci-workflow.mjs:用现有 yaml 2.8.2 解析实际工作流,保护矩阵、不可变 Actions、安装顺序、权限、报告和最终依赖。
|
||||
- test/ci-summary.test.js:运行真实 CLI 的成功/失败/无效 JSON、结果缺失与环境文件;纳入 test:node。
|
||||
- refactor/scripts/ci-workflow.test.mjs:真实 YAML 正例和 20 个破坏条件的反例;纳入 test:baseline。
|
||||
- 现有 impact-model/impact.test 继续保护六项必需命令及全生态影响传播。
|
||||
|
||||
新增 yarn check:ci(只读工作流校验,纳入 ci:check)和 yarn test:ci(以上三组 37 项)。
|
||||
没有修改依赖或 yarn.lock。新增 Actions/cache 采用官方 v5 ref 当次解析的完整 SHA
|
||||
caa296126883cff596d87d8935842f9db880ef25,其 action.yml 使用 node24;不使用浮动 tag 执行。
|
||||
升级缓存 Action 时重新核对官方 ref/action.yml 和 runner 要求,不把 Node Action 运行时
|
||||
等同于播放器消费者的最低 Node。
|
||||
|
||||
## 验证与范围
|
||||
|
||||
本地 Windows,Node 24.21.0 / Yarn 1.22.22:定向 37 项通过;完整 ci:check 2422 项通过
|
||||
(2031 单元、23 工程、368 基线)。actionlint 1.7.12 与新增文件的只读 ESLint 通过。
|
||||
原有 Yarn deprecation/生成声明提示仍存在,不声称整个 CI 无警告。
|
||||
源码和日志指纹、计数与运行时长见 [验证记录](../baselines/ci-matrix-validation.json)。
|
||||
|
||||
没有新跑完整浏览器播放、ci:build 或 GitHub hosted jobs:这次没有修改生产源码和产物,
|
||||
主要验证编排与错误传播。跨平台构建/浏览器、缓存命中和失效的实际结果仍是缺口,
|
||||
配置成功不计入这些远端验收。现有 test:package 只对 core/chapter 实际安装,不能称全包消费通过。
|
||||
|
||||
## 后续与回退
|
||||
|
||||
CI-01 继续最低 Node/TS 消费者组合、全包 installed/browser 覆盖和有完整证据的影响范围调度。
|
||||
当前保留全量运行,影响报告用于解释范围,不输出跳过必需检查的开关。CI-04 负责真实 PR
|
||||
成功/失败/取消、各系统、缓存冷/热运行及稳定检查绑定的远端验收;这些均未完成。
|
||||
公开 JS/TS、事件/DOM/CSS、分发入口无变化,不需要用户迁移。
|
||||
回退本检查点可恢复旧编排和脚本入口,不需要重新生成库产物;远端设置未改,无远端回退动作。
|
||||
提交主题:ci: [CI-01] add OS matrices, scoped caches and required result gate。
|
||||
+48
-2
@@ -18,6 +18,8 @@
|
||||
| `yarn test:contracts` | 重跑已登记断言的Node文件并采集精确事件/候选指纹,当前44项包含10个索引断言 |
|
||||
| `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 汇总真实退出码、工作流破坏反例与全包影响分析,共 37 项 |
|
||||
| `yarn ci:build` | 21 库包、i18n、编辑器声明和文档站构建,以及构建后包导入 smoke;会生成 dist 和 docs 内容 |
|
||||
| `yarn check:impact --report` | 读取实际依赖/验证关系和Git变更,核对workflow必需命令,写CI影响报告;已接入ci:check,见[影响映射](impact-analysis.md) |
|
||||
| `yarn build:all` | 保留旧入口,执行 ci:build 后只读 lint |
|
||||
@@ -26,11 +28,55 @@ scripts/build-docs.js 保留原 npm run build 子命令兼容入口,现在传
|
||||
|
||||
## PR 和主线
|
||||
|
||||
.github/workflows/nodejs.yml 在 PR、master/codex/** push 和手动运行时触发,也供 Pages 复用。Checks and build 作业只授予 contents: read,checkout 不保留 Git 凭据,没有提交、推送、npm 发布或部署步骤。PR/推送取消过期运行,手动部署不被中途取消;初始 Ubuntu 作业限时 30 分钟。
|
||||
.github/workflows/nodejs.yml 在 PR、master/codex/** push 和手动运行时触发,也供 Pages 复用。检查作业只授予 contents: read,checkout 不保留 Git 凭据,没有提交、推送、npm 发布或部署步骤。PR/推送取消过期运行;Pages 部署继续使用独立队列。
|
||||
|
||||
Actions 固定完整 SHA,Node 来自 .node-version,Yarn 固定 1.22.22;安装使用 frozen-lockfile。actionlint 1.7.12 从官方固定归档取得,先核对提交在 workflow 中的 SHA-256,再执行。没有执行 curl 管道脚本。日志在失败时也尽量上传,包含源码 SHA、工具版本、锁摘要和生成差异概览,保留 14 天。
|
||||
|
||||
不缓存 node_modules。ENG-04 已接入类型消费者;ENG-05 新增 macOS 的 `Browser playback smoke` 作业,运行三个浏览器并 always 上传 HTML/JSON、失败截图和 trace。本地 Windows 三浏览器已验证,远端 macOS 作业尚未运行。完整生态矩阵、下载缓存、覆盖率和候选 tarball 汇总由 CI-01 等后续任务扩展。稳定 required 汇总名称由 CI-01/CI-04 核实后设置,不把本地 YAML 当作分支保护已生效。
|
||||
## CI-01 当前矩阵与结果汇总
|
||||
|
||||
| 作业 | 系统 | 内容 | 超时 |
|
||||
| --- | --- | --- | --- |
|
||||
| checks | Linux、Windows | ci:check、ci:build;Linux 额外执行 actionlint | 45 分钟 |
|
||||
| coverage | Linux、Windows | 既有源码映射与生命周期覆盖检查 | 15 分钟 |
|
||||
| browser-smoke | Linux、Windows、macOS | 实际 core/chapter 安装产物、Chromium/Firefox/WebKit、iframe history、性能 | 60 分钟 |
|
||||
| CI result | Linux | 汇总以上三个作业组,所有结果必须为 success | 5 分钟 |
|
||||
|
||||
Node 均使用 .node-version 的 24.21.0。TS 5.9.3、4.3.5 和迁移运行时兼容 5.1.6
|
||||
继续由既有类型脚本按各自适用范围运行,不代表最低 Node 或所有包/TS 组合已验收。
|
||||
矩阵不设置 fail-fast,单个系统失败后仍尽量收集其他系统证据;显式 bash 保留 tee
|
||||
上游命令的失败退出码。影响报告继续扩大核心/共享变更到全生态,当前不缩减必需作业。
|
||||
|
||||
稳定名称为 **CI result**。它以 always() 依赖 checks、coverage、browser-smoke,结构化
|
||||
读取 needs;失败、取消、跳过、缺失、错误 JSON 或未知额外作业都失败。增加独立 job 时
|
||||
同时修改 ci-summary.mjs 的 requiredJobs、workflow needs、矩阵策略和测试,防止遗漏汇总。
|
||||
全局取消/runner 故障下的真实调度仍待 CI-04;本地退出码测试不证明远端作业一定被调度。
|
||||
required checks 的实际绑定尚未设置,不能宣称分支保护已经生效。
|
||||
|
||||
## 下载缓存、日志与维护
|
||||
|
||||
scripts/ci-context.mjs 从真实 checkout、package.json、.node-version 和 yarn.lock 读取
|
||||
来源与版本,将绝对缓存目录写入 GITHUB_ENV/OUTPUT,运行信息写入 refactor/.cache/ci/context.json。
|
||||
Yarn 缓存与浏览器缓存均按 OS/架构/Node/Yarn/ref/锁隔离,浏览器另绑定 Playwright 版本;
|
||||
只缓存下载内容,无宽松 restore-keys、跨 OS 恢复或 node_modules/产物缓存。每次 frozen
|
||||
install;浏览器每次 install --with-deps,缓存命中不能替代 Linux 系统依赖安装。
|
||||
相关行为依据 [Playwright CI 文档](https://playwright.dev/docs/ci)。
|
||||
|
||||
新增 actions/cache 固定 caa296126883cff596d87d8935842f9db880ef25,来自当次核实的
|
||||
[官方 v5 ref](https://api.github.com/repos/actions/cache/git/refs/tags/v5),
|
||||
[该提交 action.yml](https://github.com/actions/cache/blob/caa296126883cff596d87d8935842f9db880ef25/action.yml)
|
||||
使用 node24;升级时重新核对 tag/SHA、runner 和缓存格式,不使用浮动标签执行。
|
||||
本批没有新增 npm 依赖或修改 yarn.lock。
|
||||
|
||||
安装/构建/测试日志和已有 HTML/JSON、截图、trace、tarball 由 always 上传步骤留存;
|
||||
名称包含系统、run_id 和 run_attempt,保留 14 天。最终脚本独立写 summary.json、
|
||||
summary.md 和 GitHub Job Summary,不依赖先前下载的 artifact 来判断作业是否成功。
|
||||
下载对应作业 artifact,先核对 context.json 的 source/workflowSource、工具版本和锁摘要,
|
||||
再查看失败阶段的日志和浏览器 trace。缓存异常时可删除对应远端 key 后重跑;该动作
|
||||
必须针对实际故障,不以清缓存代替修复锁或构建问题。
|
||||
|
||||
本地验证与指纹见 [CI-01 记录](changes/2026-09-13-CI-01-matrix-summary.md)。
|
||||
最低 Node 消费/工具环境、全生态安装矩阵和有证据的影响调度仍待 CI-01;CI-04 负责
|
||||
各系统远端运行、冷热缓存、失败/取消演练及 required check 设置。没有新增远端通过证据。
|
||||
|
||||
## Pages 隔离与启用条件
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GitHub CI/CD 优化与验收
|
||||
|
||||
2026-09-10 用户明确要求在本次重构中优化和增强 GitHub CI/CD。范围包含仓库检查、测试矩阵、构建产物、文档站部署和 npm 发布准备,并持续维护脚本文档及故障处理指南。ENG-02 已实现初步检查与部署隔离,操作见 [ci-setup.md](ci-setup.md);完整矩阵及远端验收仍待后续任务。
|
||||
2026-09-10 用户明确要求在本次重构中优化和增强 GitHub CI/CD。范围包含仓库检查、测试矩阵、构建产物、文档站部署和 npm 发布准备,并持续维护脚本文档及故障处理指南。ENG-02 已实现初步检查与部署隔离,CI-01 已扩展系统矩阵、下载缓存、失败报告与稳定 CI result,操作见 [ci-setup.md](ci-setup.md)。CI-01 仍在进行:最低 Node 消费环境、全生态安装组合和影响调度尚未完成;CI-04 远端验收仍缺证据。
|
||||
|
||||
## ENG-02 前源码基线
|
||||
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 223 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 87 / doing 15 / blocked 0 / done 121 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 86 / doing 16 / blocked 0 / done 121 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| CI-01 | workspace<br>增强兼容矩阵、并发缓存与 CI 报告 | DOC-10, ENG-08, ENG-09, ENG-10 | OS/Node/TS/浏览器与影响范围矩阵、缓存、超时、汇总检查和 artifact 报告 | 固定安装、失败/取消不误报、核心影响全生态;检查只读,失败证据可追溯 | H | todo |
|
||||
| CI-01 | workspace<br>增强兼容矩阵、并发缓存与 CI 报告 | DOC-10, ENG-08, ENG-09, ENG-10 | OS/Node/TS/浏览器与影响范围矩阵、缓存、超时、汇总检查和 artifact 报告 | 固定安装、失败/取消不误报、核心影响全生态;检查只读,失败证据可追溯 | H | doing |
|
||||
| CI-02 | workspace<br>分离并改进 GitHub Pages 部署 | DOC-10, ENG-02, SITE-03 | Pages artifact 部署配置、旧路径/域名核对、预检和迁移恢复指南 | 部署只取受信任已验证产物;本地实现可验收,远端 source/环境和实际部署状态单独登记 | H | todo |
|
||||
| CI-03 | workspace<br>建立 npm 分包候选与发布工作流 | DOC-10, CI-01, REL-08, REL-04 | 候选准备、精确 artifact 发布配置、OIDC 评估、版本/tag/registry 预检和部分失败恢复 | 不自动发布;明确逐包信任前置和 dry run 限制,不能重建未验证内容或重发冲突版本;按版本清单校验各包下一 major 和预发布/正式 tag,保留旧核心支持范围 | H | todo |
|
||||
| CI-04 | workspace<br>验收 GitHub 流水线与远端发布准入 | CI-01, CI-02, CI-03, SITE-06 | 静态/干净环境检查、真实 PR 正反例、候选 dry run、required checks/Pages/npm 必需配置状态及运维指南 | 必要 Actions 证据和远端配置核对齐全;缺失保持未完成,真实 publish/deploy 仍在授权发布步骤执行 | H | todo |
|
||||
@@ -454,6 +454,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)
|
||||
- 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-02: [记录](changes/2026-09-11-CORE-02-typed-emitter.md) [记录](baselines/emitter-validation.json)
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## CI-01 系统矩阵、缓存与结果汇总检查点(doing)
|
||||
|
||||
checks 扩为 Linux/Windows,浏览器扩为 Linux/Windows/macOS;coverage 保留双系统。
|
||||
固定来源下载缓存、每次冻结安装/浏览器系统依赖、失败日志和稳定 CI result 已实现。
|
||||
最终作业只接受所有必需组 success;真实 CLI 和工作流反例共 37 项通过。
|
||||
完整本地 ci:check 2422 项通过(2031 单元、23 工程、368 基线),actionlint 与定向 lint 通过。
|
||||
见[变更](changes/2026-09-13-CI-01-matrix-summary.md)和[证据](baselines/ci-matrix-validation.json)。
|
||||
没有新生产源码/类型/依赖/版本/产物变更,没有新浏览器播放或远端 Actions 运行。
|
||||
CI-01 保持 doing:最低 Node 消费环境、全包安装矩阵与影响调度仍待完成;CI-04 验证远端。
|
||||
223 项:121 done、16 doing、86 todo;本次未新增或关闭风险。独立本地检查点,不推送、不发布。
|
||||
下一步继续消费者运行时矩阵和插件缺口;此前类型决策与第三方来源事项保持未完成。
|
||||
|
||||
|
||||
## PKG-JASSUB-01 发布与来源检查点(doing)
|
||||
|
||||
冻结两个真实 npm 版本、12 成员、9 份 Git 文本及本地 worker/WASM/font/ASS/MP4 指纹。
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable no-template-curly-in-string -- GitHub workflow expressions are literal contract data. */
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import YAML from 'yaml'
|
||||
import { requiredJobs } from '../../scripts/ci-summary.mjs'
|
||||
|
||||
const systems = {
|
||||
'checks': ['ubuntu-latest', 'windows-latest'],
|
||||
'coverage': ['ubuntu-latest', 'windows-latest'],
|
||||
'browser-smoke': ['ubuntu-latest', 'windows-latest', 'macos-latest'],
|
||||
}
|
||||
|
||||
export function validateCIWorkflow(source) {
|
||||
const workflow = YAML.parse(source, { uniqueKeys: true })
|
||||
assert.deepEqual(workflow.permissions, { contents: 'read' }, 'CI permissions must remain read-only')
|
||||
assert.deepEqual(Object.keys(workflow.jobs).sort(), [...requiredJobs, 'ci-result'].sort(), 'Every job must participate in the summary policy')
|
||||
const summary = workflow.jobs['ci-result']
|
||||
assert.equal(summary.name, 'CI result', 'Keep the required-check name stable')
|
||||
assert.equal(summary.if, 'always()', 'Summary must run after failed or cancelled dependencies')
|
||||
assert.deepEqual([...summary.needs].sort(), [...requiredJobs].sort(), 'Summary must depend on every required job')
|
||||
const gate = summary.steps.find(step => step.run === 'node scripts/ci-summary.mjs')
|
||||
assert(gate && !Object.hasOwn(gate, 'if'), 'Final gate must execute unconditionally')
|
||||
assert.deepEqual(gate.env, { ARTPLAYER_CI_NEEDS: '${{ toJSON(needs) }}' }, 'Pass results as structured data, not shell interpolation')
|
||||
for (const [id, job] of Object.entries(workflow.jobs)) {
|
||||
assert(!job['continue-on-error'], 'Jobs cannot allow failure')
|
||||
assert(job['timeout-minutes'] > 0 && job['timeout-minutes'] <= 60, 'Every job needs a bounded timeout')
|
||||
if (job.permissions)
|
||||
assert.deepEqual(job.permissions, { contents: 'read' }, 'Job permissions must remain read-only')
|
||||
const checkout = job.steps.find(step => step.uses?.startsWith('actions/checkout@'))
|
||||
assert(checkout && !Object.hasOwn(checkout, 'if') && checkout.with?.['fetch-depth'] === 0 && checkout.with['persist-credentials'] === false, 'Every job needs unconditional full-history checkout without saved credentials')
|
||||
const setup = job.steps.find(step => step.uses?.startsWith('actions/setup-node@'))
|
||||
assert(setup && !Object.hasOwn(setup, 'if') && setup.with?.['node-version-file'] === '.node-version' && setup.with['package-manager-cache'] === false && !setup.with.cache, 'Use the canonical Node and explicit download caches')
|
||||
for (const step of job.steps) {
|
||||
assert(!step['continue-on-error'], 'Steps cannot hide failures')
|
||||
if (step.uses)
|
||||
assert(/^[\w.-]+\/[\w./-]+@[a-f0-9]{40}$/.test(step.uses), 'Actions require immutable commit pins')
|
||||
}
|
||||
const upload = job.steps.find(step => step.uses?.startsWith('actions/upload-artifact@'))
|
||||
assert(upload?.if === 'always()' && upload.with?.['include-hidden-files'] === true, 'Always preserve available hidden-cache reports')
|
||||
assert(upload.with.path.includes('refactor/.cache/ci/'), 'Every job must preserve source and execution logs')
|
||||
for (const part of ['github.run_id', 'github.run_attempt']) assert(upload.with.name.includes(part), 'Artifacts must be distinct across runs and attempts')
|
||||
if (id === 'ci-result')
|
||||
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.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')
|
||||
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')
|
||||
assert(['${{ steps.context.outputs.yarn_cache }}', '${{ steps.context.outputs.browser_cache }}'].includes(cache.with.path), 'Do not cache node_modules or generated artifacts')
|
||||
for (const part of ['runner.os', 'runner.arch', 'steps.context.outputs.node', 'steps.context.outputs.yarn', 'github.ref', 'hashFiles(\'yarn.lock\')'])
|
||||
assert(cache.with.key.includes(part), `Cache key must isolate ${part}`)
|
||||
if (cache.with.path.includes('browser_cache'))
|
||||
assert(cache.with.key.includes('steps.context.outputs.playwright'), 'Browser cache must follow the exact Playwright version')
|
||||
}
|
||||
const installIndex = job.steps.findIndex(step => step.run?.split('\n').some(line => /^yarn install --frozen-lockfile --non-interactive(?: 2>&1 \| tee refactor\/\.cache\/ci\/install\.log)?$/.test(line)) && !Object.hasOwn(step, 'if'))
|
||||
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')
|
||||
const pages = workflow.jobs.checks.steps.find(step => step.uses?.startsWith('actions/upload-pages-artifact@'))
|
||||
assert.equal(pages?.if, 'inputs.pages-artifact && github.ref == \'refs/heads/master\' && matrix.os == \'ubuntu-latest\'', 'Only one trusted matrix leg can prepare Pages')
|
||||
return { jobs: requiredJobs, systems, summary: summary.name }
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
||||
console.log(JSON.stringify(validateCIWorkflow(fs.readFileSync('.github/workflows/nodejs.yml', 'utf8'))))
|
||||
@@ -0,0 +1,41 @@
|
||||
/* eslint-disable no-template-curly-in-string -- GitHub workflow expressions are literal contract data. */
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Workflow contract regression runner.
|
||||
import test from 'node:test'
|
||||
import YAML from 'yaml'
|
||||
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')
|
||||
})
|
||||
|
||||
for (const [name, mutate] of [
|
||||
['missing required dependency', w => w.jobs['ci-result'].needs.pop()],
|
||||
['success-only summary', w => w.jobs['ci-result'].if = 'success()'],
|
||||
['conditional result gate', w => w.jobs['ci-result'].steps.find(s => s.run).if = 'success()'],
|
||||
['softened gate', w => w.jobs['ci-result'].steps.find(s => s.run)['continue-on-error'] = true],
|
||||
['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()],
|
||||
['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'],
|
||||
['mutable action ref', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/cache@')).uses = 'actions/cache@v5'],
|
||||
['dependency installation cache', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/cache@')).with.path = 'node_modules'],
|
||||
['cross-branch cache fallback', w => w.jobs.coverage.steps.find(s => s.uses?.startsWith('actions/cache@')).with['restore-keys'] = 'yarn-'],
|
||||
['cache without runtime', w => w.jobs.coverage.steps.find(s => s.uses?.startsWith('actions/cache@')).with.key = '${{ runner.os }}-${{ github.ref }}'],
|
||||
['skipped dependencies on browser cache hit', w => w.jobs['browser-smoke'].steps.find(s => s.run?.startsWith('yarn test:browser:install')).if = 'steps.cache.outputs.cache-hit != \'true\''],
|
||||
['Windows pipeline without pipefail', w => w.jobs.checks.defaults.run.shell = 'sh'],
|
||||
['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'],
|
||||
['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\''],
|
||||
]) {
|
||||
test(`CI rejects ${name}`, () => {
|
||||
const workflow = YAML.parse(source)
|
||||
mutate(workflow)
|
||||
assert.throws(() => validateCIWorkflow(YAML.stringify(workflow)), { name: 'AssertionError' })
|
||||
})
|
||||
}
|
||||
+6
-2
@@ -749,11 +749,15 @@
|
||||
"ENG-09",
|
||||
"ENG-10"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "doing",
|
||||
"risk": "H",
|
||||
"deliverable": "OS/Node/TS/浏览器与影响范围矩阵、缓存、超时、汇总检查和 artifact 报告",
|
||||
"acceptance": "固定安装、失败/取消不误报、核心影响全生态;检查只读,失败证据可追溯",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"ci-setup.md",
|
||||
"changes/2026-09-13-CI-01-matrix-summary.md",
|
||||
"baselines/ci-matrix-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "CI-02",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export function ciContext(root, env = process.env) {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
||||
const node = fs.readFileSync(path.join(root, '.node-version'), 'utf8').trim()
|
||||
const yarn = manifest.packageManager?.replace(/^yarn@/, '')
|
||||
const playwright = manifest.devDependencies['@playwright/test']
|
||||
for (const version of [node, yarn, playwright]) assert(/^\d+\.\d+\.\d+$/.test(version), 'CI cache inputs must use exact versions')
|
||||
assert.equal(manifest.packageManager, `yarn@${yarn}`)
|
||||
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim()
|
||||
const downloads = path.resolve(env.RUNNER_TEMP || path.join(root, 'refactor/.cache/downloads'))
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
source,
|
||||
workflowSource: env.GITHUB_SHA || null,
|
||||
ref: env.GITHUB_REF || null,
|
||||
event: env.GITHUB_EVENT_NAME || 'local',
|
||||
run: env.GITHUB_RUN_ID || null,
|
||||
attempt: env.GITHUB_RUN_ATTEMPT || null,
|
||||
runUrl: env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : null,
|
||||
os: process.platform,
|
||||
arch: process.arch,
|
||||
node,
|
||||
actualNode: process.versions.node,
|
||||
yarn,
|
||||
playwright,
|
||||
yarnCache: path.join(downloads, 'artplayer-yarn-cache'),
|
||||
browserCache: path.join(downloads, 'artplayer-playwright'),
|
||||
lockSha256: createHash('sha256').update(fs.readFileSync(path.join(root, 'yarn.lock'))).digest('hex'),
|
||||
}
|
||||
}
|
||||
|
||||
export function writeContext(root, env = process.env) {
|
||||
const context = ciContext(root, env)
|
||||
const directory = path.join(root, 'refactor/.cache/ci')
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
fs.writeFileSync(path.join(directory, 'context.json'), `${JSON.stringify(context, null, 2)}\n`)
|
||||
for (const value of [context.yarnCache, context.browserCache]) assert(!/[\r\n]/.test(value), 'CI paths must fit one environment-file line')
|
||||
if (env.GITHUB_OUTPUT)
|
||||
fs.appendFileSync(env.GITHUB_OUTPUT, `node=${context.node}\nyarn=${context.yarn}\nplaywright=${context.playwright}\nyarn_cache=${context.yarnCache}\nbrowser_cache=${context.browserCache}\n`)
|
||||
if (env.GITHUB_ENV)
|
||||
fs.appendFileSync(env.GITHUB_ENV, `YARN_CACHE_FOLDER=${context.yarnCache}\nPLAYWRIGHT_BROWSERS_PATH=${context.browserCache}\n`)
|
||||
return context
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
||||
console.log(JSON.stringify(writeContext(process.cwd())))
|
||||
@@ -0,0 +1,65 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { ciContext } from './ci-context.mjs'
|
||||
|
||||
export const requiredJobs = ['checks', 'coverage', 'browser-smoke']
|
||||
|
||||
export function evaluateJobs(needs) {
|
||||
const object = needs !== null && typeof needs === 'object' && !Array.isArray(needs)
|
||||
const errors = []
|
||||
if (!object)
|
||||
errors.push('Job results must be an object')
|
||||
else if (Object.keys(needs).some(name => !requiredJobs.includes(name)))
|
||||
errors.push('Unexpected job entries; update the explicit required-job policy')
|
||||
const rows = requiredJobs.map((job) => {
|
||||
const value = object && Object.hasOwn(needs, job) ? needs[job]?.result : undefined
|
||||
const result = ['success', 'failure', 'cancelled', 'skipped'].includes(value) ? value : 'missing-or-invalid'
|
||||
return { job, result, passed: result === 'success' }
|
||||
})
|
||||
return { passed: errors.length === 0 && rows.every(row => row.passed), rows, errors }
|
||||
}
|
||||
|
||||
export function renderSummary(report) {
|
||||
return [
|
||||
'## ArtPlayer CI',
|
||||
'',
|
||||
`Result: **${report.passed ? 'passed' : 'failed'}**`,
|
||||
'',
|
||||
'| Required job | Result |',
|
||||
'| --- | --- |',
|
||||
...report.rows.map(row => `| ${row.job} | ${row.result} |`),
|
||||
'',
|
||||
'Success requires every configured matrix job to succeed. Skipped, cancelled and missing results do not pass.',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function summarize(root, env = process.env) {
|
||||
let needs
|
||||
let invalidJSON = false
|
||||
try {
|
||||
needs = JSON.parse(env.ARTPLAYER_CI_NEEDS || '')
|
||||
}
|
||||
catch {
|
||||
invalidJSON = true
|
||||
}
|
||||
const report = { schemaVersion: 1, context: ciContext(root, env), ...evaluateJobs(needs) }
|
||||
if (invalidJSON)
|
||||
report.errors.push('Job results were not valid JSON')
|
||||
const directory = path.join(root, 'refactor/.cache/ci')
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
fs.writeFileSync(path.join(directory, 'summary.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
const markdown = renderSummary(report)
|
||||
fs.writeFileSync(path.join(directory, 'summary.md'), markdown)
|
||||
if (env.GITHUB_STEP_SUMMARY)
|
||||
fs.appendFileSync(env.GITHUB_STEP_SUMMARY, markdown)
|
||||
return report
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const report = summarize(process.cwd())
|
||||
console.log(renderSummary(report))
|
||||
process.exitCode = report.passed ? 0 : 1
|
||||
}
|
||||
@@ -21,6 +21,7 @@ rather than assuming a fixed number of microtasks. The published defect observat
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `yarn test:unit` | Playback and DASH regressions, public contracts against released/current code, and JS/TS fixture loading |
|
||||
| `yarn test:node` | Unit contracts plus toolchain and documentation build failure propagation |
|
||||
| `yarn test:ci` | Actual CI summary exit codes, workflow regression guards and repository impact analysis |
|
||||
| `yarn test` | Node checks and the committed baseline/tooling tests in refactor/scripts |
|
||||
| `yarn ci:check` | Toolchain, plan, read-only lint, types, then yarn test |
|
||||
| `yarn test:imports` | Existing distribution import smoke examples; run after building |
|
||||
@@ -42,6 +43,13 @@ Set `ARTPLAYER_TEST_CORE` to a built `.js`, `.legacy.js` or `.mjs` core file to
|
||||
|
||||
When a production module moves, update its loader mapping and its maintenance documentation, preserving the behavioral assertions. New contracts belong in contracts/; test-specific controlled state belongs in helpers/. Do not modify frozen refactor/fixtures or baseline captures just to pass changed behavior. Record historical defects and candidate fixes separately.
|
||||
|
||||
`ci-summary.test.js` exercises the real CLI with success, cancelled and malformed provider data,
|
||||
plus missing groups and metadata/environment files. It is included in `test:node`.
|
||||
`refactor/scripts/ci-workflow.test.mjs` parses the actual YAML and rejects broken matrix,
|
||||
cache, install, report and summary requirements; it is included in `test:baseline`.
|
||||
These local tests do not replace hosted runner or branch-protection evidence.
|
||||
See [CI operations](../refactor/ci-setup.md) before changing a required job or its cache.
|
||||
|
||||
# Real browser tests
|
||||
|
||||
`test/utils.test.js` compares the published and current utilities, including exact formatting,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Exercise actual CI exit codes and report files.
|
||||
import test from 'node:test'
|
||||
import { ciContext, writeContext } from '../scripts/ci-context.mjs'
|
||||
import { evaluateJobs, renderSummary, requiredJobs } from '../scripts/ci-summary.mjs'
|
||||
|
||||
const success = () => Object.fromEntries(requiredJobs.map(job => [job, { result: 'success' }]))
|
||||
test('CI summary requires every configured group and does not mutate provider results', () => {
|
||||
const needs = success()
|
||||
const copy = structuredClone(needs)
|
||||
const report = evaluateJobs(needs)
|
||||
assert.equal(report.passed, true)
|
||||
assert.deepEqual(needs, copy)
|
||||
assert.deepEqual(report.rows.map(row => row.job), requiredJobs)
|
||||
})
|
||||
|
||||
for (const result of ['failure', 'cancelled', 'skipped', 'unknown', undefined]) {
|
||||
test(`CI summary fails when any required matrix group returns ${result}`, () => {
|
||||
for (const job of requiredJobs) {
|
||||
const needs = success()
|
||||
needs[job].result = result
|
||||
assert.equal(evaluateJobs(needs).passed, false)
|
||||
delete needs[job]
|
||||
assert.equal(evaluateJobs(needs).passed, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('CI rejects malformed or additional job results and does not render arbitrary status text', () => {
|
||||
for (const input of [null, [], 'success', {}, { ...success(), extra: { result: 'success' } }])
|
||||
assert.equal(evaluateJobs(input).passed, false)
|
||||
const needs = success()
|
||||
needs.checks.result = '<img src=x onerror=alert(1)>'
|
||||
assert(!renderSummary(evaluateJobs(needs)).includes('<img'))
|
||||
})
|
||||
|
||||
function fixture(t) {
|
||||
const root = fs.mkdtempSync(path.resolve('refactor/.cache/ci-summary-test-'))
|
||||
for (const file of ['package.json', '.node-version', 'yarn.lock']) fs.copyFileSync(file, path.join(root, file))
|
||||
t.after(() => {
|
||||
assert(path.dirname(root) === path.resolve('refactor/.cache') && path.basename(root).startsWith('ci-summary-test-'))
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
return root
|
||||
}
|
||||
|
||||
test('CI context uses exact source/tool/lock inputs and writes absolute cache paths via environment files', (t) => {
|
||||
const root = fixture(t)
|
||||
const output = path.join(root, 'outputs')
|
||||
const envFile = path.join(root, 'environment')
|
||||
const env = { GITHUB_OUTPUT: output, GITHUB_ENV: envFile, RUNNER_TEMP: path.join(root, 'runner temp') }
|
||||
const context = writeContext(root, env)
|
||||
assert.match(context.source, /^[a-f0-9]{40}$/)
|
||||
assert.equal(context.yarn, '1.22.22')
|
||||
assert(fs.readFileSync(output, 'utf8').includes(`playwright=${context.playwright}\n`))
|
||||
assert(fs.readFileSync(envFile, 'utf8').includes(`PLAYWRIGHT_BROWSERS_PATH=${context.browserCache}\n`))
|
||||
assert.equal(context.yarnCache, path.join(env.RUNNER_TEMP, 'artplayer-yarn-cache'))
|
||||
fs.appendFileSync(path.join(root, 'yarn.lock'), '\n')
|
||||
assert.notEqual(ciContext(root, env).lockSha256, context.lockSha256)
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
||||
manifest.packageManager = 'npm@1.22.22'
|
||||
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(manifest))
|
||||
assert.throws(() => ciContext(root, env), { name: 'AssertionError' })
|
||||
})
|
||||
|
||||
test('CI command returns real failure codes and preserves reports for success, failure and invalid JSON', (t) => {
|
||||
const root = fixture(t)
|
||||
const stepSummary = path.join(root, 'step-summary.md')
|
||||
for (const [needs, expected] of [[JSON.stringify(success()), 0], [JSON.stringify({ ...success(), coverage: { result: 'cancelled' } }), 1], ['{bad', 1]]) {
|
||||
const result = spawnSync(process.execPath, [path.resolve('scripts/ci-summary.mjs')], { cwd: root, encoding: 'utf8', env: { ...process.env, ARTPLAYER_CI_NEEDS: needs, GITHUB_STEP_SUMMARY: stepSummary } })
|
||||
assert.equal(result.status, expected, result.stderr)
|
||||
const report = JSON.parse(fs.readFileSync(path.join(root, 'refactor/.cache/ci/summary.json'), 'utf8'))
|
||||
assert.equal(report.passed, expected === 0)
|
||||
assert(fs.readFileSync(path.join(root, 'refactor/.cache/ci/summary.md'), 'utf8').includes(expected === 0 ? '**passed**' : '**failed**'))
|
||||
}
|
||||
assert(fs.readFileSync(stepSummary, 'utf8').includes('cancelled'))
|
||||
})
|
||||
Reference in new issue
Block a user