修复 AGC Windows 构建的 Tauri NSIS 工具链重复下载与截断失败
Project CI / AI game creator shell Rust crates (push) Successful in 3m3s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m50s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m16s
Project CI / Backend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 3m3s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m50s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m16s
Project CI / Backend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
tauri-bundler 打包时现场从 GitHub 下载 nsis-3.11.zip 与 nsis_tauri_utils.dll 且不重试, Checkout 的 git clean -fdx 又会删掉工作区内的 target/.tauri,于是每个构建都要重下, 响应一旦被截断只会抛 `io: unexpected end of file`,让发布在 Rust 编译数分钟后失败。 - Jenkins Checkout 的 git clean 增加 -e apps/ai-game-creator-shell/src-tauri/target/.tauri,只保留工具缓存 - 新增 nsis-toolset.mjs:按固定 SHA1 预置 target/.tauri/NSIS,带 4 次重试并把原始归档缓存到工作区外 - 新增 ensure-nsis-toolset.mjs 与 nsis:prepare 脚本,作为 Jenkins 预检入口 - buildRelease 在 Windows 目标打包前接入工具链预置,预置失败即失败关闭 - Jenkins 新增 Tauri NSIS toolchain 阶段,在编译前预置、跑回归测试并校验 makensis 可执行 - 新增 nsis-toolset.test.mjs 并补 3 项 build-release 预置接入测试 - 同步开发运维文档与 shared-memory 排障记录
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"dev-stack": "node scripts/start-dev-stack.mjs",
|
||||
"build": "node scripts/build-release.mjs",
|
||||
"release:upload": "node scripts/release-upload.mjs",
|
||||
"nsis:prepare": "node scripts/ensure-nsis-toolset.mjs",
|
||||
"skill-pack:check": "node scripts/check-skill-pack.mjs",
|
||||
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
|
||||
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
|
||||
@@ -75,6 +76,7 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/three": "^0.184.1",
|
||||
"jszip": "^3.10.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "~5.8.2",
|
||||
"vitest": "^0.34.6"
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
defaultEditorFeatures,
|
||||
withDefaultCargoFeatures,
|
||||
} from './cargo-features.mjs';
|
||||
import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs';
|
||||
import { stageNodeRuntime } from './stage-node-runtime.mjs';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
@@ -877,14 +878,19 @@ export async function buildRelease(
|
||||
args = [],
|
||||
{
|
||||
prepareVersion = prepareReleaseVersion,
|
||||
prepareToolset = prepareNsisToolsetForRelease,
|
||||
build = runTauriBuild,
|
||||
generateManifest = generateUpdateManifest,
|
||||
} = {},
|
||||
) {
|
||||
const context = resolveReleaseContext(args);
|
||||
if (!args.includes('--no-bundle')) await prepareVersion(context);
|
||||
const bundling = !args.includes('--no-bundle');
|
||||
if (bundling) await prepareVersion(context);
|
||||
// Tauri bundler 下载 NSIS 工具链时不重试,网络截断会直接毁掉整次打包;
|
||||
// 因此打包前先在 Windows 目标上预置(详见 nsis-toolset.mjs)。
|
||||
if (bundling) await prepareToolset(context, { bundling });
|
||||
build(args, context);
|
||||
if (!args.includes('--no-bundle')) return generateManifest(context);
|
||||
if (bundling) return generateManifest(context);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -590,6 +590,71 @@ test('no-bundle smoke skips version writes and manifest generation', async () =>
|
||||
assert.deepEqual(steps, ['dev']);
|
||||
});
|
||||
|
||||
test('Windows 打包在 Tauri 构建前预置 NSIS 工具链', async () => {
|
||||
const events = [];
|
||||
await buildRelease(['--target', windowsTarget], {
|
||||
prepareVersion: () => {
|
||||
events.push('version');
|
||||
},
|
||||
prepareToolset: (context, options) => {
|
||||
events.push(`toolset:${context.target}:${options.bundling}`);
|
||||
},
|
||||
build: () => {
|
||||
events.push('build');
|
||||
},
|
||||
generateManifest: () => {
|
||||
events.push('manifest');
|
||||
},
|
||||
});
|
||||
assert.deepEqual(events, [
|
||||
'version',
|
||||
`toolset:${windowsTarget}:true`,
|
||||
'build',
|
||||
'manifest',
|
||||
]);
|
||||
});
|
||||
|
||||
test('NSIS 工具链预置失败即失败关闭,不进入 Tauri 构建', async () => {
|
||||
const events = [];
|
||||
await assert.rejects(
|
||||
buildRelease(['--target', windowsTarget], {
|
||||
prepareVersion: () => {
|
||||
events.push('version');
|
||||
},
|
||||
prepareToolset: () => {
|
||||
throw new Error('NSIS 工具链预置失败:下载 nsis-3.11.zip 失败');
|
||||
},
|
||||
build: () => {
|
||||
events.push('build');
|
||||
},
|
||||
generateManifest: () => {
|
||||
events.push('manifest');
|
||||
},
|
||||
}),
|
||||
/NSIS 工具链预置失败/u,
|
||||
);
|
||||
assert.deepEqual(events, ['version']);
|
||||
});
|
||||
|
||||
test('--no-bundle 不预置 NSIS 工具链', async () => {
|
||||
const steps = [];
|
||||
await buildRelease(['--no-bundle', '--target', windowsTarget], {
|
||||
prepareVersion: () => {
|
||||
steps.push('version');
|
||||
},
|
||||
prepareToolset: () => {
|
||||
steps.push('toolset');
|
||||
},
|
||||
build: () => {
|
||||
steps.push('build');
|
||||
},
|
||||
generateManifest: () => {
|
||||
steps.push('manifest');
|
||||
},
|
||||
});
|
||||
assert.deepEqual(steps, ['build']);
|
||||
});
|
||||
|
||||
test('release stages Node before Tauri and injects its resource mapping only for bundles', () => {
|
||||
const context = resolveReleaseContext(['--target', windowsTarget]);
|
||||
const events = [];
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Jenkins Windows 预检入口:在数分钟的 Rust 编译之前完成 NSIS 工具链预置。
|
||||
//
|
||||
// 预置失败必须在此之前失败关闭,避免 bundler 用 `io: unexpected end of file`
|
||||
// 把网络问题伪装成打包问题。
|
||||
|
||||
import { ensureNsisToolset, LOG_PREFIX } from './nsis-toolset.mjs';
|
||||
|
||||
// Jenkins 阶段用 `$ErrorActionPreference = 'Stop'` 执行 Powershell:重试告警走
|
||||
// stderr 时可能被 PowerShell 当成终止错误,因此重试与进度一律写 stdout,只有
|
||||
// 最终失败才写 stderr 并以退出码 1 失败关闭。
|
||||
const logger = {
|
||||
log: (message) => console.log(message),
|
||||
warn: (message) => console.log(`${message}(将重试)`),
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await ensureNsisToolset({ logger });
|
||||
console.log(`${LOG_PREFIX} NSIS 工具链目录:${result.nsisDir}`);
|
||||
console.log(`${LOG_PREFIX} NSIS 原始归档缓存:${result.cacheDir}`);
|
||||
console.log(
|
||||
result.reused
|
||||
? `${LOG_PREFIX} NSIS 工具链复用已有目录,未访问网络`
|
||||
: `${LOG_PREFIX} NSIS 工具链本次预置:${result.downloaded.join('、')}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`${LOG_PREFIX} NSIS 工具链预置失败:${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
// Tauri Windows bundler 的 NSIS 工具链预置。
|
||||
//
|
||||
// 背景:`tauri build` 打 Windows NSIS 包时会现场从 GitHub 下载 `nsis-3.11.zip`
|
||||
// 与 `nsis_tauri_utils.dll`(见 tauri-bundler `bundle/windows/nsis/mod.rs`)。
|
||||
// 构建机每个检出(`git clean -fdx`)都会丢掉 `target/.tauri` 缓存,于是每次
|
||||
// 发布都要重新下载;响应一旦被截断,bundler 只会报 `io: unexpected end of file`,
|
||||
// 整条流水线在 Rust 编译数分钟之后才失败。
|
||||
//
|
||||
// 这里在打包前用固定哈希 + 重试预置同一份工具链目录:bundler 检查到必需文件齐全
|
||||
// 且 `nsis_tauri_utils.dll` 哈希一致后就不会再自行下载。原始归档(两个文件)
|
||||
// 额外缓存在工作区之外,构建机重复构建时不再依赖 GitHub 连通性。
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export const LOG_PREFIX = '[ai-game-creator-shell]';
|
||||
|
||||
/** bundler 把工具链解到 `<tools>/.tauri/NSIS`(`bundle.useLocalToolsDir: true`)。 */
|
||||
export const NSIS_TOOLSET_DIR_NAME = 'NSIS';
|
||||
|
||||
export const NSIS_ARCHIVE_ASSET_NAME = 'nsis-3.11.zip';
|
||||
export const NSIS_ARCHIVE_URL =
|
||||
'https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip';
|
||||
export const NSIS_ARCHIVE_SHA1 = 'ef7ff767e5cbd9edd22add3a32c9b8f4500bb10d';
|
||||
export const NSIS_ARCHIVE_TOP_LEVEL_DIR = 'nsis-3.11';
|
||||
|
||||
export const NSIS_TAURI_UTILS_ASSET_NAME = 'nsis_tauri_utils.dll';
|
||||
export const NSIS_TAURI_UTILS_URL =
|
||||
'https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.3/nsis_tauri_utils.dll';
|
||||
export const NSIS_TAURI_UTILS_SHA1 = '75197fee3c6a814fe035788d1c34ead39349b860';
|
||||
export const NSIS_TAURI_UTILS_REQUIRED_FILE =
|
||||
'Plugins/x86-unicode/additional/nsis_tauri_utils.dll';
|
||||
|
||||
/**
|
||||
* 与 tauri-bundler 2.9.x 的 `NSIS_REQUIRED_FILES` 逐条对齐:少一条 bundler 就会
|
||||
* 删掉整个目录重新下载,等于预置失效。升级 `@tauri-apps/cli` 时要同步核对。
|
||||
*/
|
||||
export const NSIS_REQUIRED_FILES = [
|
||||
'makensis.exe',
|
||||
'Bin/makensis.exe',
|
||||
'Stubs/lzma-x86-unicode',
|
||||
'Stubs/lzma_solid-x86-unicode',
|
||||
NSIS_TAURI_UTILS_REQUIRED_FILE,
|
||||
'Include/MUI2.nsh',
|
||||
'Include/FileFunc.nsh',
|
||||
'Include/x64.nsh',
|
||||
'Include/nsDialogs.nsh',
|
||||
'Include/WinMessages.nsh',
|
||||
'Include/Win/COM.nsh',
|
||||
'Include/Win/Propkey.nsh',
|
||||
'Include/Win/RestartManager.nsh',
|
||||
];
|
||||
|
||||
/** 需要预置的原始归档;测试可注入同结构描述替换其中的地址与哈希。 */
|
||||
export const NSIS_ASSETS = [
|
||||
{
|
||||
assetName: NSIS_ARCHIVE_ASSET_NAME,
|
||||
url: NSIS_ARCHIVE_URL,
|
||||
sha1: NSIS_ARCHIVE_SHA1,
|
||||
},
|
||||
{
|
||||
assetName: NSIS_TAURI_UTILS_ASSET_NAME,
|
||||
url: NSIS_TAURI_UTILS_URL,
|
||||
sha1: NSIS_TAURI_UTILS_SHA1,
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_DOWNLOAD_ATTEMPTS = 4;
|
||||
const DEFAULT_RETRY_DELAY_MS = 3000;
|
||||
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 180_000;
|
||||
|
||||
export function defaultAppRoot() {
|
||||
return fileURLToPath(new URL('..', import.meta.url));
|
||||
}
|
||||
|
||||
export function resolveTauriToolsDir(appRoot = defaultAppRoot()) {
|
||||
// 必须与 `src-tauri/tauri.windows.conf.json` 的 `bundle.useLocalToolsDir: true`
|
||||
// 保持一致,否则预置的文件不在 bundler 的查找路径上。
|
||||
return path.join(appRoot, 'src-tauri', 'target', '.tauri');
|
||||
}
|
||||
|
||||
export function resolveNsisCacheDir(
|
||||
env = process.env,
|
||||
platform = process.platform,
|
||||
) {
|
||||
const explicit = env.AGC_TAURI_NSIS_CACHE_DIR?.trim();
|
||||
if (explicit) return path.resolve(explicit);
|
||||
// Jenkins Windows 节点以 SYSTEM 运行,ProgramData 稳定可写且不受工作区清理影响;
|
||||
// 缓存里只有待解压的原始归档,不会从该目录执行任何程序。
|
||||
if (platform === 'win32') {
|
||||
const programData = env.ProgramData?.trim() || 'C:\\ProgramData';
|
||||
return path.join(programData, 'genarrative', 'tauri-nsis-cache');
|
||||
}
|
||||
return path.join(os.homedir(), '.cache', 'genarrative', 'tauri-nsis-cache');
|
||||
}
|
||||
|
||||
/** 与 tauri-bundler 相同的镜像开关语义,便于构建机绕过不可达的 GitHub。 */
|
||||
export function resolveDownloadUrl(url, env = process.env) {
|
||||
if (!url.startsWith('https://github.com/')) return url;
|
||||
const template = env.TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE?.trim();
|
||||
const match =
|
||||
/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/releases\/download\/([^/]+)\/(.+)$/u.exec(
|
||||
url,
|
||||
);
|
||||
if (template && match) {
|
||||
return template
|
||||
.replaceAll('<owner>', match[1])
|
||||
.replaceAll('<repo>', match[2])
|
||||
.replaceAll('<version>', match[3])
|
||||
.replaceAll('<asset>', match[4]);
|
||||
}
|
||||
const base = env.TAURI_BUNDLER_TOOLS_GITHUB_MIRROR?.trim();
|
||||
if (base) return `${base.replace(/\/+$/u, '')}/${url}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
export function sha1Of(data) {
|
||||
return createHash('sha1').update(data).digest('hex');
|
||||
}
|
||||
|
||||
function sha1OfFile(filePath) {
|
||||
try {
|
||||
return sha1Of(fs.readFileSync(filePath));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyNsisToolset(
|
||||
nsisDir,
|
||||
{ utilsSha1 = NSIS_TAURI_UTILS_SHA1 } = {},
|
||||
) {
|
||||
const missing = NSIS_REQUIRED_FILES.filter(
|
||||
(relativePath) => !fs.existsSync(path.join(nsisDir, relativePath)),
|
||||
);
|
||||
const hashMismatch =
|
||||
missing.length === 0 &&
|
||||
sha1OfFile(path.join(nsisDir, NSIS_TAURI_UTILS_REQUIRED_FILE)) !==
|
||||
utilsSha1;
|
||||
return { ok: missing.length === 0 && !hashMismatch, missing, hashMismatch };
|
||||
}
|
||||
|
||||
/** 解析 zip 条目落盘位置,并拒绝 `../` 这类越界路径。 */
|
||||
export function resolveArchiveEntryTarget(rootDir, entryName) {
|
||||
const root = path.resolve(rootDir);
|
||||
const target = path.resolve(root, entryName);
|
||||
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
|
||||
throw new Error(`NSIS 归档包含越界路径:${entryName}`);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadBuffer(url, { fetchImpl, timeoutMs }) {
|
||||
const response = await fetchImpl(url, {
|
||||
redirect: 'follow',
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} ${response.statusText}`.trim());
|
||||
}
|
||||
const data = Buffer.from(await response.arrayBuffer());
|
||||
if (data.length === 0) throw new Error('响应为空');
|
||||
return data;
|
||||
}
|
||||
|
||||
async function downloadVerifiedAsset({
|
||||
assetName,
|
||||
url,
|
||||
sha1,
|
||||
env,
|
||||
fetchImpl,
|
||||
attempts,
|
||||
retryDelayMs,
|
||||
timeoutMs,
|
||||
logger,
|
||||
}) {
|
||||
const downloadUrl = resolveDownloadUrl(url, env);
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
const data = await downloadBuffer(downloadUrl, { fetchImpl, timeoutMs });
|
||||
const actual = sha1Of(data);
|
||||
if (actual !== sha1) {
|
||||
throw new Error(`SHA1 不匹配(期望 ${sha1},实际 ${actual})`);
|
||||
}
|
||||
logger.log(
|
||||
`${LOG_PREFIX} NSIS 工具链:已下载 ${assetName}(${data.length} 字节,第 ${attempt} 次尝试)`,
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
logger.warn(
|
||||
`${LOG_PREFIX} NSIS 工具链:下载 ${assetName} 失败(第 ${attempt}/${attempts} 次):${error.message}`,
|
||||
);
|
||||
if (attempt < attempts) await sleep(retryDelayMs * attempt);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`下载 ${assetName} 失败(已重试 ${attempts} 次):${lastError?.message ?? '未知错误'}\n` +
|
||||
`下载地址:${downloadUrl}\n` +
|
||||
`可先把该文件放入缓存目录(AGC_TAURI_NSIS_CACHE_DIR)或配置 ` +
|
||||
`TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE 后重试。`,
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureCachedAsset(options) {
|
||||
const { assetName, sha1, cacheDir, logger } = options;
|
||||
const cachePath = path.join(cacheDir, assetName);
|
||||
if (sha1OfFile(cachePath) === sha1) {
|
||||
logger.log(`${LOG_PREFIX} NSIS 工具链:命中缓存 ${cachePath}`);
|
||||
return cachePath;
|
||||
}
|
||||
if (fs.existsSync(cachePath)) {
|
||||
logger.warn(
|
||||
`${LOG_PREFIX} NSIS 工具链:缓存文件校验失败,重新下载 ${cachePath}`,
|
||||
);
|
||||
}
|
||||
const data = await downloadVerifiedAsset(options);
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
const tempPath = `${cachePath}.tmp-${process.pid}`;
|
||||
fs.writeFileSync(tempPath, data);
|
||||
fs.rmSync(cachePath, { force: true });
|
||||
fs.renameSync(tempPath, cachePath);
|
||||
return cachePath;
|
||||
}
|
||||
|
||||
export async function extractNsisArchive(archivePath, destinationDir) {
|
||||
const archive = await JSZip.loadAsync(fs.readFileSync(archivePath));
|
||||
for (const [entryName, entry] of Object.entries(archive.files)) {
|
||||
if (entry.dir) continue;
|
||||
const target = resolveArchiveEntryTarget(destinationDir, entryName);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, await entry.async('nodebuffer'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预置 `target/.tauri/NSIS`:已就绪时零网络直接返回,否则用缓存或重试下载补齐。
|
||||
* 返回结构用于测试与日志,不参与发布产物。
|
||||
*/
|
||||
export async function ensureNsisToolset({
|
||||
appRoot = defaultAppRoot(),
|
||||
toolsDir = resolveTauriToolsDir(appRoot),
|
||||
cacheDir = resolveNsisCacheDir(process.env),
|
||||
env = process.env,
|
||||
fetchImpl = globalThis.fetch,
|
||||
assets = NSIS_ASSETS,
|
||||
attempts = DEFAULT_DOWNLOAD_ATTEMPTS,
|
||||
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
|
||||
timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
const nsisDir = path.join(toolsDir, NSIS_TOOLSET_DIR_NAME);
|
||||
// 生产路径下这里恒等于 bundler 固定的 `nsis_tauri_utils.dll` SHA1;测试注入
|
||||
// 自己的归档描述时,校验口径必须与被注入的资产一致。
|
||||
const missingAsset = [
|
||||
NSIS_ARCHIVE_ASSET_NAME,
|
||||
NSIS_TAURI_UTILS_ASSET_NAME,
|
||||
].find((assetName) => !assets.some((asset) => asset.assetName === assetName));
|
||||
if (missingAsset) throw new Error(`NSIS 资产描述缺少 ${missingAsset}`);
|
||||
const utilsSha1 =
|
||||
assets.find((asset) => asset.assetName === NSIS_TAURI_UTILS_ASSET_NAME)
|
||||
?.sha1 ?? NSIS_TAURI_UTILS_SHA1;
|
||||
const existing = verifyNsisToolset(nsisDir, { utilsSha1 });
|
||||
if (existing.ok) {
|
||||
logger.log(`${LOG_PREFIX} NSIS 工具链已就绪:${nsisDir}`);
|
||||
return { nsisDir, toolsDir, cacheDir, reused: true, downloaded: [] };
|
||||
}
|
||||
logger.log(
|
||||
`${LOG_PREFIX} NSIS 工具链需要预置:${nsisDir}` +
|
||||
(existing.missing.length > 0
|
||||
? `(缺少 ${existing.missing.length} 个文件)`
|
||||
: '(哈希不符)'),
|
||||
);
|
||||
|
||||
const downloadOptions = {
|
||||
env,
|
||||
fetchImpl,
|
||||
attempts,
|
||||
retryDelayMs,
|
||||
timeoutMs,
|
||||
cacheDir,
|
||||
logger,
|
||||
};
|
||||
const assetPaths = {};
|
||||
for (const asset of assets) {
|
||||
assetPaths[asset.assetName] = await ensureCachedAsset({
|
||||
...asset,
|
||||
...downloadOptions,
|
||||
});
|
||||
}
|
||||
|
||||
fs.rmSync(nsisDir, { recursive: true, force: true });
|
||||
await extractNsisArchive(assetPaths[NSIS_ARCHIVE_ASSET_NAME], toolsDir);
|
||||
const extractedDir = path.join(toolsDir, NSIS_ARCHIVE_TOP_LEVEL_DIR);
|
||||
if (!fs.existsSync(extractedDir)) {
|
||||
throw new Error(
|
||||
`NSIS 归档结构不符合预期:${assetPaths[NSIS_ARCHIVE_ASSET_NAME]} 未解出 ${NSIS_ARCHIVE_TOP_LEVEL_DIR}`,
|
||||
);
|
||||
}
|
||||
fs.renameSync(extractedDir, nsisDir);
|
||||
|
||||
const utilsTarget = path.join(nsisDir, NSIS_TAURI_UTILS_REQUIRED_FILE);
|
||||
fs.mkdirSync(path.dirname(utilsTarget), { recursive: true });
|
||||
fs.copyFileSync(assetPaths[NSIS_TAURI_UTILS_ASSET_NAME], utilsTarget);
|
||||
|
||||
const installed = verifyNsisToolset(nsisDir, { utilsSha1 });
|
||||
if (!installed.ok) {
|
||||
throw new Error(
|
||||
`NSIS 工具链预置不完整:缺少 ${installed.missing.join(', ') || '无'};` +
|
||||
`哈希不符=${installed.hashMismatch}`,
|
||||
);
|
||||
}
|
||||
logger.log(`${LOG_PREFIX} NSIS 工具链预置完成:${nsisDir}`);
|
||||
return {
|
||||
nsisDir,
|
||||
toolsDir,
|
||||
cacheDir,
|
||||
reused: false,
|
||||
downloaded: assets.map((asset) => asset.assetName),
|
||||
};
|
||||
}
|
||||
|
||||
/** `buildRelease` 用:只在 Windows 目标且需要打包时预置 NSIS 工具链。 */
|
||||
export async function prepareNsisToolsetForRelease(
|
||||
context,
|
||||
{ bundling = true, ...deps } = {},
|
||||
) {
|
||||
if (!bundling || !context.target.includes('windows')) return null;
|
||||
return ensureNsisToolset(deps);
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import {
|
||||
ensureNsisToolset,
|
||||
extractNsisArchive,
|
||||
NSIS_ARCHIVE_ASSET_NAME,
|
||||
NSIS_ARCHIVE_TOP_LEVEL_DIR,
|
||||
NSIS_REQUIRED_FILES,
|
||||
NSIS_TAURI_UTILS_ASSET_NAME,
|
||||
NSIS_TOOLSET_DIR_NAME,
|
||||
prepareNsisToolsetForRelease,
|
||||
resolveArchiveEntryTarget,
|
||||
resolveDownloadUrl,
|
||||
resolveNsisCacheDir,
|
||||
resolveTauriToolsDir,
|
||||
sha1Of,
|
||||
verifyNsisToolset,
|
||||
} from './nsis-toolset.mjs';
|
||||
|
||||
const appRoot = path.resolve(
|
||||
path.dirname(new URL(import.meta.url).pathname),
|
||||
'..',
|
||||
);
|
||||
const silentLogger = { log() {}, warn() {} };
|
||||
|
||||
function createSandbox() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'agc-nsis-toolset-'));
|
||||
}
|
||||
|
||||
/** 与真实归档同构的最小 zip:只保留 bundler 必需文件。 */
|
||||
async function createArchiveFixture(extraEntries = {}) {
|
||||
const zip = new JSZip();
|
||||
for (const relativePath of NSIS_REQUIRED_FILES) {
|
||||
zip.file(
|
||||
`${NSIS_ARCHIVE_TOP_LEVEL_DIR}/${relativePath}`,
|
||||
`fixture:${relativePath}`,
|
||||
);
|
||||
}
|
||||
for (const [name, contents] of Object.entries(extraEntries)) {
|
||||
zip.file(name, contents);
|
||||
}
|
||||
return zip.generateAsync({ type: 'nodebuffer' });
|
||||
}
|
||||
|
||||
function fixtureAssets({ archive, utils }) {
|
||||
return [
|
||||
{
|
||||
assetName: NSIS_ARCHIVE_ASSET_NAME,
|
||||
url: `https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/${NSIS_ARCHIVE_ASSET_NAME}`,
|
||||
sha1: sha1Of(archive),
|
||||
},
|
||||
{
|
||||
assetName: NSIS_TAURI_UTILS_ASSET_NAME,
|
||||
url: `https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.3/${NSIS_TAURI_UTILS_ASSET_NAME}`,
|
||||
sha1: sha1Of(utils),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function fixtureFetch({ archive, utils, failures = 0 }) {
|
||||
let remainingFailures = failures;
|
||||
const calls = [];
|
||||
const fetchImpl = async (url) => {
|
||||
calls.push(url);
|
||||
if (remainingFailures > 0) {
|
||||
remainingFailures -= 1;
|
||||
throw new Error('network truncated');
|
||||
}
|
||||
const body = url.includes(NSIS_TAURI_UTILS_ASSET_NAME) ? utils : archive;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
arrayBuffer: async () => body,
|
||||
};
|
||||
};
|
||||
return { fetchImpl, calls };
|
||||
}
|
||||
|
||||
test('NSIS 工具链目录与 Tauri useLocalToolsDir 配置保持一致', () => {
|
||||
const config = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(appRoot, 'src-tauri', 'tauri.windows.conf.json'),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
assert.equal(config.bundle.useLocalToolsDir, true);
|
||||
assert.equal(
|
||||
resolveTauriToolsDir(appRoot),
|
||||
path.join(appRoot, 'src-tauri', 'target', '.tauri'),
|
||||
);
|
||||
});
|
||||
|
||||
test('缓存目录默认落在工作区之外并支持环境变量覆盖', () => {
|
||||
assert.equal(
|
||||
resolveNsisCacheDir(
|
||||
{ AGC_TAURI_NSIS_CACHE_DIR: '/tmp/agc-cache' },
|
||||
'linux',
|
||||
),
|
||||
'/tmp/agc-cache',
|
||||
);
|
||||
assert.equal(
|
||||
resolveNsisCacheDir({ ProgramData: 'D:\\ProgramData' }, 'win32'),
|
||||
path.join('D:\\ProgramData', 'genarrative', 'tauri-nsis-cache'),
|
||||
);
|
||||
assert.ok(
|
||||
resolveNsisCacheDir({}, 'linux').endsWith(
|
||||
path.join('.cache', 'genarrative', 'tauri-nsis-cache'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('下载地址支持 tauri bundler 的两套 GitHub 镜像开关', () => {
|
||||
const url =
|
||||
'https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip';
|
||||
assert.equal(resolveDownloadUrl(url, {}), url);
|
||||
assert.equal(
|
||||
resolveDownloadUrl(url, {
|
||||
TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE:
|
||||
'https://mirror.example.com/<owner>/<repo>/<version>/<asset>',
|
||||
}),
|
||||
'https://mirror.example.com/tauri-apps/binary-releases/nsis-3.11/nsis-3.11.zip',
|
||||
);
|
||||
assert.equal(
|
||||
resolveDownloadUrl(url, {
|
||||
TAURI_BUNDLER_TOOLS_GITHUB_MIRROR: 'https://mirror.example.com/',
|
||||
}),
|
||||
`https://mirror.example.com/${url}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('工具链已就绪时零下载复用', async () => {
|
||||
const sandbox = createSandbox();
|
||||
const toolsDir = path.join(sandbox, '.tauri');
|
||||
const cacheDir = path.join(sandbox, 'cache');
|
||||
const archive = await createArchiveFixture();
|
||||
const utils = Buffer.from('nsis-tauri-utils-dll');
|
||||
const assets = fixtureAssets({ archive, utils });
|
||||
|
||||
await ensureNsisToolset({
|
||||
toolsDir,
|
||||
cacheDir,
|
||||
assets,
|
||||
fetchImpl: fixtureFetch({ archive, utils }).fetchImpl,
|
||||
logger: silentLogger,
|
||||
});
|
||||
|
||||
let fetchCalls = 0;
|
||||
const reused = await ensureNsisToolset({
|
||||
toolsDir,
|
||||
cacheDir,
|
||||
assets,
|
||||
fetchImpl: async () => {
|
||||
fetchCalls += 1;
|
||||
throw new Error('工具链已就绪时不应访问网络');
|
||||
},
|
||||
logger: silentLogger,
|
||||
});
|
||||
assert.equal(reused.reused, true);
|
||||
assert.deepEqual(reused.downloaded, []);
|
||||
assert.equal(fetchCalls, 0);
|
||||
assert.equal(reused.nsisDir, path.join(toolsDir, NSIS_TOOLSET_DIR_NAME));
|
||||
});
|
||||
|
||||
test('冷启动时下载、校验、解压并落缓存,重跑走缓存', async () => {
|
||||
const sandbox = createSandbox();
|
||||
const toolsDir = path.join(sandbox, '.tauri');
|
||||
const cacheDir = path.join(sandbox, 'cache');
|
||||
const archive = await createArchiveFixture();
|
||||
const utils = Buffer.from('nsis-tauri-utils-dll');
|
||||
const assets = fixtureAssets({ archive, utils });
|
||||
const { fetchImpl, calls } = fixtureFetch({ archive, utils });
|
||||
|
||||
const result = await ensureNsisToolset({
|
||||
toolsDir,
|
||||
cacheDir,
|
||||
assets,
|
||||
fetchImpl,
|
||||
logger: silentLogger,
|
||||
});
|
||||
assert.deepEqual(calls.length, 2);
|
||||
assert.deepEqual(result.downloaded, [
|
||||
NSIS_ARCHIVE_ASSET_NAME,
|
||||
NSIS_TAURI_UTILS_ASSET_NAME,
|
||||
]);
|
||||
assert.equal(
|
||||
verifyNsisToolset(path.join(toolsDir, NSIS_TOOLSET_DIR_NAME), {
|
||||
utilsSha1: sha1Of(utils),
|
||||
}).ok,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
toolsDir,
|
||||
NSIS_TOOLSET_DIR_NAME,
|
||||
'Plugins/x86-unicode/additional/nsis_tauri_utils.dll',
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
'nsis-tauri-utils-dll',
|
||||
);
|
||||
|
||||
// 第二次构建:清空工作区工具目录后仍应零下载恢复(模拟 Jenkins git clean -fdx)。
|
||||
fs.rmSync(path.join(toolsDir, NSIS_TOOLSET_DIR_NAME), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
const offline = await ensureNsisToolset({
|
||||
toolsDir,
|
||||
cacheDir,
|
||||
assets,
|
||||
fetchImpl: async () => {
|
||||
throw new Error('命中缓存时不应访问网络');
|
||||
},
|
||||
logger: silentLogger,
|
||||
});
|
||||
assert.equal(
|
||||
verifyNsisToolset(offline.nsisDir, { utilsSha1: sha1Of(utils) }).ok,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('下载失败按次数重试,最终成功', async () => {
|
||||
const sandbox = createSandbox();
|
||||
const archive = await createArchiveFixture();
|
||||
const utils = Buffer.from('dll');
|
||||
const { fetchImpl, calls } = fixtureFetch({ archive, utils, failures: 2 });
|
||||
const result = await ensureNsisToolset({
|
||||
toolsDir: path.join(sandbox, '.tauri'),
|
||||
cacheDir: path.join(sandbox, 'cache'),
|
||||
assets: fixtureAssets({ archive, utils }),
|
||||
fetchImpl,
|
||||
attempts: 3,
|
||||
retryDelayMs: 1,
|
||||
logger: silentLogger,
|
||||
});
|
||||
assert.equal(result.downloaded.length, 2);
|
||||
assert.equal(calls.length, 4);
|
||||
});
|
||||
|
||||
test('哈希不匹配时报错并给出可操作提示', async () => {
|
||||
const sandbox = createSandbox();
|
||||
const { fetchImpl } = fixtureFetch({
|
||||
archive: Buffer.from('corrupted'),
|
||||
utils: Buffer.from('corrupted'),
|
||||
});
|
||||
await assert.rejects(
|
||||
ensureNsisToolset({
|
||||
toolsDir: path.join(sandbox, '.tauri'),
|
||||
cacheDir: path.join(sandbox, 'cache'),
|
||||
assets: [
|
||||
{
|
||||
assetName: NSIS_ARCHIVE_ASSET_NAME,
|
||||
url: 'https://github.com/a/b/releases/download/1/n.zip',
|
||||
sha1: 'deadbeef',
|
||||
},
|
||||
{
|
||||
assetName: NSIS_TAURI_UTILS_ASSET_NAME,
|
||||
url: 'https://github.com/a/b/releases/download/1/n.dll',
|
||||
sha1: 'deadbeef',
|
||||
},
|
||||
],
|
||||
fetchImpl,
|
||||
attempts: 2,
|
||||
retryDelayMs: 1,
|
||||
logger: silentLogger,
|
||||
}),
|
||||
/SHA1 不匹配/u,
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(sandbox, 'cache', NSIS_ARCHIVE_ASSET_NAME)),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('归档越界路径与缺失必需文件都会失败关闭', async () => {
|
||||
const sandbox = createSandbox();
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveArchiveEntryTarget(path.join(sandbox, 'extract'), '../escape.txt'),
|
||||
/越界路径/u,
|
||||
);
|
||||
assert.equal(
|
||||
resolveArchiveEntryTarget(path.join(sandbox, 'extract'), 'nsis-3.11/a/b'),
|
||||
path.resolve(sandbox, 'extract', 'nsis-3.11/a/b'),
|
||||
);
|
||||
|
||||
const emptyArchive = new JSZip()
|
||||
.file(`${NSIS_ARCHIVE_TOP_LEVEL_DIR}/makensis.exe`, 'only-one')
|
||||
.generateAsync({ type: 'nodebuffer' });
|
||||
const emptyPath = path.join(sandbox, 'incomplete.zip');
|
||||
fs.writeFileSync(emptyPath, await emptyArchive);
|
||||
const toolsDir = path.join(sandbox, 'incomplete-tools');
|
||||
await extractNsisArchive(emptyPath, toolsDir);
|
||||
const status = verifyNsisToolset(
|
||||
path.join(toolsDir, NSIS_ARCHIVE_TOP_LEVEL_DIR),
|
||||
);
|
||||
assert.equal(status.ok, false);
|
||||
assert.ok(status.missing.includes('Bin/makensis.exe'));
|
||||
});
|
||||
|
||||
test('非 Windows 目标或 --no-bundle 不预置工具链', async () => {
|
||||
let called = 0;
|
||||
const deps = {
|
||||
ensure: async () => {
|
||||
called += 1;
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
await prepareNsisToolsetForRelease(
|
||||
{ target: 'x86_64-pc-windows-msvc' },
|
||||
{ bundling: false, ...deps },
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
await prepareNsisToolsetForRelease(
|
||||
{ target: 'aarch64-apple-darwin' },
|
||||
deps,
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(called, 0);
|
||||
});
|
||||
@@ -5425,6 +5425,14 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- 处理:Windows 专用 Tauri 配置设置 `bundle.useLocalToolsDir: true`,把工具缓存到 `src-tauri/target/.tauri/NSIS`;Jenkins 预检验证实际用户、项目工具目录可写,并在构建失败时打印实际缓存路径和绝对路径执行结果。
|
||||
- 验证:不要把 PATH 中 `makensis` 可发现当作 Tauri bundler 工具可执行的充分证据;需要在 Windows Agent 上检查 `target/.tauri/NSIS/makensis.exe`、ACL、EDR/Defender 和直接 `-VERSION` 结果。
|
||||
|
||||
## Tauri NSIS 工具链必须在打包前预置并重试(2026-09-21)
|
||||
|
||||
- 现象:AGC Windows 发布构建已完成 Rust release,Tauri 依次打印 `Downloading .../nsis-3.11.zip`、`Info extracting NSIS`、`Downloading .../nsis_tauri_utils.dll` 之后,直接以 ``failed to bundle project `io: unexpected end of file` `` 失败(退出码 1),安装包不会产出。
|
||||
- 原因:tauri-bundler 的 `download_and_verify` 现场从 GitHub 取 NSIS 工具链,只有一次机会、没有重试;响应体被截断即报 `io: unexpected end of file`,看起来像打包错误其实是网络问题。Checkout 阶段的 `git clean -fdx` 每次都会清掉 `target/.tauri`,所以每个构建都要重新下载,在受限网络下必然反复失败。
|
||||
- 处理:新增 `apps/ai-game-creator-shell/scripts/nsis-toolset.mjs` 与 `ensure-nsis-toolset.mjs`,在 `buildRelease`(Windows 目标且需要打包时)与 Jenkins `Tauri NSIS toolchain` 阶段按固定 SHA1 预置 `target/.tauri/NSIS`:原始归档带 4 次重试,缓存在工作区外的 `%ProgramData%\genarrative\tauri-nsis-cache`(可用 `AGC_TAURI_NSIS_CACHE_DIR` 覆盖),镜像开关沿用 bundler 的 `TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE` / `TAURI_BUNDLER_TOOLS_GITHUB_MIRROR`。该阶段同时执行 `makensis.exe -VERSION`,把 2026-09-02 记录的缓存目录不可执行问题也提前到编译之前暴露。Checkout 阶段改为 `git clean -fdx -e apps/ai-game-creator-shell/src-tauri/target/.tauri`:Tauri 的工具缓存位于工作区内,裸 `git clean -fdx` 会连它一起删,排除后同一节点的稳态构建不再需要联网,只有冷缓存(新节点、工作区重建)才下载。
|
||||
- 验证:`node --test apps/ai-game-creator-shell/scripts/nsis-toolset.test.mjs`(已就绪零下载复用、缓存离线还原、失败重试、哈希不符与归档越界失败关闭)与 `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs`;真实节点上该阶段必须早于 Rust 编译失败关闭。
|
||||
- 注意:不要改回 `bundle.useLocalToolsDir: false` 去用 `%LOCALAPPDATA%`,也不要依赖 PATH 里预装的 `makensis`;升级 `@tauri-apps/cli` 时同步核对归档 URL、SHA1 与必需文件清单。
|
||||
|
||||
## AGC 登录态续期必须同步本地运行时
|
||||
|
||||
- 模型目录 HTTP 请求与 DirectProject 的 Rust/app-server 使用同一账号,但凭据分别保存在 WebView 与 Rust / Runner;续期应复用 `requestPlatformSessionRefresh` 完成用户核验及本地会话安装,不能只写 localStorage。
|
||||
|
||||
@@ -147,6 +147,8 @@ revision 变化时,调度管线把同一个完整 commit 通过 `COMMIT_HASH`
|
||||
|
||||
手工发布入口 `Genarrative-Manual-Build-And-Deploy` 的 `DEPLOY_TARGET` 与 AGC 更新渠道是两个独立维度。手工入口必须把 `release` 映射为 `AGC_UPDATE_CHANNEL=release`、把 `development` 映射为 `dev`,并把该参数同时透传给 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`;只传 `AGC_RELEASE_VERSION` 时下游会落回各自默认 `dev`。统一号 `0.1.95` 的 release 包应分别位于 `agc/release-win/0.1.95/` 与 `agc/release-mac/0.1.95/`,渠道清单为对应目录下的 `latest.json`,不存在 `agc/release/` 这一层。补发本轮已烧号的版本时,直接以相同 `AGC_RELEASE_VERSION` 重跑两条 AGC Job,不重新发号。
|
||||
|
||||
`Genarrative-Agc-Windows-Build` 的 `Tauri NSIS toolchain` 阶段必须在 Rust 编译前预置 NSIS 工具链并失败关闭:tauri-bundler 打包时现场从 GitHub 下载 `nsis-3.11.zip` 与 `nsis_tauri_utils.dll` 且不重试,构建机每次检出都会重下,响应一旦被截断就只能抛 `io: unexpected end of file`,让发布在编译数分钟后才失败。该阶段先跑 `node apps/ai-game-creator-shell/scripts/ensure-nsis-toolset.mjs`(固定 SHA1 校验、4 次重试、解压到 `target/.tauri/NSIS`),再执行 `makensis.exe -VERSION` 验证可执行性;Checkout 的 `git clean -fdx` 必须带 `-e apps/ai-game-creator-shell/src-tauri/target/.tauri`,只保留这份工具缓存、其余 `target/` 内容照常清空,否则工作区内缓存会被每个构建删掉,退回到「每次从 GitHub 重下」(实测裸 `git clean -fdx` 会输出 `Would remove apps/ai-game-creator-shell/src-tauri/target/`);原始归档缓存在工作区外的 `%ProgramData%\genarrative\tauri-nsis-cache`(可用 `AGC_TAURI_NSIS_CACHE_DIR` 覆盖),因此同一节点只有冷缓存才需要联网,离线补缓存时把这两个文件放进缓存目录即可;构建机确实无法访问 GitHub 时使用 bundler 自带的 `TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE` / `TAURI_BUNDLER_TOOLS_GITHUB_MIRROR` 指向可达镜像。升级 `@tauri-apps/cli` 时必须同步核对 `nsis-toolset.mjs` 里的归档地址、SHA1 与必需文件清单(与 tauri-bundler 的 `NSIS_REQUIRED_FILES` 逐条对齐),否则预置会被 bundler 判为不完整。
|
||||
|
||||
调度状态是调度 Job 工作区里的 `.jenkins-last-triggered-revision`,构建描述同时回显本次 revision 与结果。工作区被清理(例如 `Wipe Out Workspace`)或状态文件缺失时,下一次运行按“版本变化”处理并触发一次,之后恢复稳定;需要重建同一版本时勾选 `FORCE_TRIGGER`。Job 按仓库内 `jenkins/scheduled-revision-trigger-job-config.xml` 创建:`scriptPath=jenkins/Jenkinsfile.scheduled-revision-trigger`、Git 入口 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`、凭据 `genarrative-local-gitea-ssh`、`<triggers/>` 留空(定时器写在 Jenkinsfile 里)。推送后必须让三个 live Job 各自加载一次新 Jenkinsfile,并只读核对 `config.xml`:Full 与 AGC 不再有 cron,定时只来自新调度 Job;只改 Jenkinsfile 而不确认 live 配置时,旧 cron 仍会继续触发。
|
||||
|
||||
Full Job 通过 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION` 明确选择完整发布成功后是否退出维护,默认勾选以保持历史行为。Full 对 Stdb Publish 和 API Deploy 两个下游阶段都固定传 `KEEP_MAINTENANCE_MODE=true`,让 maintenance marker 持续覆盖 Stdb → API → Web 整段发布;Web Deploy 成功后才进入独立 `Exit Maintenance` 阶段。该阶段只能通过 `agent none` 和显式 `node(...)` 分配目标机,直接执行 `/opt/genarrative/current/scripts/deploy/maintenance-off.sh`;目标机不得 checkout Git、挂载 Git SSH 凭据或依赖 Jenkins workspace 源码。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。`Genarrative-Api-Deploy` 也单独暴露 `KEEP_MAINTENANCE_MODE` 参数,并转换为随发布包脚本的 `--keep-maintenance-mode`;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。外部生成 queue 的 `warning` 由 API/worker 固化为可直接展示的完整文案,Web 不再补前缀,因此 API/worker 与 Web 必须在同一维护窗口按同一版本协调发布;分开运行 Job 时先保持维护态完成 API/worker,再发布 Web,二者完成后才能恢复公网,不得在公网可用期间只滚动其中一侧。
|
||||
|
||||
@@ -41,7 +41,11 @@ pipeline {
|
||||
if (Test-Path '.git') {
|
||||
& $git fetch --tags --force --prune origin "+refs/heads/$($env:SOURCE_BRANCH):refs/remotes/origin/$($env:SOURCE_BRANCH)"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Git fetch 失败' }
|
||||
& $git clean -fdx
|
||||
# Tauri 的 NSIS 工具缓存位于工作区内的 target/.tauri:裸 git clean -fdx
|
||||
# 会把它一起删掉,导致每个构建都重新从 GitHub 下载工具链(下载无重试,
|
||||
# 响应截断即 `io: unexpected end of file`)。这里只排除该缓存目录,
|
||||
# 其余 target 内容照常清空。
|
||||
& $git clean -fdx -e 'apps/ai-game-creator-shell/src-tauri/target/.tauri'
|
||||
& $git reset --hard "origin/$($env:SOURCE_BRANCH)"
|
||||
} else {
|
||||
& $git clone --no-single-branch $env:GIT_REMOTE_URL .
|
||||
@@ -144,6 +148,26 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
stage('Tauri NSIS toolchain') {
|
||||
steps {
|
||||
withEnv(["PATH=${env.AGC_WINDOWS_PATH}"]) {
|
||||
// 必须早于 Rust 编译与打包:Tauri bundler 自己下载 NSIS 工具链时不重试,
|
||||
// 响应截断只会报 `io: unexpected end of file`,让发布在数分钟后才失败。
|
||||
powershell '''
|
||||
$ErrorActionPreference = 'Stop'
|
||||
node --test apps/ai-game-creator-shell/scripts/nsis-toolset.test.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "NSIS 工具链回归测试失败,退出码=$LASTEXITCODE" }
|
||||
node apps/ai-game-creator-shell/scripts/ensure-nsis-toolset.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "NSIS 工具链预置失败,退出码=$LASTEXITCODE" }
|
||||
$nsis = Join-Path $PWD 'apps/ai-game-creator-shell/src-tauri/target/.tauri/NSIS/makensis.exe'
|
||||
if (-not (Test-Path $nsis)) { throw "NSIS makensis.exe 缺失:$nsis" }
|
||||
& $nsis -VERSION
|
||||
if ($LASTEXITCODE -ne 0) { throw "NSIS makensis.exe 无法执行,退出码=$LASTEXITCODE" }
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build and upload') {
|
||||
steps {
|
||||
script {
|
||||
|
||||
Generated
+1
@@ -138,6 +138,7 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/three": "^0.184.1",
|
||||
"jszip": "^3.10.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "~5.8.2",
|
||||
"vitest": "^0.34.6"
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"ai-game-creator-shell:dev-server": "npm --prefix apps/ai-game-creator-shell run dev-server",
|
||||
"ai-game-creator-shell:build": "npm --prefix apps/ai-game-creator-shell run build --",
|
||||
"ai-game-creator-shell:release:upload": "npm --prefix apps/ai-game-creator-shell run release:upload",
|
||||
"ai-game-creator-shell:nsis:prepare": "npm --prefix apps/ai-game-creator-shell run nsis:prepare",
|
||||
"ai-game-creator-shell:llm-status": "npm --prefix apps/ai-game-creator-shell run llm-status --",
|
||||
"ai-game-creator-shell:agent-task": "npm --prefix apps/ai-game-creator-shell run agent-task --",
|
||||
"agc:chat": "npm --prefix apps/ai-game-creator-shell run chat --",
|
||||
|
||||
Reference in New Issue
Block a user