迁移 npm workspaces 统一依赖边界
根目录统一管理九个 workspace 与唯一 lockfile 迁移 CI、Jenkins 和容器的根目录单次 npm ci 补齐 hoist 兼容、版本校验与 workspace 门禁 同步依赖边界方案、运维文档和长期记忆
This commit is contained in:
@@ -78,9 +78,6 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Install AI game creator dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
|
||||
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
|
||||
@@ -100,9 +97,6 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Install AI game creator dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
|
||||
|
||||
- name: Run frontend and script tests
|
||||
run: npm run test
|
||||
|
||||
@@ -213,9 +207,6 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Install AI game creator dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
|
||||
|
||||
- name: Prepare native Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
npm install
|
||||
```
|
||||
|
||||
该命令会按根 `package.json` 的 npm workspaces 一次安装主站、Admin、Mobile、Desktop、AGC、Preview Deployer、内部 packages 与工具依赖;仓库只使用根 `package-lock.json`,不要在子目录单独执行 `npm install` / `npm ci` 或提交嵌套 lockfile。
|
||||
|
||||
准备环境变量:
|
||||
|
||||
- 复制 `.env.example` 为 `.env.local`
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview --host 127.0.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@genarrative/shared": "0.1.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lucide-react": "^0.546.0",
|
||||
"react": "^19.0.0",
|
||||
@@ -17,8 +18,11 @@
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "~5.8.2"
|
||||
"typescript": "~5.8.2",
|
||||
"vitest": "^0.34.6"
|
||||
}
|
||||
}
|
||||
|
||||
-5214
File diff suppressed because it is too large
Load Diff
@@ -32,10 +32,13 @@
|
||||
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
|
||||
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
|
||||
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
|
||||
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cubone/react-file-manager": "^1.35.0",
|
||||
"@genarrative/image-canvas-core": "0.1.0",
|
||||
"@genarrative/image-canvas-react": "0.1.0",
|
||||
"@genarrative/shared": "0.1.0",
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
@@ -60,9 +63,12 @@
|
||||
"@openai/codex": "0.147.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tauri-apps/cli": "^2.11.2",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "~5.8.2"
|
||||
"typescript": "~5.8.2",
|
||||
"vitest": "^0.34.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
||||
let app_root = manifest_dir
|
||||
.parent()
|
||||
.expect("AI 游戏创作 Tauri manifest 必须位于应用目录下");
|
||||
let repo_root = app_root
|
||||
.parent()
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
|
||||
let source_candidates = [
|
||||
app_root.join(
|
||||
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
||||
@@ -54,6 +58,12 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
||||
app_root.join(
|
||||
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
||||
),
|
||||
repo_root.join(
|
||||
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
||||
),
|
||||
repo_root.join(
|
||||
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
||||
),
|
||||
];
|
||||
let source = source_candidates
|
||||
.iter()
|
||||
@@ -65,12 +75,12 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"内置 Codex CLI 缺失;请先在 apps/ai-game-creator-shell 执行 npm ci(已检查:{})",
|
||||
"内置 Codex CLI 缺失;请先在仓库根目录执行 npm ci(已检查:{})",
|
||||
source_candidates
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
)
|
||||
});
|
||||
let target_dir = manifest_dir.join("resources/codex/win-x64");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -8,6 +9,10 @@ import { defineConfig } from 'vite';
|
||||
|
||||
const appRoot = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(appRoot, '../..');
|
||||
const appRequire = createRequire(resolve(appRoot, 'package.json'));
|
||||
const reactFileManagerPackageRoot = dirname(
|
||||
appRequire.resolve('@cubone/react-file-manager/package.json'),
|
||||
);
|
||||
|
||||
function resolveDevApiTarget() {
|
||||
const statePath = resolve(repoRoot, '.app/dev-stack.json');
|
||||
@@ -75,8 +80,8 @@ export default defineConfig({
|
||||
{
|
||||
find: /^@cubone\/react-file-manager$/,
|
||||
replacement: resolve(
|
||||
appRoot,
|
||||
'node_modules/@cubone/react-file-manager/dist/react-file-manager.es.js',
|
||||
reactFileManagerPackageRoot,
|
||||
'dist/react-file-manager.es.js',
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "^56.0.15",
|
||||
"@genarrative/shared": "0.1.0",
|
||||
"expo": "^56.0.12",
|
||||
"expo-camera": "56.0.8",
|
||||
"expo-clipboard": "^56.0.4",
|
||||
@@ -37,7 +38,10 @@
|
||||
"react-native-webview": "^13.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"eas-cli": "^20.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vitest": "^0.34.6"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,17 @@
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const appConfigPath = new URL('../app.json', import.meta.url);
|
||||
const packagePath = new URL('../package.json', import.meta.url);
|
||||
const configPluginsPackagePath = new URL(
|
||||
'../../../node_modules/@expo/config-plugins/package.json',
|
||||
import.meta.url,
|
||||
const shellRequire = createRequire(packagePath);
|
||||
const configPluginsPackagePath = shellRequire.resolve(
|
||||
'@expo/config-plugins/package.json',
|
||||
);
|
||||
const expoPrivacyInfoPluginPath = new URL(
|
||||
'../../../node_modules/@expo/config-plugins/build/ios/PrivacyInfo.js',
|
||||
import.meta.url,
|
||||
const expoPrivacyInfoPluginPath = resolve(
|
||||
dirname(configPluginsPackagePath),
|
||||
'build/ios/PrivacyInfo.js',
|
||||
);
|
||||
const sharedContractPath = new URL(
|
||||
'../../../packages/shared/src/contracts/hostBridge.ts',
|
||||
@@ -43,7 +45,9 @@ if (result.error) {
|
||||
}
|
||||
|
||||
if (result.signal) {
|
||||
throw new Error(`Expo config smoke was terminated by signal ${result.signal}`);
|
||||
throw new Error(
|
||||
`Expo config smoke was terminated by signal ${result.signal}`,
|
||||
);
|
||||
}
|
||||
|
||||
if ((result.status ?? 0) !== 0) {
|
||||
@@ -194,7 +198,9 @@ const sharedPublicWebOrigin = extractStringConstExport(
|
||||
);
|
||||
const sharedPublicWebOriginUrl = new URL(sharedPublicWebOrigin);
|
||||
if (sharedPublicWebOriginUrl.protocol !== 'https:') {
|
||||
throw new Error('shared HostBridge public web origin must use https for mobile app links');
|
||||
throw new Error(
|
||||
'shared HostBridge public web origin must use https for mobile app links',
|
||||
);
|
||||
}
|
||||
const sharedPublicWebHost = sharedPublicWebOriginUrl.hostname;
|
||||
const sharedPublicWebAssociatedDomain = `applinks:${sharedPublicWebHost}`;
|
||||
@@ -206,7 +212,11 @@ assertEqual(expoConfig.version, packageConfig.version, 'version');
|
||||
assertEqual(expoConfig.version, appConfig.version, 'app version');
|
||||
assertEqual(expoConfig.orientation, 'default', 'orientation');
|
||||
assertEqual(expoConfig.userInterfaceStyle, 'automatic', 'userInterfaceStyle');
|
||||
assertSameList(expoConfig.assetBundlePatterns, ['**/*'], 'asset bundle patterns');
|
||||
assertSameList(
|
||||
expoConfig.assetBundlePatterns,
|
||||
['**/*'],
|
||||
'asset bundle patterns',
|
||||
);
|
||||
assertEqual(expoConfig.icon, './assets/icon.png', 'icon');
|
||||
assertEqual(expoConfig.splash?.image, './assets/icon.png', 'splash image');
|
||||
assertEqual(expoConfig.splash?.resizeMode, 'contain', 'splash resize mode');
|
||||
@@ -216,10 +226,14 @@ if (Object.keys(expoConfig.updates ?? {}).some((key) => key !== 'enabled')) {
|
||||
throw new Error('Expo config OTA update metadata must stay empty');
|
||||
}
|
||||
if ('runtimeVersion' in expoConfig) {
|
||||
throw new Error('Expo config runtimeVersion must not be set without a real OTA release channel');
|
||||
throw new Error(
|
||||
'Expo config runtimeVersion must not be set without a real OTA release channel',
|
||||
);
|
||||
}
|
||||
if ('releaseChannel' in expoConfig || 'channel' in expoConfig) {
|
||||
throw new Error('Expo config release channel must not be set without a real release process');
|
||||
throw new Error(
|
||||
'Expo config release channel must not be set without a real release process',
|
||||
);
|
||||
}
|
||||
assertEqual(
|
||||
expoConfig.extra?.genarrativeHostBridgeVersion,
|
||||
@@ -259,7 +273,9 @@ const configPluginsMajor = Number(
|
||||
String(configPluginsPackageConfig.version).split('.')[0],
|
||||
);
|
||||
if (!Number.isFinite(configPluginsMajor) || configPluginsMajor < 50) {
|
||||
throw new Error('Expo config plugins must support built-in iOS privacy manifests');
|
||||
throw new Error(
|
||||
'Expo config plugins must support built-in iOS privacy manifests',
|
||||
);
|
||||
}
|
||||
for (const snippet of [
|
||||
'function withPrivacyInfo(config)',
|
||||
@@ -269,7 +285,9 @@ for (const snippet of [
|
||||
'mergePrivacyInfo(existing, privacyManifests)',
|
||||
]) {
|
||||
if (!expoPrivacyInfoPluginSource.includes(snippet)) {
|
||||
throw new Error(`Expo config plugins PrivacyInfo support missing ${snippet}`);
|
||||
throw new Error(
|
||||
`Expo config plugins PrivacyInfo support missing ${snippet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +318,9 @@ assertSameSet(
|
||||
'Android explicit permissions',
|
||||
);
|
||||
if (appConfig.android?.permissions?.includes('android.permission.CAMERA')) {
|
||||
throw new Error('Expo source config must not hard-code Android CAMERA permission');
|
||||
throw new Error(
|
||||
'Expo source config must not hard-code Android CAMERA permission',
|
||||
);
|
||||
}
|
||||
assertIncludes(
|
||||
expoConfig.android?.blockedPermissions,
|
||||
@@ -312,11 +332,23 @@ assertIncludes(
|
||||
'android.permission.READ_EXTERNAL_STORAGE',
|
||||
'Android external download blocked permissions',
|
||||
);
|
||||
if (expoConfig.android?.blockedPermissions?.includes('android.permission.RECORD_AUDIO')) {
|
||||
throw new Error('Expo config Android permissions must not block RECORD_AUDIO needed by same-origin H5 microphone gameplay');
|
||||
if (
|
||||
expoConfig.android?.blockedPermissions?.includes(
|
||||
'android.permission.RECORD_AUDIO',
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Expo config Android permissions must not block RECORD_AUDIO needed by same-origin H5 microphone gameplay',
|
||||
);
|
||||
}
|
||||
if (expoConfig.android?.blockedPermissions?.includes('android.permission.POST_NOTIFICATIONS')) {
|
||||
throw new Error('Expo config Android permissions must not block POST_NOTIFICATIONS needed by local notification.showLocal delivery');
|
||||
if (
|
||||
expoConfig.android?.blockedPermissions?.includes(
|
||||
'android.permission.POST_NOTIFICATIONS',
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Expo config Android permissions must not block POST_NOTIFICATIONS needed by local notification.showLocal delivery',
|
||||
);
|
||||
}
|
||||
assertIncludes(
|
||||
expoConfig.android?.blockedPermissions,
|
||||
@@ -352,7 +384,9 @@ assertEqual(
|
||||
|
||||
const appLinkFilters = expoConfig.android?.intentFilters ?? [];
|
||||
if (appLinkFilters.length !== 1) {
|
||||
throw new Error('Expo config Android app link filter must be the only intent filter');
|
||||
throw new Error(
|
||||
'Expo config Android app link filter must be the only intent filter',
|
||||
);
|
||||
}
|
||||
|
||||
const [appLinkFilter] = appLinkFilters;
|
||||
@@ -428,7 +462,9 @@ assertEqual(
|
||||
);
|
||||
|
||||
if (findPlugin('expo-updates')) {
|
||||
throw new Error('Expo config must not include expo-updates without a real release channel');
|
||||
throw new Error(
|
||||
'Expo config must not include expo-updates without a real release channel',
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[mobile-shell:expo-config] OK');
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "~5.8.2",
|
||||
|
||||
@@ -56,7 +56,7 @@ Linux Docker Engine 若要从宿主机 CLI 连到容器内服务,直接用 `ht
|
||||
|
||||
## 构建工具链
|
||||
|
||||
`api-server` 容器镜像只构建 Linux release API 二进制,不构建 `spacetime-module`。当前 `api-server -> spacetime-client -> spacetimedb-sdk 2.7.0` 依赖链继续兼容 Rust 1.93,因此 `deploy/container/api-server.Dockerfile` 的 Rust builder 固定为 `rust:1.93-bookworm`。镜像构建阶段会同时复制 `public/`,用于满足 API 二进制里 `include_bytes!` 引用的内置素材;不要把 `public/generated-*` 放入镜像上下文。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。
|
||||
`api-server` 容器镜像只构建 Linux release API 二进制,不构建 `spacetime-module`。当前 `api-server -> spacetime-client -> spacetimedb-sdk 2.7.0` 依赖链继续兼容 Rust 1.93,因此 `deploy/container/api-server.Dockerfile` 的 Rust builder 固定为 `rust:1.93-bookworm`。Web builder 显式安装并校验 npm `10.9.7`,再按唯一根 workspace lock 执行一次 `npm ci`,不依赖 Node 基础镜像隐含的 npm 版本。镜像构建阶段会同时复制 `public/`,用于满足 API 二进制里 `include_bytes!` 引用的内置素材;不要把 `public/generated-*` 放入镜像上下文。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。
|
||||
|
||||
### Gitea CI 预构建 Job 镜像
|
||||
|
||||
@@ -71,9 +71,9 @@ bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-git
|
||||
bash scripts/gitea-ci-job-image.sh load-runner
|
||||
```
|
||||
|
||||
默认构建 tag 为 `genarrative/gitea-project-ci:20260807.1`。脚本通过 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本、根与 AI 游戏创作壳的 npm manifests/lock,以及 server-rs、桌面壳和 AI 游戏创作壳的 Cargo manifests/lock;当前构建 context 约 `2.13 MB`,不会把业务源码、素材或本地私密文件发送给 Docker daemon。镜像除固定工具链外,还按上述五份 lock 预热 npm / Cargo 下载缓存;三个 `cargo fetch --locked` 最多执行 5 次整命令级有界重试,再分别以断网 `cargo fetch --locked` 验证缓存闭合,不包含 `node_modules` 或 Cargo `target`。`build` 完成后会自动运行环境校验,`load-runner` 还会比对宿主和 runner 内层的完整 Image ID,并在内层执行 bwrap 与 Chrome headless canary。当前验证镜像约 `1.85 GB`,完整 Image ID 为 `sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf`。执行这些命令不要求必须使用 root,但执行账号必须有权访问宿主 Docker API 并管理 runner 容器;没有该权限时交给 runner 运维人员执行。
|
||||
默认构建 tag 为 `genarrative/gitea-project-ci:20260807.1`。脚本通过 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本、根 workspace 的唯一 npm lock 与全部 workspace manifest,以及 server-rs、桌面壳和 AI 游戏创作壳的 Cargo manifests/lock;不会把业务源码、素材或本地私密文件发送给 Docker daemon。新镜像显式安装并精确校验 `npm 10.9.7`,不依赖 Node 发行包隐含的 npm 版本;除固定工具链外,还按一份 npm workspace lock 与三份 Cargo lock 预热下载缓存。npm 只执行一次忽略 lifecycle scripts 的 workspace `npm ci`,三个 `cargo fetch --locked` 最多执行 5 次整命令级有界重试,再分别以断网 `cargo fetch --locked` 验证缓存闭合,镜像不包含 `node_modules` 或 Cargo `target`。`build` 完成后会自动运行环境校验,`load-runner` 还会比对宿主和 runner 内层的完整 Image ID,并在内层执行 bwrap 与 Chrome headless canary。workspace lock 或 manifest 变化落地后必须按下述顺序重建并装载镜像;过渡期旧固定镜像缺少 `GENARRATIVE_GITEA_CI_NPM_VERSION` 时,校验只输出 `npm_version=partial` 和 Actions warning,继续由当前 job 的根 `npm ci` 验证唯一 lock,不能据此宣称 npm 版本或新依赖缓存已经闭合。执行这些命令不要求必须使用 root,但执行账号必须有权访问宿主 Docker API 并管理 runner 容器;没有该权限时交给 runner 运维人员执行。
|
||||
|
||||
runner 配置保留原 `ubuntu-latest` 映射,另外增加 `genarrative-ci:docker://sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf`。内层 Docker 数据必须持久化,`force_pull` 保持 `false`;该精确 Image ID 在内层不存在时 job 应直接失败,不回退到浮动 tag 或现场拉取。四个 job 使用镜像内 `genarrative-gitea-checkout` 直接从当前 Gitea 拉取事件 commit,带 5 次有界重试,不再运行时下载 GitHub checkout action;随后以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,同时校验工具链、五份缓存锁命中状态、bwrap 和 Chrome headless。锁不匹配时校验会输出 `partial` 和醒目的 Actions warning,提示在可信分支落地后刷新镜像。各 job 仍运行干净的 `npm ci` 以校验当前 lockfile 并隔离 PR 依赖,但统一通过 `scripts/ci-npm-ci-with-retry.sh` 最多执行 3 次整命令级有界重试,并使用镜像内 npm cache 和 `prefer-offline`;锁文件新增依赖时允许经受控网络补齐,本阶段不启用共享 Actions cache。
|
||||
runner 配置保留原 `ubuntu-latest` 映射,`genarrative-ci` 继续映射到经 `build / verify / load-runner` 验证并写入配置的完整 Image ID。内层 Docker 数据必须持久化,`force_pull` 保持 `false`;该精确 Image ID 在内层不存在时 job 应直接失败,不回退到浮动 tag 或现场拉取。四个 job 使用镜像内 `genarrative-gitea-checkout` 直接从当前 Gitea 拉取事件 commit,带 5 次有界重试,不再运行时下载 GitHub checkout action;随后以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,同时校验工具链、一份 npm workspace 缓存锁、三份 Cargo 缓存锁、bwrap 和 Chrome headless。锁不匹配时校验会输出 `partial` 和醒目的 Actions warning,提示在可信分支落地后刷新镜像。各 job 仍各自运行一次干净的根 `npm ci`,以唯一 workspace lock 校验全部 App/package/tool 依赖并隔离 PR 依赖;统一通过 `scripts/ci-npm-ci-with-retry.sh` 最多执行 3 次整命令级有界重试,并使用镜像内 npm cache 和 `prefer-offline`。锁文件新增依赖时允许经受控网络补齐,本阶段不启用共享 Actions cache。
|
||||
|
||||
更新顺序固定为:
|
||||
|
||||
|
||||
@@ -34,8 +34,20 @@ CMD ["api-server"]
|
||||
FROM node:22-bookworm-slim AS web-builder
|
||||
WORKDIR /workspace
|
||||
|
||||
ARG NPM_VERSION=10.9.7
|
||||
RUN npm install --global "npm@${NPM_VERSION}" --no-audit --no-fund && \
|
||||
test "$(npm --version)" = "${NPM_VERSION}"
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY apps/admin-web/package.json ./apps/admin-web/package.json
|
||||
COPY apps/ai-game-creator-shell/package.json ./apps/ai-game-creator-shell/package.json
|
||||
COPY apps/desktop-shell/package.json ./apps/desktop-shell/package.json
|
||||
COPY apps/mobile-shell/package.json ./apps/mobile-shell/package.json
|
||||
COPY apps/preview-deployer-web/package.json ./apps/preview-deployer-web/package.json
|
||||
COPY packages/image-canvas-core/package.json ./packages/image-canvas-core/package.json
|
||||
COPY packages/image-canvas-react/package.json ./packages/image-canvas-react/package.json
|
||||
COPY packages/shared/package.json ./packages/shared/package.json
|
||||
COPY tools/spine-json-export-validator/package.json ./tools/spine-json-export-validator/package.json
|
||||
RUN npm ci
|
||||
|
||||
COPY index.html metadata.json tsconfig.json vite.config.ts ./
|
||||
|
||||
@@ -56,6 +56,7 @@ FROM ${RUNNER_IMAGE}
|
||||
|
||||
ARG NODE_VERSION=22.23.1
|
||||
ARG NODE_LINUX_X64_SHA256=9749e988f437343b7fa832c69ded82a312e41a03116d766797ac14f6f9eee578
|
||||
ARG NPM_VERSION=10.9.7
|
||||
ARG GOOGLE_CHROME_VERSION=150.0.7871.181-1
|
||||
ARG GOOGLE_LINUX_SIGNING_KEY_FINGERPRINT=EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796
|
||||
|
||||
@@ -122,26 +123,34 @@ RUN node_archive="node-v${NODE_VERSION}-linux-x64.tar.xz" \
|
||||
&& ln -sfn /usr/local/lib/genarrative-node/bin/node /usr/local/bin/node \
|
||||
&& ln -sfn /usr/local/lib/genarrative-node/bin/npm /usr/local/bin/npm \
|
||||
&& ln -sfn /usr/local/lib/genarrative-node/bin/npx /usr/local/bin/npx \
|
||||
&& ln -sfn /usr/local/lib/genarrative-node/bin/corepack /usr/local/bin/corepack
|
||||
&& ln -sfn /usr/local/lib/genarrative-node/bin/corepack /usr/local/bin/corepack \
|
||||
&& npm install --global "npm@${NPM_VERSION}" --no-audit --no-fund \
|
||||
&& test "$(npm --version)" = "${NPM_VERSION}"
|
||||
|
||||
COPY --from=rust-dependency-cache /usr/local/cargo /usr/local/cargo
|
||||
COPY --from=rust-dependency-cache /usr/local/rustup /usr/local/rustup
|
||||
|
||||
ARG NPM_LOCK_SHA256
|
||||
ARG AGC_NPM_LOCK_SHA256
|
||||
ARG SERVER_RUST_LOCK_SHA256
|
||||
ARG DESKTOP_RUST_LOCK_SHA256
|
||||
ARG AGC_RUST_LOCK_SHA256
|
||||
|
||||
COPY package.json package-lock.json /usr/local/share/genarrative-ci/npm/
|
||||
COPY apps/ai-game-creator-shell/package.json apps/ai-game-creator-shell/package-lock.json /usr/local/share/genarrative-ci/agc-npm/
|
||||
COPY apps/admin-web/package.json /usr/local/share/genarrative-ci/npm/apps/admin-web/package.json
|
||||
COPY apps/ai-game-creator-shell/package.json /usr/local/share/genarrative-ci/npm/apps/ai-game-creator-shell/package.json
|
||||
COPY apps/desktop-shell/package.json /usr/local/share/genarrative-ci/npm/apps/desktop-shell/package.json
|
||||
COPY apps/mobile-shell/package.json /usr/local/share/genarrative-ci/npm/apps/mobile-shell/package.json
|
||||
COPY apps/preview-deployer-web/package.json /usr/local/share/genarrative-ci/npm/apps/preview-deployer-web/package.json
|
||||
COPY packages/image-canvas-core/package.json /usr/local/share/genarrative-ci/npm/packages/image-canvas-core/package.json
|
||||
COPY packages/image-canvas-react/package.json /usr/local/share/genarrative-ci/npm/packages/image-canvas-react/package.json
|
||||
COPY packages/shared/package.json /usr/local/share/genarrative-ci/npm/packages/shared/package.json
|
||||
COPY tools/spine-json-export-validator/package.json /usr/local/share/genarrative-ci/npm/tools/spine-json-export-validator/package.json
|
||||
COPY server-rs/Cargo.lock /usr/local/share/genarrative-ci/locks/server-rs.Cargo.lock
|
||||
COPY apps/desktop-shell/src-tauri/Cargo.lock /usr/local/share/genarrative-ci/locks/desktop-shell.Cargo.lock
|
||||
COPY apps/ai-game-creator-shell/src-tauri/Cargo.lock /usr/local/share/genarrative-ci/locks/ai-game-creator-shell.Cargo.lock
|
||||
COPY deploy/container/gitea-ci-checkout.sh /usr/local/bin/genarrative-gitea-checkout
|
||||
|
||||
RUN test -n "${NPM_LOCK_SHA256}" \
|
||||
&& test -n "${AGC_NPM_LOCK_SHA256}" \
|
||||
&& test -n "${SERVER_RUST_LOCK_SHA256}" \
|
||||
&& test -n "${DESKTOP_RUST_LOCK_SHA256}" \
|
||||
&& test -n "${AGC_RUST_LOCK_SHA256}" \
|
||||
@@ -149,10 +158,6 @@ RUN test -n "${NPM_LOCK_SHA256}" \
|
||||
"${NPM_LOCK_SHA256}" \
|
||||
/usr/local/share/genarrative-ci/npm/package-lock.json \
|
||||
| sha256sum --check --strict \
|
||||
&& printf '%s %s\n' \
|
||||
"${AGC_NPM_LOCK_SHA256}" \
|
||||
/usr/local/share/genarrative-ci/agc-npm/package-lock.json \
|
||||
| sha256sum --check --strict \
|
||||
&& printf '%s %s\n' \
|
||||
"${SERVER_RUST_LOCK_SHA256}" \
|
||||
/usr/local/share/genarrative-ci/locks/server-rs.Cargo.lock \
|
||||
@@ -172,14 +177,11 @@ RUN test -n "${NPM_LOCK_SHA256}" \
|
||||
--no-fund \
|
||||
--prefer-offline \
|
||||
--prefix /usr/local/share/genarrative-ci/npm \
|
||||
&& rm -rf /usr/local/share/genarrative-ci/npm/node_modules \
|
||||
&& npm ci \
|
||||
--ignore-scripts \
|
||||
--no-audit \
|
||||
--no-fund \
|
||||
--prefer-offline \
|
||||
--prefix /usr/local/share/genarrative-ci/agc-npm \
|
||||
&& rm -rf /usr/local/share/genarrative-ci/agc-npm/node_modules \
|
||||
&& rm -rf \
|
||||
/usr/local/share/genarrative-ci/npm/node_modules \
|
||||
/usr/local/share/genarrative-ci/npm/apps/*/node_modules \
|
||||
/usr/local/share/genarrative-ci/npm/packages/*/node_modules \
|
||||
/usr/local/share/genarrative-ci/npm/tools/*/node_modules \
|
||||
&& npm cache verify
|
||||
|
||||
RUN install -m 0755 /usr/local/cargo/bin/rustup /usr/local/bin/rustup \
|
||||
@@ -188,10 +190,10 @@ RUN install -m 0755 /usr/local/cargo/bin/rustup /usr/local/bin/rustup \
|
||||
done
|
||||
|
||||
ENV CARGO_HOME=/usr/local/cargo \
|
||||
GENARRATIVE_GITEA_CI_AGC_NPM_LOCK_SHA256=${AGC_NPM_LOCK_SHA256} \
|
||||
GENARRATIVE_GITEA_CI_AGC_RUST_LOCK_SHA256=${AGC_RUST_LOCK_SHA256} \
|
||||
GENARRATIVE_GITEA_CI_DESKTOP_RUST_LOCK_SHA256=${DESKTOP_RUST_LOCK_SHA256} \
|
||||
GENARRATIVE_GITEA_CI_NPM_LOCK_SHA256=${NPM_LOCK_SHA256} \
|
||||
GENARRATIVE_GITEA_CI_NPM_VERSION=${NPM_VERSION} \
|
||||
GENARRATIVE_GITEA_CI_SERVER_RUST_LOCK_SHA256=${SERVER_RUST_LOCK_SHA256} \
|
||||
NPM_CONFIG_AUDIT=false \
|
||||
NPM_CONFIG_FETCH_RETRIES=10 \
|
||||
@@ -214,7 +216,7 @@ LABEL org.opencontainers.image.vendor="GenarrativeAI" \
|
||||
com.genarrative.ci.definition-sha256="${IMAGE_REVISION}"
|
||||
|
||||
RUN node --version \
|
||||
&& npm --version \
|
||||
&& test "$(npm --version)" = "${NPM_VERSION}" \
|
||||
&& rustup show active-toolchain \
|
||||
&& rustc --version \
|
||||
&& cargo --version \
|
||||
|
||||
@@ -12,13 +12,29 @@
|
||||
!server-rs/crates/*/
|
||||
!server-rs/crates/*/Cargo.toml
|
||||
!apps/
|
||||
!apps/admin-web/
|
||||
!apps/admin-web/package.json
|
||||
!apps/ai-game-creator-shell/
|
||||
!apps/ai-game-creator-shell/package.json
|
||||
!apps/ai-game-creator-shell/package-lock.json
|
||||
!apps/ai-game-creator-shell/src-tauri/
|
||||
!apps/ai-game-creator-shell/src-tauri/Cargo.toml
|
||||
!apps/ai-game-creator-shell/src-tauri/Cargo.lock
|
||||
!apps/desktop-shell/
|
||||
!apps/desktop-shell/package.json
|
||||
!apps/desktop-shell/src-tauri/
|
||||
!apps/desktop-shell/src-tauri/Cargo.toml
|
||||
!apps/desktop-shell/src-tauri/Cargo.lock
|
||||
!apps/mobile-shell/
|
||||
!apps/mobile-shell/package.json
|
||||
!apps/preview-deployer-web/
|
||||
!apps/preview-deployer-web/package.json
|
||||
!packages/
|
||||
!packages/image-canvas-core/
|
||||
!packages/image-canvas-core/package.json
|
||||
!packages/image-canvas-react/
|
||||
!packages/image-canvas-react/package.json
|
||||
!packages/shared/
|
||||
!packages/shared/package.json
|
||||
!tools/
|
||||
!tools/spine-json-export-validator/
|
||||
!tools/spine-json-export-validator/package.json
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
### 测试与协作
|
||||
|
||||
- [npm workspaces 统一依赖边界](./technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md)
|
||||
- [React 组件测试准则](./technical/【前端测试】React组件测试准则-2026-06-26.md)
|
||||
- [AI Web 工程静态预览验收清单](./technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md)
|
||||
- [当前阶段规划](./planning/README.md)
|
||||
|
||||
@@ -14317,3 +14317,11 @@
|
||||
- 缺失当前服务器对应的本机开发者凭据时,客户端仍必须在请求远端创建 Key 前完成私有目录准备。既有 `~/.config/genarrative` 若 owner 已严格匹配当前进程 `TokenUser`,客户端自动把 DACL 收紧为禁止继承且仅当前用户 Full Control,用户不再需要手工执行 PowerShell ACL 修复。
|
||||
- 自动收紧不等于接管:owner 不匹配、链接、reparse point、非目录或无法安全写入 DACL 时继续在远端请求前失败关闭;客户端不得删除、移动、覆盖或读取旧凭据内容,也不得因收紧失败自动创建远端 Key。
|
||||
- Windows 回归测试必须构造“owner 为当前用户但仍继承 ACL”的既有目录,先证明严格校验失败,再通过正式目录准备入口收紧并复核私有 DACL。
|
||||
|
||||
## 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`。
|
||||
- 依赖边界:每个 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 的平台构建证据。
|
||||
- 权威方案:`docs/technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md`。
|
||||
|
||||
@@ -45,7 +45,7 @@ hermes
|
||||
npm install
|
||||
```
|
||||
|
||||
仓库当前不使用 npm workspaces,根目录 `npm install` 是统一安装入口。子包新增运行时依赖时,必须同步写入根 `package.json` 和根 `package-lock.json`;不能只修改子包 `package.json`。
|
||||
仓库使用 npm workspaces,根目录 `npm install` / `npm ci` 是全部 App、内部包和工具的唯一安装入口,唯一 npm lockfile 为根 `package-lock.json`。子包新增直接依赖时只修改所属 workspace 的 `package.json`,再从根更新 lock;禁止提交 workspace 内嵌套 `package-lock.json` 或继续执行子目录 `npm ci --prefix`。完整边界见 [`npm workspaces 统一依赖边界`](../../technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md)。
|
||||
|
||||
完整联调开发环境:
|
||||
|
||||
@@ -525,7 +525,7 @@ npm run check:native-shells
|
||||
```
|
||||
|
||||
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、完整相对路径文档反查、微信 capability 到真实 WebView / 支付 / 分享页面流程和测试清单的映射门禁、H5 HostBridge 事件订阅双能力门控反查、H5 `navigation.canGoBack` 消费 hook 与直达二级页返回锚点测试、移动端和桌面端单端源码清单门禁、Expo 壳 typecheck / test / EAS build config smoke / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 EAS 原生包构建 profile、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口、H5 页面内导航保留完整原生宿主上下文和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、Tauri `Info.plist`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件,但不扫描 Expo export、Tauri `target/`、Cargo / Metro 缓存或 release 构建产物。移动壳配置检查必须反查 EAS 生产 profile、文本 / 文档 / 图片 / 音频导入边界都来自共享 HostBridge 契约。登录与支付外链跳转必须保持在该调用链扫描内,`src/services/authService.ts` 和 `src/services/payment/paymentRedirect.ts` 是必扫文件;`AuthGate` 的登录成功、退出登录、身份边界刷新和登录状态异常重试都必须通过 `app.reloadWebView` 优先路径,并由 `src/components/auth/AuthGate.test.tsx` 进入该门禁。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造 / 未实现 / 临时;H5 业务调用链允许正常表单 `placeholder` 属性、业务占位图文案和真实兼容 / 故障语义中的“未实现”“临时”表述,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
|
||||
根仓 Vitest 加载独立 AI 游戏客户端源码时,不得为了模块解析把 `@tauri-apps/api` 或 `@tauri-apps/plugin-*` 加入根 H5 依赖;根测试只通过 `vitest.config.ts` 的精确别名使用无副作用测试替身,独立客户端的正式 Tauri guest 依赖继续只由 `apps/ai-game-creator-shell/package.json` 与其 lock 管理。隔离 worktree 验收前需分别执行根 `npm ci` 和 `npm ci --prefix apps/ai-game-creator-shell`。
|
||||
根仓 Vitest 加载独立 AI 游戏客户端源码时,不得为了模块解析把 `@tauri-apps/api` 或 `@tauri-apps/plugin-*` 加入根 H5 或 Desktop workspace 的 manifest;根测试只通过 `vitest.config.ts` 的精确别名使用无副作用测试替身,正式 Tauri guest 只由 `apps/ai-game-creator-shell/package.json` 声明。统一根 lock 出现 AGC 的 Tauri guest 解析是正常聚合结果,依赖归属按 workspace manifest 检查。隔离 worktree 验收前只从仓库根执行一次 `npm ci`。
|
||||
反馈页上传凭证在原生壳声明 `file.importImage` 时必须优先走宿主图片导入;移动壳声明 `file.captureImage` 时才显示拍摄凭证入口,并把拍摄图片同样转为 `File` 后复用反馈页原有数量、大小、MIME、data URL 预览和提交 payload 校验。
|
||||
Expo / Tauri 声明 `navigation.openNativePage` 时,只用于现役同源 H5 路由的受控导航和宿主上下文续接;微信小程序不再声明该能力。旧儿童动作 Demo、模板工作台、生成页、结果页和运行态不得作为 HostBridge 导航验收入口。
|
||||
H5 支付链接跳转在原生壳声明 `app.openExternalUrl` 时必须优先走宿主系统浏览器;原生壳未接真实支付 SDK 前不得声明 `payment.request`,也不得把外部 H5 支付跳转伪装成原生支付成功。
|
||||
|
||||
@@ -4203,11 +4203,11 @@
|
||||
|
||||
- 现象:`Repository checks`、`Frontend tests`、`Backend tests` 和 `Native shell tests` 都从全新 job 容器开始,apt、setup-node、rustup 和原生系统库在不同 job 里重复安装;后端与原生壳的安装时间可达数分钟,并把软件源和代理瞬时失败放大为四份。
|
||||
- 原因:Gitea Actions job 彼此隔离,上一个 job 在容器内安装的包不会自动进入下一个 job;把同一套不随 PR 变化的工具链写在 workflow step 中,必然每次重做。
|
||||
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、Rust 1.96、`rustfmt`、Chrome、`bwrap`、`rg`、`ffmpeg`、`clang/lld` 和 Tauri / 后端系统依赖,并按锁预热根与 AI 游戏创作壳 npm、server-rs、桌面壳与 AI 游戏创作壳 Cargo 下载缓存。四个 job 统一 `runs-on: genarrative-ci`,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh`,同时检查五份缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0`;`rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
|
||||
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、固定 npm、Rust 1.96、`rustfmt`、Chrome、`bwrap`、`rg`、`ffmpeg`、`clang/lld` 和 Tauri / 后端系统依赖,并按锁预热唯一根 npm workspace、server-rs、桌面壳与 AI 游戏创作壳 Cargo 四份下载缓存。四个 job 统一 `runs-on: genarrative-ci`,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh`,同时检查四份缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0`;`rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
|
||||
- 依赖边界:每个 job 仍必须各自执行 `npm ci`,让当前 lockfile 和 PR 依赖在干净环境中验证;区别是命中镜像 cache 时只做本地解包,锁新增依赖时才走受控网络。不要把 `node_modules` 或 Cargo `target` 烘进镜像,也不要向不受信任 PR 挂载跨 job 可写 cache。
|
||||
- 锁漂移边界:runtime 校验输出任一 `*_cache_lock=partial` 说明镜像内 lock 与当前 checkout 不同,不代表新增依赖已经缓存;必须同时输出 Actions warning,提示可信分支落地后刷新镜像。必须在新镜像中对 server-rs、桌面壳和 AI 游戏创作壳当前 lock 执行真实 `cargo fetch --locked --offline`;`cargo metadata --no-deps` 不会证明依赖 archive 可用,不能作为替代。
|
||||
- 构建网络边界:`CARGO_NET_RETRY` 只覆盖部分 crate 下载,registry `config.json` / index TLS 握手仍可能直接终止整次 fetch。Dockerfile 对每个 `cargo fetch --locked` 再做最多 5 次整命令级有界重试,最终仍执行断网 fetch,不能降低为无锁重试或省略离线闭合验证。
|
||||
- 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像能按五份当前 lock 完成缓存闭合,四个 job 的环境校验、经 3 次整命令级有界重试保护的干净 `npm ci` 和原有测试门禁仍全部执行。
|
||||
- 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像能按四份当前 lock 完成缓存闭合,四个 job 的环境校验、经 3 次整命令级有界重试保护的单次根 `npm ci` 和原有测试门禁仍全部执行。
|
||||
|
||||
## Gitea Actions HTTPS CONNECT 隧道必须双向收束 socket(2026-08-07)
|
||||
|
||||
@@ -4216,16 +4216,16 @@
|
||||
- 处理:CONNECT 一开始就为 client socket 注册 `error / close`,解析完成后为 upstream socket注册同样的双向销毁处理;DNS 返回、写 200 和开始 pipe 前都检查 client 是否已销毁。任一端 error、close 或 timeout 都幂等 destroy 两端,不把普通客户端 reset 写成错误日志。不要用进程级 `uncaughtException` 吞掉问题,也不要只增加 npm/Cargo 重试掩盖 gateway 崩溃。
|
||||
- 验证:在独立 canary 和正式 gateway 上分别并发制造至少 500 次“CONNECT 后立即断开”,随后确认容器仍运行、restart count 不增加、日志无 EPIPE;再通过同一 proxy 对 npm registry 与 crates index 建立完整 TLS 隧道。切换前仍须确认 Gitea 无活跃 run 且 Runner 内层无 job 容器。
|
||||
|
||||
## 独立 AGC lockfile 不能丢失可选 WASM 包的 bundled 依赖节点(2026-08-21)
|
||||
## 统一 npm workspace lock 不能丢失可选 WASM 包的 bundled 依赖节点(2026-08-21)
|
||||
|
||||
- 现象:`npm ci --prefix apps/ai-game-creator-shell` 在安装前失败,报告 lockfile 缺少 `@emnapi/core` / `@emnapi/runtime`;错误版本可能是 registry 当前满足 `^1.11.1` 的最新版,而不是原 lock 中曾记录的版本。
|
||||
- 原因:重写或解决 `apps/ai-game-creator-shell/package-lock.json` 冲突时,保留了 `@tailwindcss/oxide-wasm32-wasi` 对 bundled `@emnapi` 包的声明,却删掉了对应嵌套 package 节点。npm 会重新解析当前 registry 版本并判定 manifest 与 lock 不同步;这不是单一 npm 版本问题,也不表示应用应直接依赖两个 `@emnapi` 包。
|
||||
- 处理:只在最新目标分支执行 `npm install --package-lock-only --ignore-scripts --prefix apps/ai-game-creator-shell`,保留 npm 对 bundled 节点及 `peer` / `optional` 标记的完整规范化结果;确认子包 `package.json` 没有变化,不要手工只补报错中的两个版本。
|
||||
- 验证:至少用 Jenkins 对应 npm major 和当前开发 npm 分别执行干净的 `npm ci --prefix apps/ai-game-creator-shell`,再运行 AGC typecheck、编码检查和 `git diff --check`;根目录 `npm ci` 不能替代独立子包 lock 验证。
|
||||
- 现象:根 `npm ci` 在安装前失败,报告统一 lock 缺少 `@emnapi/core` / `@emnapi/runtime`;错误版本可能是 registry 当前满足 `^1.11.1` 的最新版,而不是原 lock 中曾记录的版本。
|
||||
- 原因:重写或解决根 workspace lock 冲突时,保留了 AGC 使用的 `@tailwindcss/oxide-wasm32-wasi` 对 bundled `@emnapi` 包的声明,却删掉了对应嵌套 package 节点。npm 会重新解析当前 registry 版本并判定 manifest 与 lock 不同步;这不是单一 npm 版本问题,也不表示应用应直接依赖两个 `@emnapi` 包。
|
||||
- 处理:只在最新目标分支的仓库根执行固定 npm 的 `npm install --package-lock-only --ignore-scripts`,保留 npm 对全部 workspaces、bundled 节点及 `peer` / `optional` 标记的完整规范化结果;确认各 workspace manifest 没有意外变化,不要手工只补报错中的两个版本。
|
||||
- 验证:至少用 Jenkins 对应固定 npm 和当前开发环境分别执行干净的根 `npm ci`,核对 bundled 节点后再运行 `npm run check:npm-workspaces`、AGC typecheck、编码检查和 `git diff --check`;禁止恢复独立 AGC lock 或子目录 `npm ci`。
|
||||
|
||||
## Windows 专属 Tauri resource 不能写进通用配置(2026-08-21)
|
||||
|
||||
- 现象:Linux CI 已完成 AGC `npm ci`,却在 Tauri custom build command 中报 `resources/codex/win-x64/...exe doesn't exist`;Windows 侧车的 Rust staging 受 `cfg(windows)` 保护,因此非 Windows 构建不会生成这些文件。
|
||||
- 现象:Linux CI 已完成根 workspace `npm ci`,却在 Tauri custom build command 中报 `resources/codex/win-x64/...exe doesn't exist`;Windows 侧车的 Rust staging 受 `cfg(windows)` 保护,因此非 Windows 构建不会生成这些文件。
|
||||
- 原因:Tauri 会在所有平台校验通用 `tauri.conf.json` 的 bundle resource 源路径;把 Windows x64 资源映射写进通用配置,等于要求 Linux / macOS 也预先拥有不属于其安装闭包的 Windows 可执行文件。
|
||||
- 处理:通用配置只保留跨平台 bundle 项;Windows 原生侧车的完整白名单放入 Tauri 自动合并的 `tauri.windows.conf.json`。不要提交二进制占位文件,也不要让非 Windows build script 下载或伪造 Windows 资源。
|
||||
- 验证:配置门禁断言通用配置没有 Windows resource、Windows 平台配置保留完整固定白名单;Linux 运行原生壳门禁必须越过 Tauri resource 校验,Windows release 仍由 build script 对 npm 原生包、SHA-256 清单和目标布局失败关闭。
|
||||
|
||||
@@ -1206,3 +1206,10 @@ game-project/
|
||||
- **真实 Provider 开发验收入口**:`--game-chat-smoke` 是受限 CLI 标记,只允许与默认 `project-supervisor` 的 `--swarm-chat --autonomous-game-build` 组合,将新根 Run 绑定为 `project-supervisor-game-chat`,不作为产品 UI、公开 API 或通用 source 覆盖能力。现有 playable harness 通过 `npm run ai-game-creator-shell:agent-runtime:supervisor-game-chat-single-main-playable-real-e2e` 显式启动该模式,启动前同时校验 `project-supervisor` 与 `code-prototype` 的 Provider 配置,并在隔离 AppData 中验收唯一单主 child、当前 revision 的 static smoke 及 desktop/mobile 试玩回执。只有显式执行这条真实 E2E 命令才会发起 Provider 请求;普通 self-test 不读取凭据、不调用 Provider。现场 smoke 若在配置门因缺少 API Key 阻断,必须报告 `providerUsed=false`,只能证明入口、验收逻辑与无 Provider 自测已落地,不能宣称真实现场验收完成。
|
||||
- **fresh-init 取证边界**:`supervisor-game-chat-single-main-playable` 不再预写 `package.json`、`verify-e2e.mjs` 或 `game/index.html`,由正式 `--init` 生成生产 `DEFAULT_GAME_INDEX_HTML`。self-test 必须逐字节核对 harness 中的 canonical 默认入口与 Rust 生产常量,并证明空项目仍保留 Git、`AGENTS.md`、隔离 evidence 和敏感诱饵基线;真实报告必须同时证明初始 SHA-256 命中生产默认入口、根下唯一固定 child 为 `code-prototype`、最终入口已变化、static-smoke SHA-256 绑定最终入口且 desktop/mobile 各自通过。
|
||||
- **不外推范围**:上述真实 Provider 命令只验收普通 Web 工作台所采用的 `project-supervisor-game-chat + autonomous-game-build` 单 Supervisor 链。显式 `professional-dag` 与固定 16 节点 CLI/GUI 链的 owner-artifact verify、产物所有权和 path-scope 仍是独立未解决项;seeded deterministic E2E、普通 self-test 或本 game-chat 报告都不得冒充该链已修复或已完成真实验收。
|
||||
|
||||
## 2026-08-21 npm workspace 安装与打包边界
|
||||
|
||||
- AGC 进入仓库根 npm workspaces,`apps/ai-game-creator-shell/package.json` 继续独占其 Tauri guest、Codex CLI 和 App 直接依赖,但不再维护独立 `package-lock.json`。开发、CI、Jenkins 与打包前只从仓库根执行一次 `npm ci`;禁止恢复子目录独立安装。
|
||||
- 统一根 lock 出现 `@tauri-apps/api`、`@tauri-apps/plugin-*` 或 `@openai/codex` 是 AGC workspace 的合法聚合结果,不代表根 H5 或 Desktop 获得这些能力。配置门禁必须按 workspace manifest/lock entry 判断归属。
|
||||
- npm 默认 hoist。Vite/Vitest 对 `@cubone/react-file-manager` 的已发布 ESM bundle、AGC TypeScript/Tauri CLI 和 Windows `@openai/codex-win32-x64` sidecar 解析必须兼容 workspace 本地与根提升位置,不得硬编码依赖只存在于 `apps/ai-game-creator-shell/node_modules`。
|
||||
- Windows game-chat release 仍必须核对固定 Codex 文件和 SHA-256 manifest;Linux 根 lock 与 typecheck 通过不能替代 Windows sidecar 打包 smoke。完整安装、锁与 CI 口径见 [`npm workspaces 统一依赖边界`](./【技术方案】npm-workspaces统一依赖边界-2026-08-21.md)。
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# npm workspaces 统一依赖边界
|
||||
|
||||
更新时间:`2026-08-21`
|
||||
|
||||
## 目标
|
||||
|
||||
Genarrative 的 JavaScript 工程统一使用 npm workspaces。仓库只提交根 `package-lock.json`,开发、CI、Jenkins 和容器构建都从仓库根执行一次干净安装;各 App、内部包和工具仍由自己的 `package.json` 声明直接依赖和脚本。
|
||||
|
||||
本方案只迁移包管理与依赖边界,不切换 pnpm,不改变 Cargo workspace、SpacetimeDB schema、前后端 DTO 或业务运行时。
|
||||
|
||||
## Workspace 范围
|
||||
|
||||
根 `package.json` 固定声明:
|
||||
|
||||
```json
|
||||
{
|
||||
"packageManager": "npm@10.9.7",
|
||||
"workspaces": [
|
||||
"apps/admin-web",
|
||||
"apps/ai-game-creator-shell",
|
||||
"apps/desktop-shell",
|
||||
"apps/mobile-shell",
|
||||
"apps/preview-deployer-web",
|
||||
"packages/image-canvas-core",
|
||||
"packages/image-canvas-react",
|
||||
"packages/shared",
|
||||
"tools/spine-json-export-validator"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
当前纳入:
|
||||
|
||||
- `apps/admin-web`
|
||||
- `apps/ai-game-creator-shell`
|
||||
- `apps/desktop-shell`
|
||||
- `apps/mobile-shell`
|
||||
- `apps/preview-deployer-web`
|
||||
- `packages/image-canvas-core`
|
||||
- `packages/image-canvas-react`
|
||||
- `packages/shared`
|
||||
- `tools/spine-json-export-validator`
|
||||
|
||||
`.rag/runtime`、`.worktrees/`、`tmp/`、构建目录和各级 `node_modules` 不属于 workspace。RAG 继续保持独立、gitignored 的本地运行时,不进入根依赖。
|
||||
|
||||
## Manifest 与依赖所有权
|
||||
|
||||
- 根 manifest 只声明根 H5、仓库级脚本和统一测试/格式化工具的直接依赖,不再为 Mobile、AGC 或工具重复声明其专属依赖。
|
||||
- 每个 App 在自己的 manifest 声明运行时直接依赖;测试只由根统一 runner 承担的工具可以留在根,App 自己提供测试脚本时必须声明其直接测试依赖。
|
||||
- `@genarrative/image-canvas-react` 必须显式依赖 `@genarrative/image-canvas-core`;主站和 AGC 必须显式声明它们直接消费的内部画布包。
|
||||
- npm `10.9.7` 不支持依赖值 `workspace:*`。内部 workspace 依赖使用匹配本地包版本的普通 semver,例如 `"@genarrative/image-canvas-core": "0.1.0"`;npm 在根安装时自动生成本地 `link`。
|
||||
- npm 默认提升依赖,统一根 `node_modules` 中存在某包不代表根 H5 拥有该依赖。归属门禁必须检查对应 workspace manifest 和根 lock 中的 workspace package entry,不能按统一 lock 的全局 `node_modules/*` 条目判断归属。
|
||||
|
||||
`packages/shared` 本次纳入统一安装和 lock,但不顺手重写现有源码 import;后续若要把所有相对源码引用改为 `@genarrative/shared`,需先补完整 exports/build 合同并单独实施。
|
||||
|
||||
## 唯一 Lockfile
|
||||
|
||||
- 唯一权威 npm lockfile 为根 `package-lock.json`。
|
||||
- 删除 `apps/ai-game-creator-shell/package-lock.json` 与 `tools/spine-json-export-validator/package-lock.json`。
|
||||
- 新增或修改任一 workspace 依赖后,只能从仓库根使用固定 npm 版本更新根 lock。
|
||||
- 仓库门禁必须校验 workspace 清单、固定 `packageManager`、根 lock 的 workspace package/link 条目,并拒绝受管 workspace 再提交嵌套 `package-lock.json` 或 `npm-shrinkwrap.json`。
|
||||
- optional dependency、平台二进制和 bundled dependency 仍由根 lock 完整记录;Linux 生成 lock 后仍需 Windows/macOS 对应构建门禁,不能把单平台安装等同于跨平台通过。
|
||||
|
||||
## 安装与脚本
|
||||
|
||||
标准安装入口:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
```
|
||||
|
||||
开发者首次拉取或主动更新依赖时可使用根 `npm install`。Husky 的 `prepare` 只在根执行一次;各 workspace 不重复安装 hook。
|
||||
|
||||
现有 `npm run agc`、`npm run mobile-shell:*`、`npm run desktop-shell:*`、`npm run preview-deployer:web:*` 和 `npm run spine-export-validator:*` 等对外入口保持名称稳定。内部可继续使用 `npm --prefix`,或使用 `npm run <script> --workspace=<name>`;无论哪种写法,都必须保证 Expo、EAS 和 Tauri 命令在目标 App cwd 中执行。
|
||||
|
||||
AGC 的 TypeScript、Vite/Vitest bundle 和 Windows Codex sidecar 不允许硬编码依赖一定位于子 App 的 `node_modules`。解析必须兼容 npm 将依赖提升到根安装树,并在错误文案中统一要求从仓库根安装。
|
||||
|
||||
## 原生壳边界
|
||||
|
||||
- Mobile workspace 继续使用 Expo 默认 Metro 配置;当前不增加 `node-linker` 或自定义 `watchFolders`。首次迁移验收清理 Metro cache。
|
||||
- Desktop H5 仍不得声明 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*` guest 依赖。
|
||||
- AGC 可以在自己的 workspace manifest 声明 Tauri guest;统一根 lock 出现这些解析包是正常结果,不代表 Desktop 或根 H5 获得 guest 权限。
|
||||
- 根 H5 manifest 也不得直接声明 Tauri guest。配置门禁按根、Desktop、AGC 三个 manifest 的所有权分别判断。
|
||||
- Windows AGC release 必须从 workspace 或根提升位置找到 `@openai/codex-win32-x64` 并把固定 sidecar 资源打包;Windows 构建 smoke 是迁移完成条件,不由 Linux lock 检查替代。
|
||||
|
||||
## CI、Jenkins 与容器
|
||||
|
||||
- Gitea 四个 job 每个只执行一次带重试的根 `npm ci`,不再单独安装 AGC。
|
||||
- Jenkins Web Build 在安装前必须精确校验 npm `10.9.7`;`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`,之后才复制源码并构建主站与后台。
|
||||
- 根 lock 或任一 workspace manifest 变化都需要刷新 CI 镜像 npm cache。缓存未命中只能报告 `partial` 并受控补齐,不能跳过当前 lock 的干净安装。
|
||||
|
||||
## 验收
|
||||
|
||||
最低本地门禁:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm ls --workspaces --include-workspace-root --depth=0
|
||||
npm run check:npm-workspaces
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run admin-web:typecheck
|
||||
npm run preview-deployer:web:test
|
||||
npm run mobile-shell:typecheck
|
||||
npm run mobile-shell:test
|
||||
npm run desktop-shell:typecheck
|
||||
npm run ai-game-creator-shell:typecheck
|
||||
npm run spine-export-validator:typecheck
|
||||
npm run check:native-shells
|
||||
npm run check:repository-ci
|
||||
npm run check:production-ops
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
干净安装验收必须在没有历史根或子 App `node_modules` 的隔离工作树执行。平台补充门禁:
|
||||
|
||||
- Linux:根、后台、预览部署器、Spine 工具构建,Desktop/AGC Tauri release smoke。
|
||||
- Windows x64:根 `npm ci` 后执行 AGC game-chat release,核对 Codex sidecar 完整性。
|
||||
- Android:Expo config/export,并在可用 EAS 环境执行一次本地 Android build。
|
||||
- macOS/iOS:在可用 runner 执行 simulator build;缺少 runner 时必须标记未验证。
|
||||
|
||||
任何 workspace 仍需要第二次 `npm ci --prefix`、嵌套 lock、未声明直接依赖或依赖某个固定 `node_modules` 层级时,迁移都不能视为完成。
|
||||
@@ -409,13 +409,13 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-19 追加:移动壳 H5 入口 query 和 `host.getRuntime` 回包统一读取 `MOBILE_SHELL_HOST_VERSION`,该值由移动壳 `app.json` 的 Expo `version` 配置解析,异常配置只回退到与 `app.json` / `package.json` 一致的受检 fallback。配置检查会拒绝在 `apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/host-bridge/appearance.ts`、`apps/mobile-shell/src/host-bridge/badge.ts`、`apps/mobile-shell/src/host-bridge/bridge.ts` 或 `runtime.ts` 内重新散落硬编码版本,避免升级移动安装包时 H5 首屏上下文和宿主 runtime 回读版本不一致。该收口不引入 `expo-constants`、OTA 更新、渠道分发或应用安装信息业务。
|
||||
|
||||
2026-06-18 追加:移动壳默认显式关闭 Expo OTA 更新,直到存在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程后再接入。`app.json` 只允许 `updates.enabled=false`,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖;`apps/mobile-shell/scripts/check-config.mjs` 和 Expo public config smoke 会共同拒绝这些发布通道能力被提前打开,移动壳生产入口、HostBridge 和 URL/runtime 配置也不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates。由于移动壳运行依赖会从根安装树解析,根 H5 `package.json` 也不得直接安装这些移动端发布通道、崩溃上报、analytics 或 CodePush SDK;根 `package-lock.json` 也不得解析 `expo-updates`、Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 等真实发布 / 观测 SDK。`expo-application` 可能由 Expo 自身传递解析,但项目不得把它作为 direct dependency 主动用于渠道逻辑。
|
||||
2026-06-18 追加,2026-08-21 按 npm workspaces 更新:移动壳默认显式关闭 Expo OTA 更新,直到存在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程后再接入。`app.json` 只允许 `updates.enabled=false`,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖;`apps/mobile-shell/scripts/check-config.mjs` 和 Expo public config smoke 会共同拒绝这些发布通道能力被提前打开,移动壳生产入口、HostBridge 和 URL/runtime 配置也不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates。根 H5 与 Mobile workspace 的 manifest 都不得直接声明这些 SDK;统一根 lock 可能因其它合法传递依赖出现同名包,禁用能力归属应检查 workspace manifest 和 Mobile 依赖闭包,不能把聚合 lock 的全局存在性直接当成 Mobile 启用。`expo-application` 可能由 Expo 自身传递解析,但项目不得把它作为 direct dependency 主动用于渠道逻辑。
|
||||
|
||||
2026-06-18 追加:移动壳可分发身份、外观和默认权限进入配置门禁。Expo `name` 固定为 `Genarrative`,`slug` 固定为 `genarrative-mobile-shell`,`userInterfaceStyle` 固定为 `automatic`,`assetBundlePatterns` 固定为 `["**/*"]`,`extra.genarrativeHostBridgeVersion` 固定为 `1`;源 `app.json` 的 Android `permissions` 只允许显式声明 `android.permission.RECORD_AUDIO`,用于同源 H5 实时声音玩法,最终 Expo public config 只允许 `android.permission.CAMERA` 与 `android.permission.RECORD_AUDIO` 两类权限,其中相机来自扫码 / 拍摄能力,麦克风来自同源 H5 实时玩法。所有其它当前不需要的高风险权限只能通过 `blockedPermissions` 阻断。后续新增权限必须先有真实宿主能力、系统权限说明和 H5 fallback 方案,再补配置与检查。`apps/mobile-shell/scripts/check-config.mjs` 会检查源 `app.json`,`apps/mobile-shell/scripts/check-expo-config.mjs` 会检查 Expo CLI 最终解析出的 public config,避免 config plugin 或 Expo 解析阶段引入身份、资源或权限漂移。
|
||||
|
||||
2026-06-18 追加:移动壳命令入口进入配置门禁。`apps/mobile-shell/package.json` 的 `dev`、`android`、`ios`、`test`、`config:smoke`、`export:smoke`、`typecheck` 以及根 `package.json` 的 `mobile-shell:*` 入口必须保持指向真实 Expo / RN / Vitest / Expo config / Metro export / 配置检查流程,不能替换成只跑静态脚本或绕过生产 bundler 的快捷命令。
|
||||
|
||||
2026-06-18 追加:移动壳关键依赖版本进入配置门禁。`apps/mobile-shell/package.json` 和根 `package.json` 的 Expo SDK 56、React 19、React Native 0.86、`react-native-webview` 13.16、`react-native-safe-area-context`、Expo Camera / Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 和 Vitest 版本必须保持一致;根 `package-lock.json` 里的实际解析版本也必须由配置检查锁定。升级这些依赖必须同步审查 WebView 安全开关、Expo managed config、production export、HostBridge 能力实现和 H5 fallback,不能只更新依赖声明或锁文件。
|
||||
2026-06-18 追加,2026-08-21 按 npm workspaces 更新:移动壳关键依赖版本进入配置门禁。Expo SDK 56、React 19、React Native 0.86、`react-native-webview` 13.16、`react-native-safe-area-context`、Expo Camera / Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 和 Vitest 由 `apps/mobile-shell/package.json` 拥有,根 H5 不再重复声明移动端专属依赖;统一根 `package-lock.json` 中 Mobile workspace entry 与实际解析版本必须由配置检查锁定。升级这些依赖必须同步审查 WebView 安全开关、Expo managed config、production export、HostBridge 能力实现和 H5 fallback,不能只更新依赖声明或锁文件。
|
||||
|
||||
### Phase 3:Tauri 桌面壳 MVP
|
||||
|
||||
@@ -486,9 +486,9 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-18 追加:桌面壳 capability 作用域进入配置门禁。Tauri 配置只能声明一个 `label=main` 的主窗口,`src-tauri/capabilities/` 只能存在 `main.json`,该 capability 的 `identifier` 必须为 `main`、`windows` 必须只包含 `main`、`permissions` 必须只包含 `allow-host-bridge-request`。这保证 opener、clipboard、dialog、notification 等插件只由 Rust 内部通过受控 HostBridge 分发使用,不把 core 默认命令、插件 JS guest API 或额外窗口权限授给 H5 主站。
|
||||
|
||||
2026-06-18 追加:桌面壳 JS guest 依赖进入门禁。`apps/desktop-shell/package.json`、根 H5 `package.json` 和根 `package-lock.json` 不得安装或解析 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*` 包,避免生产前端绕过 `nativeAppHostBridge` 直接调用 Tauri JS 客户端 API;Tauri CLI 仍只作为构建工具留在 devDependencies,桌面系统能力继续由 Rust 侧 Cargo 插件和唯一 `host_bridge_request` command 承接。
|
||||
2026-06-18 追加,2026-08-21 按 npm workspaces 更新:桌面壳 JS guest 依赖进入门禁。`apps/desktop-shell/package.json` 与根 H5 `package.json` 不得声明 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*`,避免生产前端绕过 `nativeAppHostBridge` 直接调用 Tauri JS 客户端 API;统一根 `package-lock.json` 会合法聚合 AGC workspace 的 Tauri guest,不能按全局 lock 条目禁止。Tauri CLI 仍只作为构建工具留在 devDependencies,桌面系统能力继续由 Rust 侧 Cargo 插件和唯一 `host_bridge_request` command 承接。
|
||||
|
||||
2026-06-18 追加:桌面壳关键依赖版本进入配置门禁。`apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI 和 TypeScript 版本必须一致;根 `package-lock.json` 里的实际解析版本也必须一致;`src-tauri/Cargo.toml` 的 `tauri-build`、`tauri`、`base64`、`serde`、`serde_json` 以及 clipboard、dialog、notification、opener、single-instance、window-state 插件版本和 Tauri `tray-icon` feature 都由 `apps/desktop-shell/scripts/check-config.mjs` 固定检查,`src-tauri/Cargo.lock` 中桌面壳 direct dependency 的实际解析版本也必须同步受检。升级这些依赖必须同步审查 capability、CSP、唯一 command、插件初始化、release build smoke 和本文档。
|
||||
2026-06-18 追加,2026-08-21 按 npm workspaces 更新:桌面壳关键依赖版本进入配置门禁。Tauri CLI 和 TypeScript 由 `apps/desktop-shell/package.json` 声明,统一根 `package-lock.json` 中 Desktop workspace entry 与实际解析版本必须一致;根 H5 只有自身确实使用同一构建工具时才单独声明,不再为 Desktop 镜像依赖。`src-tauri/Cargo.toml` 的 `tauri-build`、`tauri`、`base64`、`serde`、`serde_json` 以及 clipboard、dialog、notification、opener、single-instance、window-state 插件版本和 Tauri `tray-icon` feature 都由 `apps/desktop-shell/scripts/check-config.mjs` 固定检查,`src-tauri/Cargo.lock` 中桌面壳 direct dependency 的实际解析版本也必须同步受检。升级这些依赖必须同步审查 capability、CSP、唯一 command、插件初始化、release build smoke 和本文档。
|
||||
|
||||
2026-06-18 追加:H5 到 Tauri 的 command 名进入共享契约。`packages/shared/src/contracts/hostBridge.ts` 导出 `HOST_BRIDGE_TAURI_COMMAND='host_bridge_request'`,`nativeAppHostBridge` 只能通过该常量调用 Tauri 注入的 `core.invoke`;桌面壳配置检查会对齐共享常量、Tauri build manifest、Rust `generate_handler!` 和 H5 transport,禁止 H5 侧写死或调用其它 Tauri command。
|
||||
|
||||
|
||||
@@ -260,10 +260,12 @@ 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,不能把过渡状态当作工具链已闭合。
|
||||
|
||||
- `Repository checks`:调用唯一入口 `npm run check:repository-ci`,执行 `npm run lint`、AI 游戏创作壳 AppSurface 定向测试、主站与后台生产构建和提交差异空白检查。本地 master `pre-push` 复用同一入口,禁止在 workflow 与 hook 中维护两份近似命令。
|
||||
- `Frontend tests`:按根 lockfile 与 `apps/ai-game-creator-shell/package-lock.json` 分别执行干净的 `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` 收集规则。
|
||||
- `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` 收集规则。
|
||||
- `Backend tests`:先对 `server-rs/Cargo.lock` 执行带 5 次整命令级有界重试的 `cargo fetch --locked`,再执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast`、`api-server --all-targets` 编译和 `spacetime-module` 编译;依赖准备必须位于会触发 Cargo build 的 DDD / 产物边界门禁之前,避免锁新增依赖未命中镜像缓存时绕过既有下载重试。runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。
|
||||
- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,对所有触发方式一致覆盖微信壳、Expo 和 Tauri 的完整验收,并执行 `npm run ai-game-creator-shell:check` 与 AI 游戏创作壳 release build smoke;最后确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写。共享 Agent Runtime 后台锁 suite 固定 `--test-threads=1`,不能用并行偶发失败后的逐项通过替代整套稳定门禁。
|
||||
- `Native shell tests`:按唯一根 workspace lockfile 安装全部 App 依赖后执行 `npm run check:native-shells`,对所有触发方式一致覆盖微信壳、Expo 和 Tauri 的完整验收,并执行 `npm run ai-game-creator-shell:check` 与 AI 游戏创作壳 release build smoke;最后确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写。共享 Agent Runtime 后台锁 suite 固定 `--test-threads=1`,不能用并行偶发失败后的逐项通过替代整套稳定门禁。
|
||||
|
||||
四个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 BgFilter worker smoke harness、无密钥生产巡检 / 发布 / 部署行为 fixture、server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。
|
||||
|
||||
@@ -273,7 +275,7 @@ PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME
|
||||
|
||||
当前 `genarrative-station` 使用 Gitea `1.26.4` 和基于 Gitea Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像。Runner 2.0.0 会先把 `systempaths=unconfined` 解析为空 `MaskedPaths` / `ReadonlyPaths`,再被 `mergo.WithOverride` 当成 empty value 丢失;站点修补只在 merge 后保留这两个显式空 slice,不改其它 runner 行为。真实 job inspect 必须看到 `MaskedPaths=[]`、`ReadonlyPaths=[]`、`SecurityOpt=[seccomp=unconfined]`、`Privileged=false`、无 CapAdd 且 `Binds=[]`。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置保持 `docker_host: "-"`、`valid_volumes: []`、`bind_workdir: false` 和 `force_pull: false`,防止内部 Docker socket 或宿主 bind mount 进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway 解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket 的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。
|
||||
|
||||
CI job 镜像由 `deploy/container/gitea-ci-job.Dockerfile` 定义:Ubuntu job base 固定为 `sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614`,Rust stage 固定为 `sha256:19817ead3289c8c631c73df281e18b59b172f6a31f4f563290f69cddd06c30e9`,Node `22.23.1` 发行包执行 SHA-256 校验,Google Linux 主签名指纹固定,Chrome 固定为 `150.0.7871.181-1`。构建脚本以 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本,以及根、AI 游戏创作壳、server-rs 与桌面壳所需的 npm / Cargo manifests/lock;当前 context 约 `2.13 MB`。镜像按根 npm 锁、AI 游戏创作壳 npm 锁、server-rs 锁、桌面壳锁和 AI 游戏创作壳 Cargo 锁预热下载缓存,不包含 `node_modules` 或 Cargo `target`;三个 `cargo fetch --locked` 在 Cargo 自身重试之外再执行最多 5 次整命令级有界重试,处理 registry index 握手失败,最终仍分别以断网 `cargo fetch --locked` 关闭验证。当前验证镜像约 `1.85 GB`,默认 tag 为 `genarrative/gitea-project-ci:20260807.1`,完整 Image ID 为 `sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf`;runner 标签保留 `ubuntu-latest`,并将 `genarrative-ci` 映射到 `docker://sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf`。内层 Docker 数据必须持久化;`force_pull: false` 表示只使用这个已装载的精确内容,Image ID 缺失时 job 必须失败关闭,不得回退浮动 tag 或临时连 registry。
|
||||
CI job 镜像由 `deploy/container/gitea-ci-job.Dockerfile` 定义:Ubuntu job base 固定为 `sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614`,Rust stage 固定为 `sha256:19817ead3289c8c631c73df281e18b59b172f6a31f4f563290f69cddd06c30e9`,Node `22.23.1` 发行包执行 SHA-256 校验,Google Linux 主签名指纹固定,Chrome 固定为 `150.0.7871.181-1`。构建脚本以 NUL 分隔白名单 tar 流发送 Dockerfile、checkout 脚本、根 lock、全部 workspace manifests,以及 server-rs、桌面壳和 AI 游戏创作壳 Cargo manifests/lock。镜像按唯一根 npm workspace 锁、server-rs 锁、桌面壳锁和 AI 游戏创作壳 Cargo 锁预热四份下载缓存,不包含 `node_modules` 或 Cargo `target`;三个 `cargo fetch --locked` 在 Cargo 自身重试之外再执行最多 5 次整命令级有界重试,处理 registry index 握手失败,最终仍分别以断网 `cargo fetch --locked` 关闭验证。CI 镜像定义或根 lock/workspace manifests 变化后必须重建并发布新的固定 Image ID;不得继续沿用旧镜像 digest。runner 标签保留 `ubuntu-latest`,并将 `genarrative-ci` 映射到当前已验证的完整 Image ID。内层 Docker 数据必须持久化;`force_pull: false` 表示只使用已装载的精确内容,Image ID 缺失时 job 必须失败关闭,不得回退浮动 tag 或临时连 registry。
|
||||
|
||||
镜像更新命令:
|
||||
|
||||
@@ -286,7 +288,7 @@ bash scripts/gitea-ci-job-image.sh load-runner
|
||||
|
||||
执行账号只要有权访问宿主 Docker API 并管理 runner 容器即可,不强制使用 root;无该权限时由 runner 运维人员执行。更新顺序必须是 `build/verify -> export 仓库外镜像归档与 SHA-256 sidecar -> load-runner -> 确认无活跃 job -> 备份当前 config -> 增加或替换 label -> docker restart --timeout 660 gitea-runner`。`--timeout 660` 只是停止宽限,不是 drain API;rootless DinD supervisor 可能同时停止内层 dockerd,因此重启前必须确认 Gitea 没有 `in_progress` run 且内层 `docker ps` 为空。config 和镜像归档只保存到仓库外受控位置,不在文档、仓库或日志中记录注册信息。重启后先重跑真实 PR 的四个 job,复核隔离边界并确认全部通过,再清理旧镜像。回滚时先把 workflow 的 `runs-on` 改回 `ubuntu-latest`,再恢复 config 备份并重启 runner。
|
||||
|
||||
四个 job 先运行镜像内 `genarrative-gitea-checkout`,再以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,校验 Node 主版本、仓库 Rust toolchain、受信任 PATH、五份缓存锁命中状态、原生命令、pkg-config 依赖、完整 bwrap sandbox 和 Chrome headless。运行时发现锁不匹配时必须输出对应 `*_cache_lock=partial` 和 Actions warning,提示可信分支落地后刷新镜像,不能把陈旧缓存误报为闭合。`RUSTUP_AUTO_INSTALL=0`,因此仓库 `rust-toolchain.toml` 变更必须先更新镜像,不能让 job 现场下载。每个 job 仍独立运行 `npm ci`,以当前 lockfile 为准验证 PR 依赖;统一通过 `scripts/ci-npm-ci-with-retry.sh` 做最多 3 次整命令级有界重试,同时保留 `NPM_CONFIG_PREFER_OFFLINE=true` 和 npm 自身 10 次 fetch retry。命中镜像 cache 时只做干净解包,lock 变化时允许补齐差量。不在镜像内烘入 `node_modules`,也不挂载跨 PR 可写缓存。任何 job 的 sandbox canary 失败都必须停止,不允许跳过。Cargo 通过受控 proxy 下载 lock 差量时继续关闭 HTTP multiplexing,并设置 `CARGO_NET_RETRY=10`。
|
||||
四个 job 先运行镜像内 `genarrative-gitea-checkout`,再以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,校验 Node 与 npm 固定版本、仓库 Rust toolchain、受信任 PATH、四份缓存锁命中状态、原生命令、pkg-config 依赖、完整 bwrap sandbox 和 Chrome headless。运行时发现锁不匹配时必须输出对应 `*_cache_lock=partial` 和 Actions warning,提示可信分支落地后刷新镜像,不能把陈旧缓存误报为闭合。`RUSTUP_AUTO_INSTALL=0`,因此仓库 `rust-toolchain.toml` 变更必须先更新镜像,不能让 job 现场下载。每个 job 仍独立运行一次根 `npm ci`,以唯一 workspace lock 验证 PR 的全部 App 依赖;统一通过 `scripts/ci-npm-ci-with-retry.sh` 做最多 3 次整命令级有界重试,同时保留 `NPM_CONFIG_PREFER_OFFLINE=true` 和 npm 自身 10 次 fetch retry。命中镜像 cache 时只做干净解包,lock 变化时允许补齐差量。不在镜像内烘入 `node_modules`,也不挂载跨 PR 可写缓存。任何 job 的 sandbox canary 失败都必须停止,不允许跳过。Cargo 通过受控 proxy 下载 lock 差量时继续关闭 HTTP multiplexing,并设置 `CARGO_NET_RETRY=10`。
|
||||
|
||||
站点 stack 仍由宿主受控目录管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到与宿主相同的绝对路径再执行;挂载到不同路径会让相对 bind source 被 Docker daemon 解析到错误的宿主目录并启动空数据。升级或 runner 迁移前先停止 Gitea 写入,并把 Gitea 冷快照、数据库导出、compose/env 与 runner config/.runner 保存到仓库外受控备份位置。备份文件、绝对宿主配置和注册 token 不得提交 Git,也不在共享文档中记录具体路径或注册内容。
|
||||
|
||||
@@ -608,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` 和 `apps/ai-game-creator-shell/package-lock.json` 执行干净依赖安装,即先执行根 `npm ci`,再执行 `npm ci --prefix apps/ai-game-creator-shell`。根 Vitest 会收集 AI 游戏创作壳的测试,如果只安装根 lockfile,收集阶段会因缺少 `@tauri-apps/api`、`@tauri-apps/plugin-http` 等 Tauri guest 依赖而失败。这些依赖属于 AI 游戏创作壳的独立安装边界,不得为了让 Jenkins 通过而将它们加入根 H5 `package.json`;排查同类报错时先核对两份 lockfile 是否都已安装。
|
||||
`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` 会把 `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` 成功归档产物。
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ pipeline {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
booleanParam(name: 'RUN_NPM_CI', defaultValue: true, description: 'Web 构建前是否按根 lockfile 与 AGC 独立 lockfile 执行 npm ci')
|
||||
booleanParam(name: 'RUN_NPM_CI', defaultValue: true, description: 'Web 构建前是否按根 workspace lockfile 执行一次 npm ci')
|
||||
string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 genarrative-notification-emails 合并发送')
|
||||
string(name: 'MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: 'genarrative-spacetime-bootstrap-secret-dev-file', description: '必填:Stdb module 构建与发布共用的 Jenkins Secret File 凭据 ID')
|
||||
booleanParam(name: 'INCLUDE_PINGORA_GATEWAY', defaultValue: true, description: 'API release 是否构建、归档并部署 Pingora 影子网关;release 默认必须包含')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user