修复Jenkins Web构建npm版本漂移
新增 Jenkins 用户级 npm 10.9.7 版本隔离引导 在 Web Build 各独立 shell 复用固定 npm 后执行 workspace 安装与门禁 收紧生产运维检查并同步 npm workspaces 文档与共享决策
This commit is contained in:
@@ -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 的平台构建证据。
|
||||
|
||||
@@ -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`,之后才复制源码并构建主站与后台。
|
||||
|
||||
@@ -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` 的 `build<Play>PublicWorkCode` 与 `isSame<Play>PublicWorkCode` 成对导出,并补 `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/<version>/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` 成功归档产物。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 时必须跳过该安装。',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}"
|
||||
Reference in New Issue
Block a user