mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-15 00:56:16 -08:00
ci(pages): [CI-02] validate staged artifacts and preserve legacy demo paths
This commit is contained in:
1 parent
b1e0b976b0
commit
72d2b9c36b
20 files changed
+6120
-13
No files matched your search
@@ -215,6 +215,16 @@ jobs:
|
||||
run: yarn ci:check 2>&1 | tee refactor/.cache/ci/check.log
|
||||
- name: Build libraries and documentation
|
||||
run: yarn ci:build 2>&1 | tee refactor/.cache/ci/build.log
|
||||
- name: Validate and stage Pages paths
|
||||
id: pages
|
||||
if: inputs.pages-artifact && github.ref == 'refs/heads/master' && matrix.os == 'ubuntu-latest'
|
||||
run: yarn prepare:pages 2>&1 | tee refactor/.cache/ci/pages.log
|
||||
- name: Install staged Pages browser dependencies
|
||||
if: inputs.pages-artifact && github.ref == 'refs/heads/master' && matrix.os == 'ubuntu-latest'
|
||||
run: yarn test:browser:install --with-deps 2>&1 | tee refactor/.cache/ci/pages-browser-install.log
|
||||
- name: Verify staged Pages entrypoints
|
||||
if: inputs.pages-artifact && github.ref == 'refs/heads/master' && matrix.os == 'ubuntu-latest'
|
||||
run: yarn test:pages:browser 2>&1 | tee refactor/.cache/ci/pages-browser.log
|
||||
- name: Record source and tool versions
|
||||
if: always()
|
||||
run: |
|
||||
@@ -229,7 +239,10 @@ jobs:
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: checks-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: refactor/.cache/ci/
|
||||
path: |
|
||||
refactor/.cache/ci/
|
||||
refactor/.cache/pages/run-*/report.json
|
||||
refactor/.cache/pages/run-*/browser.json
|
||||
include-hidden-files: true
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
@@ -237,7 +250,7 @@ jobs:
|
||||
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
|
||||
path: ${{ steps.pages.outputs.site }}
|
||||
include-hidden-files: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
+8
-5
@@ -40,12 +40,12 @@
|
||||
"test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js",
|
||||
"dev": "npx cross-env NODE_ENV=development node ./scripts/dev.js",
|
||||
"build": "npx cross-env NODE_ENV=production node ./scripts/build.js",
|
||||
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build,library}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
|
||||
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build,library,pages}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
|
||||
"build:all": "yarn ci:build && yarn lint",
|
||||
"check:toolchain": "node scripts/check-toolchain.mjs",
|
||||
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build,library}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
|
||||
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build,library,pages}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"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/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js test/dev-server.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/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js test/dev-server.test.js test/pages-artifact.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 check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn check:release-ledger && yarn lint && yarn typecheck:library && yarn typecheck:scaffold && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:llm && yarn typecheck:site-assets && yarn check:site-assets && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test",
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:test && yarn build:docs && yarn test:imports",
|
||||
@@ -94,7 +94,7 @@
|
||||
"test:jassub-types": "node --test refactor/scripts/jassub-types.test.mjs",
|
||||
"test:jassub-types-package": "node refactor/scripts/jassub-package-types.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 test/package-runtime.test.js",
|
||||
"test:ci": "node --test test/ci-summary.test.js refactor/scripts/ci-workflow.test.mjs refactor/scripts/impact.test.mjs test/package-runtime.test.js test/pages-artifact.test.js",
|
||||
"test:package:runtime": "node scripts/package-runtime.mjs --canonical",
|
||||
"test:asr": "node --test test/asr.test.js test/asr-audio.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-distribution.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js",
|
||||
"test:asr-types-package": "node refactor/scripts/asr-package-types.mjs",
|
||||
@@ -137,7 +137,10 @@
|
||||
"check:release-ledger": "node refactor/scripts/release-ledger.mjs",
|
||||
"report:release-ledger": "node refactor/scripts/release-ledger.mjs --report",
|
||||
"release:preflight": "yarn check:toolchain --strict && node refactor/scripts/release-ledger.mjs --report --strict",
|
||||
"test:release-ledger": "node --test refactor/scripts/release-ledger.test.mjs"
|
||||
"test:release-ledger": "node --test refactor/scripts/release-ledger.test.mjs",
|
||||
"prepare:pages": "node scripts/prepare-pages.mjs",
|
||||
"test:pages": "node --test test/pages-artifact.test.js",
|
||||
"test:pages:browser": "node test/pages-browser.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
| [全包大版本策略](version-policy.md) | 每包分别升级一个 major 的目标清单、兼容要求和版本落地步骤 |
|
||||
| [已实现的 CI 入口](ci-setup.md) | Yarn 检查/构建、只读 lint、Pages 隔离及远端待验收状态 |
|
||||
| [GitHub CI/CD](github-ci-cd.md) | PR/兼容矩阵、构建报告、Pages、npm 发布及远端准入验证 |
|
||||
| [Pages部署与恢复](pages-deployment.md) | 独立暂存、旧URL/域名预检、只读远端快照与人工切换恢复顺序 |
|
||||
| [AI 协作流程](ai-workflow.md) | AI 接续工作、任务边界、验证、记录和交接模板 |
|
||||
| [每任务提交审计](commit-audit.md) | 实际Git状态迁移、独立提交、初始例外、分支合并和CI报告 |
|
||||
| [全包影响映射](impact-analysis.md) | 变更范围、依赖/验证关系、必需CI命令及尚缺的包验证 |
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
# CI-02 Pages产物预检与独立部署
|
||||
|
||||
生产构建只更新compiled,而现有移动页、iframe和编辑器示例继续使用uncompiled。
|
||||
直接上传docs可能保留旧开发产物。本次在独立暂存目录按现有开发配置构建21个旧入口,
|
||||
保持旧URL/全局名称/开发模式,工作区docs和包产物不被改写。没有更换播放器API、版本或依赖。
|
||||
|
||||
新增pages/artifact.ts负责文件、路径、域名和compiled一致性,prepare.ts负责暂存和
|
||||
调用现有library构建模块,入口CLI只处理命令/输出。包源码、构建输入和完整站点输出
|
||||
都有指纹。CNAME/.nojekyll、旧HTML、语言/编辑器/i18n入口必须存在;规则不声称覆盖
|
||||
所有动态外部链接。test:pages及test:pages:browser加入package scripts,普通Node测试
|
||||
运行文件/工作流回归,浏览器验证仅在显式命令或受信任Pages准备分支中执行。
|
||||
|
||||
工作流先build,再prepare,再安装浏览器和执行暂存入口检查,最后上传准确site路径。
|
||||
部署继续等待全部CI作业成功,保留master/启用变量/manual/environment限制;仅deploy
|
||||
有Pages/OIDC写权限。不增加远端操作,失败不上传用于部署的站点,但保留诊断报告。
|
||||
|
||||
## 实际验证
|
||||
|
||||
- 21个入口构建成功,63项compiled/dist比较一致,暂存546个文件;正常目录与包源码未改。
|
||||
- 55项CI/文件/权限回归通过;严格library TS、定向lint与actionlint1.7.12通过。
|
||||
- 同一暂存目录的demo/ESM两种入口,Windows Chromium153.0.8010.12、Firefox155.0、
|
||||
WebKit26.6共6项实际播放、章节更新、网页全屏和销毁检查通过。
|
||||
- 首轮拦截MP4完整响应使WebKit两项超时;第二轮跨端口原生HTTP使Chromium两项媒体
|
||||
ERR_FAILED,其余通过。最终使用同源HTTP/Range。没有确定跨端口错误的浏览器内部根因。
|
||||
- 同源首轮全部走完播放,但Chromium两项把媒体ERR_ABORTED并入脚本错误断言而失败。
|
||||
修正为记录网络事件、检查脚本加载错误/JS异常以及播放时的video.error。保留原失败报告,
|
||||
没有为掩盖产品错误增加等待或关闭浏览器;首轮默认30秒,最终显式10秒。
|
||||
- Pages API、分支/环境只读查询和7个线上HEAD200留档;部署变量返回404、启用状态未知。
|
||||
旧gh-pages提交的本地ZIP已保存;没有修改Pages source或发布站点。
|
||||
|
||||
证据见[机器记录](../baselines/pages-artifact-validation.json),维护地图和恢复流程见
|
||||
[部署说明](../pages-deployment.md)。CI-02只完成本地实现;CI-04仍负责真实远端run、
|
||||
环境迁移、部署后探测及故障演练。站点/插件/设备/许可和发布复盘未自动完成。
|
||||
|
||||
回退本提交恢复原工作流上传docs的配置,移除新脚本/测试/状态;远端设置不会随Git回退,
|
||||
必须按部署说明单独处理。不push、不publish,也不执行远端恢复写入。
|
||||
@@ -97,6 +97,13 @@ CORE-25 已修复无 navigator 默认选项缺陷,严格 test:package:release
|
||||
|
||||
## Pages 隔离与启用条件
|
||||
|
||||
CI-02已实现独立暂存与旧路径预检,并验证21个重新构建的uncompiled入口、63个
|
||||
compiled/dist匹配和6项三浏览器暂存播放检查。上传路径为prepare:pages的精确输出,
|
||||
不再直接上传工作区docs;新增脚本和回退说明见[Pages部署](pages-deployment.md)。
|
||||
2026-09-14只读API确认线上仍为gh-pages根目录的legacy部署,域名github.artplayer.org;
|
||||
环境允许master/gh-pages,启用变量查询404,状态未知。后面的初始记录保留为历史背景,
|
||||
其“远端未知”描述不代表本次没有读取配置;CI-04实际部署/切换仍未验证。
|
||||
|
||||
ENG-07 将浏览器候选改为 `yarn test:package` 实际打包并安装后的文件,使用输出映射禁止
|
||||
回退到源码。安装/构建日志、tarball、成员指纹和浏览器结果一并上传,构建快照的 node_modules
|
||||
链接不上传。初始包范围为 core/chapter;PKG-CHAPTER-04 已将这两个包的已知类型诊断清零,
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
|
||||
## 目标流水线
|
||||
|
||||
CI-02的本地Pages产物链已实现:build后在独立目录更新全部旧开发入口,验证路径/
|
||||
域名/分发一致性并检查暂存播放,然后上传准确输出。只读远端配置快照、迁移与恢复
|
||||
顺序见[Pages记录](pages-deployment.md);实际部署仍待CI-04和明确授权。
|
||||
|
||||
| 流程 | 触发和范围 | 交付/门槛 |
|
||||
| --- | --- | --- |
|
||||
| PR 和主线 CI | PR、主线及重构分支的相关提交;支持人工重跑 | 固定安装、只读 lint、类型、单元/生命周期、影响范围内的浏览器和产物测试、计划/文档检查 |
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Pages 迁移、验收与恢复
|
||||
|
||||
CI-02交付本地配置与预检;CI-04负责实际远端运行和配置切换。当前没有部署、推送、
|
||||
修改仓库变量或调整Pages source。完整站点内容、外部SDK和三轮发布复盘门槛仍适用。
|
||||
|
||||
## 2026-09-14只读快照
|
||||
|
||||
- Pages API:`build_type=legacy`,source为`gh-pages`、根目录,status=built。
|
||||
- CNAME与html_url:`github.artplayer.org`、`https://github.artplayer.org/`。
|
||||
HTTPS enforced,证书state=approved;这些是API返回值,不是独立TLS/DNS审计。
|
||||
- gh-pages提交:`a6d2b81d05c68c8c78cbe62ca0b06761c983f8b8`。本地对象已存在,
|
||||
已用git archive保存ZIP恢复副本,路径/内容哈希见CI-02机器记录。
|
||||
- github-pages环境为custom branch policies,读取到master与gh-pages两个branch规则。
|
||||
protection_rules只有branch_policy;未据此新增或删除规则。
|
||||
- PAGES_DEPLOY_ENABLED查询返回404:不能据此证明变量不存在或权限完整;启用状态未确认。
|
||||
- 对根页、mobile.html、iframe.html、esm.html、document/、document/en/、
|
||||
compiled/artplayer.js的HTTPS HEAD均返回200。未执行线上播放器或广告交互。
|
||||
|
||||
以上是时间点快照。发布前再次只读查询,核对源码SHA、旧分支、域名、环境保护和变量,
|
||||
不能把它们写成永久配置或实际Actions部署已成功。
|
||||
|
||||
## 本地与CI产物链
|
||||
|
||||
1. 使用.node-version与Yarn1.22.22,frozen安装,执行ci:check和ci:build。
|
||||
2. prepare:pages检查compiled与dist一致,将docs复制到独立cache目录,然后正常构建
|
||||
21个旧uncompiled入口。保留CNAME/.nojekyll、旧HTML与两种语言的文档入口。
|
||||
3. test:pages:browser检查暂存目录的demo/ESM入口。全部所需CI作业成功后,官方
|
||||
upload-pages-artifact只上传本次步骤输出的site,deploy-pages在github-pages环境执行。
|
||||
4. 报告记录source、来源文件/锁/构建脚本、63个分发入口及完整暂存文件指纹;没有
|
||||
用工作区docs路径替代暂存目录,也没有从外部workflow run下载同名artifact。
|
||||
|
||||
脚本边界和命令见[scripts/pages](../scripts/pages/README.md)。GitHub对Pages artifact
|
||||
要求无符号/硬链接,部署需要对应权限、environment及构建依赖,见
|
||||
[官方自定义工作流说明](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages)。
|
||||
当前固定Actions的SHA沿用已验证配置,未升级依赖。
|
||||
|
||||
## 获得实际部署授权后的切换顺序
|
||||
|
||||
先确认完整发布门槛和目标master源码,再重新保存当前gh-pages提交及完整旧目录。
|
||||
核对github-pages只允许受信任分支及必要审批,把Pages source切换为GitHub Actions。
|
||||
确认自定义域名/DNS/TLS保持原值后,设置PAGES_DEPLOY_ENABLED=true并手动运行Deploy Pages。
|
||||
记录run ID/attempt/SHA、预检报告哈希、artifact与部署URL;不要只根据workflow名称信任产物。
|
||||
|
||||
部署后逐项GET验证contract.json的旧路径、MIME和关键文本,再在真实页面验证桌面/
|
||||
移动编辑器、中文/英文文档、ESM、iframe及播放/章节。HEAD200只证明可访问,不能代替
|
||||
这些交互。检查被重写或丢失的路径,以及compiled/uncompiled确实来自批准的候选。
|
||||
|
||||
## 恢复
|
||||
|
||||
发生故障先禁用启用变量,避免新部署进入队列;不要取消已经开始的写入或自动force push。
|
||||
保存失败run/部署/请求证据,并重新读取当前source和分支SHA。
|
||||
|
||||
如果旧gh-pages仍指向已保存的提交,经授权将Pages source恢复为该分支根目录,保留
|
||||
原域名/HTTPS并等待Pages完成构建,然后重复旧路径和交互验证。如果分支已被其他人
|
||||
修改,不能直接移动它:先核对保存ZIP的哈希与Git提交内容,准备明确的恢复提交/操作
|
||||
供批准。恢复本次仓库代码可以revert CI-02提交,但revert不会自行恢复远端设置。
|
||||
|
||||
旧自动force-push工作流不是默认恢复方案。任何再次构建、内容调整或候选替换都要重跑
|
||||
适用检查;不能把之前的绿灯套用到新的站点字节。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 255 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 53 / doing 17 / blocked 0 / done 185 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 52 / doing 17 / blocked 0 / done 186 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 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-02 | workspace<br>分离并改进 GitHub Pages 部署 | DOC-10, ENG-02, SITE-03 | Pages artifact 部署配置、旧路径/域名核对、预检和迁移恢复指南 | 部署只取受信任已验证产物;本地实现可验收,远端 source/环境和实际部署状态单独登记 | H | done |
|
||||
| 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 |
|
||||
|
||||
@@ -497,6 +497,7 @@
|
||||
- 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)
|
||||
- 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)
|
||||
- CORE-02: [记录](changes/2026-09-11-CORE-02-typed-emitter.md) [记录](baselines/emitter-validation.json)
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# 进度与证据
|
||||
|
||||
## CI-02 Pages产物预检与恢复流程完成
|
||||
|
||||
生产构建后的独立暂存目录重新生成21个旧uncompiled入口,验证63个compiled/dist
|
||||
匹配、域名/隐藏文件/旧路径,登记546个文件指纹;工作流只上传该步骤输出,且必须
|
||||
经过暂存入口三浏览器验证和全部CI作业。55项CI回归、严格TS、lint/actionlint通过,
|
||||
最终6项demo/ESM原生播放、章节更新、网页全屏和销毁检查通过。前几轮媒体服务/
|
||||
网络错误断言导致的失败全部留档,不改变产品、不放宽候选断言来掩盖故障。
|
||||
只读API确认当前线上仍是gh-pages分支根目录legacy部署,域名github.artplayer.org;
|
||||
环境允许master/gh-pages,启用变量404仍未知。七个关键路径HEAD200,旧提交ZIP
|
||||
已保存。没有远端写入、部署、push或publish。见[变更](changes/2026-09-14-CI-02-pages-artifact.md)、
|
||||
[证据](baselines/pages-artifact-validation.json)与[恢复说明](pages-deployment.md)。
|
||||
255项:186 done、17 doing、52 todo。CI-04保留实际切换/远端运行/发布后验收,插件、
|
||||
站点、设备及复盘门槛未关闭。Chapter桌面记录已核对,时序疑点和真机缺口继续保持doing。
|
||||
|
||||
## MOD-03 核心热路径测量与优化完成
|
||||
|
||||
完成检查点23a4f22b7后的剩余验收:原生存储三引擎三组旧新9项、安装包布局/设置
|
||||
|
||||
@@ -92,8 +92,42 @@ export function validateCIWorkflow(source) {
|
||||
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 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')
|
||||
const prepareIndex = workflow.jobs.checks.steps.findIndex(step => step.id === 'pages')
|
||||
const prepare = workflow.jobs.checks.steps[prepareIndex]
|
||||
assert(prepare && prepare.if === pages.if && prepare.run === 'yarn prepare:pages 2>&1 | tee refactor/.cache/ci/pages.log', 'Pages upload needs the matching strict preparation step')
|
||||
assert(prepareIndex > workflow.jobs.checks.steps.findIndex(step => step.run?.startsWith('yarn ci:build ')) && prepareIndex < workflow.jobs.checks.steps.indexOf(pages), 'Build, preflight and upload must be ordered')
|
||||
assert.equal(pages.with.path, '${{ steps.pages.outputs.site }}', 'Upload the exact validated site, never the working docs tree')
|
||||
let previous = prepareIndex
|
||||
for (const command of ['yarn test:browser:install --with-deps 2>&1 | tee refactor/.cache/ci/pages-browser-install.log', 'yarn test:pages:browser 2>&1 | tee refactor/.cache/ci/pages-browser.log']) {
|
||||
const index = workflow.jobs.checks.steps.findIndex(step => step.run === command)
|
||||
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 }
|
||||
}
|
||||
|
||||
export function validatePagesWorkflow(source) {
|
||||
const workflow = YAML.parse(source, { uniqueKeys: true })
|
||||
assert.deepEqual(Object.keys(workflow.on), ['workflow_dispatch'], 'Pages needs an explicit trusted dispatch')
|
||||
assert.deepEqual(workflow.permissions, { contents: 'read' })
|
||||
assert.deepEqual(workflow.concurrency, { 'group': 'pages', 'cancel-in-progress': false })
|
||||
assert.deepEqual(Object.keys(workflow.jobs).sort(), ['deploy', 'validate'])
|
||||
const { validate, deploy } = workflow.jobs
|
||||
for (const job of [validate, deploy]) {
|
||||
assert.equal(job.if, 'github.ref == \'refs/heads/master\' && vars.PAGES_DEPLOY_ENABLED == \'true\'')
|
||||
assert(!job['continue-on-error'])
|
||||
}
|
||||
assert.equal(validate.uses, './.github/workflows/nodejs.yml')
|
||||
assert.deepEqual(validate.with, { 'pages-artifact': true })
|
||||
assert.equal(deploy.needs, 'validate', 'Deploy only after every reusable CI job succeeds')
|
||||
assert.deepEqual(deploy.permissions, { 'pages': 'write', 'id-token': 'write' })
|
||||
assert.equal(deploy.environment.name, 'github-pages')
|
||||
assert.equal(deploy['timeout-minutes'], 10)
|
||||
assert.equal(deploy.steps.length, 1, 'Deploy must not execute or rebuild downloaded code')
|
||||
assert.match(deploy.steps[0].uses, /^actions\/deploy-pages@[a-f0-9]{40}$/)
|
||||
assert(!deploy.steps[0].if && !deploy.steps[0]['continue-on-error'] && !deploy.steps[0].with, 'Use the validated current-run Pages artifact without overrides')
|
||||
return { dispatch: 'master', deployment: 'github-pages', enabledBy: 'PAGES_DEPLOY_ENABLED' }
|
||||
}
|
||||
|
||||
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'))))
|
||||
console.log(JSON.stringify({ ci: validateCIWorkflow(fs.readFileSync('.github/workflows/nodejs.yml', 'utf8')), pages: validatePagesWorkflow(fs.readFileSync('.github/workflows/pages.yml', 'utf8')) }))
|
||||
+6
-2
@@ -773,11 +773,15 @@
|
||||
"ENG-02",
|
||||
"SITE-03"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "Pages artifact 部署配置、旧路径/域名核对、预检和迁移恢复指南",
|
||||
"acceptance": "部署只取受信任已验证产物;本地实现可验收,远端 source/环境和实际部署状态单独登记",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-14-CI-02-pages-artifact.md",
|
||||
"baselines/pages-artifact-validation.json",
|
||||
"pages-deployment.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "CI-03",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Pages artifact preparation
|
||||
|
||||
`yarn prepare:pages` runs after `yarn ci:build`. It never changes `docs/`, a
|
||||
distribution, a Git ref or remote settings. It creates a fresh
|
||||
`refactor/.cache/pages/run-*/site` and prints that exact directory as the Pages
|
||||
step output. `latest.json` points only to the last successfully validated build;
|
||||
a failed preparation must stop the workflow, not reuse an earlier output.
|
||||
|
||||
- `contract.json` preserves the domain and selected historical HTML, language,
|
||||
editor, compiled, i18n and smoke routes. It is a route contract, not an exhaustive
|
||||
HTML/CSS/JavaScript link crawler or external-service verification.
|
||||
- `artifact.ts` inventories ordinary files, rejects symbolic/hard links and path
|
||||
escapes, verifies the domain/required files, and compares all 63 main/legacy/ESM
|
||||
compiled entries against the corresponding 21 package distributions.
|
||||
- `prepare.ts` copies the checked tree, then rebuilds all 21 historical
|
||||
`uncompiled/<package>/index.js` URLs with the same IIFE/development configuration
|
||||
as `yarn dev`. This avoids publishing checked-in dev output from an older source.
|
||||
Package source/build/lock inputs and every staged file are fingerprinted.
|
||||
- `../prepare-pages.mjs` is the argument-free CLI and GitHub output adapter.
|
||||
|
||||
The output does not promise that the input `dist` was freshly built: the workflow
|
||||
must run `ci:build` first. Local users follow the same sequence. Existing static
|
||||
vendor assets are copied; provenance and full editor validation remain SITE/REL
|
||||
gates. No package-manager or dependency change is required.
|
||||
|
||||
`yarn test:pages` covers missing/empty routes, changed CNAME, links/path escape,
|
||||
stale/missing distribution output and deployment bypasses. `yarn test:pages:browser`
|
||||
serves staged core/chapter demo and ESM entries to Chromium/Firefox/WebKit. A
|
||||
controlled page and same-origin native HTTP/Range MP4 exercise playback, chapter
|
||||
update, web fullscreen and destroy. It records exact script hashes and network
|
||||
failures separately from JavaScript errors; media cancellation is not automatically
|
||||
a script-load failure. This is not a full real-editor, SDK or physical-device test.
|
||||
|
||||
The deploy workflow remains manual, restricted to master with
|
||||
`PAGES_DEPLOY_ENABLED=true`, and depends on every reusable CI job. Its sole deploy
|
||||
step accepts the current run's official Pages artifact. No PR artifact download,
|
||||
arbitrary artifact ID, lifecycle rebuild or Git push is introduced. The checks job
|
||||
has read-only contents permissions; only deployment receives Pages/OIDC writes.
|
||||
|
||||
Failed staging builds keep their report once the output directory exists. Earlier
|
||||
preflight failures stay in the CI log. Reports are uploaded even on failure; staged
|
||||
files are uploaded for deployment only after preparation and browser checks pass.
|
||||
See `refactor/pages-deployment.md` for remote state, activation and recovery.
|
||||
@@ -0,0 +1,68 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export interface PagesContract { domain: string, requiredPaths: string[] }
|
||||
export interface FileIdentity { bytes: number, sha256: string }
|
||||
|
||||
export function digest(file: string): FileIdentity {
|
||||
const bytes = fs.readFileSync(file)
|
||||
return { bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') }
|
||||
}
|
||||
|
||||
export function inside(root: string, relative: string): string {
|
||||
assert(relative && !relative.includes('\\') && !path.isAbsolute(relative), `Invalid artifact path: ${relative}`)
|
||||
const result = path.resolve(root, relative)
|
||||
const actual = path.relative(path.resolve(root), result)
|
||||
assert(actual && actual !== '..' && !actual.startsWith(`..${path.sep}`) && !path.isAbsolute(actual), `Artifact path escaped root: ${relative}`)
|
||||
return result
|
||||
}
|
||||
|
||||
export function inventory(root: string): Record<string, FileIdentity> {
|
||||
assert(fs.lstatSync(root).isDirectory() && !fs.lstatSync(root).isSymbolicLink(), 'Artifact root must be a real directory')
|
||||
const files: Record<string, FileIdentity> = {}
|
||||
function walk(directory: string) {
|
||||
for (const name of fs.readdirSync(directory).sort()) {
|
||||
const file = path.join(directory, name)
|
||||
const stat = fs.lstatSync(file)
|
||||
assert(!stat.isSymbolicLink(), `Pages cannot contain symbolic links: ${file}`)
|
||||
if (stat.isDirectory()) {
|
||||
walk(file)
|
||||
}
|
||||
else {
|
||||
assert(stat.isFile() && stat.nlink === 1, `Pages requires ordinary files without hard links: ${file}`)
|
||||
const relative = path.relative(root, file).split(path.sep).join('/')
|
||||
files[relative] = digest(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
assert(Object.values(files).reduce((total, file) => total + file.bytes, 0) < 10_000_000_000, 'Pages artifact exceeds the 10GB archive ceiling')
|
||||
return files
|
||||
}
|
||||
|
||||
export function validateArtifact(root: string, contract: PagesContract) {
|
||||
const files = inventory(root)
|
||||
for (const relative of contract.requiredPaths) {
|
||||
inside(root, relative)
|
||||
assert(Object.hasOwn(files, relative), `Missing required Pages route: ${relative}`)
|
||||
assert(relative === '.nojekyll' || files[relative]!.bytes > 0, `Empty Pages route: ${relative}`)
|
||||
}
|
||||
assert.equal(fs.readFileSync(inside(root, 'CNAME'), 'utf8').trim(), contract.domain, 'Pages domain changed')
|
||||
return files
|
||||
}
|
||||
|
||||
export function compareCompiled(root: string, site: string, names: string[]) {
|
||||
const checked: string[] = []
|
||||
for (const name of names) {
|
||||
for (const suffix of ['.js', '.legacy.js', '.mjs']) {
|
||||
const relative = `${name}${suffix}`
|
||||
const source = inside(root, `packages/${name}/dist/${relative}`)
|
||||
const target = inside(site, `compiled/${relative}`)
|
||||
assert.equal(digest(target).sha256, digest(source).sha256, `Stale compiled Pages artifact: ${relative}`)
|
||||
checked.push(relative)
|
||||
}
|
||||
}
|
||||
return checked
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"domain": "github.artplayer.org",
|
||||
"requiredPaths": [
|
||||
"index.html",
|
||||
"mobile.html",
|
||||
"esm.html",
|
||||
"iframe.html",
|
||||
"i18n.html",
|
||||
".nojekyll",
|
||||
"CNAME",
|
||||
"ads.txt",
|
||||
"googlebcdef0d1e9238554.html",
|
||||
"document/index.html",
|
||||
"document/en/index.html",
|
||||
"document/start/option.html",
|
||||
"document/en/start/option.html",
|
||||
"test/index.html",
|
||||
"test/test.js",
|
||||
"assets/js/bootstrap.js",
|
||||
"assets/js/common.js",
|
||||
"assets/js/mobile.js",
|
||||
"compiled/artplayer.js",
|
||||
"compiled/artplayer.legacy.js",
|
||||
"compiled/artplayer.mjs",
|
||||
"compiled/i18n/zh-tw.js",
|
||||
"compiled/i18n/id.mjs",
|
||||
"uncompiled/artplayer/index.js",
|
||||
"uncompiled/artplayer-tool-iframe/index.js",
|
||||
"llms.txt",
|
||||
"llms.manifest.json"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { PagesContract } from './artifact.ts'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { build } from 'vite'
|
||||
import { getViteBuildConfig } from '../library/config.ts'
|
||||
import { getGlobalName } from '../library/names.ts'
|
||||
import { getEntryFile, getProjects } from '../library/projects.ts'
|
||||
import { compareCompiled, digest, inside, inventory, validateArtifact } from './artifact.ts'
|
||||
|
||||
export async function preparePages(root: string) {
|
||||
const contract: PagesContract = JSON.parse(fs.readFileSync(path.join(root, 'scripts/pages/contract.json'), 'utf8'))
|
||||
const projects = getProjects(root)
|
||||
const names = Object.keys(projects).sort()
|
||||
const docs = path.join(root, 'docs')
|
||||
// Validate before copying, so redirected input paths cannot be followed by cp.
|
||||
inventory(docs)
|
||||
const compiled = compareCompiled(root, docs, names)
|
||||
const cache = path.join(root, 'refactor/.cache/pages')
|
||||
fs.mkdirSync(cache, { recursive: true })
|
||||
const output = fs.mkdtempSync(path.join(cache, 'run-'))
|
||||
const site = inside(output, 'site')
|
||||
fs.cpSync(docs, site, { recursive: true })
|
||||
const inputs = Object.fromEntries(names.map(name => [name, { manifest: digest(path.join(projects[name]!, 'package.json')), source: inventory(path.join(projects[name]!, 'src')) }]))
|
||||
const report = { source: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(), inputs, buildInputs: { library: inventory(path.join(root, 'scripts/library')), pages: inventory(path.join(root, 'scripts/pages')), lock: digest(path.join(root, 'yarn.lock')) }, output, compiled, contract: digest(path.join(root, 'scripts/pages/contract.json')), aliases: [] as string[], status: 'building', error: '', files: {} as ReturnType<typeof inventory> }
|
||||
try {
|
||||
for (const name of names) {
|
||||
// Preserve old demo URLs with fresh builds using the same configuration as yarn dev.
|
||||
const outDir = inside(site, `uncompiled/${name}`)
|
||||
await build({ root: projects[name], ...getViteBuildConfig({ entry: getEntryFile(projects[name]!), outDir, name: getGlobalName(name), format: 'iife', fileName: 'index.js', minify: false, emptyOutDir: true }) })
|
||||
report.aliases.push(`uncompiled/${name}/index.js`)
|
||||
}
|
||||
report.files = validateArtifact(site, { ...contract, requiredPaths: [...contract.requiredPaths, ...report.aliases] })
|
||||
report.status = 'validated'
|
||||
fs.writeFileSync(path.join(cache, 'latest.json'), `${JSON.stringify({ output }, null, 2)}\n`)
|
||||
return { output, site, report }
|
||||
}
|
||||
catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = String(error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
fs.writeFileSync(path.join(output, 'report.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { preparePages } from './pages/prepare.ts'
|
||||
|
||||
assert.equal(process.argv.length, 2, 'Use yarn prepare:pages without arguments')
|
||||
const result = await preparePages(process.cwd())
|
||||
if (process.env.GITHUB_OUTPUT)
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `site=${result.site}\n`)
|
||||
console.log(`Pages artifact validated: ${result.site}; ${Object.keys(result.report.files).length} files. No deployment was performed.`)
|
||||
@@ -6,5 +6,5 @@
|
||||
"allowImportingTsExtensions": true,
|
||||
"checkJs": true
|
||||
},
|
||||
"include": ["library/**/*.ts", "build.js", "dev.js", "utils.js", "projects.js", "rebuild.js", "build-analysis.mjs"]
|
||||
"include": ["library/**/*.ts", "pages/**/*.ts", "prepare-pages.mjs", "build.js", "dev.js", "utils.js", "projects.js", "rebuild.js", "build-analysis.mjs"]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Exercise real filesystem and workflow rejection paths.
|
||||
import test from 'node:test'
|
||||
import YAML from 'yaml'
|
||||
import { validateCIWorkflow, validatePagesWorkflow } from '../refactor/scripts/ci-workflow.mjs'
|
||||
import { compareCompiled, inside, inventory, validateArtifact } from '../scripts/pages/artifact.ts'
|
||||
|
||||
function fixture(t) {
|
||||
const root = fs.mkdtempSync(path.resolve('refactor/.cache/pages-test-'))
|
||||
t.after(() => {
|
||||
assert.equal(path.dirname(root), path.resolve('refactor/.cache'))
|
||||
assert(path.basename(root).startsWith('pages-test-'))
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
fs.writeFileSync(path.join(root, 'CNAME'), 'github.artplayer.org\n')
|
||||
fs.writeFileSync(path.join(root, '.nojekyll'), '')
|
||||
fs.writeFileSync(path.join(root, 'index.html'), '<!doctype html><title>Player</title>')
|
||||
return root
|
||||
}
|
||||
const contract = { domain: 'github.artplayer.org', requiredPaths: ['index.html', '.nojekyll', 'CNAME'] }
|
||||
|
||||
test('Pages preserves hidden files and rejects missing routes, empty pages and changed domains', (t) => {
|
||||
const root = fixture(t)
|
||||
const files = validateArtifact(root, contract)
|
||||
assert.equal(files['.nojekyll'].bytes, 0)
|
||||
assert.match(files['index.html'].sha256, /^[a-f0-9]{64}$/)
|
||||
assert.throws(() => validateArtifact(root, { ...contract, requiredPaths: [...contract.requiredPaths, 'mobile.html'] }), /Missing required/)
|
||||
fs.writeFileSync(path.join(root, 'CNAME'), 'example.com')
|
||||
assert.throws(() => validateArtifact(root, contract), /domain changed/)
|
||||
fs.writeFileSync(path.join(root, 'index.html'), '')
|
||||
assert.throws(() => validateArtifact(root, contract), /Empty Pages/)
|
||||
})
|
||||
|
||||
test('Pages rejects directory escape, symbolic directories and hard-linked files', (t) => {
|
||||
const root = fixture(t)
|
||||
for (const file of ['../outside', '/outside', 'nested/../../outside', 'nested\\outside', '.'])
|
||||
assert.throws(() => inside(root, file))
|
||||
fs.linkSync(path.join(root, 'index.html'), path.join(root, 'hard.html'))
|
||||
assert.throws(() => inventory(root), /hard links/)
|
||||
fs.unlinkSync(path.join(root, 'hard.html'))
|
||||
const directory = path.join(root, 'assets')
|
||||
fs.mkdirSync(directory)
|
||||
fs.symlinkSync(directory, path.join(root, 'redirect'), 'junction')
|
||||
assert.throws(() => inventory(root), /symbolic links/)
|
||||
})
|
||||
|
||||
test('Pages cannot upload stale compiled output or omit one distribution format', (t) => {
|
||||
const root = fixture(t)
|
||||
fs.mkdirSync(path.join(root, 'packages/artplayer/dist'), { recursive: true })
|
||||
fs.mkdirSync(path.join(root, 'compiled'))
|
||||
for (const suffix of ['.js', '.legacy.js', '.mjs']) {
|
||||
fs.writeFileSync(path.join(root, `packages/artplayer/dist/artplayer${suffix}`), suffix)
|
||||
fs.writeFileSync(path.join(root, `compiled/artplayer${suffix}`), suffix)
|
||||
}
|
||||
assert.equal(compareCompiled(root, root, ['artplayer']).length, 3)
|
||||
fs.writeFileSync(path.join(root, 'compiled/artplayer.js'), 'old')
|
||||
assert.throws(() => compareCompiled(root, root, ['artplayer']), /Stale compiled/)
|
||||
fs.unlinkSync(path.join(root, 'compiled/artplayer.js'))
|
||||
assert.throws(() => compareCompiled(root, root, ['artplayer']), /ENOENT/)
|
||||
})
|
||||
|
||||
test('Pages deployment guard rejects untrusted, bypassed and rebuilding deployments', () => {
|
||||
const source = fs.readFileSync('.github/workflows/pages.yml', 'utf8')
|
||||
validatePagesWorkflow(source)
|
||||
for (const mutate of [
|
||||
w => w.on.push = {},
|
||||
w => w.jobs.validate.if = 'true',
|
||||
w => w.jobs.deploy.needs = [],
|
||||
w => w.jobs.deploy.permissions.contents = 'write',
|
||||
w => w.jobs.deploy.steps[0].with = { 'artifact-name': 'arbitrary' },
|
||||
w => w.jobs.deploy.steps.push({ run: 'yarn build' }),
|
||||
w => w.concurrency['cancel-in-progress'] = true,
|
||||
]) {
|
||||
const workflow = YAML.parse(source)
|
||||
mutate(workflow)
|
||||
assert.throws(() => validatePagesWorkflow(YAML.stringify(workflow)))
|
||||
}
|
||||
})
|
||||
|
||||
test('Pages upload cannot bypass preflight or upload the working directory', () => {
|
||||
const source = fs.readFileSync('.github/workflows/nodejs.yml', 'utf8')
|
||||
for (const mutate of [
|
||||
w => w.jobs.checks.steps.find(s => s.id === 'pages').if = 'false',
|
||||
w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/upload-pages-artifact@')).with.path = 'docs',
|
||||
w => w.jobs.checks.steps.find(s => s.id === 'pages').run = 'echo passed',
|
||||
w => w.jobs.checks.steps.find(s => s.run?.startsWith('yarn test:pages:browser')).if = 'false',
|
||||
]) {
|
||||
const workflow = YAML.parse(source)
|
||||
mutate(workflow)
|
||||
assert.throws(() => validateCIWorkflow(YAML.stringify(workflow)))
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
/* eslint-disable antfu/no-top-level-await -- Sequential standalone browser command; always closes its own server and browsers. */
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { chromium, firefox, webkit } from 'playwright'
|
||||
import { serveAsset } from '../scripts/library/assets.ts'
|
||||
import { digest } from '../scripts/pages/artifact.ts'
|
||||
|
||||
const { output } = JSON.parse(fs.readFileSync('refactor/.cache/pages/latest.json', 'utf8'))
|
||||
assert.equal(path.dirname(path.resolve(output)), path.resolve('refactor/.cache/pages'))
|
||||
assert(path.basename(output).startsWith('run-'))
|
||||
const prepared = JSON.parse(fs.readFileSync(path.join(output, 'report.json'), 'utf8'))
|
||||
assert.equal(prepared.status, 'validated')
|
||||
const site = path.join(output, 'site')
|
||||
const reports = []
|
||||
const failures = []
|
||||
const server = http.createServer((request, response) => {
|
||||
const pathname = new URL(request.url, 'http://localhost').pathname
|
||||
const media = pathname === '/__media.mp4'
|
||||
serveAsset(media ? path.resolve('test/browser/media') : site, media ? '/pattern.mp4' : pathname, request, response).catch((error) => {
|
||||
failures.push(String(error))
|
||||
response.destroy()
|
||||
})
|
||||
})
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const serverUrl = `http://127.0.0.1:${server.address().port}`
|
||||
try {
|
||||
for (const [engine, launcher] of Object.entries({ chromium, firefox, webkit })) {
|
||||
const browser = await launcher.launch()
|
||||
try {
|
||||
for (const format of ['demo', 'module']) {
|
||||
const page = await browser.newPage()
|
||||
page.setDefaultTimeout(10000)
|
||||
page.setDefaultNavigationTimeout(10000)
|
||||
const errors = []
|
||||
const network = []
|
||||
page.on('pageerror', error => errors.push(String(error)))
|
||||
page.on('requestfailed', request => network.push({ url: request.url(), type: request.resourceType(), error: request.failure()?.errorText }))
|
||||
process.stdout.write(`Checking ${engine}/${format}\n`)
|
||||
const core = format === 'demo' ? 'uncompiled/artplayer/index.js' : 'compiled/artplayer.mjs'
|
||||
const chapter = format === 'demo' ? 'uncompiled/artplayer-plugin-chapter/index.js' : 'compiled/artplayer-plugin-chapter.mjs'
|
||||
for (const file of [core, chapter]) assert.equal(digest(path.join(site, file)).sha256, prepared.files[file].sha256)
|
||||
const setup = `window.art = new window.Artplayer({ container: '.player', url: '/__media.mp4', muted: true, fullscreenWeb: true, plugins: [window.artplayerPluginChapter({chapters:[{start:0,end:Infinity,title:'Pages staging'}]})] }); document.querySelector('button').onclick = () => art.play();`
|
||||
const scripts = format === 'demo'
|
||||
? `<script src="/${core}"></script><script src="/${chapter}"></script><script>${setup}</script>`
|
||||
: `<script type="module">import Artplayer from '/${core}'; import chapter from '/${chapter}'; window.Artplayer=Artplayer; window.artplayerPluginChapter=chapter; ${setup}</script>`
|
||||
await page.route('**/__pages_probe.html', route => route.fulfill({ contentType: 'text/html', body: `<!doctype html><button>Play</button><div class="player" style="width:640px;height:360px"></div>${scripts}` }))
|
||||
try {
|
||||
await page.goto(`${serverUrl}/__pages_probe.html`)
|
||||
await page.waitForFunction(() => window.art?.isReady)
|
||||
await page.locator('button').click()
|
||||
await page.waitForFunction(() => window.art.playing && window.art.currentTime > 0.2)
|
||||
assert.equal(await page.locator('.art-chapter').count(), 1)
|
||||
await page.evaluate(() => {
|
||||
window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: 0, end: Infinity, title: 'Updated' }] })
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await page.waitForFunction(() => window.art.fullscreenWeb)
|
||||
assert.equal(await page.locator('.art-chapter').getAttribute('data-title'), 'Updated')
|
||||
const state = await page.evaluate(() => ({ time: window.art.currentTime, width: window.art.video.videoWidth, readyState: window.art.video.readyState, mediaError: window.art.video.error?.code ?? null }))
|
||||
assert(state.width > 0)
|
||||
assert.equal(state.mediaError, null)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
assert.equal(await page.locator('.art-video-player').count(), 0)
|
||||
assert.deepEqual(errors, [])
|
||||
assert.deepEqual(network.filter(entry => entry.type === 'script'), [])
|
||||
reports.push({ engine, version: browser.version(), platform: process.platform, format, passed: true, state, network, inputs: { core: prepared.files[core], chapter: prepared.files[chapter] } })
|
||||
}
|
||||
catch (error) {
|
||||
process.stderr.write(`${engine}/${format}: ${String(error)}; ${JSON.stringify(errors)}\n`)
|
||||
reports.push({ engine, version: browser.version(), format, passed: false, error: String(error), errors, network })
|
||||
failures.push(`${engine}/${format}: ${String(error)}`)
|
||||
}
|
||||
finally {
|
||||
await page.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
await browser.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
server.closeAllConnections()
|
||||
await new Promise(resolve => server.close(resolve))
|
||||
fs.writeFileSync(path.join(output, 'browser.json'), `${JSON.stringify({ prepared: digest(path.join(output, 'report.json')), reports, failures, limitation: 'Controlled probe page and fixed native MP4; scripts served from exact staged directory. Not full live editor, external SDK, physical device or remote Pages verification.' }, null, 2)}\n`)
|
||||
}
|
||||
assert.deepEqual(failures, [])
|
||||
process.stdout.write(`Staged Pages browser checks passed: ${reports.length}. Evidence: ${output}/browser.json\n`)
|
||||
Reference in new issue
Block a user