diff --git a/.github/workflows/recover-pages-0.4.0.yml b/.github/workflows/recover-pages-0.4.0.yml deleted file mode 100644 index 0070f76..0000000 --- a/.github/workflows/recover-pages-0.4.0.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Recover the verified 0.4.0 Pages artifact - -on: - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: docs-open-source-stable - cancel-in-progress: false - -jobs: - recover: - if: ${{ github.repository == 'weappjs/weapp.dev' && github.ref == 'refs/heads/main' }} - runs-on: ubuntu-24.04 - timeout-minutes: 30 - environment: docs-open-source-stable - permissions: - contents: write - actions: read - pages: write - id-token: write - env: - LEFTHOOK: '0' - REPO_DOCS_CONCURRENCY_GROUP: docs-open-source-stable - steps: - - name: Checkout recovery controller - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Checkout the frozen release adapter - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: 0b26eea7da1b67f66d31b639e5bd11087b5295d6 - path: .recovery/source - persist-credentials: false - - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - package_json_file: .recovery/source/package.json - - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: 24.22.1 - - name: Install frozen adapter dependencies without build scripts - working-directory: .recovery/source - run: pnpm install --frozen-lockfile --ignore-scripts - - name: Recover only the verified original tarball - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - github-token: ${{ github.token }} - script: | - const { spawnSync } = require('node:child_process') - const result = spawnSync('node', ['scripts/recover-pages-0.4.0.mjs'], { - stdio: 'inherit', env: process.env, - }) - if (result.error || result.status !== 0) throw new Error('Pages artifact recovery stopped; inspect sanitized diagnostics.') - - name: Preserve recovery evidence - if: always() - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: pages-recovery-${{ github.run_id }}-${{ github.run_attempt }} - path: .cache/pages-recovery - include-hidden-files: true - if-no-files-found: ignore - retention-days: 30 diff --git a/docs/maintenance.md b/docs/maintenance.md index 5d3cdef..2b1ec37 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -54,7 +54,7 @@ pnpm metrics:sync ## CI 与发布 -[2026-10-23 repoctl 6.0.1 迁移与上线记录](repoctl-migration-2026-10-23.md) 记录历史 0.3.0 补发、nightly 首次真实发布、新发布身份验证、双站 main/PR 预览及线上验收,以及仍待完成的首次受管稳定部署。 +[2026-10-23 repoctl 6.0.1 迁移与上线记录](repoctl-migration-2026-10-23.md) 记录历史 0.3.0 补发、nightly 首次真实发布、新发布身份验证、双站 main/PR 预览及线上验收、0.4.0 完整稳定包发布,以及 0.4.0 两站首次受管正式部署、线上验收、截图核对和无重复提交的恢复审计。 版本与发布由 repoctl 6.0.1 统一管理,底层继续使用 pnpm 12 原生 versioning。开发分支用 `pnpm change weapp --bump minor --summary "变更说明"` 记录功能(修复用 `patch`);`.changeset/*.md` 是 pnpm change intent,不使用 Changesets CLI/action。`pnpm release:plan --json` 只读预览,`pnpm change check` 检查 intent。`pnpm-workspace.yaml` 的 `versioning.ignore` 精确排除 root 与四个私有 workspace,新增私有包时同步名单。 @@ -74,13 +74,13 @@ CLI 命令与数据按需加载;QR core/SVG、semver、string-width、cross-sp 本地 `pnpm --filter weapp preflight` 只验证包身份、Node 18、public access、资源清单、bundle policy 和构建版本;`--artifact ` 还会安装并 smoke 测试指定 tarball、确认验证前后 SHA256 不变。这些检查不请求发布凭据。npm registry 状态、认证、上传、确认与恢复由 repoctl 管理,不保留另一套手写 OIDC 或 tag 发布实现。 -npm trusted publisher 必须配置 owner `weappjs`、repository `weapp.dev`、workflow filename `release.yml`、空 environment;release/nightly/audit 都通过同一文件和 GitHub 托管 runner 使用 OIDC。历史 0.3.0 已用旧发布身份补齐 npm、canonical tag 和 GitHub Release;迁移及新 publisher 配置已完成。新身份已完成 OIDC audit 和首次 nightly 真实发布,npm 连接状态为 Valid;旧 `publish-weapp.yml` trusted publisher 连接已删除,仅保留 `release.yml`。后续更换发布身份时,也须等待新身份真实发布成功后再删除旧连接,不能只依赖 OIDC audit。新身份可用 `gh workflow run release.yml --ref main -f mode=oidc-audit` 审计;`npm publish --dry-run` 不证明 OIDC 可用。异常恢复使用受管 `reconcile` 或 `publish-unpublished` 模式并明确 package/version/source-sha,不另行手工上传。 +npm trusted publisher 必须配置 owner `weappjs`、repository `weapp.dev`、workflow filename `release.yml`、空 environment;release/nightly/audit 都通过同一文件和 GitHub 托管 runner 使用 OIDC。历史 0.3.0 已用旧发布身份补齐 npm、canonical tag 和 GitHub Release;迁移及新 publisher 配置已完成。新身份已完成 OIDC audit、首次 nightly 和 0.4.0 稳定包真实发布,npm 连接状态为 Valid;旧 `publish-weapp.yml` trusted publisher 连接已删除,仅保留 `release.yml`。后续更换发布身份时,也须等待新身份真实发布成功后再删除旧连接,不能只依赖 OIDC audit。新身份可用 `gh workflow run release.yml --ref main -f mode=oidc-audit` 审计;`npm publish --dry-run` 不证明 OIDC 可用。异常恢复使用受管 `reconcile` 或 `publish-unpublished` 模式并明确 package/version/source-sha,不另行手工上传。 网站也由 repoctl 管理。`repoctl-docs.yml` 的 main push 为 `web` 和 `open-source` 分别部署固定 preview;正式网站由成功 release record 触发,或明确选择已完成的 release ID 重试。正式站不会因普通 main push 更新。`repoctl-docs-deploy.yml` 保持 `docs--` 环境与同名、不可取消的并发组;Cloudflare 与 Pages 分开授予权限。每个站在隔离源码中通过边界、lint、类型、单测、构建验证和桌面/移动 E2E 后,部署同一产物并核对部署身份。 商业正式站保留 Worker `weapp-dev`、`weapp.dev` 和 `www.weapp.dev` 以及现有 www Redirect Rule;固定预览使用独立 Worker `weapp-dev-preview`。开源正式站继续使用 GitHub Pages 和 `weapp.js.org`,固定预览使用 `weapp-open-source-preview`。预览产物包含 noindex、禁用生产统计、保留生产 canonical;Pages 正式产物继续包含 CNAME、兼容 `/weapp.dev/` 路径、不生成 `.nojekyll`。两个 preview Worker 先完成 main 预览部署,后续 PR 才能上传 Worker Version。当前固定预览入口为 [weapp.dev 预览](https://weapp-dev-preview.sonofmagic.workers.dev) 和 [开源站预览](https://weapp-open-source-preview.sonofmagic.workers.dev);PR 返回绑定该次源码 SHA 的固定版本 URL,具体回执见迁移记录。 -Commercial CI 和 Open source CI 保留独立验证以及同仓 PR 的临时 Worker Version 预览。PR 构建完成后记录 site、PR head SHA、deployment ID,再执行 E2E;部署任务只下载并验证该产物,不重新构建。fork PR 只运行验证,不注入 Cloudflare 凭据。PR 关闭不会改变固定预览或正式流量。Cloudflare 凭据由 GitHub Actions 的 secret 上下文提供;当前仓库继承组织级 `CLOUDFLARE_ACCOUNT_ID` 和 `CLOUDFLARE_API_TOKEN`,main 与同仓 PR 均复用已有授权。核对凭据时同时检查组织、仓库和环境级设置,仓库级 secret 列表为空不代表没有继承凭据。GitHub Pages 的 `pages: write`、`id-token: write` 只授予开源正式部署 job;其 JavaScript action 将 artifact 服务环境传给 repoctl,避免独立 shell 丢失上传上下文。 +Commercial CI 和 Open source CI 保留独立验证以及同仓 PR 的临时 Worker Version 预览。PR 构建完成后记录 site、PR head SHA、deployment ID,再执行 E2E;部署任务只下载并验证该产物,不重新构建。fork PR 只运行验证,不注入 Cloudflare 凭据。PR 关闭不会改变固定预览或正式流量。Cloudflare 凭据由 GitHub Actions 的 secret 上下文提供;当前仓库继承组织级 `CLOUDFLARE_ACCOUNT_ID` 和 `CLOUDFLARE_API_TOKEN`,main 与同仓 PR 均复用已有授权。核对凭据时同时检查组织、仓库和环境级设置,仓库级 secret 列表为空不代表没有继承凭据。GitHub Pages 的 `pages: write`、`id-token: write` 只授予开源正式部署 job;其 JavaScript action 将 artifact 服务环境传给 repoctl,避免独立 shell 丢失上传上下文。Pages API 的 `pages_build_version` 使用真实源码 SHA;完整 repoctl deployment ID 仍保存在平台回执和公开 `repoctl-deployment.json` 中,不用该 ID 替代 Git commit。 站点配置的 `releaseInputs` 除 `weapp` 本身外,也包含 CLI 打包所需的中立 catalog、Logo 与共享 UI 输入,以及根 `package.json`、锁文件、workspace 配置和 dependency patches。`patchPaths` 为空,尚不允许绕过正式发布追加文档修补提交。普通站点内容更新可先在 main preview 验收,正式上线绑定下一条已完成 release record。首条受管正式部署必须来自包含本次迁移配置、构建脚本和 adapter 的新版本 release record;补齐历史 0.3.0 发布只完成 npm 基线,不会把新部署代码加入历史源码。不要以旧 0.3.0 记录启动新网站部署,也不要修改旧 tag。双站互不导入,开源产物继续扫描商业文案、联系方式与私有资源。 @@ -92,6 +92,8 @@ Commercial CI 和 Open source CI 保留独立验证以及同仓 PR 的临时 Wor PR 验证产物仍为 web-dist 和 open-source-dist;受管部署产物名称包含 site、channel、run ID 与 attempt,保存 30 天,测试和部署证据保存 7 天。发布前记录上一成功部署对应的运行号与提交;若接近保留期,先下载该运行的产物备份,再发布。 +状态分支的跨文件更新发生 HTTP 409 时,仅在目标文件 revision 未变的情况下有限重试;同一文件已改变则停止,避免覆盖并发结果。Cloudflare 上传后仅轮询确认版本,不重复上传。Pages 已有平台 ID 时复用回执查询;结果未知时停止,不能再次提交。日志只保留状态、请求 ID 与发布身份,不记录凭据。 + 同一条失败或未完成的部署可按其 release ID 重试;repoctl 6 不允许较旧来源覆盖已完成的新来源,因此选择旧 release record 不能用于常规回滚。需要恢复旧行为时,在 main 提交 forward revert 并完成新的稳定发布,保留来源单调前进。紧急情况下可在 Cloudflare 回滚至上一 Worker Version,或把已归档的开源产物重新部署到 Pages;这属于平台直接恢复,必须记录实际版本、产物身份及与 repoctl 状态的差异,随后用新的稳定发布重新对齐。不要重新构建覆盖待回滚文件。两站可分别恢复;共享包回退前评估两站影响。 上线后核对中英文首页、项目目录、一个确认服务项目与规划中项目、隐私页、sitemap、静态资源和统计请求;开源站额外验证 /weapp.dev/ 子路径及历史跳转,商业站额外验证联系方式与赞助页面。只有真实部署后的检查通过,才能报告线上验收完成。 diff --git a/docs/repoctl-migration-2026-10-23.md b/docs/repoctl-migration-2026-10-23.md index 5d7f5ee..b0c00d5 100644 --- a/docs/repoctl-migration-2026-10-23.md +++ b/docs/repoctl-migration-2026-10-23.md @@ -22,60 +22,76 @@ 历史 `weapp@0.3.0` 已从原始版本提交 `1fe201176b55f965e76bc81941a02edaed57553c` 补齐发布,未改写原 ledger: -| 验证或变更 | 结果与证据 | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 原提交包预检与旧身份 OIDC 交换 | [预检运行 39936937257](https://github.com/weappjs/weapp.dev/actions/runs/39936937257) 成功 | -| 以原 `publish-weapp.yml` 身份发布 npm | [发布运行 39938924495](https://github.com/weappjs/weapp.dev/actions/runs/39938924495) 成功,运行 ref 为 `weapp@0.3.0` | -| canonical tag 与 GitHub Release | [weapp@0.3.0](https://github.com/weappjs/weapp.dev/releases/tag/weapp%400.3.0),指向原始版本提交 | -| npm 独立安装 | `npm exec --yes --package=weapp@0.3.0 -- weapp --version` 返回 `0.3.0` | -| 旧发布工作流 | `publish-weapp.yml` 已停用,避免补建 GitHub Release 时重复发布 | -| 迁移合并与受管工作流 | [PR #20](https://github.com/weappjs/weapp.dev/pull/20) 已合并,main 提交为 `94bf5d3374fd98963ab195db96a8b79df3703947`;受管 release 与 docs 工作流已启用 | -| 新 npm Trusted Publisher 与 OIDC | [OIDC audit 39950110421](https://github.com/weappjs/weapp.dev/actions/runs/39950110421) 成功;首次 nightly 真实发布后,`release.yml`、空 environment 的连接显示 Valid。旧 `publish-weapp.yml` 连接已完成二次验证并删除,仅保留新连接;截图 `.cache/repoctl-migration/npm-publisher-final.png` | -| Nightly 首次真实发布 | [运行 39950585363](https://github.com/weappjs/weapp.dev/actions/runs/39950585363) 成功,发布 `weapp@0.4.0-snapshot.nightly.c94bf5d3374fd98963ab195db96a8b79df3703947.ba045bb7e139cfe892b2cb3cea124c318f54757579d622df70b295d915c55944c`;npm `latest` 仍为 0.3.0,正式站和稳定发布基线未变 | -| Nightly 独立安装与无变更跳过 | `nightly-live-install.json` 确认真实 npm 安装的 `weapp@snapshot-nightly` 版本、内嵌 meta 和 CLI `--version` 一致,`--help` 通过;同一安装产物在 Node 18.20.8 下执行 `dist/cli.js --version` 返回相同版本、退出码 0;`nightly-live-skip.json` 确认只读 planner 使用新的 schedule run ID 查询真实 registry 后返回 `skipped`,仅两个 GET,无 OIDC、命令或写入 | -| Nightly 对稳定来源的隔离 | `nightly-live-skip.json` 复核远端 main 仍为 `94bf5d3374fd98963ab195db96a8b79df3703947`,manifest、intent、ledger、changelog 和稳定 tags 未改变,npm `latest` 仍为 0.3.0 | -| 0.4.0 版本准备 | [Release 运行 39950062226](https://github.com/weappjs/weapp.dev/actions/runs/39950062226) 创建[版本 PR #21](https://github.com/weappjs/weapp.dev/pull/21);显式调度的 [CLI CI 39950248718](https://github.com/weappjs/weapp.dev/actions/runs/39950248718) 对 PR head `9f2ffc62ccefe4b3ab9e5d2d12cf8fdb9fdb4e72` 的 verify 和五项真实 tarball 矩阵均成功,仍待人工审核合并 | -| GitHub 部署环境 | 四个 `docs--` 环境已创建,均仅允许 main 分支 | -| Cloudflare 账号与部署凭据 | 仓库变量 `CLOUDFLARE_ACCOUNT_ID` 已设置;组织级 `CLOUDFLARE_ACCOUNT_ID` 和 `CLOUDFLARE_API_TOKEN` 已继承,PR 部署请求得到 Cloudflare API 响应。复用现有组织凭据,无需创建新 token;未提交的创建表单已关闭 | -| Cloudflare 旧 Git 构建 | 生产 Worker `weapp-dev` 的旧 `sonofmagic/weapp.dev` 构建已执行 Disable builds 并保存;截图 `.cache/repoctl-migration/cloudflare-builds-disabled.png` | -| 重复正式自动部署入口 | 合并后的 `ci.yml` 与 `open-source.yml` 只部署同仓 PR 预览;`repoctl-docs.yml` 的 main push 固定选择 preview,正式部署由完整 release record 触发。旧发布工作流已移除,Cloudflare Git builds 已关闭,生产自动部署已统一到受管入口 | -| Cloudflare 生产版本复核 | `.cache/repoctl-migration/production-cloudflare-after-rollout.json` 在预览上线后再次确认 `unchanged: true`,生产 Worker 版本与流量分配未变 | -| CLI GitHub CI | [运行 39943800403](https://github.com/weappjs/weapp.dev/actions/runs/39943800403) 对 `818384680a37302b8a7351c1271754d41ae031d1` 成功;Linux Node 18/22、Windows Node 18/24、macOS Node 24 五项真实 tarball 安装均通过 | -| web GitHub CI | [验证 job 120381392388](https://github.com/weappjs/weapp.dev/actions/runs/39943800404/job/120381392388) 成功,E2E 为 539 项通过、61 项跳过;证据 `ci-web-verify.log`。随后预览部署因 `weapp-dev-preview` 尚未初始化停止 | -| open-source GitHub CI | [验证 job 120381392718](https://github.com/weappjs/weapp.dev/actions/runs/39943800530/job/120381392718) 成功,E2E 为 495 项通过、61 项跳过;证据 `ci-open-source-verify.log`。随后[部署 job 120386447843](https://github.com/weappjs/weapp.dev/actions/runs/39943800530/job/120386447843) 因预览 Worker 尚未初始化失败,部署凭据已注入 | -| open-source main 预览 | [Docs job 120402278774](https://github.com/weappjs/weapp.dev/actions/runs/39950062178/job/120402278774) 成功,495 项 E2E 通过;source `94bf5d3374fd98963ab195db96a8b79df3703947`,deployment ID `9ab3f481b8c6418181ea3687ab51c1cc97b1e32056de17b2648a40cb04258d02`,Worker version `a01dc3db-6a99-4e38-993c-a0f943f18566`;[固定预览](https://weapp-open-source-preview.sonofmagic.workers.dev) | -| open-source PR 预览 | [PR #20 运行 39946258085,第 2 次尝试](https://github.com/weappjs/weapp.dev/actions/runs/39946258085/attempts/2) 成功;source `6816ea2c637f3983e5cbe9f3c5a59de1c5b32a4b`,Worker version `eaf8b8ea-3727-4f91-9e03-71903a510924`;[固定版本预览](https://eaf8b8ea-weapp-open-source-preview.sonofmagic.workers.dev) | -| web main 预览 | [Docs 运行 39950062178](https://github.com/weappjs/weapp.dev/actions/runs/39950062178) 整体成功,web 的 539 项 E2E 通过;source `94bf5d3374fd98963ab195db96a8b79df3703947`,deployment ID `b6963c9d63ccbe5d437571a676cf7de14a5f9ae6dba9077bc43b5bb756f22882`,Worker version `ead6572f-5800-4d8a-b7c0-16e7f5f8b09b`;[固定预览](https://weapp-dev-preview.sonofmagic.workers.dev) | -| web PR 预览 | [PR #20 运行 39946258086,第 2 次尝试](https://github.com/weappjs/weapp.dev/actions/runs/39946258086/attempts/2) 成功;source `6816ea2c637f3983e5cbe9f3c5a59de1c5b32a4b`,Worker version `e651b4f0-5aac-479f-b26f-b635579f2713`;[固定版本预览](https://e651b4f0-weapp-dev-preview.sonofmagic.workers.dev) | -| fork PR 凭据隔离 | 工作流部署条件和事件上下文回归已覆盖 fork 无凭据检查;本次未创建真实 fork PR,属于验证范围说明,不构成额外上线待办 | -| 四个预览入口线上验收 | [无凭据 GitHub runner 运行 39954190106](https://github.com/weappjs/weapp.dev/actions/runs/39954190106) 成功:两站 main/PR 共四个入口 × 中英文 × 桌面/移动,16 个页面场景全部通过;源码身份、静态资源、metadata、robots 与 `noindex` token 检查通过,统计请求为 0。报告与截图见[验收产物 12501533414](https://github.com/weappjs/weapp.dev/actions/runs/39954190106/artifacts/12501533414)(`preview-rollout-acceptance-39954190106-1`) | - -补齐 0.3.0 只建立 npm 发布基线。它的原始源码没有本次网站部署适配器,不能作为两站首条受管正式部署记录。现有 `.changeset/thin-tables-boil.md` minor intent 保留;最终只读 release plan 为 `ready`、无 blocker,明确计划将 `weapp` 从 0.3.0 升到 0.4.0(`.cache/repoctl-migration/release-plan-final.json`)。迁移合并后的 Release 已据此创建 0.4.0 版本 PR #21;版本变更只在该 PR 分支,尚未人工合并或正式发布。 - -合并提交 `94bf5d3` 的 [Commercial CI](https://github.com/weappjs/weapp.dev/actions/runs/39950062041)、[Open source CI](https://github.com/weappjs/weapp.dev/actions/runs/39950062069) 和 [CLI CI](https://github.com/weappjs/weapp.dev/actions/runs/39950062152) 均成功,CLI 包含全部五项跨平台安装任务。线上验收截图保存在 `.cache/repoctl-migration/online-final/`;已目视核对两站桌面与手机的中英文样本,预览提示、中文字体与布局正常。验收浏览器均已关闭,临时 QA 分支不合并到 main。 - -Cloudflare 旧 Git 账号虽已断连,原构建入口仍未禁用,因此本次明确关闭了该入口。此设置变更没有向生产 Worker 部署新版本,当前正式站内容保持原状。 +| 验证或变更 | 结果与证据 | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 原提交包预检与旧身份 OIDC 交换 | [预检运行 39936937257](https://github.com/weappjs/weapp.dev/actions/runs/39936937257) 成功 | +| 以原 `publish-weapp.yml` 身份发布 npm | [发布运行 39938924495](https://github.com/weappjs/weapp.dev/actions/runs/39938924495) 成功,运行 ref 为 `weapp@0.3.0` | +| canonical tag 与 GitHub Release | [weapp@0.3.0](https://github.com/weappjs/weapp.dev/releases/tag/weapp%400.3.0),指向原始版本提交 | +| npm 独立安装 | `npm exec --yes --package=weapp@0.3.0 -- weapp --version` 返回 `0.3.0` | +| 旧发布工作流 | `publish-weapp.yml` 已停用,避免补建 GitHub Release 时重复发布 | +| 迁移合并与受管工作流 | [PR #20](https://github.com/weappjs/weapp.dev/pull/20) 已合并,main 提交为 `94bf5d3374fd98963ab195db96a8b79df3703947`;受管 release 与 docs 工作流已启用 | +| 新 npm Trusted Publisher 与 OIDC | [OIDC audit 39950110421](https://github.com/weappjs/weapp.dev/actions/runs/39950110421) 成功;首次 nightly 真实发布后,`release.yml`、空 environment 的连接显示 Valid。旧 `publish-weapp.yml` 连接已完成二次验证并删除,仅保留新连接;截图 `.cache/repoctl-migration/npm-publisher-final.png` | +| Nightly 首次真实发布 | [运行 39950585363](https://github.com/weappjs/weapp.dev/actions/runs/39950585363) 成功,发布 `weapp@0.4.0-snapshot.nightly.c94bf5d3374fd98963ab195db96a8b79df3703947.ba045bb7e139cfe892b2cb3cea124c318f54757579d622df70b295d915c55944c`;该次 nightly 验收时 npm `latest` 仍为 0.3.0,正式站和稳定发布基线未因 nightly 改变 | +| Nightly 独立安装与无变更跳过 | `nightly-live-install.json` 确认真实 npm 安装的 `weapp@snapshot-nightly` 版本、内嵌 meta 和 CLI `--version` 一致,`--help` 通过;同一安装产物在 Node 18.20.8 下执行 `dist/cli.js --version` 返回相同版本、退出码 0;`nightly-live-skip.json` 确认只读 planner 使用新的 schedule run ID 查询真实 registry 后返回 `skipped`,仅两个 GET,无 OIDC、命令或写入 | +| Nightly 对稳定来源的隔离 | `nightly-live-skip.json` 在 nightly 验收时复核远端 main 仍为 `94bf5d3374fd98963ab195db96a8b79df3703947`,manifest、intent、ledger、changelog 和稳定 tags 未改变,当时 npm `latest` 仍为 0.3.0 | +| 0.4.0 版本准备与合并 | [Release 运行 39950062226](https://github.com/weappjs/weapp.dev/actions/runs/39950062226) 创建[版本 PR #21](https://github.com/weappjs/weapp.dev/pull/21);[合并前 CLI CI 39955493417](https://github.com/weappjs/weapp.dev/actions/runs/39955493417) 对实际合并前的最新 PR head `27745920be29fb4052a48c24361bcbc1906abdc6` 的 verify 和五项跨平台安装矩阵均成功(`version-pr21-latest-summary.json`)。用户明确授权后,由代理合并至 main `0b26eea7da1b67f66d31b639e5bd11087b5295d6`;未启用版本 PR 自动合并;[合并后 CLI CI 39956928532](https://github.com/weappjs/weapp.dev/actions/runs/39956928532) 对该 main 提交也已通过 | +| 0.4.0 稳定包与发布来源 | [Release 运行 39956928408](https://github.com/weappjs/weapp.dev/actions/runs/39956928408) 的发布 job 成功;npm `weapp@0.4.0` 已发布,`latest` 已更新为 0.4.0,原 `snapshot-nightly` 保留。canonical tag 与非 draft、非 prerelease 的 [GitHub Release weapp@0.4.0](https://github.com/weappjs/weapp.dev/releases/tag/weapp%400.4.0) 均指向 `0b26eea7da1b67f66d31b639e5bd11087b5295d6`;npm provenance 的来源、完整性与 `release.yml` 身份一致 | +| 0.4.0 完整 release record | record ID 为 `719206ee91ab8028f980ed8a0981fd72d4ce3c255e611094c1f774d9f3e490c9`,source 为 `0b26eea7da1b67f66d31b639e5bd11087b5295d6`,`weapp@0.4.0`、`distTag: latest`;证据 `stable-weapp-0.4.0-record.json` 与 `stable-release-lifecycle-summary.json` | +| web 首次受管稳定部署 | [Cloudflare job 120426266125](https://github.com/weappjs/weapp.dev/actions/runs/39956928408/job/120426266125) 成功,535 项 E2E 通过、65 项跳过;source `0b26eea7da1b67f66d31b639e5bd11087b5295d6`,release `719206ee91ab8028f980ed8a0981fd72d4ce3c255e611094c1f774d9f3e490c9`。repoctl deployment ID `93a42e00d393bc29d02abac91f05ff7ae5864b6426bc9f3816c595e2297fa986`,Cloudflare deployment `ca49a40d-27ac-416b-9a3c-77677614b482`,Worker version `94bededc-b70c-4a87-9ff4-e203694fb33a` 承接 100% 流量;`stable-web-public-identity.json` 与 `stable-web-state.json` 一致 | +| open-source 初次稳定部署与恢复 | 首次 491 项 E2E 通过、65 项跳过后,跨文件状态分支更新引发 HTTP 409,尚未调用 Pages provider。随后[恢复运行 39960105796](https://github.com/weappjs/weapp.dev/actions/runs/39960105796) 再次完成 491/65,但 Pages POST 返回 404;官方 action 行为与公开案例确认 `pages_build_version` 必须使用真实源码 SHA。恢复始终保留原 release/source,不重发 npm 或重部署 web | +| open-source 原产物恢复成功 | [恢复运行 39965427716](https://github.com/weappjs/weapp.dev/actions/runs/39965427716) 成功:原 tar SHA256 `122273804a5c8def5ef904d656dddd12d2bf69828ac128306ce2f6c1ce3c1012` 保持不变,新运行上传 artifact `12506551855`,仅一次 Pages POST、HTTP 200;pages ID 为 `0b26eea7da1b67f66d31b639e5bd11087b5295d6`,source 与 release 仍为原 0.4.0 记录,deployment ID `addbffafaaf1fca47b893343905e0dd4f005aaad22e96b2dd65f44c3d01e9ed2` 与真实公开 identity 匹配。安全证据目录 `pages-recovery-result-39965427716/` | +| open-source 受管恢复与去重完成 | [正常 repoctl 恢复运行 39965641616](https://github.com/weappjs/weapp.dev/actions/runs/39965641616) 成功;`current` revision 为 `5f900cdff0c888aa01245a8ae35be0928fba6c61`,`pending` 已清除,source/release/deployment 与原 0.4.0 身份一致,version ID 为 `0b26eea7da1b67f66d31b639e5bd11087b5295d6`、URL 为 `https://weapp.js.org`。平台 receipt 的值与 blob SHA `19e6b06d082e9d67dedc95a492c54710b7416aaa` 完全未变,仍关联运行 `39965427716` 和 artifact `12506551855`;resume 仅归档常规 dist/evidence,无 `pages-*` artifact,无重复平台提交。证据 `pages-managed-resume-{state,receipt,artifacts-39965641616}.json` | +| 两站正式页面线上验收 | [公开页面 QA 39966980683](https://github.com/weappjs/weapp.dev/actions/runs/39966980683) 成功:两站 × 中英文 × 桌面/手机共 8 个页面场景、两种视口的 Pages 项目入口与语言/项目导航、discovery 和精确 deployment identity 均通过;真实 GA4/Baidu 脚本加载并自然产生 page_view。报告与截图为[artifact 12507666227](https://github.com/weappjs/weapp.dev/actions/runs/39966980683/artifacts/12507666227)(`stable-rollout-acceptance-39966980683-1`),SHA256 `1e2f63cb43f517cec8c7d0dc1cb91c654d72b2b1b482449a72d920e5ac71d85a` | +| 正式站截图与验收资源 | `stable-live-visual-review.json` 记录 web 桌面中文/手机英文、open-source 桌面英文/手机中文四个样本目视通过:品牌、导航、项目 Logo 与粒子标题正常,无横向溢出、破图或预览提示。报告 `status: passed`,8 页面各产生 1 次自然 page_view,4 份 identity、4 份 discovery、2 次 Pages 入口检查通过;4 个 context 与验收 browser 均确认关闭。临时 QA 分支和 clone 已删除,保留 `stable-smoke.bundle` 及 `stable-live-final/` 证据 | +| 线上 robots 验证范围 | 首次 QA 因要求 robots 字节全等而拒绝 Cloudflare 的合法 Managed content。修正后的验收按 User-agent 分组确认通用 `*` 允许根路径且没有非空 Disallow,并包含正式 Sitemap;平台专用机器人限制保留,原始 robots 随结果归档,生产策略未改动 | +| 恢复修复 PR 与 main 验证 | PR #23 整合后的合并提交 `5aad971e8c5a14675c5a88861481461714f01a8c` 已通过 [Commercial CI 39965392990](https://github.com/weappjs/weapp.dev/actions/runs/39965392990)、[Open source CI 39965393182](https://github.com/weappjs/weapp.dev/actions/runs/39965393182)、[CLI CI 39965392908](https://github.com/weappjs/weapp.dev/actions/runs/39965392908) 全六 job 和[两站 main 预览 39965392960](https://github.com/weappjs/weapp.dev/actions/runs/39965392960);精确 head 与预览 current 证据为 `runtime-fix-main5aad-verification.json` | +| 0.4.0 npm 独立安装 | `stable-live-install.json` 确认真实 npm 安装的 manifest、内嵌 meta 与 CLI `--version` 均为 0.4.0,`--help` 通过;Node 18.20.8 smoke 返回 0.4.0、退出码 0 | +| 发布后版本计划 | `stable-post-release-plan.json` 为 `status: empty`、`packages: []`、`blockers: []`,没有待发布版本或残留阻塞 | +| GitHub 部署环境 | 四个 `docs--` 环境已创建,均仅允许 main 分支 | +| Cloudflare 账号与部署凭据 | 仓库变量 `CLOUDFLARE_ACCOUNT_ID` 已设置;组织级 `CLOUDFLARE_ACCOUNT_ID` 和 `CLOUDFLARE_API_TOKEN` 已继承,PR 部署请求得到 Cloudflare API 响应。复用现有组织凭据,无需创建新 token;未提交的创建表单已关闭 | +| Cloudflare 旧 Git 构建 | 生产 Worker `weapp-dev` 的旧 `sonofmagic/weapp.dev` 构建已执行 Disable builds 并保存;截图 `.cache/repoctl-migration/cloudflare-builds-disabled.png` | +| 重复正式自动部署入口 | 合并后的 `ci.yml` 与 `open-source.yml` 只部署同仓 PR 预览;`repoctl-docs.yml` 的 main push 固定选择 preview,正式部署由完整 release record 触发。旧发布工作流已移除,Cloudflare Git builds 已关闭,生产自动部署已统一到受管入口 | +| 旧 main 预览防覆盖 | [Docs 运行 39955313582](https://github.com/weappjs/weapp.dev/actions/runs/39955313582) 的 web / open-source 分别完成 539 / 495 项 E2E 后,均因 main 已前进而在上传前拒绝旧源码:`Main advanced during the preview build; refusing to deploy an outdated preview`。证据为 `docs-web-run39955313582.log` 和 `docs-oss-run39955313582.log`;这是安全边界生效,无需重跑旧来源 | +| Cloudflare 生产版本复核 | `.cache/repoctl-migration/production-cloudflare-after-rollout.json` 在预览上线后、0.4.0 稳定部署前确认 `unchanged: true`,生产 Worker 版本与流量分配未变 | +| CLI GitHub CI | [运行 39943800403](https://github.com/weappjs/weapp.dev/actions/runs/39943800403) 对 `818384680a37302b8a7351c1271754d41ae031d1` 成功;Linux Node 18/22、Windows Node 18/24、macOS Node 24 五项真实 tarball 安装均通过 | +| web GitHub CI | [验证 job 120381392388](https://github.com/weappjs/weapp.dev/actions/runs/39943800404/job/120381392388) 成功,E2E 为 539 项通过、61 项跳过;证据 `ci-web-verify.log`。随后预览部署因 `weapp-dev-preview` 尚未初始化停止 | +| open-source GitHub CI | [验证 job 120381392718](https://github.com/weappjs/weapp.dev/actions/runs/39943800530/job/120381392718) 成功,E2E 为 495 项通过、61 项跳过;证据 `ci-open-source-verify.log`。随后[部署 job 120386447843](https://github.com/weappjs/weapp.dev/actions/runs/39943800530/job/120386447843) 因预览 Worker 尚未初始化失败,部署凭据已注入 | +| open-source main 预览 | [Docs job 120402278774](https://github.com/weappjs/weapp.dev/actions/runs/39950062178/job/120402278774) 成功,495 项 E2E 通过;source `94bf5d3374fd98963ab195db96a8b79df3703947`,deployment ID `9ab3f481b8c6418181ea3687ab51c1cc97b1e32056de17b2648a40cb04258d02`,Worker version `a01dc3db-6a99-4e38-993c-a0f943f18566`;[固定预览](https://weapp-open-source-preview.sonofmagic.workers.dev) | +| open-source PR 预览 | [PR #20 运行 39946258085,第 2 次尝试](https://github.com/weappjs/weapp.dev/actions/runs/39946258085/attempts/2) 成功;source `6816ea2c637f3983e5cbe9f3c5a59de1c5b32a4b`,Worker version `eaf8b8ea-3727-4f91-9e03-71903a510924`;[固定版本预览](https://eaf8b8ea-weapp-open-source-preview.sonofmagic.workers.dev) | +| web main 预览 | [Docs 运行 39950062178](https://github.com/weappjs/weapp.dev/actions/runs/39950062178) 整体成功,web 的 539 项 E2E 通过;source `94bf5d3374fd98963ab195db96a8b79df3703947`,deployment ID `b6963c9d63ccbe5d437571a676cf7de14a5f9ae6dba9077bc43b5bb756f22882`,Worker version `ead6572f-5800-4d8a-b7c0-16e7f5f8b09b`;[固定预览](https://weapp-dev-preview.sonofmagic.workers.dev) | +| web PR 预览 | [PR #20 运行 39946258086,第 2 次尝试](https://github.com/weappjs/weapp.dev/actions/runs/39946258086/attempts/2) 成功;source `6816ea2c637f3983e5cbe9f3c5a59de1c5b32a4b`,Worker version `e651b4f0-5aac-479f-b26f-b635579f2713`;[固定版本预览](https://e651b4f0-weapp-dev-preview.sonofmagic.workers.dev) | +| fork PR 凭据隔离 | 工作流部署条件和事件上下文回归已覆盖 fork 无凭据检查;本次未创建真实 fork PR,属于验证范围说明,不构成额外上线待办 | +| 四个预览入口线上验收 | [无凭据 GitHub runner 运行 39954190106](https://github.com/weappjs/weapp.dev/actions/runs/39954190106) 成功:两站 main/PR 共四个入口 × 中英文 × 桌面/移动,16 个页面场景全部通过;源码身份、静态资源、metadata、robots 与 `noindex` token 检查通过,统计请求为 0。报告与截图见[验收产物 12501533414](https://github.com/weappjs/weapp.dev/actions/runs/39954190106/artifacts/12501533414)(`preview-rollout-acceptance-39954190106-1`) | + +补齐 0.3.0 只建立 npm 发布基线。它的原始源码没有本次网站部署适配器,不能作为两站首条受管正式部署记录。迁移时保留 `.changeset/thin-tables-boil.md` minor intent,只读 release plan 为 `ready`、无 blocker,计划将 `weapp` 从 0.3.0 升到 0.4.0(`.cache/repoctl-migration/release-plan-final.json`)。repoctl 据此创建版本 PR #21,随后经用户明确授权合并并完成稳定包发布。长期流程仍要求人工审核与合并版本 PR,此次授权不构成自动合并规则。 + +合并提交 `94bf5d3` 的 [Commercial CI](https://github.com/weappjs/weapp.dev/actions/runs/39950062041)、[Open source CI](https://github.com/weappjs/weapp.dev/actions/runs/39950062069) 和 [CLI CI](https://github.com/weappjs/weapp.dev/actions/runs/39950062152) 均成功,CLI 包含全部五项跨平台安装任务。线上验收截图保存在 `.cache/repoctl-migration/online-final/`;已目视核对两站桌面与手机的中英文样本,预览提示、中文字体与布局正常。预览自动验收浏览器均已关闭,临时 QA 分支不合并到 main。 + +Cloudflare 旧 Git 账号虽已断连,原构建入口仍未禁用,因此本次明确关闭了该入口。该次设置变更本身没有向生产 Worker 部署新版本;后续正式更新由 0.4.0 的完整 release record 驱动。 ## 本地验证 以下证据保存在本次工作目录的 `.cache/repoctl-migration/`,不包含发布凭据。 -| 范围 | 已验证结果 | 证据 | -| ------------------------------ | --------------------------------------------------------------- | ------------------------------------------------ | -| repoctl 配置与升级幂等 | 配置有效;四个受管资产均 `identical`,无 blocker | `config-final.json`、`upgrade-final.json` | -| repoctl doctor | 34 pass、4 warn、0 fail | `doctor-final.json` | -| 根工具与发布生命周期 | lint、包边界与 65 项测试通过 | `root-check-final.log` | -| 工作区 lint、样式、类型与单测 | 最终复验 19/19 个任务通过,CLI lab 保留 4 条非错误 lint warning | `workspace-check-final.log` | -| CLI 后台浏览器 | 通过,含桌面、移动、减少动态效果与静态降级 | `cli-browser.log` | -| CLI 真实终端 | 28 个 PTY 场景通过,终端恢复正常 | `cli-pty.log` | -| CLI tarball | 独立安装与 artifact smoke 通过 | `cli-pack.log` | -| CLI 发布前包检查 | 身份、资源、bundle、构建版本检查通过 | `cli-preflight.log` | -| Nightly 隔离制品准备 | `prepared`、`published: false`,consumer smoke 通过 | `nightly/registry-fixed/validation-summary.json` | -| web 预览部署预演 | Wrangler `--dry-run` 通过,无真实上传 | `wrangler-web-preview-dry-run.log` | -| open-source 预览部署预演 | Wrangler `--dry-run` 通过,无真实上传 | `wrangler-open-source-preview-dry-run.log` | -| web 正式产物浏览器全量 | 535 项通过、65 项跳过 | `web-stable-e2e.log` | -| open-source 正式产物构建 | 45 项必需输出、38 个页面、SEO 资源和全部内部链接通过 | `open-source-stable-build.log` | -| open-source 正式产物浏览器全量 | 491 项通过、65 项跳过 | `open-source-stable-e2e.log` | +| 范围 | 已验证结果 | 证据 | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| repoctl 配置与升级幂等 | 配置有效;四个受管资产均 `identical`,无 blocker | `config-final.json`、`upgrade-final.json` | +| repoctl doctor | 34 pass、4 warn、0 fail | `doctor-final.json` | +| 状态修复后升级与 doctor | 升级计划四个受管资产仍为 `identical`、无 blocker;doctor 34 pass、4 warn、0 fail | `state-fix-upgrade.json`、`state-fix-doctor.json` | +| 根工具与发布生命周期 | lint、包边界与 65 项测试通过 | `root-check-final.log` | +| 部署恢复修复验证 | [PR #23](https://github.com/weappjs/weapp.dev/pull/23) 已合并到 `5aad971e8c5a14675c5a88861481461714f01a8c`:目标 revision 不变时受限重试 HTTP 409、Cloudflare 版本只读轮询、应用 CI 45 分钟限时、Pages 使用 source SHA 及安全诊断;根测试合计 75 项通过 | 状态与部署回归;PR #23 | +| 一次性恢复入口收尾 | `.github/workflows/recover-pages-0.4.0.yml` 与 `scripts/recover-pages-0.4.0.mjs` 已从工作区移除,将随清理 PR 合入;历史恢复实现保留在 PR #23 与验收证据中,不作为后续日常部署入口 | 清理 PR 与历史恢复证据 | +| 工作区 lint、样式、类型与单测 | 最终复验 19/19 个任务通过,CLI lab 保留 4 条非错误 lint warning | `workspace-check-final.log` | +| CLI 后台浏览器 | 通过,含桌面、移动、减少动态效果与静态降级 | `cli-browser.log` | +| CLI 真实终端 | 28 个 PTY 场景通过,终端恢复正常 | `cli-pty.log` | +| CLI tarball | 独立安装与 artifact smoke 通过 | `cli-pack.log` | +| CLI 发布前包检查 | 身份、资源、bundle、构建版本检查通过 | `cli-preflight.log` | +| Nightly 隔离制品准备 | `prepared`、`published: false`,consumer smoke 通过 | `nightly/registry-fixed/validation-summary.json` | +| web 预览部署预演 | Wrangler `--dry-run` 通过,无真实上传 | `wrangler-web-preview-dry-run.log` | +| open-source 预览部署预演 | Wrangler `--dry-run` 通过,无真实上传 | `wrangler-open-source-preview-dry-run.log` | +| web 正式产物浏览器全量 | 535 项通过、65 项跳过 | `web-stable-e2e.log` | +| open-source 正式产物构建 | 45 项必需输出、38 个页面、SEO 资源和全部内部链接通过 | `open-source-stable-build.log` | +| open-source 正式产物浏览器全量 | 491 项通过、65 项跳过 | `open-source-stable-e2e.log` | Doctor 的四条 warning 分别为三个受管工作流的本地定制,以及 nightly schedule 的触发与路由同步提示。升级复检保留这些本地变更,没有待写入的受管资产;后续真实 nightly 发布和 schedule 无变更跳过的验证结果见上表。 @@ -83,19 +99,20 @@ Nightly 验证使用独立源码快照,生成的 tarball manifest、`dist/data 真实预演发现并修复了两个发布入口问题:源码归档没有 `.git` 时跳过 Lefthook 安装,同时保持开发 checkout 的真实安装失败可见;包身份校验接受 npm 官方 registry 根 URL 有无尾斜杠的等价形式,仍拒绝其他来源或额外 URL 内容。 -部署适配器回归覆盖 32 项测试,包括 PR 预览公开链接绑定固定 Worker version、Pages 回执写入响应丢失后回读确认,以及同一 workflow run 的 artifact 名称使用 UUID 防止碰撞。根检查日志中的模拟发布中断消息来自恢复测试,最终 65 项测试全部通过。 +部署适配器回归覆盖 32 项测试,包括 PR 预览公开链接绑定固定 Worker version、Pages 回执写入响应丢失后回读确认,以及同一 workflow run 的 artifact 名称使用 UUID 防止碰撞。初次根检查日志中的模拟发布中断消息来自恢复测试,当时 65 项测试全部通过;后续部署恢复修复将根回归扩展到 75 项。 Web 预览首次全量测试报告 534 项通过、3 项失败。失败定位到提示条占用首屏空间后的几何布局,以及测试重挂载图表后未恢复其懒加载可见条件。修复按提示条实际高度扣除 hero 可用空间,并让 caption 根据实际 stage 容器高度响应;重挂载测试先恢复图表可见性,以遵守原有懒加载边界。 修复后的 hero layout 与 publication channel 复验为 56 项通过、2 项跳过(`web-preview-layout-final.log`);图表重挂载和预览统计隔离复验为 4 项通过(`web-preview-remaining.log`)。首次全量运行中发现的三项失败均已通过针对性复验,没有已知未解决失败。 -开源站预览首次全量测试为 491 项通过、61 项跳过、4 项失败(`open-source-preview-e2e.log`)。四项失败来自桌面和移动端复用的旧 SEO 断言,没有区分预览的 noindex 与正式站索引规则。断言按发布渠道修正后,四项定向复验全部通过(`open-source-preview-remaining.log`)。两站正式产物本地验证、远程预览 E2E 与四个预览入口的真实线上验收均已通过;首次受管正式部署仍等待版本 PR 人工合并。 +开源站预览首次全量测试为 491 项通过、61 项跳过、4 项失败(`open-source-preview-e2e.log`)。四项失败来自桌面和移动端复用的旧 SEO 断言,没有区分预览的 noindex 与正式站索引规则。断言按发布渠道修正后,四项定向复验全部通过(`open-source-preview-remaining.log`)。两站正式产物本地验证、远程预览 E2E 与四个预览入口的真实线上验收均已通过;0.4.0 版本 PR 已经用户授权合并,web 首次受管正式部署已成功,open-source 原产物已恢复上线;两站正式页面线上自动验收已通过,正式站截图目视已通过,repoctl 恢复运行、最终 current 与平台回执去重审计均已通过。 -## 待完成的上线步骤 +## 上线完成状态 -| 步骤 | 当前状态 | 完成时应记录 | -| -------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -| 下一版本 PR | [版本 PR #21](https://github.com/weappjs/weapp.dev/pull/21) 已生成 0.4.0;等待人工审核合并,不能自动合并 | 人工审核合并记录与对应稳定发布运行 | -| 首次受管正式网站部署 | 等待人工合并版本 PR #21 后的完整 0.4.0 稳定发布 | release ID、两站独立部署回执和线上验收结果 | +repoctl 迁移、nightly、main/PR 预览、0.4.0 稳定包与两站首条受管正式部署均已完成。两个正式站的 source 均为 `0b26eea7da1b67f66d31b639e5bd11087b5295d6`,绑定同一完整 release record;npm `latest` 为 0.4.0。两站正式页面的自动验收和中英文桌面/手机截图目视均通过,开源站 `pending` 已清除,恢复未重复提交 Pages。 -两个正式站在首条受管稳定部署完成前继续提供原有内容。上线后分别核对中英文页面、静态资源、canonical、统计隔离与预览禁止索引;开源站额外检查 Pages 子路径和历史跳转。失败重试、未知部署结果处理与两站独立恢复规则见[维护手册](maintenance.md#发布与回滚验收)和[部署适配器说明](../scripts/docs-deployment/README.md)。 +回滚归档保存在 `.cache/repoctl-migration/stable-artifacts/`:商业站使用 `web/`,开源站实际部署文件使用 `open-source-provider/`,对应原 tar 的 SHA256 为 `122273804a5c8def5ef904d656dddd12d2bf69828ac128306ce2f6c1ce3c1012`。开源站正常恢复运行保留的 `open-source/` 是重新验证的构建输出,没有再次上传;它与实际部署文件仅有一个 Astro scope ID、对应 CSS 文件名及两首页引用差异,不能代替原 tar 作为逐字节回滚依据。逐文件校验与归档清单见该目录的 `manifest.json`。 + +后续继续执行版本 PR 人工审核流程。失败重试、未知部署结果处理与两站独立恢复规则见[维护手册](maintenance.md#发布与回滚验收)和[部署适配器说明](../scripts/docs-deployment/README.md)。一次性恢复入口已随本次收尾删除,不作为后续常规发布入口。 + +资源收尾范围:Playwright 自动验收资源已确认关闭,IAB 检查为零标签页。旧 Chrome npm 迁移标签页因原生服务 `-10005` 超时、备用清单也没有 Chrome 连接,无法确认已关闭;该标签页不影响上线状态,未操作归属不明的浏览器资源。 diff --git a/scripts/recover-pages-0.4.0.mjs b/scripts/recover-pages-0.4.0.mjs deleted file mode 100644 index 916d373..0000000 --- a/scripts/recover-pages-0.4.0.mjs +++ /dev/null @@ -1,227 +0,0 @@ -import { strict as assert } from 'node:assert' -import { Buffer } from 'node:buffer' -import { execFileSync } from 'node:child_process' -import { createHash } from 'node:crypto' -import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import path from 'node:path' -import process from 'node:process' -import { pathToFileURL } from 'node:url' - -const repository = 'weappjs/weapp.dev' -const sourceSha = '0b26eea7da1b67f66d31b639e5bd11087b5295d6' -const releaseId = '719206ee91ab8028f980ed8a0981fd72d4ce3c255e611094c1f774d9f3e490c9' -const deploymentId = 'addbffafaaf1fca47b893343905e0dd4f005aaad22e96b2dd65f44c3d01e9ed2' -const artifactId = 12505735843 -const originalRun = 39960105796 -const zipDigest = '35a102604b34007ec658b8decacf6118f02fb6634a989e9f1904bbe766c9eaf1' -const artifactName = 'pages-open-source-addbffafaaf1-39960105796-1-f1a54a1c-3272-497a-831d-d6d74812d095' -const source = path.resolve('.recovery/source') -const evidence = path.resolve('.cache/pages-recovery') -const resultFile = path.join(evidence, 'result.json') -const token = process.env.GITHUB_TOKEN -assert.equal(process.env.GITHUB_ACTIONS, 'true') -assert.equal(process.env.GITHUB_REPOSITORY, repository) -assert.equal(process.env.GITHUB_REF, 'refs/heads/main') -assert.equal(process.env.GITHUB_EVENT_NAME, 'workflow_dispatch') -assert.equal(process.env.REPO_DOCS_CONCURRENCY_GROUP, 'docs-open-source-stable') -assert.ok(token) -assert.equal(execFileSync('git', ['rev-parse', 'HEAD'], { cwd: source, encoding: 'utf8' }).trim(), sourceSha) -await mkdir(evidence, { recursive: true }) -const moduleAt = filename => import(pathToFileURL(path.join(source, 'scripts/docs-deployment', filename)).href) -const { deployPages } = await moduleAt('pages.mjs') -const { githubClient, pagesReceiptStore } = await moduleAt('github.mjs') -const { managedContext, assertIdentity } = await moduleAt('context.mjs') -const { sanitizeMessage } = await moduleAt('diagnostics.mjs') -const apiEvidence = [] -async function safeFetch(input, options) { - const response = await fetch(input, options) - const url = new URL(input) - if (url.origin === 'https://api.github.com') { - const item = { method: options?.method || 'GET', path: url.pathname, status: response.status, requestId: response.headers.get('x-github-request-id') } - if (!response.ok) { - try { - const body = await response.clone().json() - if (typeof body.message === 'string') { - item.message = sanitizeMessage(body.message).slice(0, 2000) - } - } - catch {} - } - apiEvidence.push(item) - await writeFile(path.join(evidence, 'api.json'), `${JSON.stringify(apiEvidence, null, 2)}\n`) - console.log(JSON.stringify(item)) - } - return response -} -const env = { - ...process.env, - REPO_DOCS_SITE: 'open-source', - REPO_DOCS_CHANNEL: 'stable', - REPO_DOCS_SOURCE_SHA: sourceSha, - REPO_DOCS_RELEASE_ID: releaseId, - REPO_DOCS_DEPLOYMENT_ID: deploymentId, - REPO_DOCS_RESULT_FILE: resultFile, -} -const client = githubClient(env, safeFetch) -async function providerClient(endpoint, options = {}) { - if (options.method === 'POST') { - assert.equal(endpoint, 'pages/deployments') - assert.equal(options.body.pages_build_version, deploymentId) - // Provider compatibility translation only: Pages requires a real source - // commit for build-version lookup. The frozen artifact and full repoctl - // identity remain unchanged in the public file and provider receipt. - return client(endpoint, { ...options, body: { ...options.body, pages_build_version: sourceSha } }) - } - return client(endpoint, options) -} -const context = managedContext(env, source) -const store = pagesReceiptStore(client, deploymentId) -async function readState(filename) { - const file = await client(`contents/${filename}?ref=repoctl-release-state`) - assert.equal(file.encoding, 'base64') - return { revision: file.sha, value: JSON.parse(Buffer.from(file.content, 'base64').toString()) } -} -async function guard() { - const record = (await readState(`.repoctl-automation/releases/${releaseId}.json`)).value - assert.equal(record.id, releaseId) - assert.equal(record.repository, repository) - assert.equal(record.sourceSha, sourceSha) - assert.equal(record.branch, 'main') - assert.equal(record.distTag, 'latest') - assert.deepEqual(record.packages, [{ name: 'weapp', version: '0.4.0', sourceSha }]) - assert.deepEqual(record.releasedPackages, ['weapp']) - const pending = (await readState('.repoctl-automation/docs/open-source/stable.json')).value - assert.equal(pending.repository, repository) - assert.equal(pending.site, 'open-source') - assert.equal(pending.channel, 'stable') - assert.equal(pending.pending?.phase, 'deploying') - assert.equal(pending.pending.deploymentId, deploymentId) - assert.equal(pending.pending.snapshot.sourceSha, sourceSha) - assert.equal(pending.pending.snapshot.releaseId, releaseId) - assert.deepEqual(pending.pending.snapshot.commits, []) - assert.deepEqual(pending.pending.snapshot.requestedCommits, []) - const receipt = await store.read() - assert.ok(receipt.value, 'Existing Pages provider receipt required') - assert.equal(receipt.value.deploymentId, deploymentId) - if (receipt.value.phase === 'failed') { - assert.equal(receipt.value.rejectedStatus, 404, 'Only the explicit initial 404 rejection may create a replacement') - assert.equal(receipt.value.artifactId, artifactId) - assert.equal(receipt.value.runId, String(originalRun)) - assert.equal(receipt.value.pagesId, undefined) - assert.deepEqual(receipt.value.previousAttempts, []) - } - else { - assert.ok(['submitting', 'created', 'complete'].includes(receipt.value.phase)) - // A prior invocation may already have submitted. The frozen adapter will - // reuse its Pages ID or check public identity; it will not POST again. - } - return receipt -} - -const metadata = await client(`actions/artifacts/${artifactId}`) -assert.equal(metadata.id, artifactId) -assert.equal(metadata.name, artifactName) -assert.equal(metadata.expired, false) -assert.equal(metadata.digest, `sha256:${zipDigest}`) -assert.equal(metadata.size_in_bytes, 1774700) -assert.equal(metadata.workflow_run.id, originalRun) -assert.equal(metadata.workflow_run.repository_id, 1346469709) -assert.equal(metadata.workflow_run.head_repository_id, 1346469709) -assert.equal(metadata.workflow_run.head_branch, 'main') -assert.equal(metadata.workflow_run.head_sha, sourceSha) -await guard() -const temporary = await mkdtemp(path.join(tmpdir(), 'weapp-pages-recovery-')) -try { - // Read the signed redirect separately; never send GITHUB_TOKEN to storage. - const archive = await safeFetch(`https://api.github.com/repos/${repository}/actions/artifacts/${artifactId}/zip`, { - headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2026-03-10' }, - redirect: 'manual', - signal: AbortSignal.timeout(30000), - }) - assert.equal(archive.status, 302) - const location = new URL(archive.headers.get('location')) - assert.equal(location.protocol, 'https:') - assert.equal(location.username, '') - assert.equal(location.password, '') - const downloaded = await fetch(location, { signal: AbortSignal.timeout(60000) }) - assert.equal(downloaded.status, 200) - const bytes = Buffer.from(await downloaded.arrayBuffer()) - assert.equal(bytes.byteLength, 1774700) - assert.equal(createHash('sha256').update(bytes).digest('hex'), zipDigest) - const archiveFile = path.join(temporary, 'archive.zip') - const tarFile = path.join(temporary, 'artifact.tar') - await writeFile(archiveFile, bytes) - // Validate every member before writing. No links, devices, absolute paths, - // traversal, duplicate paths or archives exceeding the bounded size. - execFileSync('python3', ['-c', ` -import json,pathlib,sys,tarfile,zipfile -archive,tar_path,dist=map(pathlib.Path,sys.argv[1:]) -limit=100*1024*1024 -with zipfile.ZipFile(archive) as z: - assert z.namelist()==['artifact.tar'], 'Unexpected ZIP members' - item=z.getinfo('artifact.tar') - assert item.file_size <= limit and not item.flag_bits & 1, 'Invalid tar entry' - tar_path.write_bytes(z.read(item)) -with tarfile.open(tar_path,mode='r:') as t: - members=t.getmembers() - assert len(members)<=10000 and sum(m.size for m in members)<=limit - paths=set() - for member in members: - p=pathlib.PurePosixPath(member.name) - assert not p.is_absolute() and '..' not in p.parts and '\\\\' not in member.name, 'Unsafe tar path' - assert member.isfile() or member.isdir(), 'Unsupported tar member' - normalized=str(p) - assert normalized not in paths, 'Duplicate tar path' - paths.add(normalized) - assert 'repoctl-deployment.json' in paths and 'CNAME' in paths - dist.mkdir(parents=True,exist_ok=False) - for member in members: - destination=dist/pathlib.PurePosixPath(member.name) - if member.isdir(): destination.mkdir(parents=True,exist_ok=True) - else: - destination.parent.mkdir(parents=True,exist_ok=True) - with t.extractfile(member) as stream: destination.write_bytes(stream.read()) -`, archiveFile, tarFile, context.dist], { stdio: ['ignore', 'pipe', 'pipe'] }) - const identity = JSON.parse(await readFile(path.join(context.dist, 'repoctl-deployment.json'), 'utf8')) - assertIdentity(identity, context.identity) - assert.equal((await readFile(path.join(context.dist, 'CNAME'), 'utf8')).trim(), 'weapp.js.org') - const tarDigest = createHash('sha256').update(await readFile(tarFile)).digest('hex') - await writeFile(path.join(evidence, 'verified-artifact.json'), `${JSON.stringify({ artifactId, originalRun, zipDigest, tarDigest, identity }, null, 2)}\n`) - const receipt = await guard() - const sdkRoot = path.join(source, 'node_modules/@actions/artifact') - const sdkManifest = JSON.parse(await readFile(path.join(sdkRoot, 'package.json'), 'utf8')) - assert.equal(sdkManifest.version, '6.3.1') - const { DefaultArtifactClient } = await import(pathToFileURL(path.join(sdkRoot, 'lib/artifact.js')).href) - const sdk = new DefaultArtifactClient() - let uploadCount = 0 - const artifact = { - async uploadArtifact(name, files, root, options) { - assert.equal(++uploadCount, 1) - assert.equal(receipt.value.phase, 'failed', 'Recovery must not upload for an uncertain existing submission') - assert.equal(files.length, 1) - assert.equal(createHash('sha256').update(await readFile(files[0])).digest('hex'), tarDigest) - const uploaded = await sdk.uploadArtifact(name, files, root, options) - assert.ok(uploaded.id && uploaded.id !== artifactId) - await writeFile(path.join(evidence, 'new-artifact.json'), `${JSON.stringify({ id: uploaded.id, originalArtifactId: artifactId, tarDigest, runId: process.env.GITHUB_RUN_ID })}\n`) - return uploaded - }, - } - const execute = async (command, args) => { - assert.equal(command, 'tar') - const output = args[args.indexOf('-cf') + 1] - assert.ok(output && path.basename(output) === 'artifact.tar') - await copyFile(tarFile, output) - } - const result = await deployPages(context, { client: providerClient, store, artifact, run: execute, fetch: safeFetch }) - await writeFile(path.join(evidence, 'completed.json'), `${JSON.stringify({ result, uploadCount, sourceSha, releaseId, deploymentId })}\n`) - console.log(JSON.stringify({ result, uploadCount, next: 'Resume the same release with the managed repo docs workflow to finalize its state.' })) -} -catch (error) { - await writeFile(path.join(evidence, 'failure.json'), `${JSON.stringify({ message: sanitizeMessage(error.message).slice(0, 4000) })}\n`) - throw new Error(sanitizeMessage(error.message)) -} -finally { - await rm(temporary, { recursive: true, force: true }) - await rm(context.dist, { recursive: true, force: true }) -}