From 78ebe063f3cff20e86d9c776181d0d3aeb32d486 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 23 Aug 2026 04:11:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DJenkins=20Web=E6=9E=84?= =?UTF-8?q?=E5=BB=BAnpm=E7=89=88=E6=9C=AC=E6=BC=82=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 Jenkins 用户级 npm 10.9.7 版本隔离引导 在 Web Build 各独立 shell 复用固定 npm 后执行 workspace 安装与门禁 收紧生产运维检查并同步 npm workspaces 文档与共享决策 --- .../shared-memory/decision-log.md | 3 +- ...案】npm-workspaces统一依赖边界-2026-08-21.md | 2 +- ...发运维】本地开发验证与生产运维-2026-05-15.md | 4 +- jenkins/Jenkinsfile.production-web-build | 10 ++++- scripts/check-production-ops-guardrails.mjs | 27 +++++++++++- scripts/jenkins-prepare-npm-env.sh | 43 +++++++++++++++++++ 6 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 scripts/jenkins-prepare-npm-env.sh diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 2436eb813..98a5169eb 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -14242,6 +14242,7 @@ - Windows 锁文件决策:提升权限进程新建 `.agent/.manifest.json.lock` 时,Windows 可能把 owner 设为 `Administrators`。仅在固定锁路径已取得不共享独占句柄并确认是普通、非 reparse、单链接文件后,才初始化为当前 `TokenUser`;随后再次复核句柄并执行原有 owner/DACL 校验,不放宽既有异常对象的安全规则。 - Provider Schema 决策:`agent.route_manifest.missingAssetSlots` 不再广告 OpenAI-compatible 代理拒绝的 `uniqueItems`;Runtime 继续排序去重,Schema 子集门禁新增该关键字,真实 Provider smoke 必须在发布前证明工具目录可被接受。 - 运行决策:Godot 项目提交给 Project Supervisor 时使用 `standard` Run Profile,避免触发 Web 专用 `game/index.html`、HTTP preview 与自主 Web 完成门。Godot 编辑器启动和内嵌运行预览不在本切片范围。 + ## 2026-08-15 Jenkins 容器预览部署使用独立控制面 - 决策:多人内网容器预览不把操作表单塞进 Jenkins 页面,也不让 SPA 直接操作 Docker。独立 `preview-deployer` SPA 通过同源 Axum 代理触发固定 `shared/Genarrative-Preview-Deployer` Job;浏览器只持有控制面 HttpOnly 会话,Jenkins service account 和 API Token 只存在服务端环境。 @@ -14320,7 +14321,7 @@ ## 2026-08-21 JavaScript 工程统一为 npm workspaces -- 决策:根、Admin、AGC、Desktop、Mobile、Preview Deployer、三个 `packages/*` 和 Spine validator 统一进入显式 npm workspaces;固定 `packageManager=npm@10.9.7`,CI 镜像显式安装并校验同版 npm。仓库只提交根 `package-lock.json`,安装、CI、Jenkins 和容器缓存都只从根执行一次 `npm ci`。 +- 决策:根、Admin、AGC、Desktop、Mobile、Preview Deployer、三个 `packages/*` 和 Spine validator 统一进入显式 npm workspaces;固定 `packageManager=npm@10.9.7`,CI 镜像显式安装并校验同版 npm。Jenkins Web Build 不能假定系统 npm 已同步,每个独立 `bash -lc` 都必须 source `scripts/jenkins-prepare-npm-env.sh`,由该入口在 Jenkins 用户的版本隔离目录持久准备 npm `10.9.7`。仓库只提交根 `package-lock.json`,安装、CI、Jenkins 和容器缓存都只从根执行一次 `npm ci`。 - 依赖边界:每个 workspace manifest 拥有自身直接依赖,根不再为子 App 重复声明。内部私有包使用匹配版本的普通 semver `0.1.0`,由 npm 自动链接;当前 npm 不接受 `workspace:*`。npm 默认 hoist,因此依赖所有权按 manifest 和 lock 的 workspace entry 检查,不能按统一 `node_modules` 或 lock 全局包条目判断。 - 原生边界:根 H5 与 Desktop manifest 继续禁止 Tauri JS guest,AGC workspace 可以声明;统一 lock 出现 AGC guest 是合法聚合结果。Expo 沿用默认 npm monorepo 支持。AGC Cubone bundle、TypeScript、Tauri CLI 与 Windows Codex sidecar 都必须兼容根提升位置,不得依赖子 App 固定 `node_modules` 层级。 - 锁与平台:删除 AGC 和 Spine 子 lock;统一根 lock 必须保留 optional、bundled 和跨平台二进制节点。Linux 干净安装不能替代 Windows AGC sidecar、Android Expo/EAS 或可用 macOS/iOS runner 的平台构建证据。 diff --git a/docs/technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md b/docs/technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md index f6d3ecb0d..a0f3bd851 100644 --- a/docs/technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md +++ b/docs/technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md @@ -86,7 +86,7 @@ AGC 的 TypeScript、Vite/Vitest bundle 和 Windows Codex sidecar 不允许硬 ## CI、Jenkins 与容器 - Gitea 四个 job 每个只执行一次带重试的根 `npm ci`,不再单独安装 AGC。 -- Jenkins Web Build 在安装前必须精确校验 npm `10.9.7`;`RUN_NPM_CI` 只控制一次根 `npm ci`。 +- Jenkins Web Build 必须在每个独立 shell 中加载 `scripts/jenkins-prepare-npm-env.sh`;该入口在 Jenkins 运行用户的版本隔离目录准备并优先使用 npm `10.9.7`,根 workspace 安装前再精确校验版本;`RUN_NPM_CI` 只控制一次根 `npm ci`。 - Gitea CI 镜像只维护一个 npm lock SHA 与一份 npm cache;预热上下文必须包含根 lock 和全部 workspace manifests,使根 `npm ci` 能解析 workspace。 - CI 镜像仍分别维护 server-rs、Desktop Tauri、AGC Tauri 三份 Cargo lock cache;npm 单锁不改变 Rust lock 边界。 - API 镜像的 Web builder 必须显式安装并校验 npm `10.9.7`,再复制全部 workspace manifests、执行根 `npm ci`,之后才复制源码并构建主站与后台。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index a171a6744..728451be1 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -260,7 +260,7 @@ npm run check 仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成四个必须通过的 job: -所有 CI job 和 Jenkins Web Build 在根 workspace 安装前都必须确认 `npm --version` 为 `10.9.7`;旧固定镜像缺少版本元数据时只能报告 `npm_version=partial` 并由当前 job 的根 `npm ci` 继续校验 lock,不能把过渡状态当作工具链已闭合。 +所有 CI job 和 Jenkins Web Build 在根 workspace 安装前都必须确认 `npm --version` 为 `10.9.7`。Gitea job 使用预构建镜像内的固定版本;Jenkins Web Build 在每个独立 `bash -lc` 中 source `scripts/jenkins-prepare-npm-env.sh`,首次为 Jenkins 运行用户的版本隔离目录引导同版 npm,后续复用并把该 `bin` 放到 `PATH` 首位。旧固定镜像缺少版本元数据时只能报告 `npm_version=partial` 并由当前 job 的根 `npm ci` 继续校验 lock,不能把过渡状态当作工具链已闭合。 - `Repository checks`:调用唯一入口 `npm run check:repository-ci`,执行 `npm run lint`、AI 游戏创作壳 AppSurface 定向测试、主站与后台生产构建和提交差异空白检查。本地 master `pre-push` 复用同一入口,禁止在 workflow 与 hook 中维护两份近似命令。 - `Frontend tests`:按唯一根 workspace lockfile 执行一次干净的 `npm ci`,再独立执行根 `npm run test`、`npm run bgfilter-worker:smoke-test`、`npm run check:production-health-patrol`、`npm run check:production-api-release` 和 `npm run check:production-api-deploy`,让 Vitest、Node test smoke harness 及不依赖真实服务的生产巡检 / 发布 / 部署行为 fixture 在 Gitea job 中持续执行;其中 `.test.mjs` 使用 Node test runner,不依赖 Vitest 的 `scripts/**/*.test.ts` 收集规则。 @@ -610,7 +610,7 @@ Jenkins Copy Artifact 必须保持 `Production` 权限模式;产物生产者 `Genarrative-Web-Build` 的主站构建失败若出现 Rollup 报错 `"xxx" is not exported by "src/services/publicWorkCode.ts"`,优先按前端公开作品号工具缺失处理,而不是排查 Jenkins 节点环境。修复时要让 `publicWorkCode.ts` 的 `buildPublicWorkCode` 与 `isSamePublicWorkCode` 成对导出,并补 `src/services/publicWorkCode.test.ts` 覆盖对应玩法前缀;随后用 `npm run build:production-release -- --component web --name <临时名>` 复现 Jenkins web 构建路径。 -`Genarrative-Web-Build` 在运行根 Vitest 前必须按唯一根 `package-lock.json` 执行一次干净的 `npm ci`。根 lock 聚合全部 workspace,因此会安装 AI 游戏创作壳合法声明的 `@tauri-apps/api`、`@tauri-apps/plugin-http` 等 Tauri guest;这些依赖仍只归属 AGC workspace,不得加入根 H5 或 Desktop manifest。排查收集失败时先运行 `npm run check:npm-workspaces` 并核对根 lock 的 workspace entry,禁止恢复第二份 lock 或子目录安装。 +`Genarrative-Web-Build` 先通过 `scripts/jenkins-prepare-npm-env.sh` 在 Jenkins 运行用户的持久版本目录准备 npm `10.9.7`,显式提升该 `bin` 后校验真实版本,不依赖系统 `/usr/bin/npm` 或 `packageManager` 声明自动切版。在运行根 Vitest 前必须按唯一根 `package-lock.json` 执行一次干净的 `npm ci`。根 lock 聚合全部 workspace,因此会安装 AI 游戏创作壳合法声明的 `@tauri-apps/api`、`@tauri-apps/plugin-http` 等 Tauri guest;这些依赖仍只归属 AGC workspace,不得加入根 H5 或 Desktop manifest。排查收集失败时先运行 `npm run check:npm-workspaces` 并核对根 lock 的 workspace entry,禁止恢复第二份 lock 或子目录安装。 `Genarrative-Web-Build` 会把 `build//web.tar.gz`、`web.tar.gz.sha256`、`release-manifest.json` 和 `scripts/deploy/production-web-deploy.sh` 直接归档为 Jenkins 构建产物;`Genarrative-Web-Deploy` 只通过 `copyArtifacts` 从指定上游构建复制这些产物和部署脚本,不再在目标机器 checkout Git,再执行随构建归档的 `scripts/deploy/production-web-deploy.sh`。Web 发布不再读取构建机本地缓存目录,也不再通过 release agent `rsync` 回构建机拉取大包;如果 deploy 找不到 `web.tar.gz`,应先检查上游 Web Build 是否按同一 `BUILD_VERSION` 成功归档产物。 diff --git a/jenkins/Jenkinsfile.production-web-build b/jenkins/Jenkinsfile.production-web-build index 6058eec2e..475545560 100644 --- a/jenkins/Jenkinsfile.production-web-build +++ b/jenkins/Jenkinsfile.production-web-build @@ -92,6 +92,7 @@ pipeline { sh ''' bash -lc ' set -euo pipefail + source scripts/jenkins-prepare-npm-env.sh actual_npm_version="$(npm --version)" if [[ "${actual_npm_version}" != "${GENARRATIVE_NPM_VERSION}" ]]; then echo "npm 版本不匹配:期望 ${GENARRATIVE_NPM_VERSION},实际 ${actual_npm_version}" >&2 @@ -101,12 +102,19 @@ pipeline { ''' script { if (params.RUN_NPM_CI) { - sh 'bash -lc "npm ci"' + sh ''' + bash -lc ' + set -euo pipefail + source scripts/jenkins-prepare-npm-env.sh + npm ci + ' + ''' } } sh ''' bash -lc ' set -euo pipefail + source scripts/jenkins-prepare-npm-env.sh npm run check:encoding npm run check:production-ops npm run lint:eslint diff --git a/scripts/check-production-ops-guardrails.mjs b/scripts/check-production-ops-guardrails.mjs index 227db8291..c567e8232 100644 --- a/scripts/check-production-ops-guardrails.mjs +++ b/scripts/check-production-ops-guardrails.mjs @@ -7582,6 +7582,10 @@ const webBuildContent = readFileSync( 'jenkins/Jenkinsfile.production-web-build', 'utf8', ); +const webNpmPrepareContent = readFileSync( + 'scripts/jenkins-prepare-npm-env.sh', + 'utf8', +); const webBuildStageOffset = webBuildContent.indexOf("stage('Build Web')"); const webArchiveStageOffset = webBuildContent.indexOf( "stage('Archive')", @@ -7592,7 +7596,13 @@ const webBuildStageContent = ? webBuildContent.slice(webBuildStageOffset, webArchiveStageOffset) : ''; const webNpmCiBlock = `if (params.RUN_NPM_CI) { - sh 'bash -lc "npm ci"' + sh ''' + bash -lc ' + set -euo pipefail + source scripts/jenkins-prepare-npm-env.sh + npm ci + ' + ''' }`; const webNpmCiBlockOffset = webBuildStageContent.indexOf(webNpmCiBlock); const webTestOffset = webBuildStageContent.indexOf('npm run test'); @@ -7600,18 +7610,31 @@ const webNpmCiCalls = webBuildStageContent.match(/\bnpm ci(?:\s|["'])/gu); const webNpmVersionCheckOffset = webBuildStageContent.indexOf( 'actual_npm_version="$(npm --version)"', ); +const webNpmPrepareOffset = webBuildStageContent.indexOf( + 'source scripts/jenkins-prepare-npm-env.sh', +); +const webNpmPrepareCalls = webBuildStageContent.match( + /source scripts\/jenkins-prepare-npm-env\.sh/gu, +); if ( !webBuildContent.includes("GENARRATIVE_NPM_VERSION = '10.9.7'") || + !webNpmPrepareContent.includes( + '"${bootstrap_npm}" install --global --prefix "${npm_prefix}" "npm@${expected_version}" --ignore-scripts --no-audit --no-fund', + ) || + !webNpmPrepareContent.includes('export PATH="${npm_prefix}/bin:${PATH}"') || + webNpmPrepareOffset < 0 || webNpmVersionCheckOffset < 0 || + webNpmPrepareOffset >= webNpmVersionCheckOffset || webNpmCiBlockOffset < 0 || webTestOffset < 0 || webNpmVersionCheckOffset >= webNpmCiBlockOffset || webNpmCiBlockOffset >= webTestOffset || + (webNpmPrepareCalls?.length ?? 0) !== 3 || (webNpmCiCalls?.length ?? 0) !== 1 ) { failed = true; console.error( - '[check:production-ops] Web Build 必须先精确校验 npm 10.9.7,再在 RUN_NPM_CI 条件块内按根 workspace lockfile 执行唯一一次 npm ci,并在 npm run test 前完成;RUN_NPM_CI=false 时必须跳过该安装。', + '[check:production-ops] Web Build 必须先为每个独立 shell 准备并加载 npm 10.9.7,再精确校验版本,然后在 RUN_NPM_CI 条件块内按根 workspace lockfile 执行唯一一次 npm ci,并在 npm run test 前完成;RUN_NPM_CI=false 时必须跳过该安装。', ); } diff --git a/scripts/jenkins-prepare-npm-env.sh b/scripts/jenkins-prepare-npm-env.sh new file mode 100644 index 000000000..fdd136c48 --- /dev/null +++ b/scripts/jenkins-prepare-npm-env.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -euo pipefail + +expected_version="${GENARRATIVE_NPM_VERSION:?GENARRATIVE_NPM_VERSION 不能为空}" +if [[ ! "${expected_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "[jenkins-npm] 非法 npm 版本: ${expected_version}" >&2 + return 1 2>/dev/null || exit 1 +fi + +if [[ -n "${GENARRATIVE_JENKINS_NPM_PREFIX:-}" ]]; then + npm_prefix="${GENARRATIVE_JENKINS_NPM_PREFIX}" +else + npm_home="${HOME:?HOME 不能为空}" + npm_prefix="${npm_home}/.local/share/genarrative/npm-${expected_version}" +fi + +pinned_npm="${npm_prefix}/bin/npm" +actual_version="" +if [[ -x "${pinned_npm}" ]]; then + actual_version="$("${pinned_npm}" --version 2>/dev/null || true)" +fi + +if [[ "${actual_version}" != "${expected_version}" ]]; then + bootstrap_npm="$(command -v npm || true)" + if [[ -z "${bootstrap_npm}" ]]; then + echo "[jenkins-npm] 缺少用于引导固定版本的 npm" >&2 + return 1 2>/dev/null || exit 1 + fi + + echo "[jenkins-npm] 准备 npm ${expected_version} (bootstrap=${bootstrap_npm})" + mkdir -p "${npm_prefix}" + "${bootstrap_npm}" install --global --prefix "${npm_prefix}" "npm@${expected_version}" --ignore-scripts --no-audit --no-fund +fi + +export PATH="${npm_prefix}/bin:${PATH}" +actual_version="$(npm --version)" +if [[ "${actual_version}" != "${expected_version}" ]]; then + echo "[jenkins-npm] npm 版本不匹配:期望 ${expected_version},实际 ${actual_version}" >&2 + return 1 2>/dev/null || exit 1 +fi + +echo "[jenkins-npm] npm=$(command -v npm) version=${actual_version}"